---
title: "API повідомлень"
description: "Повна документація API системи повідомлень платформи Ring з Tunnel Protocol realtime"
locale: "uk"
---
# API повідомлень

Платформа Ring поєднує **REST** (`/api/conversations`) з **Tunnel Protocol** для live-подій.

`publishToChannel` на `conversation:${id}`, клієнт `useTunnel().subscribe()`. k8s: native WSS; Vercel: SSE → poll.

## 🏗️ Архітектура системи

### Архітектура потоку повідомлень

```
Дія користувача → Кінцева точка API → Збереження у БД → Трансляція у режимі реального часу → Оновлення UI
                                      ↓
                            Вкладення файлів (якщо є) → Збереження у CDN
```

### Транспортний шар стратегії

- **Native WSS** — первинний при `RING_DEPLOY_TARGET=k8s|self-hosted` (`/api/tunnel/ws`)
- **SSE** — первинний на Vercel; фолбек після WSS
- **Long-polling** — `/api/tunnel/poll`
- **Server-Sent Events (SSE)**: Фолбек для Vercel Edge Runtime
- **Long-polling**: Універсальний фолбек для обмежувальних середовищ
- **HTTP REST**: Стандартні CRUD операції із синхронізацією реального часу

### Стратегія зберігання

- **Повідомлення**: PostgreSQL із метаданими JSONB та повнотекстовим пошуком
- **Файли**: CDN сховище (Cloudflare R2, AWS S3 або сумісне)
- **Стан реального часу**: Redis pub/sub для міжекземплярної комунікації
- **Черга офлайн**: IndexedDB для черги повідомлень офлайн

## 📋 Довідка кінцевих точок API

### `GET /api/conversations`

Список розмов користувачів із розширеним фільтруванням та пагінацією.

#### Параметри

| Параметр | Тип | Обов'язковий | Опис |
|-----------|-----|--------------|------|
| `page` | number | Ні | Номер сторінки (за замовчуванням: 1) |
| `limit` | number | Ні | Розмов на сторінку (за замовчуванням: 20, макс: 50) |
| `type` | string | Ні | Фільтр за типом: `direct`, `group`, `channel` |
| `status` | string | Ні | Фільтр за статусом: `active`, `archived`, `muted` |
| `before` | string | Ні | ISO дата - розмови оновлені до цієї дати |
| `search` | string | Ні | Пошук за іменами учасників або повідомленнями |

#### Приклад запиту

{`-H "Authorization: Bearer your-session-token"`}

#### Відповідь

{`"conversations": [
    {
      "id": "conv_123456",
      "type": "direct",
      "participants": [
        {
          "userId": "user_789",
          "name": "John Doe",
          "avatar": "https://...",
          "role": "member",
          "lastSeen": "2025-10-16T12:30:00Z"
        }
      ],
      "title": null,
      "description": null,
      "lastMessage": {
        "id": "msg_999999",
        "content": "Привіт, як просувається проект?",
        "senderId": "user_789",
        "timestamp": "2025-10-16T12:30:00Z",
        "type": "text"
      },
      "unreadCount": 2,
      "updatedAt": "2025-10-16T12:30:00Z",
      "createdAt": "2025-10-15T09:00:00Z",
      "settings": {
        "muted": false,
        "pinned": false,
        "notifications": true
      }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 25,
    "totalPages": 3,
    "hasNext": true,
    "hasPrev": false
  }
}`}

### `POST /api/conversations`

Створити нову розмову (пряме повідомлення або груповий чат).

#### Тіло запиту для прямого повідомлення

{`"type": "direct",
  "participantIds": ["user_456"],
  "initialMessage": {
    "content": "Привіт! Я хочу обговорити можливість.",
    "type": "text"
  }
}`}

#### Тіло запиту для групової розмови

{`"type": "group",
  "title": "Команда проекту Alpha",
  "description": "Обговорення розробки проекту Alpha",
  "participantIds": ["user_456", "user_789", "user_101"],
  "settings": {
    "isPrivate": true,
    "allowInvites": true,
    "moderationEnabled": false
  }
}`}

