Tích hợp máy khách AeroNyx Chat Relay

AeroNyx29 phút đọc

Tài liệu tham chiếu tích hợp cho AeroNyx Chat Relay: xác thực WebSocket, tin nhắn 1:1 và tin nhắn nhóm được niêm phong, truyền tải ngoại tuyến, xác nhận, biểu cảm, trạng thái trực tuyến, tệp đính kèm được mã hóa, thông báo đẩy và giới hạn tốc độ.

Tạo yêu cầu tích hợp cho AI

Chọn công nghệ và tác vụ rồi sao chép cho trợ lý lập trình. Nội dung được tạo cục bộ, không gửi đến dịch vụ AI.

Yêu cầu đã tạo

Đây là tài liệu tham chiếu tích hợp cho AeroNyx Chat Relay: dịch vụ WebSocket và HTTPS tại api.aeronyx.network, chịu trách nhiệm truyền tải tin nhắn 1:1 được mã hóa đầu cuối, tin nhắn nhóm, xác nhận, biểu cảm, trạng thái trực tuyến và tệp đính kèm được mã hóa giữa các danh tính AeroNyx.

Tài liệu này dành cho các kỹ sư xây dựng máy khách, bot và dịch vụ tương thích với AeroNyx, cũng như cho các tác tử lập trình AI triển khai chúng. Mọi khung, trường và giới hạn trên trang này phản ánh relay đang vận hành chính thức và AeroNyx App tính đến tháng 10 năm 2026.

Chat Relay là đường truyền tải tập trung. Nó cùng tồn tại với đường truyền qua nút phi tập trung (định tuyến onion và hộp thư ẩn danh, xem Truyền tải hai chặng có xác minh); một máy khách có thể sử dụng cả hai. Để tích hợp HTTPS theo mô hình yêu cầu/phản hồi mà không dùng WebSocket, xem Central Chat HTTPS API v1.

Mô hình tin cậy

Relay không nhìn thấy nội dung. Máy khách mã hóa và ký mọi dữ liệu trước khi dữ liệu đến relay, còn relay chỉ định tuyến, xếp vào hàng đợi và áp dụng giới hạn tốc độ đối với bản mã không thể đọc được.

Relay không bao giờ nhận được:

  • văn bản tin nhắn, emoji biểu cảm, nội dung chỉnh sửa hoặc payload nhóm ở dạng bản rõ
  • khóa trò chuyện, khóa nhóm, khóa tệp đính kèm hoặc nonce
  • nội dung tệp đính kèm, tên tệp, ảnh thu nhỏ, dạng sóng âm thanh hoặc bản chép lời
  • khóa danh tính riêng tư

Tuy nhiên, relay có quan sát siêu dữ liệu truyền tải, và bên tích hợp nên coi đây là những thông tin nhà vận hành có thể nhìn thấy:

  • khóa công khai của người gửi và người nhận, ID nhóm và ID tin nhắn
  • dấu thời gian, kích thước payload cùng trạng thái truyền tải, xác nhận và trạng thái đã đọc
  • trạng thái trực tuyến, trạng thái tiền cảnh và chỉ báo đang nhập (được gửi dưới dạng khung bản rõ)
  • siêu dữ liệu báo hiệu cuộc gọi (tên phòng, ID cuộc gọi, cờ video)
  • kích thước bản mã của tệp đính kèm, loại phương tiện được khai báo và thời hạn hết hiệu lực
  • cờ contact_request và siêu dữ liệu kết nối ở cấp IP

Tính bảo mật của nội dung đến từ mã hóa đầu cuối, không phải từ cơ chế kiểm soát truy cập trên relay. Hãy thiết kế theo nguyên tắc đó: kẻ tấn công lấy được bản mã đã lưu trữ vẫn phải không thể đọc được nó.

Danh tính và khóa

Một danh tính trò chuyện AeroNyx là một cặp khóa Ed25519. Khóa công khai 32 byte, được viết dưới dạng 64 ký tự thập lục phân chữ thường, chính là địa chỉ. Hãy tạo khóa trên thiết bị và không bao giờ gửi khóa riêng tư đi bất cứ đâu.

Hai khóa được dẫn xuất từ một cặp khóa danh tính:

  • Khóa trò chuyện (1:1). HKDF-SHA256(salt = empty, ikm = X25519(my_secret, peer_public), info = "AERONYX-P2P-KEY", length = 32), trong đó cả hai khóa Ed25519 được chuyển đổi sang X25519 (SHA-512(seed)[0..32] được clamp đối với khóa bí mật, chuyển đổi Edwards sang Montgomery đối với khóa công khai). Cả hai bên đều dẫn xuất ra cùng một khóa.
  • Chữ ký tin nhắn. Ed25519 bằng khóa danh tính, ký trên chính xác các chuỗi byte được định nghĩa trong trang này.

Luôn dùng hex chữ thường cho khóa công khai trong các khung. Relay không chuẩn hóa chữ hoa/chữ thường ở mọi khóa hàng đợi.

Xác thực

Chữ ký RelayAuth

Đăng nhập WebSocket và mọi endpoint HTTPS yêu cầu xác thực đều dùng cùng một chữ ký:

text
digest    = SHA256("AeroNyx-RelayAuth-v1" || pubkey[32] || timestamp as u64 little-endian)
signature = Ed25519(identity_key, digest)

"AeroNyx-RelayAuth-v1" là 20 byte ASCII, không có ký tự kết thúc. timestamp tính bằng giây Unix và phải nằm trong phạm vi 300 giây so với thời gian máy chủ.

Chữ ký chỉ ràng buộc danh tính và thời gian. Nó không ràng buộc phương thức, đường dẫn, phần thân hay kết nối, và không có nonce. Hãy tạo dấu thời gian mới cho mỗi yêu cầu, chỉ gửi qua TLS và không bao giờ ghi nó vào nhật ký.

Header HTTPS

http
Authorization: Relay <pubkey-hex>:<timestamp>:<signature-hex>

Khi kiểm tra thất bại, máy chủ trả về HTTP 401 kèm {"success": false, "error": "<reason>"}. Các lý do bao gồm missing_auth_header, malformed_auth_header, invalid_timestamp, timestamp_expired, invalid_pubkey và invalid_signature.

