TAG API — MISCELLANEOUS EXTRAS
================================
Base URL : https://tag.nsamaandcompany.com/tag_api
Auth     : Authorization: Bearer <token>  required on all endpoints

Covers: E2E encryption public keys, contacts/save (post-accept add),
        DM archive/unarchive/delivered, moment update and reactions.


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
END-TO-END ENCRYPTION  (public keys)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Used to exchange X25519 public keys for client-side E2E encryption of DMs.

PUT  /api/v1/users/public-key                   [AUTH]
  Register or update your own E2E public key.
  Call this on every login so the key is always current.
  Body (JSON): { "public_key": "<base64-encoded X25519 32-byte key>" }
  Response 200: { "public_key": "<the registered key>" }
  Errors:
    422 — public_key missing or empty

GET  /api/v1/users/{userId}/public-key          [AUTH]
  Fetch another user's E2E public key so you can encrypt messages for them.
  Path param: userId — the recipient's numeric user_id
  Response 200: { "user_id": 7, "public_key": "<base64-encoded key>" }
  Errors:
    404 — user not found or has not yet registered a key


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CONTACTS  — SAVE BY USER ID
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

POST /api/v1/contacts/save                      [AUTH]
  Add a user directly to your tag-book by their numeric user_id.
  No QR scan required. Requires a prior accepted contact request as proof
  of mutual consent (see CONTACT_REQUEST_FEATURE.txt).
  Body (JSON): { "user_id": 5 }
  Response 201:
    {
      contact_user_id, public_tag_id, full_name,
      phone, email, avatar_url
    }
  Errors:
    422 — self-add | already in tag-book | user_id missing
    403 — no accepted contact request exists between the two users
    404 — user not found

  Typical flow after accepting a contact request:
    1. Receiver accepts via PATCH /contacts/requests/{id}/accept
    2. Accept response includes the sender's profile
    3. App prompts "Save [name] to your tag-book?"
    4. If yes → call POST /contacts/save { "user_id": <sender.user_id> }


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DIRECT MESSAGES  — ARCHIVE / DELIVERED
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

PATCH /api/v1/conversations/{id}/archive        [AUTH]
  Hide a conversation from the main DM list (archive it).
  Only affects the calling user; the other person's view is unchanged.
  Blocking a user auto-archives any shared conversation.
  Path param: id — the conversation id
  Body: (none)
  Response 200: { "message": "Conversation archived." }
  Errors:
    404 — conversation not found or you are not a participant

PATCH /api/v1/conversations/{id}/unarchive      [AUTH]
  Restore an archived conversation back to the main DM list.
  Path param: id — the conversation id
  Body: (none)
  Response 200: { "message": "Conversation unarchived." }
  Errors:
    404 — conversation not found or you are not a participant

  How to list archived conversations:
    GET /api/v1/conversations?archived=1
    (the standard list endpoint supports this query param)

POST /api/v1/conversations/{id}/delivered       [AUTH]
  Mark all messages in a conversation as delivered to this device.
  Call this when the app comes into the foreground or FCM is received.
  Path param: id — the conversation id
  Body: (none)
  Response 200 (or 204): (no data)
  Errors:
    404 — conversation not found or you are not a participant


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MOMENTS  — EDIT & REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
See CHAT_SEARCH_DOCS.txt for the core moments endpoints (GET, POST, seen, viewers, DELETE).

PATCH /api/v1/moments/{id}                      [AUTH]
  Edit the caption and/or background_color of one of your unexpired moments.
  Text moments only — media URL cannot be changed after upload.
  Path param: id — the moment id
  Body (JSON, provide at least one):
    { "caption": "updated caption", "background_color": "#FF5733" }
  Response 200: updated moment object (same shape as GET /moments own item)
    { id, author_id, author, media_type, media_url, caption, background_color,
      is_mine, is_viewed, views_count, reactions_count, created_at, expires_at }
  Errors:
    422 — neither caption nor background_color provided
    403 — not your moment
    404 — moment not found or already expired

POST /api/v1/moments/{id}/react                 [AUTH]
  Toggle a reaction on a contact's moment.
  Cannot react to your own moment (403).
  Path param: id — the moment id
  Body (JSON): { "reaction_type": "heart" | "like" | "love" | "fire" | "laugh" | "sad" | "angry" }
  Response 200 (removed): { "reactions_count": 2, "is_reacted": false }
  Response 201 (added):   { "reactions_count": 3, "is_reacted": true }
  Errors:
    422 — missing or invalid reaction_type
    403 — cannot react to your own moment
    404 — moment not found or expired


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PROFILE UPDATE — is_private FLAG
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

The existing POST /api/v1/profile/update endpoint accepts an additional
optional field that was not in the original documentation:

  { "full_name": "...", "age": 25, "sex": "male", "is_private": true | false }

  is_private — when true, new followers must send a follow request instead
               of being able to follow directly.
  The field is optional; omitting it leaves the current setting unchanged.