#### Відповідь

{`"conversation": {
    "id": "conv_123457",
    "type": "direct",
    "participants": [
      {
        "userId": "user_123",
        "name": "Поточний користувач",
        "role": "admin"
      },
      {
        "userId": "user_456",
        "name": "Jane Smith",
        "role": "member"
      }
    ],
    "title": null,
    "createdAt": "2025-10-16T12:35:00Z",
    "settings": {
      "muted": false,
      "pinned": false,
      "notifications": true
    }
  },
  "initialMessage": {
    "id": "msg_100000",
    "conversationId": "conv_123457",
    "senderId": "user_123",
    "content": "Привіт! Я хочу обговорити можливість.",
    "type": "text",
    "timestamp": "2025-10-16T12:35:00Z",
    "status": "sent"
  }
}`}

### `GET /api/conversations/{id}/messages`

Отримати повідомлення з конкретної розмови з пагінацією.

#### Параметри

| Параметр | Тип | Обов'язковий | Опис |
|-----------|-----|--------------|------|
| `page` | number | Ні | Номер сторінки (за замовчуванням: 1) |
| `limit` | number | Ні | Повідомлень на сторінку (за замовчуванням: 50, макс: 100) |
| `before` | string | Ні | ISO дата - повідомлення до цього часу |
| `after` | string | Ні | ISO дата - повідомлення після цього часу |
| `type` | string | Ні | Фільтр за типом: `text`, `file`, `image`, `system` |

#### Приклад запиту

{`-H "Authorization: Bearer your-session-token"`}

#### Відповідь

{`"messages": [
    {
      "id": "msg_100001",
      "conversationId": "conv_123456",
      "senderId": "user_456",
      "content": "Дякую за звернення! Проект просувається добре.",
      "type": "text",
      "timestamp": "2025-10-16T12:36:00Z",
      "status": "delivered",
      "readBy": ["user_123"],
      "reactions": [],
      "replyTo": null,
      "edited": false,
      "attachments": []
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 25,
    "total": 47,
    "totalPages": 2,
    "hasNext": true,
    "hasPrev": false
  },
  "conversation": {
    "id": "conv_123456",
    "type": "direct",
    "participants": ["user_123", "user_456"]
  }
}`}

### `POST /api/conversations/{id}/messages`

Надіслати нове повідомлення до розмови.

#### Тіло запиту для текстового повідомлення

{`"content": "Ось документ, який ви просили.",
  "type": "text",
  "replyTo": null
}`}

#### Тіло запиту з вкладенням файлу

{`"content": "Будь ласка, перегляньте цей документ",
  "type": "text",
  "attachments": [
    {
      "name": "requirements.pdf",
      "type": "application/pdf",
      "size": 2048576,
      "url": "https://cdn.example.com/files/requirements.pdf"
    }
  ]
}`}

#### Відповідь

{`"message": {
    "id": "msg_100003",
    "conversationId": "conv_123456",
    "senderId": "user_123",
    "content": "Будь ласка, перегляньте цей документ",
    "type": "text",
    "timestamp": "2025-10-16T12:38:00Z",
    "status": "sent",
    "readBy": [],
    "reactions": [],
    "replyTo": null,
    "edited": false,
    "attachments": [
      {
        "id": "att_123456",
        "name": "requirements.pdf",
        "type": "application/pdf",
        "size": 2048576,
        "url": "https://cdn.example.com/files/att_123456.pdf",
        "thumbnailUrl": null
      }
    ]
  },
  "delivered": true,
  "realTimeSent": true
}`}

### `PUT /api/messages/{id}`

Оновити/редагувати повідомлення (тільки відправником, у межах часу).

#### Тіло запиту

{`"content": "Будь ласка, перегляньте цей оновлений документ",
  "attachments": []
}`}

#### Відповідь

{`"message": {
    "id": "msg_100003",
    "content": "Будь ласка, перегляньте цей оновлений документ",
    "edited": true,
    "editedAt": "2025-10-16T12:39:00Z"
  }
}`}