Kết nối WebSocket

Endpoint

text
wss://api.aeronyx.network/ws/relay/

Máy khách native kết nối mà không có header Origin. Trình duyệt phải kết nối từ một origin được cho phép; mọi origin khác, hoặc Host không xác định, sẽ bị đóng với mã 1008.

Đăng nhập

Máy chủ chấp nhận socket, sau đó chờ một khung auth trong vòng 30 giây:

json
{
  "type": "auth",
  "pubkey": "<64 hex>",
  "timestamp": 1780000000,
  "signature": "<128 hex>"
}

Thành công:

json
{ "type": "auth_ack", "session_id": "<uuid>", "server_ts": 1780000001 }

Dùng server_ts để ước tính độ lệch đồng hồ; dấu thời gian của tin nhắn được kiểm tra theo cùng khoảng ±300 giây.

Sau auth_ack, máy chủ bắt đầu gửi heartbeat, đăng ký kết nối vào các kênh tương ứng và phát lại ngay hàng đợi ngoại tuyến (xem Truyền tải ngoại tuyến).

Khi thất bại, máy chủ gửi {"type": "auth_error", "reason": "<reason>"} và đóng socket với mã 4001. Các lý do: missing_fields, timestamp_expired, invalid_pubkey, invalid_signature_length, invalid_signature_encoding, invalid_signature, internal_error. Khi hết thời gian đăng nhập, máy chủ gửi lý do timeout và đóng với mã 4002.

Mọi khung khác được gửi trước khi đăng nhập đều được trả lời bằng auth_error với lý do authentication_required; socket vẫn được giữ mở.

Heartbeat và trạng thái tiền cảnh

ChiềuKhungHành vi
máy chủ → máy khách{"type":"ping"} mỗi 30 giâyTrả lời bằng {"type":"pong"}.
máy khách → máy chủ{"type":"ping"}Máy chủ trả lời {"type":"pong"}.
máy khách → máy chủ{"type":"presence_state","foreground":true}Đánh dấu kết nối này đang hoạt động.
máy khách → máy chủ{"type":"presence_state","foreground":false}Đánh dấu kết nối đang chạy nền, để relay có thể gửi thông báo đẩy cho tin nhắn mới.

Relay chỉ coi một danh tính là đang trực tuyến khi kết nối của nó gửi ping, pong hoặc presence_state với foreground: true ít nhất mỗi 90 giây. AeroNyx App gửi ping mỗi 15 giây khi ở tiền cảnh và coi kết nối là đã chết nếu bỏ lỡ quá ba pong.

Các kết nối duy trì lâu nên kết nối lại ít nhất một lần mỗi 24 giờ. Tin nhắn không bao giờ bị mất khi một kết nối âm thầm ngừng nhận khung trực tiếp, vì mọi tin nhắn đều đồng thời được đưa vào hàng đợi và phát lại khi đăng nhập, nhưng việc truyền tải trực tiếp chỉ tiếp tục sau khi kết nối lại.

Quy tắc về khung

  • Chỉ dùng khung văn bản, mỗi khung một đối tượng JSON. Khung nhị phân bị bỏ qua.
  • Kích thước khung tối đa là 1.048.576 ký tự. Khung lớn hơn bị từ chối với {"type":"error","reason":"message_too_large"}. Giữ payload_b64 dưới khoảng 800 KiB để chừa chỗ cho phần còn lại của khung.
  • Khung sai định dạng trả về error với lý do invalid_json, invalid_json_type hoặc unknown_type.

Khung lỗi chung:

json
{ "type": "error", "reason": "<reason>", "retry_after": 0 }

Lỗi giới hạn tốc độ còn kèm theo scope.

Mã đóng kết nối

MãÝ nghĩa
1008Host hoặc Origin không được cho phép.
4000Máy chủ không thể gửi heartbeat.
4001Đăng nhập thất bại.
4002Hết thời gian đăng nhập.

Kết nối lại bằng exponential backoff kèm jitter. AeroNyx App chờ 2^(attempt-1) giây, giới hạn trong khoảng 1–60 giây, rồi nhân với một hệ số ngẫu nhiên từ 0,8 đến 1,2.

Gửi tin nhắn 1:1

1. Tạo phong bì niêm phong

Payload của tin nhắn 1:1 là một ChatEnvelope đã được ký và mã hóa. Cấu trúc chính xác, bản triển khai tham chiếu bằng Python và vector kiểm thử chuẩn có tại Central Chat HTTPS API v1: Định dạng phong bì niêm phong. Cùng một phong bì được dùng cho cả hai API.

Tóm lại: XChaCha20-Poly1305 với khóa trò chuyện, chữ ký Ed25519 trên một bản ghi 121 byte và bố cục nhị phân cố định. content_type là 0 cho mọi tin nhắn, kể cả tin nhắn có tệp đính kèm.

Bản rõ là văn bản UTF-8 đối với tin nhắn thông thường, hoặc một đối tượng JSON đối với tin nhắn có tệp đính kèm, trả lời, chuyển tiếp hoặc bản xem trước liên kết:

json
{
  "type": "aeronyx_message",
  "text": "See the attached file",
  "attachments": [ { "blob_id": "...", "file_key": "...", "media_type": "image/jpeg", "file_name": "photo.jpg", "file_size": 482113 } ],
  "reply": { "msg_id": "...", "sender_pubkey": "...", "text": "..." },
  "forwarded": true,
  "forwarded_from_name": "...",
  "forwarded_from_pubkey": "...",
  "link_preview": { "url": "https://...", "title": "...", "description": "...", "site_name": "..." }
}

Mọi trường ngoài type và text đều là tùy chọn. Bên nhận nên hiển thị mọi bản rõ không phải là đối tượng JSON có "type": "aeronyx_message" dưới dạng văn bản thuần. Đối tượng tệp đính kèm được định nghĩa trong phần Tệp đính kèm được mã hóa.

2. Ký khung

Mỗi khung tin nhắn mang một chữ ký thứ hai, payload_sig, mà relay xác minh trước khi chấp nhận khung:

text
payload_sig = Ed25519(identity_key,
    SHA256(receiver_pubkey[32] || sender_pubkey[32] || discriminant as 1 byte
           || timestamp as u64 little-endian || envelope_bytes))

envelope_bytes là dữ liệu payload_b64 sau khi giải mã Base64. Dùng discriminant 11 cho tin nhắn và bản chỉnh sửa, 12 cho biểu cảm. payload_sig được mã hóa dạng hex.

3. Gửi

json
{
  "type": "relay_send",
  "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
  "receiver_pubkey": "<64 hex>",
  "discriminant": 11,
  "payload_b64": "<base64 envelope>",
  "timestamp": 1780000000,
  "payload_sig": "<128 hex>"
}
TrườngQuy tắc
msg_id32 ký tự hex chữ thường: message_id 16 byte của phong bì. App bên nhận lưu tin nhắn theo ID này, vì vậy giá trị phải khớp với phong bì.
receiver_pubkeyKhóa công khai của người nhận.
discriminant11.
timestampGiây Unix, trong phạm vi ±300 giây so với thời gian máy chủ.
suppress_pushTùy chọn. true sẽ không gửi thông báo đẩy.
contact_requestTùy chọn. Đánh dấu tin nhắn đầu tiên gửi đến người dùng yêu cầu xác minh liên hệ (xem Xác minh liên hệ).

4. Báo nhận

json
{ "type": "relay_delivered", "msg_id": "...", "delivered": true, "queued": true, "durable": true }
  • durable (và queued) là true khi tin nhắn đã được lưu vào hàng đợi ngoại tuyến của người nhận. Hãy coi durable: true là "đã gửi".
  • delivered nghĩa là người nhận đang có kết nối tiền cảnh hoạt động. Đây không phải là bằng chứng đã nhận; hãy dùng xác nhận đã nhận cho mục đích đó.

Chờ báo nhận tối đa 15 giây trước khi coi lần gửi là thất bại.

Từ chối và lỗi

json
{ "type": "send_rejected", "msg_id": "...", "reason": "verification_required" }
Phản hồiLý doHành động
send_rejectedverification_requiredNgười nhận chỉ chấp nhận tin nhắn từ liên hệ. Không thử lại.
send_rejectedcontact_request_rate_limitedĐã đạt giới hạn yêu cầu liên hệ đối với người nhận này. Không thử lại.
errormissing_fieldsThiếu một trường bắt buộc hoặc trường đó rỗng.
errorinvalid_timestamp, timestamp_expiredHiệu chỉnh đồng hồ và tạo lại dấu thời gian.
errorinvalid_payload_sigpayload_sig không xác minh được.
errorrate_limitedThử lại sau retry_after giây.

Thử lại

Thử lại tin nhắn chưa được báo nhận với cùng msg_id và cùng các byte phong bì. Vì relay từ chối các khung cũ hơn 300 giây, hãy tính timestamp của khung và payload_sig mới cho mỗi lần thử lại; phong bì giữ nguyên dấu thời gian ban đầu. Relay và bên nhận loại bỏ trùng lặp theo msg_id.

Phương án dự phòng HTTPS

Khi WebSocket không khả dụng, có thể gửi cùng tin nhắn đó qua HTTPS:

http
POST /api/relay/push/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
json
{
  "receiver_pubkey": "<64 hex>",
  "discriminant": 11,
  "payload_b64": "<base64 envelope>",
  "timestamp": 1780000000,
  "payload_sig": "<128 hex>"
}

Khi thành công, máy chủ trả về HTTP 200 với {"success": true}. Lỗi xác minh liên hệ trả về 400 với verification_required hoặc contact_request_rate_limited, còn giới hạn tốc độ trả về 429 kèm Retry-After. Tin nhắn gửi theo cách này được đưa vào hàng đợi cho người nhận nhưng không kích hoạt thông báo đẩy, vì vậy hãy gửi lại qua WebSocket khi kết nối được khôi phục.

Nhận tin nhắn

Tin nhắn đến được chuyển tới dưới dạng relay_envelope:

json
{
  "type": "relay_envelope",
  "sender_pubkey": "<64 hex>",
  "discriminant": 11,
  "payload_b64": "<base64 envelope>",
  "timestamp": 1780000000,
  "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
  "from_offline": false
}

Với discriminant: 11:

  1. Loại bỏ trùng lặp theo msg_id. Cùng một tin nhắn có thể đến trực tiếp và đến thêm lần nữa từ hàng đợi ngoại tuyến.
  2. Phân tích phong bì và kiểm tra rằng receiver_pubkey trong đó là danh tính của bạn.
  3. Với khung trực tiếp (from_offline: false), loại bỏ tin nhắn nếu dấu thời gian của phong bì lệch quá 300 giây so với đồng hồ của bạn.
  4. Xác minh chữ ký phong bì bằng sender_pubkey của phong bì. Chính khóa đó, chứ không phải sender_pubkey của khung, mới là người gửi đã được xác thực.
  5. Giải mã bằng khóa trò chuyện dẫn xuất từ người gửi đó. Các phiên bản App rất cũ mã hóa bằng đầu ra X25519 thô; hãy thử khóa này nếu khóa HKDF thất bại.
  6. Lưu tin nhắn một cách bền vững, sau đó gửi xác nhận đã nhận và, đối với from_offline: true, gửi thêm báo nhận ngoại tuyến.

Phong bì 1:1 được chuyển đến mà không có payload_sig; chữ ký phong bì chính là cơ chế kiểm tra tính xác thực. Khung cũng có thể mang contact_request: true.

Nếu tin nhắn không gửi cho bạn, xác minh thất bại hoặc giải mã thất bại, hãy loại bỏ tin nhắn mà không hiển thị gì.

Truyền tải ngoại tuyến

Mọi tin nhắn, bản chỉnh sửa, lệnh thu hồi, biểu cảm, xác nhận đã nhận và xác nhận đã đọc đều được ghi vào hàng đợi ngoại tuyến của người nhận trước khi truyền tải trực tiếp. Hàng đợi được phát lại tự động sau mỗi lần đăng nhập và khi có yêu cầu:

json
{ "type": "relay_pull" }

Relay phát lại tất cả các mục trong hàng đợi dưới dạng các loại khung thông thường của chúng với from_offline: true, sắp xếp theo dấu thời gian, sau đó là:

json
{ "type": "relay_pull_done", "count": 12, "has_more": false }

Việc truyền tải tuân theo cơ chế ít nhất một lần. Các mục ở lại trong hàng đợi cho đến khi được báo nhận:

json
{ "type": "relay_offline_ack", "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834" }

Báo nhận biểu cảm bằng "reaction_id" thay vì "msg_id". Chỉ báo nhận sau khi mục đã được lưu bền vững trên thiết bị. Relay trả lời bằng {"type":"relay_offline_ack","msg_id":"...","success":true}.

Giới hạn hàng đợi:

Giới hạnGiá trị
Số mục cho mỗi người nhận1.000. Khi đầy, các mục mới bị từ chối và người gửi nhận được durable: false.
Thời gian lưu giữ72 giờ kể từ khi mục gần nhất được thêm vào.
Kích thước payload1 MiB sau khi giải mã Base64, trong giới hạn khung 1 MiB.

Relay là bộ đệm truyền tải, không phải lịch sử tin nhắn. Hãy lưu lịch sử trên thiết bị.

Xác nhận đã nhận và xác nhận đã đọc

Xác nhận đã nhận

Gửi sau khi lưu tin nhắn:

json
{ "type": "message_receipt", "receiver_pubkey": "<original sender>", "msg_id": "...", "timestamp": 1780000010 }

Relay trả lời message_receipt_ack và chuyển {"type":"message_receipt","sender_pubkey":"...","msg_id":"...","timestamp":...} đến người gửi ban đầu.

Xác nhận đã đọc

json
{ "type": "message_read", "receiver_pubkey": "<original sender>", "msg_id": "<latest read message>", "timestamp": 1780000200, "enabled": true }

Xác nhận đã đọc hoạt động như một mốc đánh dấu: App đánh dấu tin nhắn được chỉ định cùng mọi tin nhắn gửi đi trước đó trong cuộc trò chuyện là đã đọc. Chỉ gửi khi người dùng đã bật xác nhận đã đọc.

Relay áp dụng quy tắc có đi có lại khi gửi, khi truyền tải trực tiếp và khi phát lại. Xác nhận đã đọc chỉ được chuyển đến nếu hai người dùng là liên hệ của nhau và cả hai đều bật read_receipts_enabled. Nếu không, người gửi nhận được:

json
{ "type": "message_read_ack", "msg_id": "...", "delivered": false, "suppressed": true, "reason": "receiver_read_receipts_disabled" }
Lý doÝ nghĩa
client_disabledKhung mang enabled: false hoặc read_receipts_enabled: false.
not_mutual_contactHai người dùng không phải là liên hệ của nhau.
reader_read_receipts_disabledNgười đọc đã tắt xác nhận đã đọc.
receiver_read_receipts_disabledNgười gửi ban đầu đã tắt xác nhận đã đọc.
invalid_pubkeyKhóa công khai sai định dạng.

Xác nhận đã đọc được chuyển thành công sẽ được báo nhận bằng {"type":"message_read_ack","msg_id":"...","delivered":true}.

Chỉnh sửa và thu hồi

Chỉnh sửa

Bản chỉnh sửa là một phong bì niêm phong mới (với message_id ngẫu nhiên riêng) chứa toàn bộ nội dung thay thế, được gửi gắn với ID của tin nhắn gốc:

json
{
  "type": "message_edit",
  "receiver_pubkey": "<64 hex>",
  "msg_id": "<original msg_id>",
  "target_msg_id": "<original msg_id>",
  "payload_b64": "<base64 envelope>",
  "payload_sig": "<128 hex>",
  "timestamp": 1780000400
}

payload_sig dùng công thức trong mục Ký khung với discriminant 11. Bản chỉnh sửa xóa toàn bộ tệp đính kèm sẽ đặt "attachments_edited": true trong JSON bản rõ. Relay trả lời message_edit_ack. Bên nhận xác minh và giải mã phong bì như với một tin nhắn, và chỉ được áp dụng bản chỉnh sửa nếu người gửi chính là tác giả của tin nhắn gốc.

Thu hồi

json
{
  "type": "message_revoke",
  "receiver_pubkey": "<64 hex>",
  "sender_pubkey": "<64 hex>",
  "msg_id": "<original msg_id>",
  "timestamp": 1780000500,
  "payload_sig": "<128 hex>"
}

Chữ ký thu hồi được tính trên byte thô, không băm:

text
payload_sig = Ed25519(identity_key,
    "aeronyx-message-revoke-v1" || sender_pubkey[32] || receiver_pubkey[32]
    || UTF-8(msg_id) || timestamp as u64 little-endian)

Relay trả lời message_revoke_ack. Relay không kiểm tra quyền tác giả: bên nhận phải xác minh chữ ký và chỉ áp dụng lệnh thu hồi nếu người gửi chính là tác giả của tin nhắn gốc. Để xóa tệp đính kèm của tin nhắn đã thu hồi, hãy gọi POST /api/relay/blob/{blob_id}/delete/.

Biểu cảm

Biểu cảm 1:1 là một phong bì niêm phong có message_id là ID biểu cảm, với content_type 2 và bản rõ {"emoji": "❤️", "op": "add"} (hoặc "remove"):

json
{
  "type": "message_reaction",
  "receiver_pubkey": "<64 hex>",
  "msg_id": "<message being reacted to>",
  "reaction_id": "<32 hex>",
  "payload_b64": "<base64 envelope>",
  "payload_sig": "<128 hex>",
  "timestamp": 1780000300
}
  • payload_sig dùng discriminant 12.
  • reaction_id là khóa đảm bảo tính lũy đẳng. reaction_id lặp lại trong vòng 72 giờ được báo nhận với "duplicate": true và không được chuyển lại.
  • Relay trả lời {"type":"message_reaction_ack","msg_id":"...","reaction_id":"...","delivered":...,"queued":...}.
  • Biểu cảm bị giới hạn 20 lần cho mỗi người gửi và cuộc trò chuyện trong khung thời gian giới hạn tốc độ.

Biểu cảm nhóm được mô tả trong phần Nhóm.

Trạng thái trực tuyến và chỉ báo đang nhập

