Возможности Как это работает Тарифы Документация Cookbook Партнёрам Личный кабинет
Основы
💬

Чат между пользователями

15 мин чтения Средне

Полноценный мессенджер с историей сообщений, статусами прочтения и групповыми беседами.

JavaScript PHP Vue База данных

📋 Что получится

  • Мгновенная доставка сообщений
  • Индикатор "печатает..."
  • Статусы: отправлено → доставлено → прочитано
  • Групповые чаты
  • История сообщений
Любой стек: Примеры серверного кода на PHP, но концепции применимы к любому языку — Node.js, Python, Go, Ruby, Java, C# и др. Pushler.ru работает через REST API.

🏗️ Архитектура

🌐
Браузер A
Алиса
Pushler.ru
WebSocket
🌐
Браузер B
Борис
🖥️
Ваш сервер
PHP + MySQL

Поток сообщения:

  1. Алиса отправляет сообщение → POST на сервер
  2. Сервер сохраняет в БД
  3. Сервер отправляет через Pushler в канал чата
  4. Борис получает сообщение мгновенно

🗄️ Структура базы данных

SQL
-- Чаты (комнаты)
CREATE TABLE chats (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    type ENUM('private', 'group') DEFAULT 'private',
    name VARCHAR(255) NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Участники чата
CREATE TABLE chat_participants (
    chat_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_read_at TIMESTAMP NULL,
    PRIMARY KEY (chat_id, user_id),
    FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE
);

-- Сообщения
CREATE TABLE messages (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    chat_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    content TEXT NOT NULL,
    type ENUM('text', 'image', 'file') DEFAULT 'text',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE,
    INDEX idx_chat_created (chat_id, created_at)
);

💻 Код сервера

Серверный код отвечает за сохранение сообщений и отправку событий в Pushler. Ниже примеры на PHP — адаптируйте под свой стек.

Установка PHP SDK

Bash
composer require pushler/php-sdk

Инициализация Pushler

PHP
<?php
// pushler.php — подключение к Pushler

require_once 'vendor/autoload.php';

use PushlerRu\PushlerClient;

// Создаём клиент Pushler
$pushler = new PushlerClient(
    'key_ваш_ключ',      // App Key
    'secret_ваш_секрет'  // App Secret
);

Отправка сообщения в чат

PHP
<?php
// send_message.php — отправка сообщения

require_once 'pushler.php';

// Получаем данные из запроса
$chatId = (int) $_POST['chat_id'];
$content = trim($_POST['content']);
$userId = getCurrentUserId(); // Ваша функция авторизации

// Проверяем доступ к чату
if (!userHasAccessToChat($userId, $chatId)) {
    http_response_code(403);
    echo json_encode(['error' => 'Доступ запрещён']);
    exit;
}

// Сохраняем сообщение в БД
$pdo = getDbConnection();
$stmt = $pdo->prepare('
    INSERT INTO messages (chat_id, user_id, content, created_at) 
    VALUES (?, ?, ?, NOW())
');
$stmt->execute([$chatId, $userId, $content]);
$messageId = $pdo->lastInsertId();

// Получаем данные пользователя
$user = getUserById($userId);

// Формируем данные сообщения
$message = [
    'id' => $messageId,
    'chat_id' => $chatId,
    'user_id' => $userId,
    'content' => $content,
    'user' => [
        'id' => $user['id'],
        'name' => $user['name'],
        'avatar' => $user['avatar']
    ],
    'created_at' => date('c')
];

// Отправляем через Pushler всем участникам чата
$pushler->trigger(
    "private-chat-{$chatId}",
    'message:new',
    ['message' => $message]
);

// Уведомляем других участников (для badge/звука)
$participants = getChatParticipants($chatId);
foreach ($participants as $participantId) {
    if ($participantId != $userId) {
        $pushler->trigger(
            "private-user-{$participantId}",
            'chat:new-message',
            [
                'chat_id' => $chatId,
                'preview' => mb_substr($content, 0, 100),
                'sender' => [
                    'id' => $user['id'],
                    'name' => $user['name']
                ]
            ]
        );
    }
}

echo json_encode(['message' => $message]);

Авторизация приватного канала

PHP
<?php
// auth.php — авторизация каналов Pushler

require_once 'pushler.php';

header('Content-Type: application/json');

$channelName = $_POST['channel_name'];
$socketId = $_POST['socket_id'];
$userId = getCurrentUserId();

if (!$userId) {
    http_response_code(401);
    echo json_encode(['error' => 'Unauthorized']);
    exit;
}

// Проверяем доступ к каналу
// Формат: private-chat-{chatId}
if (preg_match('/^private-chat-(\d+)$/', $channelName, $matches)) {
    $chatId = (int) $matches[1];
    
    if (!userHasAccessToChat($userId, $chatId)) {
        http_response_code(403);
        echo json_encode(['error' => 'Forbidden']);
        exit;
    }
}

// Формат: private-user-{userId} — только для своего канала
if (preg_match('/^private-user-(\d+)$/', $channelName, $matches)) {
    if ((int) $matches[1] !== $userId) {
        http_response_code(403);
        echo json_encode(['error' => 'Forbidden']);
        exit;
    }
}

// Генерируем подпись для авторизации канала
$auth = $pushler->authorizeChannel($channelName, $socketId);
echo json_encode($auth);

Отметка прочитанным

PHP
<?php
// mark_read.php — отметить сообщения как прочитанные

require_once 'pushler.php';

$chatId = (int) $_POST['chat_id'];
$userId = getCurrentUserId();

// Обновляем время последнего прочтения
$pdo = getDbConnection();
$stmt = $pdo->prepare('
    UPDATE chat_participants 
    SET last_read_at = NOW() 
    WHERE chat_id = ? AND user_id = ?
');
$stmt->execute([$chatId, $userId]);

// Уведомляем отправителей о прочтении
$pushler->trigger(
    "private-chat-{$chatId}",
    'messages:read',
    [
        'user_id' => $userId,
        'read_at' => date('c')
    ]
);

echo json_encode(['success' => true]);

🌐 Код клиента (JavaScript)

Подключение SDK

HTML
<!-- Подключение из CDN -->
<script src="https://cdn.pushler.ru/sdk/pushler.min.js"></script>

<!-- Или npm: npm install @pushler/js -->

Базовая реализация чата

JavaScript
// Инициализация Pushler
const pushler = new PushlerClient({
    appKey: 'key_ваш_ключ',
    authEndpoint: '/auth.php'  // Ваш endpoint авторизации
});

const chatId = 123;
const currentUserId = 456;
let channel = null;

// Подключение к чату
pushler.on('connected', () => {
    console.log('Подключено к Pushler');
    
    // Подписываемся на канал чата
    channel = pushler.subscribe(`private-chat-${chatId}`);
    
    // Новое сообщение
    channel.on('message:new', (data) => {
        // Не добавляем своё сообщение (уже добавлено оптимистично)
        if (data.message.user_id !== currentUserId) {
            addMessageToChat(data.message);
        }
    });
    
    // Кто-то печатает
    channel.on('user:typing', (data) => {
        if (data.user_id !== currentUserId) {
            showTypingIndicator(data.user_name);
        }
    });
    
    // Сообщения прочитаны
    channel.on('messages:read', (data) => {
        updateMessageStatuses(data.user_id, data.read_at);
    });
});

// Отправка сообщения
async function sendMessage(content) {
    // Оптимистичное добавление
    const tempMessage = {
        id: 'temp_' + Date.now(),
        user_id: currentUserId,
        content: content,
        created_at: new Date().toISOString(),
        _pending: true
    };
    addMessageToChat(tempMessage);
    
    // Отправляем на сервер
    const response = await fetch('/send_message.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: `chat_id=${chatId}&content=${encodeURIComponent(content)}`
    });
    
    const data = await response.json();
    
    // Заменяем временное сообщение на реальное
    replaceMessage(tempMessage.id, data.message);
}

// Индикатор "печатает"
let lastTypingSent = 0;
function handleTyping() {
    const now = Date.now();
    if (now - lastTypingSent > 2000) {
        lastTypingSent = now;
        fetch('/typing.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: `chat_id=${chatId}&socket_id=${pushler.socketId}`
        });
    }
}

// UI функции
function addMessageToChat(message) {
    const container = document.getElementById('messages');
    const div = document.createElement('div');
    div.className = 'message' + (message.user_id === currentUserId ? ' own' : '');
    div.id = 'msg-' + message.id;
    div.innerHTML = `
        <div class="content">${escapeHtml(message.content)}</div>
        <div class="meta">
            ${formatTime(message.created_at)}
            ${message._pending ? '🕐' : '✓'}
        </div>
    `;
    container.appendChild(div);
    container.scrollTop = container.scrollHeight;
}

Vue 3 компонент

Vue
<!-- ChatWindow.vue -->
<template>
  <div class="chat-window">
    <div class="chat-header">
      <span class="name">{{ otherUser.name }}</span>
      <span v-if="isTyping" class="typing">печатает...</span>
    </div>

    <div ref="messagesEl" class="messages">
      <div
        v-for="msg in messages"
        :key="msg.id"
        :class="['message', { own: msg.user_id === currentUserId }]"
      >
        <div class="content">{{ msg.content }}</div>
        <div class="meta">
          {{ formatTime(msg.created_at) }}
          <span v-if="msg.user_id === currentUserId">{{ getStatus(msg) }}</span>
        </div>
      </div>
    </div>

    <div class="chat-input">
      <textarea 
        v-model="newMessage" 
        v-on:input="handleTyping"
        v-on:keydown.enter.exact.prevent="send"
        placeholder="Сообщение..."
      ></textarea>
      <button v-on:click="send">→</button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import PushlerClient from '@pushler/js';

const props = defineProps(['chatId', 'currentUserId', 'otherUser', 'appKey']);

const messages = ref([]);
const newMessage = ref('');
const isTyping = ref(false);
const lastReadAt = ref(null);
const messagesEl = ref(null);

let pushler = null;
let channel = null;
let lastTypingSent = 0;

onMounted(async () => {
  // Загружаем историю
  const res = await fetch(`/messages.php?chat_id=${props.chatId}`);
  messages.value = (await res.json()).messages;
  
  // Подключаемся к Pushler
  pushler = new PushlerClient({
    appKey: props.appKey,
    authEndpoint: '/auth.php'
  });
  
  pushler.on('connected', () => {
    channel = pushler.subscribe(`private-chat-${props.chatId}`);
    
    channel.on('message:new', (data) => {
      if (data.message.user_id !== props.currentUserId) {
        messages.value.push(data.message);
        scrollToBottom();
      }
    });
    
    channel.on('user:typing', (data) => {
      if (data.user_id !== props.currentUserId) {
        isTyping.value = true;
        setTimeout(() => { isTyping.value = false; }, 3000);
      }
    });
    
    channel.on('messages:read', (data) => {
      if (data.user_id === props.otherUser.id) {
        lastReadAt.value = data.read_at;
      }
    });
  });
});

onUnmounted(() => {
  channel?.unsubscribe();
  pushler?.disconnect();
});

async function send() {
  const content = newMessage.value.trim();
  if (!content) return;
  
  const temp = {
    id: 'temp_' + Date.now(),
    user_id: props.currentUserId,
    content,
    created_at: new Date().toISOString(),
    _pending: true
  };
  
  messages.value.push(temp);
  newMessage.value = '';
  scrollToBottom();
  
  const res = await fetch('/send_message.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: `chat_id=${props.chatId}&content=${encodeURIComponent(content)}`
  });
  
  const data = await res.json();
  const idx = messages.value.findIndex(m => m.id === temp.id);
  if (idx > -1) messages.value[idx] = data.message;
}