### `DELETE /api/messages/{id}`

Видалити повідомлення (м'яке видалення, тільки відправником або адміном).

#### Параметри

| Параметр | Тип | Обов'язковий | Опис |
|-----------|-----|--------------|------|
| `hard` | boolean | Ні | Жорстке видалення (тільки адмін, за замовчуванням: false) |

#### Приклад запиту

{`-H "Authorization: Bearer your-session-token"`}

#### Відповідь

{`"deleted": true,
  "messageId": "msg_100003",
  "deletedBy": "user_123",
  "deletedAt": "2025-10-16T12:40:00Z"
}`}

### `POST /api/conversations/{id}/typing`

Надіслати індикатор набору тексту учасникам розмови.

#### Тіло запиту

{`"isTyping": true
}`}

#### Відповідь

{`"sent": true,
  "expiresAt": "2025-10-16T12:40:05Z"
}`}

### `PUT /api/messages/{id}/read`

Позначити повідомлення як прочитані у розмові.

#### Тіло запиту

{`"messageIds": ["msg_100001", "msg_100002"]
}`}

#### Відповідь

{`"markedRead": 2,
  "readAt": "2025-10-16T12:41:00Z"
}`}

## 🔄 Повідомлення у режимі реального часу

### Інтеграція Tunnel (native WSS + SSE)

Платформа Ring використовує **Tunnel Transport** — сервер `publishToChannel` на `conversation:${id}`, клієнт `useTunnel().subscribe()`. На k8s/self-hosted первинний транспорт — **native WSS** (`/api/tunnel/ws`); на Vercel — SSE → long-polling.

{`'use client'

import { useEffect } from 'react'
import { useTunnel } from '@/hooks/use-tunnel'

export function ConversationRealtime({ conversationId }: { conversationId: string }) {
  const { subscribe, isConnected } = useTunnel()
  const channel = \`conversation:\${conversationId}\`

  useEffect(() => {
    return subscribe(channel, (message) => {
      if (message.event === 'message:new') addMessageToUI(message.payload)
      if (message.event === 'typing') showTypingIndicator(message.payload)
      if (message.event === 'read-receipt') updateReadStatus(message.payload)
    })
  }, [channel, subscribe])

  // Надсилання — REST POST; сервер розсилає через TunnelHub
  return {isConnected ? 'live' : 'polling'}
}`}

### Фолбек Server-Sent Events (SSE)

Для середовищ без підтримки WebSocket:

// SSE з'єднання для повідомлень

{`const eventSource = new EventSource('/api/messaging/stream')

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data)

  switch (data.type) {
    case 'message':
      handleNewMessage(data.message)
      break
    case 'typing':
      handleTypingIndicator(data)
      break
    case 'read-receipt':
      handleReadReceipt(data)
      break
  }
}

// Надіслати повідомлення через HTTP, коли WebSocket недоступний
const sendMessage = async (conversationId: string, content: string) => {
  const response = await fetch(`/api/conversations/${conversationId}/messages`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ content, type: 'text' })
  })

  const result = await response.json()
  // Оновлення реального часу прийде через SSE
  return result
}`}

## 📎 Вкладення файлів

### Підтримувані типи файлів

| Категорія | Типи | Макс розмір | Функції |
|-----------|------|-------------|---------|
| **Документи** | PDF, DOC, DOCX, TXT, MD | 10MB | Попередній перегляд, завантаження |
| **Зображення** | JPG, PNG, GIF, WebP, SVG | 5MB | Мініатюра, вбудований попередній перегляд |
| **Відео** | MP4, WebM, MOV | 50MB | Відеоплеєр, мініатюра |
| **Аудіо** | MP3, WAV, M4A | 20MB | Аудіоплеєр |
| **Архіви** | ZIP, RAR, 7Z | 100MB | Тільки завантаження |

### Процес завантаження файлів

// Клієнтське завантаження файлів

{`const uploadFile = async (file: File): Promise => {
  // 1. Отримати URL завантаження
  const { uploadUrl, attachment } = await fetch('/api/upload', {
    method: 'POST',
    body: JSON.stringify({
      filename: file.name,
      contentType: file.type,
      size: file.size
    })
  }).then(r => r.json())

  // 2. Завантажити до CDN
  await fetch(uploadUrl, {
    method: 'PUT',
    body: file,
    headers: {
      'Content-Type': file.type
    }
  })

  return attachment
}

// Використання у повідомленні
const handleSendMessage = async (content: string, files: File[]) => {
  const attachments = await Promise.all(files.map(uploadFile))

  await fetch(`/api/conversations/${conversationId}/messages`, {
    method: 'POST',
    body: JSON.stringify({
      content,
      type: 'text',
      attachments
    })
  })
}`}

## 💬 Типи повідомлень

### Текстові повідомлення

Стандартна текстова комунікація з підтримкою багатих форматувань.

{`"type": "text",
  "content": "Привіт @john, чи можеш ти переглянути [вимоги](https://docs.example.com) документ?",
  "metadata": {
    "mentions": ["user_456"],
    "links": ["https://docs.example.com"],
    "formatting": "markdown"
  }
}`}

### Вкладення файлів

Повідомлення, що містять завантаження файлів.

{`"type": "file",
  "content": "Ось оновлений файл дизайну",
  "attachments": [
    {
      "id": "att_123457",
      "name": "design.fig",
      "type": "application/figma",
      "size": 15728640,
      "url": "https://cdn.example.com/files/design.fig"
    }
  ]
}`}

### Системні повідомлення

Автоматизовані повідомлення для подій розмови.

{`"type": "system",
  "content": "john.doe приєднався до розмови",
  "metadata": {
    "event": "participant_joined",
    "participantId": "user_456",
    "participantName": "John Doe"
  }
}`}

### Повідомлення-відповіді

Повідомлення, що відповідають на інші повідомлення.

{`"type": "text",
  "content": "Я згоден з цим підходом",
  "replyTo": "msg_100001",
  "metadata": {
    "replyPreview": "Запропонований підхід виглядає добре..."
  }
}`}

## 🔧 Приклади реалізації

### React компонент інтерфейсу повідомлень

// components/MessagingInterface.tsx

{`import { useState, useEffect, useRef } from 'react'
import { useTunnel } from '@/hooks/use-tunnel'

interface Message {
  id: string
  content: string
  senderId: string
  timestamp: string
  type: string
}

export function MessagingInterface({ conversationId }: { conversationId: string }) {
  const [messages, setMessages] = useState<Message[]>([])
  const [newMessage, setNewMessage] = useState('')
  const { subscribe } = useTunnel()
  const messagesEndRef = useRef(null)

  useEffect(() => {
    fetchMessages()
    const channel = \`conversation:\${conversationId}\`
    return subscribe(channel, (message) => {
      if (message.event === 'message:new') {
        setMessages(prev => [...prev, message.payload as Message])
        scrollToBottom()
      }
    })
  }, [conversationId, subscribe])

  const fetchMessages = async () => {
    const response = await fetch(\`/api/conversations/\${conversationId}/messages\`)
    const data = await response.json()
    setMessages(data.messages)
    scrollToBottom()
  }

  const sendMessage = async () => {
    if (!newMessage.trim()) return
    await fetch(\`/api/conversations/\${conversationId}/messages\`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content: newMessage, type: 'text' })
    })
    setNewMessage('')
  }

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }

  return (
    
      
        {messages.map(message => (
          
        ))}
        
      
      
         setNewMessage(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
          placeholder="Введіть повідомлення..."
        />
        Надіслати
      
    
  )
}`}

## 🚨 Обробка помилок

### Поширені відповіді про помилки

// Розмова не знайдена

{`{
  "error": "NotFoundError",
  "message": "Розмову не знайдено",
  "conversationId": "conv_123456",
  "statusCode": 404
}

// Відмова у доступі
{
  "error": "ForbiddenError",
  "message": "Ви не є учасником цієї розмови",
  "statusCode": 403
}

// Повідомлення занадто довге
{
  "error": "ValidationError",
  "message": "Вміст повідомлення перевищує максимальну довжину 10000 символів",
  "maxLength": 10000,
  "statusCode": 400
}

// Помилка завантаження файлу
{
  "error": "UploadError",
  "message": "Розмір файлу перевищує ліміт 10MB",
  "maxSize": "10MB",
  "statusCode": 413
}`}

## 🔒 Заходи безпеки

### Аутентифікація
- Усі кінцеві точки повідомлень вимагають дійсної аутентифікації сесії
- Tunnel-з'єднання автентифікуються через `/api/tunnel/token` (JWT)
- Завантаження файлів вимагають додаткової перевірки власності

### Обмеження швидкості
- Надсилання повідомлень: 30 повідомлень/хвилину на користувача
- Завантаження файлів: 10 файлів/годину на користувача
- Tunnel: керується `TransportManager` з автоматичним фолбеком

### Безпека контенту
- Вміст повідомлень санітізується для запобігання XSS атакам
- Завантаження файлів сканується на віруси та шкідливе ПЗ
- URL у повідомленнях валідуються та можуть блокуватися

### Конфіденційність
- На вибір end-to-end шифрування для конфіденційних розмов
- Видалення повідомлень видаляє вміст від усіх учасників
- Квитанції прочитання можуть відключатися на розмову

## 📊 Моніторинг та аналітика

### Метрики повідомлень

// Відстеження подій повідомлень

{`import { trackEvent } from '@/lib/analytics'
import { useTunnel } from '@/hooks/use-tunnel'

// useTunnel subscribe on conversation channel
trackEvent('message_sent', { deliveryMethod: 'tunnel' })

trackEvent('message_delivery_time', {
  deliveryTime: Date.now() - sendTime,
  transport: 'tunnel'
})`}

## 🎛️ Конфігурація

### Змінні середовища

Налаштування повідомлень RING_DEPLOY_TARGET Сховище файлів

{`MESSAGING_RATE_LIMIT=30
MESSAGING_FILE_SIZE_LIMIT=10485760
MESSAGING_RETENTION_DAYS=365

RING_DEPLOY_TARGET=k8s
NEXT_PUBLIC_RING_DEPLOY_TARGET=k8s

FILE_STORAGE_PROVIDER=cloudflare_r2
FILE_STORAGE_BUCKET=messaging-files
FILE_CDN_URL=https://cdn.ring-platform.org`}

### Схема бази даних

{`CREATE TABLE conversations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  type VARCHAR(20) NOT NULL, -- direct, group, channel
  title VARCHAR(255),
  description TEXT,
  created_by UUID REFERENCES users(id),
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  settings JSONB DEFAULT '{}'
);

-- Учасники розмов
CREATE TABLE conversation_participants (
  conversation_id UUID REFERENCES conversations(id),
  user_id UUID REFERENCES users(id),
  role VARCHAR(20) DEFAULT 'member', -- admin, member
  joined_at TIMESTAMP DEFAULT NOW(),
  last_seen_at TIMESTAMP,
  settings JSONB DEFAULT '{}',
  PRIMARY KEY (conversation_id, user_id)
);

-- Таблиця повідомлень
CREATE TABLE messages (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  conversation_id UUID REFERENCES conversations(id),
  sender_id UUID REFERENCES users(id),
  content TEXT,
  type VARCHAR(20) DEFAULT 'text',
  metadata JSONB DEFAULT '{}',
  reply_to UUID REFERENCES messages(id),
  edited BOOLEAN DEFAULT FALSE,
  edited_at TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW()
);

-- Статус прочитання повідомлень
CREATE TABLE message_reads (
  message_id UUID REFERENCES messages(id),
  user_id UUID REFERENCES users(id),
  read_at TIMESTAMP DEFAULT NOW(),
  PRIMARY KEY (message_id, user_id)
);`}

---

*Система повідомлень платформи Ring забезпечує комунікацію корпоративного рівня у режимі реального часу з комплексним обміном файлами, кросплатформною сумісністю та розширеними функціями конфіденційності.*