Khung trạng thái trực tuyến và chỉ báo đang nhập là siêu dữ liệu bản rõ và relay có thể nhìn thấy.

Trạng thái trực tuyến

Đăng ký theo dõi các liên hệ:

json
{ "type": "presence_subscribe", "pubkeys": ["<64 hex>", "<64 hex>"] }

Mỗi khung được xử lý tối đa 200 khóa; khóa sai định dạng và khóa trùng lặp bị bỏ qua. Các đăng ký được tích lũy trong suốt vòng đời của kết nối. Chỉ đăng ký theo dõi các liên hệ của chính bạn.

json
{
  "type": "presence_subscribe_ack",
  "count": 2,
  "updates": [
    { "pubkey": "<a>", "visible": true, "presence_visible": true, "last_seen_visible": true, "online": false, "last_seen_ts": 1780000000, "reason": "allowed" },
    { "pubkey": "<b>", "visible": false, "presence_visible": false, "last_seen_visible": false, "reason": "not_mutual_contact" }
  ],
  "server_ts": 1780000001
}

Trạng thái trực tuyến chỉ hiển thị giữa những người dùng là liên hệ của nhau, và chỉ khi đối tượng đã bật presence_enabled. Các mục bị ẩn (reason là not_mutual_contact hoặc presence_hidden) không chứa online hay last_seen_ts. last_seen_ts chỉ có mặt khi last_seen_visible là true; nếu không, hãy hiển thị một trạng thái chung như "truy cập gần đây".

Các thay đổi trực tiếp được gửi đến dưới dạng:

json
{ "type": "presence_update", "pubkey": "<a>", "online": true, "presence_visible": true, "last_seen_visible": true, "last_seen_ts": 1780000100 }

Chỉ báo đang nhập

json
{ "type": "typing", "receiver_pubkey": "<64 hex>", "is_typing": true }
{ "type": "group_typing", "group_id": "<uuid>", "is_typing": true }

Chỉ báo đang nhập chỉ được chuyển tiếp khi trạng thái trực tuyến của người gửi hiển thị được với người nhận (là liên hệ của nhau và có presence_enabled), hoặc tới các thành viên khác của nhóm mà người gửi tham gia. Chỉ báo này không bao giờ được lưu trữ, đưa vào hàng đợi hay gửi qua thông báo đẩy.

Hồ sơ và cài đặt quyền riêng tư

http
GET /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
json
{
  "success": true,
  "profile": {
    "pubkey_hex": "<64 hex>",
    "display_name": "AeroNyx User",
    "bio": "",
    "handle": "",
    "avatar_url": "",
    "status_text": "",
    "privacy": { "presence_enabled": true, "last_seen_enabled": true, "read_receipts_enabled": true },
    "updated_at": "2026-10-09T10:00:00Z"
  }
}

Danh tính chưa có hồ sơ sẽ nhận các trường rỗng và cả ba cờ quyền riêng tư đều là true.

http
PATCH /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
json
{ "display_name": "Alice", "privacy": { "presence_enabled": true, "last_seen_enabled": false, "read_receipts_enabled": false } }
TrườngQuy tắc
display_nameTối đa 50 ký tự.
bioTối đa 200 ký tự.
avatar_urlURL https:// hoặc để trống.
handlea-z và 0-9, 5–24 ký tự. Handle tuân theo quy tắc hội viên và thời gian chờ giữa các lần thay đổi.
privacy.*Giá trị boolean. Ba cờ này cũng có thể được gửi ở cấp cao nhất.

Các lỗi bao gồm no_valid_fields, <flag>_invalid_boolean, handle_taken và handle_change_cooldown:<date>.

Giữ cho máy khách nhất quán với các cài đặt này: không gửi xác nhận đã đọc khi tính năng này đang tắt, và không hiển thị trạng thái đã đọc của đối phương khi xác nhận đã đọc của chính bạn đang tắt.

Xác minh liên hệ

Người dùng có thể yêu cầu người lạ xác minh trước khi nhắn tin. Khi người nhận đã bật tính năng này và chưa thêm người gửi làm liên hệ, relay_send bị từ chối với verification_required.

Để bắt đầu cuộc trò chuyện, hãy gửi một tin nhắn với "contact_request": true. Yêu cầu liên hệ được miễn kiểm tra này và bị giới hạn 3 lần cho mỗi cặp người gửi và người nhận trong khung thời gian trượt 24 giờ; các yêu cầu tiếp theo bị từ chối với contact_request_rate_limited. Cờ contact_request được chuyển tới người nhận để App có thể hiển thị tin nhắn dưới dạng một yêu cầu.

Xác minh liên hệ áp dụng cho relay_send và phương án dự phòng HTTPS.

Nhóm

Tin nhắn nhóm

Nội dung nhóm được mã hóa bằng khóa nhóm dùng chung 32 byte với AES-256-GCM:

text
payload_b64 = base64(nonce[12] || AES-256-GCM(group_key, plaintext_json) || tag[16])

JSON bản rõ chứa text, type (text, media, system hoặc reaction), sender_pubkey, created_at, và có thể kèm attachments, mentions, reply, forwarded, forwarded_from_name, forwarded_from_pubkey và link_preview.

json
{
  "type": "group_send",
  "msg_id": "<32 hex>",
  "group_id": "<uuid>",
  "payload_b64": "<base64>",
  "timestamp": 1780000000,
  "payload_sig": "<128 hex>",
  "key_version": 3
}
text
payload_sig = Ed25519(identity_key,
    SHA256(UTF-8(group_id) || sender_pubkey[32] || 0x19 || timestamp as u64 little-endian
           || decoded payload))

Relay kiểm tra chữ ký và xác nhận người gửi là thành viên đang hoạt động, lưu tin nhắn cho mọi thành viên đang hoạt động khác, rồi truyền tải trực tiếp. Payload nhóm không dùng phong bì 1:1, và phong bì nhóm được chuyển đến kèm group_id, key_version và payload_sig để bên nhận có thể xác minh người gửi trước khi giải mã.

json
{
  "type": "group_delivered",
  "msg_id": "...",
  "group_id": "...",
  "accepted": true,
  "accepted_count": 5,
  "delivered_count": 2,
  "queued_count": 5,
  "failed_count": 0,
  "member_count": 6
}