function handleTyping() {
  const now = Date.now();
  if (now - lastTypingSent > 2000) {
    lastTypingSent = now;
    fetch('/typing.php', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: `chat_id=${props.chatId}&socket_id=${pushler?.socketId}`
    });
  }
}

function getStatus(msg) {
  if (msg._pending) return '🕐';
  if (lastReadAt.value && new Date(msg.created_at) <= new Date(lastReadAt.value)) return '✓✓';
  return '✓';
}

function formatTime(date) {
  return new Date(date).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
}

function scrollToBottom() {
  nextTick(() => {
    if (messagesEl.value) messagesEl.value.scrollTop = messagesEl.value.scrollHeight;
  });
}
</script>

⚠️ Типичные ошибки

1. Дублирование сообщений

PHP
// ❌ Неправильно — сообщение придёт и отправителю
$pushler->trigger("private-chat-{$chatId}", 'message:new', $message);

// ✅ Правильно — исключаем отправителя по socket_id
$socketId = $_POST['socket_id'] ?? null;
$pushler->trigger("private-chat-{$chatId}", 'message:new', $message, $socketId);

2. Неправильная авторизация канала

PHP
// ❌ Неправильно — не проверяем доступ
$auth = $pushler->authorizeChannel($channelName, $socketId);
echo json_encode($auth);

// ✅ Правильно — проверяем права пользователя
if (!userHasAccessToChat($userId, $chatId)) {
    http_response_code(403);
    exit;
}
$auth = $pushler->authorizeChannel($channelName, $socketId);
echo json_encode($auth);

3. Утечка подписок на клиенте

JavaScript
// ❌ Неправильно — не отписываемся при выходе
pushler.subscribe(`private-chat-${chatId}`);

// ✅ Правильно — отписываемся
// При закрытии чата:
channel.unsubscribe();
// При полном выходе:
pushler.disconnect();

Готовы попробовать?

Создайте бесплатный аккаунт и начните интеграцию за пару минут