Chat systém — Specifikace¶
Přehled¶
Integrovaný chat v AVAX platformě. Zprávy uloženy jako JSON soubory na dedikovaném
S3 bucketu (MinIO), doručení přes adaptive polling s pending_deliveries frontou.
Žádný WebSocket server, žádný Redis Pub/Sub — nízká HW náročnost pro 100 000+ uživatelů.
Architektura — S3 jako primární store¶
Tradiční chat: klient → WS server → Redis Pub/Sub → DB → klient
AVAX chat: klient → backend API → S3 (zápis) → pending_deliveries
klient ← polling ←────────────────── pending_deliveries
Výhody pro self-hosted MinIO: - Žádné per-request poplatky (vlastní hardware) - GC pro zprávy = stejný mechanismus jako pro zálohy - Horizontálně škálovatelné bez sdíleného stavu - Presigned URLs pro soubory bez proxy přes backend
Dedikovaný S3 bucket¶
Bucket: avax-chat
Přístup: privátní (žádné veřejné ACL)
IAM user: chat-service (read/write/delete na celý bucket)
Šifrování: SSE-S3 (serverové AES-256) — výchozí pro nešifrovaný chat
SSE-C (client-side key) — volitelný šifrovaný chat
Oddělení od zálohovacího bucketu (claudeai) zajišťuje:
- nezávislá IAM politika
- vlastní GC pravidla (zprávy vs. zálohy mají jinou retenci)
- čistší billing a monitoring
Pojmenování adresářů¶
Login v adresáři = e-mail bez @, ostatní znaky zachovány:
Přímé zprávy (1:1)¶
dm_{login_a}_{login_b}/
Pravidlo: loginy seřazeny abecedně (zajišťuje unikátnost bez ohledu na směr)
Příklad: dm_jan.novotnymail.cz_petr.novakfirma.cz/
Skupinová konverzace¶
gr_{login_zakladatele}_{NNN}/
NNN = trojmístné pořadové číslo skupin tohoto uživatele (001, 002, ...)
Příklad: gr_jan.novotnymail.cz_001/
gr_jan.novotnymail.cz_002/
DB mapování — directory_name → conversation_id:
conversations (
id UUID PRIMARY KEY,
type VARCHAR(10), -- 'dm' | 'group' | 'support'
s3_prefix VARCHAR(255), -- dm_jan.novotnymail.cz_petr.novakfirma.cz
created_by UUID REFERENCES users(id),
name VARCHAR(255), -- jen pro skupiny
encrypted BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ
)
Pokud se uživatel přejmenuje (nick), S3 prefix zůstane nezměněn — mapování zajistí DB.
Struktura S3 objektů¶
avax-chat/
└── {s3_prefix}/
├── messages/
│ ├── 2026-05-03/ ← dnešní den: individual soubory
│ │ ├── {ts}_{msg_id}.json ← okamžitě zapsáno při odeslání
│ │ └── ...
│ ├── 2026-05-02.json ← včera+: denní bundle (GC job)
│ ├── 2026-05-01.json
│ └── ...
└── attachments/
└── {msg_id}/
├── original.{ext} ← původní soubor
└── thumb.jpg ← jen pro obrázky (server-side resize)
Denní bundle — logika¶
Zprávy se píší jako individual soubory během dne (S3 neumí append). Každý den ve 02:00 Celery job agreguje předchozí den do jednoho JSON souboru:
# Celery task: bundle_daily_messages(date, s3_prefix)
# 1. LIST objects: messages/{date}/
# 2. GET + parse každý soubor → seřadit dle timestamp
# 3. PUT messages/{date}.json (pole zpráv)
# 4. DELETE objects messages/{date}/
Formát souboru zprávy¶
Individual soubor: messages/2026-05-03/{timestamp_ms}_{msg_id}.json
Pole type — povolené hodnoty:
text → textová zpráva (+ volitelné přílohy)
mixed → text + přílohy
image → pouze obrázek
file → pouze soubor
system → systémová zpráva (přidal se člen, opustil chat...)
call_invite → záznam hlasového hovoru — připraveno, zatím šedivé v UI
{
"id": "abc123",
"sender": "jan.novotnymail.cz",
"sender_name": "Jan Novotný",
"sender_nick": "Honza",
"type": "text",
"content": "Ahoj, jak se máš?",
"sent_at": "2026-05-03T10:42:00.000Z",
"edited_at": null,
"deleted": false,
"reply_to": null,
"attachments": [],
"call": null
}
Zpráva typu call_invite — záznam hovoru:
{
"id": "call_abc123",
"sender": "jan.novotnymail.cz",
"sender_name": "Jan Novotný",
"sender_nick": "Honza",
"type": "call_invite",
"content": "Jan Novotný zahájil hovor",
"sent_at": "2026-05-03T10:42:00.000Z",
"edited_at": null,
"deleted": false,
"reply_to": null,
"attachments": [],
"call": {
"call_id": "call_abc123",
"status": "ended",
"started_at": "2026-05-03T10:42:05.000Z",
"ended_at": "2026-05-03T10:57:13.000Z",
"duration_seconds": 908
}
}
Stavy hovoru (call.status):
ringing → vyzvání, čeká na odpověď
accepted → hovor probíhá
declined → příjemce odmítl
missed → příjemce neodpověděl (timeout 30s)
ended → hovor ukončen
failed → technická chyba / P2P spojení se nepodařilo sestavit
Denní bundle: messages/2026-05-02.json
Zprávy s přílohami¶
{
"id": "def456",
"sender": "jan.novotnymail.cz",
"type": "mixed",
"content": "Viz příloha",
"sent_at": "2026-05-03T10:43:00.000Z",
"attachments": [
{
"id": "att789",
"name": "faktury_Q1.pdf",
"mime": "application/pdf",
"size": 204800,
"s3_key": "dm_.../attachments/def456/original.pdf"
},
{
"id": "att790",
"name": "foto.jpg",
"mime": "image/jpeg",
"size": 1048576,
"s3_key": "dm_.../attachments/def456/original.jpg",
"thumb_s3_key": "dm_.../attachments/def456/thumb.jpg"
}
]
}
Zobrazení souborů v chatu¶
Soubor: [📄 faktury_Q1.pdf 200 KB] ← karta, při hoveru/kliknutí:
[⬇ Stáhnout na disk]
[🔗 Sdílet odkaz] → vygeneruje presigned GET URL (platnost 7 dní)
Obrázek: [thumbnail inline v chatu] ← presigned GET na thumb_s3_key
klik → full-size overlay
[⬇ Stáhnout na disk]
[🔗 Sdílet odkaz]
Sdílení:
POST /chat/attachments/{att_id}/share-link
→ server vygeneruje presigned GET URL (7 dní) a vrátí ho klientovi
→ uživatel ho zkopíruje / pošle mimo AVAX
→ odkaz je anonymní (bez autentizace), platný 7 dní
Doručení zpráv — Adaptive Polling¶
Fáze odeslání¶
Odesílatel klikne Odeslat:
1. POST /chat/messages → backend ověří, zapíše JSON na S3
2. Backend vloží do pending_deliveries pro každého příjemce
3. Vrátí { msg_id, s3_key, sent_at } → sender zobrazí zprávu okamžitě
Fáze doručení¶
Příjemce online (aktivní poll):
→ poll vrátí okamžitě novou zprávu z pending_deliveries
→ client stáhne JSON z S3 (presigned GET)
→ smaže z pending_deliveries (ACK)
Příjemce offline / tray:
→ pending_deliveries zůstane
→ při přihlášení / otevření launcheru:
GET /chat/pending → stáhne vše co přišlo, zpracuje
pending_deliveries (
id UUID PRIMARY KEY,
conversation_id UUID REFERENCES conversations(id),
recipient_id UUID REFERENCES users(id),
s3_key TEXT, -- cesta k JSON souboru zprávy
msg_id VARCHAR(64),
created_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ -- NULL = dosud nedoručeno
)
-- Index pro rychlé načtení: (recipient_id, delivered_at) WHERE delivered_at IS NULL
Polling interval — adaptive¶
Stav klienta Interval
────────────────────────────────────────────────
Chat záložka otevřena + psaní 2 s
Chat záložka otevřena, idle 5 s
Launcher aktivní, chat zavřen 30 s
Launcher minimalizován (tray) 60 s
Launcher zavřen --- (bez pollingu)
Endpoint: GET /chat/pending?since={timestamp}
→ vrátí seznam nových zpráv (s3_key + msg_id)
→ klient stáhne každou zprávu presigned URL
→ POST /chat/ack Body: { msg_ids: [...] } → smaže z pending_deliveries
Presence / Online status¶
Pro 100 000 uživatelů — minimální zátěž:
Heartbeat: POST /presence/heartbeat každých 60 s
→ Redis key: presence:{user_id} TTL 90s
→ hodnota: "online" | "away"
Výpočet:
ONLINE → klíč existuje, hodnota "online"
PRYČ → klíč existuje, hodnota "away" (launcher idle > 5 min)
OFFLINE → klíč neexistuje (TTL expiroval)
Zátěž při 5 000 aktivních uživatelích:
5 000 heartbeatů / 60s = ~83 req/s → triviální pro Redis
Multi-device:
Redis key: presence:{user_id}:{device_id} TTL 90s
Uživatel je ONLINE pokud alespoň jeden klíč s "online" existuje
Uživatelská identita¶
Zobrazení v chatu:
[Initials] Honza ← nick (pokud nastaven)
Jan Novotný ← celé jméno (vždy)
jan.novotnymail.cz ← login/email (v detailu profilu)
Nick je globální — stejný ve všech konverzacích. Uživatel ho může kdykoli změnit.
Šifrování (volitelné)¶
Uživatel může v nastavení zapnout Šifrovaný chat.
Princip: End-to-end šifrování zpráv před zápisem na S3
→ obsah zprávy je šifrovaný = backend ho nevidí = full-text search nefunguje
Aktivace:
Uživatel A zapne šifrovaný chat v nastavení
→ při zahájení konverzace s uživatelem B:
systém zkontroluje: má B také šifrovaný chat?
ANO → konverzace probíhá šifrovaně, v záhlaví chatu: 🔒 Šifrovaný chat
NE → zobrazí hláška: "Druhá strana nepoužívá šifrovaný chat.
Pro šifrování musí šifrování aktivovat i oni."
UI:
🔒 Šifrovaný chat ← badge v záhlaví konverzace
🔍 [████████████████] (šedé) ← pole pro vyhledávání zatmaveno + tooltip:
"Vyhledávání není dostupné v šifrovaném chatu"
Technicky:
Algoritmus: AES-256-GCM, klíč derivován z hesla uživatele (PBKDF2)
Výměna klíčů: ECDH (X25519) při prvním kontaktu
Uložení: šifrovaný soubor zprávy na S3 (pole content je base64-encrypted)
Zpráva v S3 (šifrovaná):
{ ..., "content": "BASE64ENCRYPTED==", "encrypted": true }
Dešifrování probíhá výhradně na klientovi — server obsah nevidí.
Skupinový šifrovaný chat:
Každý člen dostane kopii symetrického klíče skupiny,
šifrovanou jeho veřejným klíčem.
Při přidání nového člena admin re-šifruje klíč skupiny pro nového člena.
Při odebrání člena se vygeneruje nový klíč skupiny (forward secrecy).
-- Klíče pro E2E šifrování
user_chat_keys (
user_id UUID REFERENCES users(id) PRIMARY KEY,
public_key TEXT, -- X25519 veřejný klíč (base64)
key_version INT DEFAULT 1,
created_at TIMESTAMPTZ
)
conversation_keys (
conversation_id UUID REFERENCES conversations(id),
user_id UUID REFERENCES users(id),
encrypted_key TEXT, -- symetrický klíč skupiny, šifrovaný veřejným klíčem člena
key_version INT DEFAULT 1,
PRIMARY KEY (conversation_id, user_id)
)
Smazání zpráv a konverzací¶
1:1 — single-sided¶
Smazání zprávy: client zapíše do lokálního indexu hidden_msg_ids[]
→ zpráva se nezobrazí jen tomuto uživateli
→ S3 soubor se NEmaže (druhý uživatel zprávu vidí dál)
→ uloženo v DB: message_visibility(conv_id, user_id, msg_id, hidden_at)
Smazání konverzace: conversation_deletions INSERT (conv_id, user_id)
→ konverzace zmizí jen z pohledu tohoto uživatele
→ S3 soubory zůstanou dokud to nesmaže i druhá strana → pak GC
Úprava zprávy: message_visibility.custom_content
→ druhý uživatel vidí originál
Skupinový chat — globální¶
Admin maže zprávu:
1. GET message JSON z S3
2. Přepíše: { "deleted": true, "content": "[zpráva smazána adminem]", "deleted_by": "..." }
3. PUT zpět na S3 (přepíše původní soubor)
→ všichni uživatelé vidí "[zpráva smazána adminem]" při příštím načtení
Zrušení skupiny:
Celery task: smaže celý S3 prefix (DELETE objects s3_prefix/*) + DB záznamy
Garbage Collector — zprávy¶
Integrace s existujícím GC systémem (stejná logika jako zálohy):
Textové zprávy: žádná automatická expirace (dokud uživatel nesmaže)
Přílohy: zachovány dokud konverzace existuje (nemaže se po 24h jako v původní spec)
→ attachment_expires_at = NULL pro trvalé soubory v aktivní konverzaci
→ GC smaže přílohy jen při zrušení celé konverzace
Celery tasks:
bundle_daily_messages → každý den 02:00, agreguje individual soubory předchozího dne
gc_deleted_conversations → smaže S3 prefix + DB záznamy smazaných konverzací
cleanup_share_links → smaže expirované presigned share linky z DB
API endpointy¶
# Konverzace
GET /chat/conversations seznam s unread count
POST /chat/conversations nová konverzace
Body: { type, members, name, encrypted }
GET /chat/conversations/{id} detail + členové
DELETE /chat/conversations/{id} smazat (1:1: single-sided; skupina: admin→vše)
# Zprávy
GET /chat/conversations/{id}/messages ?date=2026-05-03 (denní bundle nebo individual)
POST /chat/conversations/{id}/messages odeslat zprávu
Body: { content, type, attachments, reply_to, encrypted_content }
PATCH /chat/conversations/{id}/messages/{msg_id} upravit zprávu (vlastní; skupina: global; 1:1: single-sided)
DELETE /chat/conversations/{id}/messages/{msg_id}
# Doručení
GET /chat/pending ?since={timestamp} nové zprávy ke stažení
POST /chat/ack Body: { msg_ids: [...] } potvrzení přijetí
# Přílohy
POST /chat/conversations/{id}/upload-url Body: { filename, mime, size }
Vrátí: { presigned_put_url, attachment_id, s3_key }
POST /chat/attachments/{att_id}/presigned Vrátí: { url } (privátní, TTL 1h)
POST /chat/attachments/{att_id}/share-link Vrátí: { url, expires_at } (veřejný, TTL 7d)
# Presence
POST /presence/heartbeat Body: { status: "online"|"away" }
GET /presence?user_ids=[...] online status více uživatelů
# Nick
PUT /users/me/chat-nick Body: { nick }
# Šifrování — výměna klíčů
GET /chat/keys/{user_id} veřejný klíč uživatele pro E2E
PUT /chat/keys/me Body: { public_key } uložení/aktualizace klíče
POST /chat/conversations/{id}/keys Body: { encrypted_key } přidání klíče pro nového člena
# Skupinový chat — moderace
POST /chat/conversations/{id}/members Body: { email } přidat člena
DELETE /chat/conversations/{id}/members/{uid} odebrat člena
PUT /chat/conversations/{id}/members/{uid}/ban Body: { banned: bool, reason }
POST /chat/conversations/{id}/leave
# Kontaktní tokeny (cross-company pozvánky)
POST /chat/contact-tokens vygeneruje token (8 znaků, 7d)
POST /chat/contact-connect Body: { token }
PUT /chat/contact-requests/{id} Body: { action: accept|reject }
DB tabulky (minimální)¶
-- Konverzace a členství
conversations (
id UUID PRIMARY KEY,
type VARCHAR(10), -- 'dm' | 'group' | 'support'
s3_prefix VARCHAR(255), -- dm_jan.novotnymail.cz_petr.novakfirma.cz
name VARCHAR(255),
created_by UUID REFERENCES users(id),
encrypted BOOLEAN DEFAULT false,
is_active BOOLEAN DEFAULT true,
deleted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ
)
conversation_members (
conversation_id UUID REFERENCES conversations(id),
user_id UUID REFERENCES users(id),
role VARCHAR(10), -- 'admin' | 'member'
is_banned BOOLEAN DEFAULT false,
banned_at TIMESTAMPTZ,
banned_by UUID REFERENCES users(id),
ban_reason TEXT,
joined_at TIMESTAMPTZ,
last_read_date DATE, -- poslední přečtený den (pro unread badge)
last_read_msg VARCHAR(64), -- poslední přečtený msg_id
notification_pref VARCHAR(10) DEFAULT 'all',
PRIMARY KEY (conversation_id, user_id)
)
-- Doručení zpráv
pending_deliveries (
id UUID PRIMARY KEY,
conversation_id UUID REFERENCES conversations(id),
recipient_id UUID REFERENCES users(id),
msg_id VARCHAR(64),
s3_key TEXT,
created_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ
)
CREATE INDEX ON pending_deliveries (recipient_id) WHERE delivered_at IS NULL;
-- Single-sided operace (1:1)
message_visibility (
conversation_id UUID REFERENCES conversations(id),
user_id UUID REFERENCES users(id),
msg_id VARCHAR(64),
hidden_at TIMESTAMPTZ,
custom_content TEXT,
PRIMARY KEY (conversation_id, user_id, msg_id)
)
conversation_deletions (
conversation_id UUID REFERENCES conversations(id),
user_id UUID REFERENCES users(id),
deleted_at TIMESTAMPTZ,
PRIMARY KEY (conversation_id, user_id)
)
-- E2E šifrování
user_chat_keys (
user_id UUID REFERENCES users(id) PRIMARY KEY,
public_key TEXT,
key_version INT DEFAULT 1,
created_at TIMESTAMPTZ
)
conversation_keys (
conversation_id UUID REFERENCES conversations(id),
user_id UUID REFERENCES users(id),
encrypted_key TEXT,
key_version INT DEFAULT 1,
PRIMARY KEY (conversation_id, user_id)
)
-- Skupinové chat numbering
user_group_counter (
user_id UUID REFERENCES users(id) PRIMARY KEY,
counter INT DEFAULT 0
)
HW náročnost — odhad pro 100 000 uživatelů¶
Předpoklady:
Aktivní simultánně: 5 000 (5 %)
Zpráv za den: 500 000
Průměrná zpráva: 1 KB JSON
RAM (bez WS/Redis Pub/Sub):
Polling HTTP: ~0 MB persistent (spojení se uzavře po odpovědi)
Redis presence: 100 000 klíčů × 50 B = ~5 MB
pending_deliveries: ~10 000 záznamů průměrně × 200 B = ~2 MB
→ Redis celkem: ~10 MB
CPU (polling):
5 000 aktivních × poll každých 5s = 1 000 req/s → 1 vCPU zvládne
S3 (MinIO):
500 000 zpráv × 1 KB = 500 MB/den nových dat
Denní bundle: 500 000 PUT + 500 000 DELETE + 1 bundle GET/PUT = jednorázové
Přílohy: dle použití
Srovnání s WebSocket:
WebSocket 5 000 conn × ~100 KB RAM = 500 MB RAM extra
Redis Pub/Sub: dalších 50–100 MB
→ S3 polling ušetří ~600 MB RAM a eliminuje Redis Pub/Sub
Voice Chat — příprava (UI zatím šedivé)¶
Tlačítko hovoru je v UI viditelné, ale deaktivované (šedé + tooltip "Brzy dostupné"). Infrastruktura a formát zpráv jsou připraveny — aktivace bez změny schématu.
Technologie — WebRTC¶
1:1 hovor: WebRTC P2P — zvuk jde přímo mezi uživateli
server přenáší jen signalizaci (SDP, ICE) → minimální bandwidth
Skupinový hovor: Livekit SFU (Selective Forwarding Unit)
každý uživatel posílá 1 stream na SFU
SFU přeposílá ostatním — žádné N×(N-1) P2P spojení
Infrastruktura¶
coturn (STUN + TURN server):
Účel: STUN = sdělí uživateli jeho veřejnou IP (potřebné pro P2P)
TURN = relay zvuku při selhání P2P (symetrický NAT, ~15–20 % hovorů)
HW: ~10 MB RAM, 0 CPU v idle; bandwidth jen při TURN fallback
Deploy: Docker na existující vm-gateway nebo samostatné VM
Adresa: stun.avaxis.cz:3478 / turn.avaxis.cz:3478
Livekit SFU (skupinové hovory):
Účel: přeposílač audio/video streamů pro skupiny 3+ uživatelů
HW: ~50 MB RAM, ~1 vCPU pro desítky simultánních hovorů
Deploy: Docker na nové vm-voice (Proxmox)
Licence: open-source (Apache 2.0)
Průběh 1:1 hovoru (WebRTC P2P)¶
Volající (A):
1. POST /chat/calls Body: { conversation_id, type: "audio" }
→ server vytvoří call_id, vloží pending_delivery pro B (call_invite)
→ vrátí { call_id, ice_servers: [stun/turn URLs] }
2. WebRTC: vytvoří SDP offer
3. POST /chat/calls/{call_id}/signal Body: { type: "offer", sdp: "..." }
Příjemce (B):
4. Poll vrátí pending call_invite → UI zobrazí příchozí hovor 📞
5. Uživatel přijme → WebRTC: vytvoří SDP answer
6. POST /chat/calls/{call_id}/signal Body: { type: "answer", sdp: "..." }
7. Oba si vyměňují ICE candidates:
POST /chat/calls/{call_id}/signal Body: { type: "ice", candidate: "..." }
8. P2P spojení sestaveno → zvuk teče přímo A ↔ B
Ukončení:
9. POST /chat/calls/{call_id}/end
→ server zapíše call_invite zprávu na S3 se status: "ended" + duration
Signalizační API (připraveno, neaktivní)¶
POST /chat/calls zahájit hovor
Body: { conversation_id, type: "audio"|"video" }
Vrátí: { call_id, ice_servers }
GET /chat/calls/{call_id}/signals pending signály pro tohoto uživatele (polling)
Vrátí: [{ type: "offer"|"answer"|"ice", data, from_user }]
POST /chat/calls/{call_id}/signal odeslat signál (SDP / ICE candidate)
Body: { type: "offer"|"answer"|"ice", data }
POST /chat/calls/{call_id}/accept přijmout hovor
POST /chat/calls/{call_id}/decline odmítnout hovor
POST /chat/calls/{call_id}/end ukončit hovor
DB tabulky pro hovory¶
calls (
id VARCHAR(64) PRIMARY KEY, -- call_id
conversation_id UUID REFERENCES conversations(id),
initiated_by UUID REFERENCES users(id),
type VARCHAR(10), -- 'audio' | 'video'
status VARCHAR(10), -- 'ringing' | 'accepted' | 'declined' | 'missed' | 'ended' | 'failed'
started_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ,
duration_seconds INT,
created_at TIMESTAMPTZ
)
-- Signalizace (krátkodobá, TTL ~5 min)
call_signals (
id UUID PRIMARY KEY,
call_id VARCHAR(64) REFERENCES calls(id),
from_user UUID REFERENCES users(id),
to_user UUID REFERENCES users(id),
type VARCHAR(10), -- 'offer' | 'answer' | 'ice' | 'end'
data TEXT, -- SDP nebo ICE candidate JSON
created_at TIMESTAMPTZ,
consumed BOOLEAN DEFAULT false
)
CREATE INDEX ON call_signals (to_user, consumed) WHERE NOT consumed;
-- GC: smaže consumed záznamy starší 10 minut
UI — deaktivované tlačítko¶
Záhlaví konverzace:
[📞 Zavolat] ← šedivé, kurzor not-allowed
tooltip: "Hlasové hovory — připravujeme"
Záznam hovoru v chatu (call_invite zpráva):
┌────────────────────────────────────┐
│ 📞 Zmeškaný hovor │ ← status: missed, červeně
│ 📞 Hovor · 15:08 │ ← status: ended, duration
│ 📞 Hovor odmítnut │ ← status: declined, šedě
└────────────────────────────────────┘
Rozhodnuté otázky¶
| Otázka | Rozhodnutí |
|---|---|
| Transport | Adaptive HTTP polling (ne WebSocket) |
| Message store | S3 JSON soubory (individual → daily bundle přes GC) |
| S3 bucket | Dedikovaný avax-chat, oddělený od záloh |
| Directory naming | dm_login1_login2 / gr_login_NNN (email bez @) |
| Soubory přístup | Privátní, presigned URL (TTL 1h pro čtení, 7d pro sdílení) |
| Sdílení souboru | POST generuje anonymní presigned GET URL (7 dní) |
| Bundlování zpráv | Individual soubory během dne → GC bundle do {datum}.json každou noc |
| Polling interval | 2s (aktivní chat) / 5s (launcher aktivní) / 60s (tray) |
| Doručení offline | pending_deliveries DB tabulka, doručení při přihlášení |
| Presence | Redis per-device TTL 90s, heartbeat každých 60s |
| Šifrování | Volitelné E2E (AES-256-GCM + X25519), obě strany musí mít zapnuto |
| Šifrování — UI | 🔒 badge, vyhledávání zatmaveno s tooltipem |
| Skupinový šifrovaný chat | Symetrický klíč skupiny, šifrovaný individuálně pro každého člena |
| Smazání 1:1 zprávy | Single-sided (message_visibility), druhý vidí originál |
| Smazání skupinové zprávy | Přepis JSON na S3 (deleted: true), vidí všichni |
| Nick | Globální, volitelný, zobrazuje se vedle jména |
| Web klient | Zatím ne |
| GC | Přílohy zachovány po dobu aktivní konverzace, smaží se jen při zrušení |
| Voice chat | Připraveno (WebRTC P2P pro 1:1, Livekit SFU pro skupiny), UI šedivé |
| Voice infrastruktura | coturn (STUN+TURN) + Livekit SFU na Proxmox VM |
| Voice signalizace | HTTP polling stejný mechanismus jako zprávy (call_signals tabulka) |
| call_invite v JSON | Typ zprávy připraven, zaznamenává status + duration hovoru |
Poslední aktualizace: 2026-05-03