Người gửi không phải là thành viên sẽ nhận error với lý do not_a_member.

Chỉnh sửa, thu hồi và biểu cảm trong nhóm

KhungChữ ký
group_message_edit (group_id, msg_id, target_msg_id, payload_b64, payload_sig, key_version, timestamp)Công thức nhóm ở trên.
group_message_reaction (group_id, msg_id, reaction_id, payload_b64, payload_sig, key_version, timestamp)Công thức nhóm ở trên. Payload là payload nhóm với type: "reaction".
group_message_revoke (group_id, sender_pubkey, msg_id, timestamp, payload_sig)Ed25519 thô trên `"aeronyx-group-message-revoke-v1"

Mỗi khung được báo nhận bằng khung _ack tương ứng, kèm theo các số đếm truyền tải. Bên nhận chỉ áp dụng chỉnh sửa và thu hồi từ tác giả gốc.

Khóa nhóm

Khóa nhóm được chủ nhóm phân phối dưới dạng gói khóa riêng cho từng thành viên: base64(nonce[12] || AES-256-GCM(chat_key(owner, member), group_key) || tag[16]). Các endpoint REST dưới /api/relay/groups/ dùng để tạo nhóm, quản lý thành viên và lời mời, tải lên gói khóa, xoay vòng khóa (keys/rotate/) và lấy gói khóa hiện tại của bên gọi (keys/me/). Bên gửi mã hóa bằng phiên bản khóa mới nhất mà họ có; bên nhận thiếu một phiên bản khóa nên lấy keys/me/ và giữ tin nhắn lại cho đến khi có khóa.

Tệp đính kèm được mã hóa

Tệp đính kèm được mã hóa trên thiết bị, tải lên dưới dạng bản mã không thể đọc được và được tham chiếu từ bên trong tin nhắn được mã hóa.

Mã hóa tệp

Với mỗi tệp, tạo một khóa ngẫu nhiên 32 byte và một nonce ngẫu nhiên 12 byte:

text
blob = nonce[12] || AES-256-GCM(file_key, file_bytes) || tag[16]

Tải lên blob. Đặt file_key và mọi trường mô tả bên trong tin nhắn được mã hóa, không bao giờ đặt chúng trong yêu cầu tải lên.

Đối tượng tệp đính kèm

KhóaBắt buộcÝ nghĩa
blob_idcóID do bước tải lên trả về.
file_keycóBase64 của khóa tệp 32 byte.
media_typecóKiểu MIME của tệp bản rõ.
file_namecóTên hiển thị.
file_sizecóKích thước bản rõ tính bằng byte.
thumb_b64khôngẢnh thu nhỏ JPEG dạng Base64, tối đa 64 KiB.
duration_mskhôngThời lượng âm thanh hoặc video.
waveformkhôngTối đa 96 số trong khoảng [0, 1] cho tin nhắn thoại.
sticker, sticker_pack, sticker_posekhôngĐịnh danh nhãn dán.
livekhôngPhần chuyển động của Live Photo: {blob_id, file_key, media_type, file_size, duration_ms?, width?, height?}.

Bên nhận bỏ qua các tệp đính kèm thiếu blob_id hoặc file_key. Tin nhắn thoại từ App dùng định dạng AAC-LC trong vùng chứa MP4 (audio/mp4).

Tải lên: một yêu cầu (tối đa 10 MiB)

http
POST /api/relay/blob/presign/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
json
{ "file_size": 482141, "media_type": "image/jpeg", "media_kind": "image", "ttl_days": 7 }

file_size là kích thước bản mã. media_kind là một trong các giá trị voice, image, video, file, avatar, other. ttl_days được giới hạn trong khoảng 1–30 (mặc định là 7).

json
{
  "blob_id": "<uuid>",
  "upload_url": "https://...",
  "upload_method": "PUT",
  "storage": "r2",
  "expires_at": "2026-10-16T10:00:00Z",
  "media_type": "image/jpeg",
  "media_kind": "image",
  "access_mode": "capability",
  "ttl_days": 7,
  "max_bytes": 10485760
}

Sau đó:

  1. Thực hiện PUT bản mã lên upload_url trong vòng 15 phút, chỉ kèm header Content-Type. Không gửi header Authorization tới kho lưu trữ.
  2. Gọi POST /api/relay/blob/{blob_id}/complete/ với RelayAuth. Relay xác nhận đối tượng và trả về {blob_id, file_size, expires_at, storage}.

Tải lên: multipart (tối đa 100 MiB)

http
POST /api/relay/blob/multipart/create/
json
{ "total_size": 52428800, "media_type": "video/mp4", "media_kind": "video", "ttl_days": 7 }
json
{
  "blob_id": "<uuid>",
  "upload_id": "...",
  "part_size": 8388608,
  "total_parts": 7,
  "part_urls": ["https://...", "..."],
  "storage": "r2",
  "expires_at": "...",
  "max_bytes": 104857600
}

part_size mặc định là 8 MiB và có thể yêu cầu trong khoảng từ 5 đến 16 MiB. URL của từng phần có hiệu lực trong 60 phút. Thực hiện PUT từng phần lên URL tương ứng và ghi lại header phản hồi ETag, sau đó:

http
POST /api/relay/blob/multipart/complete/
json
{ "blob_id": "<uuid>", "upload_id": "...", "parts": [ { "part_number": 1, "etag": "\"...\"" } ] }

AeroNyx App dùng cách tải lên một yêu cầu cho bản mã có kích thước tới 8 MiB và dùng multipart cho kích thước lớn hơn.

Tải xuống

http
GET /api/relay/blob/{blob_id}/

Relay trả về 302 với Location trỏ đến mạng phân phối nội dung (CDN) cùng các header X-AeroNyx-Blob-Size, X-AeroNyx-Blob-Media-Type và X-AeroNyx-Blob-Storage. Tự xử lý chuyển hướng mà không chuyển tiếp bất kỳ header Authorization nào, sau đó xác minh và giải mã blob bằng file_key của nó. Giới hạn dung lượng tải xuống ở kích thước dự kiến.

Một blob_id là quyền truy cập dạng bearer: bất kỳ ai nắm giữ nó đều có thể lấy bản mã, đó là lý do khóa chỉ được truyền bên trong tin nhắn được mã hóa. access_mode và thời hạn được áp dụng tại bước chuyển hướng của relay. Đừng dựa vào chúng để bảo đảm tính bảo mật.

Xóa tệp đính kèm

http
POST /api/relay/blob/{blob_id}/delete/

Chỉ người tải lên mới có thể xóa blob. Phản hồi là {"blob_id": "...", "deleted": true}.

Lỗi tệp đính kèm

Lỗi dùng định dạng {"success": false, "error": "<text>", "error_code": "<code>"}. Hãy rẽ nhánh xử lý theo error_code.

HTTPerror_codeÝ nghĩa
400blob_id_invalidID blob sai định dạng.
400blob_missing_file_size, blob_missing_total_size, blob_missing_parts, blob_bad_parts, blob_bad_part_countYêu cầu tải lên không hợp lệ.
400blob_not_r2, blob_multipart_complete_failedHoàn tất thất bại; hãy bắt đầu một lượt tải lên mới.
401auth_requiredBlob yêu cầu RelayAuth.
403blob_not_uploader, download_forbiddenBên gọi không được phép.
404blob_not_found, blob_not_uploadedBlob không xác định, hoặc gọi hoàn tất trước khi tải lên xong.
410blob_expiredBlob đã hết hạn. Yêu cầu người gửi gửi lại.
413blob_too_largeVượt quá giới hạn; phản hồi kèm max_bytes và chunked_max_bytes.
503blob_r2_unavailableKho lưu trữ tạm thời không khả dụng; thử lại với backoff.

Endpoint tải lên cũ

POST /api/relay/blob/ (biểu mẫu multipart, tối đa 10 MiB) và API phiên có thể tiếp tục dưới /api/relay/blob/session/ (tối đa 100 MiB, mỗi khối từ 64 KiB đến 4 MiB, phiên có hiệu lực 24 giờ) vẫn khả dụng làm phương án dự phòng. Máy khách mới nên dùng các endpoint ở trên.

Thông báo đẩy

Relay gửi thông báo qua Apple Push Notification service (APNs) cho iOS và macOS. Máy khách Android chỉ nhận tin nhắn qua WebSocket.

Thông báo đẩy được gửi cho relay_send và group_send khi người nhận không có kết nối tiền cảnh đang hoạt động, người gửi không đặt suppress_push, và người nhận chưa tắt tiếng cuộc trò chuyện. Chỉnh sửa, thu hồi, biểu cảm và xác nhận không tạo thông báo đẩy. Payload thông báo đẩy không chứa bản mã:

json
{
  "aps": { "alert": { "title": "AeroNyx", "body": "New encrypted message" }, "sound": "default", "badge": 1, "content-available": 1 },
  "kind": "p2p_message",
  "sender_pubkey": "<64 hex>",
  "message_id": "..."
}

kind là p2p_message (kèm sender_pubkey) hoặc group_message (kèm group_id). Thông báo cuộc gọi dùng missed_call và các payload cuộc gọi chuyên biệt. Khi nhận thông báo đẩy, hãy kết nối và chạy relay_pull.

EndpointPhần thân
POST /api/relay/push/register/token (64 hex), platform (ios hoặc macos), bundle_id, environment (production hoặc sandbox), cùng các trường tùy chọn token_type (alert hoặc voip) và provider (apns).
POST /api/relay/push/unregister/token, cùng các trường tùy chọn platform, token_type, provider.
POST /api/relay/push/mute/kind (p2p hoặc group), target (khóa công khai hoặc ID nhóm), muted (boolean).

Cả ba đều yêu cầu RelayAuth. Việc đăng ký một token sẽ chuyển token đó sang danh tính của bên gọi.

Cuộc gọi

Báo hiệu cuộc gọi thoại và video đi qua cùng WebSocket dưới dạng siêu dữ liệu bản rõ; luồng phương tiện được truyền riêng. Các khung gồm call_invite, call_answer, call_reject, call_hangup và call_busy cho cuộc gọi 1:1, cùng group_call_invite, group_call_invite_broadcast, group_call_answer và group_call_hangup_broadcast cho nhóm, kèm theo các khung tiếp nhận người tham gia cho các cuộc họp có người chủ trì.

Relay xác thực room_name dựa trên những người tham gia: p2p_ theo sau là 16 ký tự hex đầu tiên của SHA256(lower_key + ":" + higher_key) cho cuộc gọi 1:1, và grp_<first 8 characters of group_id>_<first 8 hex of SHA256(group_id)> cho nhóm. Tín hiệu cuộc gọi cho người nhận đang ngoại tuyến được giữ trong 120 giây.

Giới hạn tốc độ

Phạm viGiới hạnPhản hồi
relay_send, group_send và đẩy qua HTTPS, cho mỗi danh tính50 trong mỗi khung thời gian ngắn; 100.000 mỗi ngàyWebSocket: error rate_limited kèm retry_after. HTTPS: 429 kèm Retry-After.
group_send cho mỗi người gửi và nhóm10 trong mỗi khung thời gian ngắnerror rate_limited, scope: "sender".
group_send cho mỗi nhóm50 trong mỗi khung thời gian ngắnerror rate_limited, scope: "group".
Biểu cảm cho mỗi người gửi và cuộc trò chuyện20 trong mỗi khung thời gian ngắnerror rate_limited, scope: "reaction".
Yêu cầu liên hệ cho mỗi người gửi và người nhận3 mỗi 24 giờsend_rejected contact_request_rate_limited.

Hãy chờ lùi ít nhất retry_after giây. Không gửi lại trong vòng lặp liên tục: bộ đếm vẫn tiếp tục chạy trong lúc bạn thử lại.

Danh sách kiểm tra triển khai

  1. Tạo và lưu trữ danh tính Ed25519; dùng khóa hex chữ thường ở mọi nơi.
  2. Triển khai RelayAuth cùng đăng nhập WebSocket, heartbeat và presence_state.
  3. Triển khai phong bì niêm phong và đối chiếu với vector chuẩn trong Central Chat HTTPS API v1.
  4. Gửi bằng relay_send, coi durable: true là đã gửi, tạo lại timestamp và payload_sig khi thử lại.
  5. Nhận relay_envelope: loại bỏ trùng lặp, xác minh, giải mã, lưu trữ, sau đó gửi message_receipt và relay_offline_ack.
  6. Chạy relay_pull sau khi đăng nhập và khi được thông báo đẩy đánh thức; chỉ báo nhận sau khi đã lưu trữ bền vững.
  7. Đọc các cờ quyền riêng tư trong hồ sơ và tuân thủ chúng đối với trạng thái trực tuyến và xác nhận đã đọc.
  8. Mã hóa tệp đính kèm trên thiết bị và tải lên qua presign hoặc multipart; giữ file_key bên trong tin nhắn được mã hóa.
  9. Xác minh quyền tác giả trước khi áp dụng chỉnh sửa và thu hồi.
  10. Giữ chức năng tìm kiếm và lịch sử tin nhắn trên thiết bị. Relay không có chức năng tìm kiếm nội dung và không phải là kho lưu trữ.
<!-- faq:start -->

Câu hỏi thường gặp

AeroNyx Chat Relay có thể đọc tin nhắn của tôi không?

Không. Tin nhắn, nội dung chỉnh sửa, biểu cảm, tin nhắn nhóm và tệp đính kèm được mã hóa và ký trên thiết bị của người gửi trước khi đến relay, và các khóa không bao giờ rời khỏi thiết bị của những người tham gia cuộc trò chuyện. Relay chỉ lưu trữ và chuyển tiếp bản mã.

AeroNyx Chat Relay có thể nhìn thấy những gì?

Relay nhìn thấy siêu dữ liệu truyền tải: khóa công khai của người gửi và người nhận, ID nhóm và ID tin nhắn, dấu thời gian, kích thước payload, trạng thái truyền tải và trạng thái đã đọc, tín hiệu trạng thái trực tuyến và đang nhập, siêu dữ liệu báo hiệu cuộc gọi, kích thước tệp đính kèm và địa chỉ IP của kết nối. Relay không nhìn thấy nội dung tin nhắn, nội dung tệp đính kèm hay các khóa. Danh sách đầy đủ nằm trong mô hình tin cậy ở đầu trang này.

Trò chuyện AeroNyx sử dụng phương thức mã hóa nào?

Mỗi danh tính là một cặp khóa Ed25519. Hai người dẫn xuất một khóa trò chuyện chung bằng X25519 và HKDF-SHA256. Tin nhắn 1:1 được mã hóa bằng XChaCha20-Poly1305 và ký bằng Ed25519. Tin nhắn nhóm và tệp đính kèm được mã hóa bằng AES-256-GCM. Định dạng byte chính xác cùng một vectơ kiểm thử được công bố trong tài liệu Central Chat HTTPS API v1.

Ảnh, video và tệp được bảo vệ như thế nào?

Mỗi tệp được mã hóa trên thiết bị bằng một khóa AES-256-GCM ngẫu nhiên riêng trước khi tải lên. Hệ thống lưu trữ chỉ nhận được bản mã. Khóa của tệp được truyền bên trong tin nhắn được mã hóa đầu cuối, vì vậy chỉ người nhận mới có thể giải mã tệp.

Điều gì xảy ra nếu người nhận đang ngoại tuyến?

Relay giữ các tin nhắn đã mã hóa trong hàng đợi ngoại tuyến của người nhận trong tối đa 72 giờ và chuyển giao chúng khi người nhận kết nối lại. Trên iOS và macOS, người nhận cũng nhận được một thông báo đẩy không chứa nội dung tin nhắn.

AeroNyx có lưu lịch sử trò chuyện của tôi không?

Không. Relay là một bộ đệm truyền tải: các mục sẽ bị xóa sau khi thiết bị của người nhận xác nhận đã lưu chúng. Lịch sử trò chuyện và chức năng tìm kiếm nằm trên các thiết bị của bạn.

Tôi có thể tự xây dựng máy khách hoặc bot AeroNyx của riêng mình không?

Có. Bất kỳ phần mềm nào nắm giữ một danh tính Ed25519 và triển khai các định dạng trên trang này đều có thể trao đổi tin nhắn với người dùng AeroNyx App. Để tích hợp theo mô hình yêu cầu/phản hồi đơn giản hơn mà không cần WebSocket, hãy dùng Central Chat HTTPS API v1.

AeroNyx Chat Relay có phi tập trung không?

Chat Relay là dịch vụ truyền tải tập trung của AeroNyx. AeroNyx cũng vận hành một mạng lưới nút mã nguồn mở (AGPL-3.0) có thể truyền bản mã trò chuyện qua một tuyến hai chặng đi qua các mạng khác nhau. Hai đường truyền này cùng tồn tại: relay mang lại khả năng truyền tải nhanh, đáng tin cậy cùng hàng đợi ngoại tuyến, còn đường truyền qua nút là một tuyến tùy chọn giúp giảm lượng thông tin mà bất kỳ nhà vận hành đơn lẻ nào có thể quan sát.

Tại sao tin nhắn của tôi bị từ chối với verification_required?

Người nhận chỉ chấp nhận tin nhắn từ các liên hệ. Hãy gửi một tin nhắn đầu tiên duy nhất với contact_request: true, tin nhắn này sẽ hiển thị với người nhận dưới dạng yêu cầu liên hệ. Mỗi người nhận cho phép tối đa ba yêu cầu liên hệ trong bất kỳ khoảng thời gian 24 giờ nào.

<!-- faq:end --> <!-- verified-two-hop-delivery-v1:start -->

Truyền tải hai chặng có xác minh

Đối với lưu lượng ChatRelay đã xác thực và đủ điều kiện, nguồn gửi có thể chọn một đường truyền hai chặng đa dạng về mạng và chỉ tính là đã truyền tải sau khi xác thực biên nhận có chữ ký của điểm cuối dự kiến. Các nút relay định tuyến bản mã và không phân tích payload E2E. Xem mô hình bằng chứng đầy đủ tại Khám phá nút và truyền tải relay được mã hóa có xác minh.

<!-- verified-two-hop-delivery-v1:end -->