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

Документация Pushler.ru

Pushler.ru — это надежный WebSocket-сервис для мгновенной доставки сообщений в реальном времени. Простая интеграция, высокая производительность, масштабируемость.

📦

JavaScript SDK

Для браузера и Node.js. Подписка на каналы, получение событий в реальном времени.

Подробнее
💚

Vue SDK

Для Vue 3 приложений. Реактивные composables, автопереподключение, TypeScript.

Подробнее
🐘

PHP SDK

Для серверной интеграции. Отправка событий, авторизация каналов, вебхуки.

Подробнее

Быстрый старт

Интеграция Pushler занимает всего несколько минут. Следуйте этим шагам:

Создайте приложение

Зарегистрируйтесь в панели управления и создайте новое приложение. Вы получите ключи app_key и app_secret.

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

Добавьте JavaScript SDK на ваш сайт:

HTML
<!-- Подключение SDK -->
<script src="https://cdn.jsdelivr.net/npm/@pushler/js/dist/pushler-ru.min.js"></script>

<script>
  // Инициализация клиента
  const pushler = new PushlerClient({
    appKey: 'your_app_key'
  });
  
  // Подписка на канал
  const channel = pushler.subscribe('notifications');
  
  // Получение событий
  channel.on('new-message', (data) => {
    console.log('Получено:', data);
  });
</script>

Отправляйте события с сервера

Используйте PHP SDK для отправки событий:

PHP
use PushlerRu\PushlerClient;

$pushler = new PushlerClient(
    'your_app_key',
    'your_app_secret'
);

// Отправка события
$pushler->trigger('notifications', 'new-message', [
    'title' => 'Новое сообщение',
    'text' => 'Привет, мир!'
]);

Основные концепции

Каналы

Каналы — это способ организации сообщений. Клиенты подписываются на каналы и получают все события, отправленные в эти каналы.

Публичные Public

Открытые каналы для всех. Не требуют авторизации. Идеальны для новостей, объявлений, общих уведомлений.

Приватные Private

Защищённые каналы. Требуют подпись для подключения. Используйте для персональных уведомлений.

Presence Presence

Каналы присутствия. Позволяют отслеживать, кто сейчас онлайн. Идеальны для чатов.

События

События — это сообщения, которые отправляются в каналы. Каждое событие имеет имя и данные (payload). Клиенты подписываются на конкретные события внутри канала.

Автоматическая изоляция

Все каналы автоматически получают префикс с вашим app_key. Это обеспечивает полную изоляцию между разными приложениями — невозможно случайно получить доступ к каналам другого приложения.

JavaScript SDK

JavaScript SDK работает в браузере и в Node.js. Включает клиентскую и серверную части.

Установка

NPM

Bash
npm install @pushler/js

CDN

HTML
<script src="https://cdn.jsdelivr.net/npm/@pushler/js/dist/pushler-ru.min.js"></script>

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

JavaScript
const pushler = new PushlerClient({
  appKey: 'your_app_key',
  apiUrl: 'https://your-backend.com/api', // Для авторизации каналов
  autoConnect: true,
  reconnectDelay: 1000,
  maxReconnectAttempts: 5
});

Параметры конструктора

Параметр Тип По умолчанию Описание
appKey string Ключ приложения (обязательно)
wsUrl string wss://ws.pushler.ru/app/{appKey} URL WebSocket сервера (формируется автоматически)
apiUrl string URL вашего бэкенда для авторизации
autoConnect boolean true Автоматическое подключение
reconnectDelay number 1000 Задержка переподключения (мс)
maxReconnectAttempts number 5 Максимум попыток переподключения

События клиента

JavaScript
// Подключение установлено
pushler.on('connected', (data) => {
  console.log('Socket ID:', data.socketId);
});

// Отключение
pushler.on('disconnected', () => {
  console.log('Соединение разорвано');
});

// Ошибка
pushler.on('error', (error) => {
  console.error('Ошибка:', error);
});

// Изменение состояния
pushler.on('connection_state_changed', (state) => {
  console.log('Состояние:', state);
  // 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'failed'
});

Подписка на каналы

JavaScript
// Публичный канал
const channel = pushler.subscribe('news');

// Подписка на событие
channel.on('article-published', (data) => {
  console.log('Новая статья:', data.title);
});

// Подписка на все события канала
channel.on('*', (event, data) => {
  console.log(event, data);
});

// Отписка от события
channel.off('article-published', handler);

// Отписка от канала
pushler.unsubscribe('news');

Vue SDK

Vue SDK — реактивная обёртка над JavaScript SDK для Vue 3 приложений. Включает composables, Vue Plugin и полную поддержку TypeScript.

Установка

Bash
npm install @pushler/vue
Требования

Vue 3.0+

Особенности

  • 🎯 Composition API — полностью реактивный с ref, reactive, computed
  • 🔄 Автопереподключение — автоматическое восстановление соединения
  • 📦 Типизация — полная поддержка TypeScript
  • 🔌 Vue Plugin — глобальная инициализация через app.use()
  • 🎣 ComposablesusePushler, usePushlerChannel

Использование через Vue Plugin

JavaScript
// main.js
import { createApp } from 'vue';
import { PushlerPlugin } from '@pushler/vue';
import App from './App.vue';

const app = createApp(App);

app.use(PushlerPlugin, {
  appKey: 'key_your_app_key',
  autoConnect: true
});

app.mount('#app');

Использование через Composable

Vue
<script setup>
import { usePushler } from '@pushler/vue';
import { ref } from 'vue';

const messages = ref([]);

const pushler = usePushler({
  appKey: 'key_your_app_key'
});

// Подключение
pushler.connect();

// Подписка на канал
const channel = pushler.subscribe('notifications');
channel.on('new', (data) => {
  messages.value.push(data);
});
</script>

<template>
  <div>
    <p>Статус: {{ pushler.connectionState }}</p>
    <ul>
      <li v-for="msg in messages" :key="msg.id">{{ msg.text }}</li>
    </ul>
    <button @click="pushler.disconnect()">Отключиться</button>
  </div>
</template>

Composable usePushlerChannel

Для простых сценариев, когда нужен только один канал:

Vue
<script setup>
import { usePushlerChannel } from '@pushler/vue';

const { channel, isSubscribed, lastEvent } = usePushlerChannel('chat-room', {
  appKey: 'key_your_app_key',
  autoConnect: true
});

// Реактивное свойство lastEvent обновляется автоматически
</script>

<template>
  <div>
    <p v-if="isSubscribed">✅ Подписан на канал</p>
    <p v-else>⏳ Подключение...</p>
    <div v-if="lastEvent">
      Последнее событие: {{ lastEvent.event }} — {{ lastEvent.data }}
    </div>
  </div>
</template>

Реактивное состояние соединения

Vue SDK предоставляет реактивные свойства для отслеживания состояния:

Свойство Тип Описание
connectionState Ref<string> Состояние: 'connecting', 'connected', 'disconnected', 'reconnecting'
isConnected ComputedRef<boolean> true если соединение активно
socketId Ref<string | null> ID текущего WebSocket соединения

PHP SDK

PHP SDK предназначен для серверной интеграции: отправка событий, авторизация каналов, обработка вебхуков.

Установка

Bash
composer require pushler/php-sdk

packagist.org/packages/pushler/php-sdk

Требования

PHP >= 8.1, ext-curl

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

PHP
use PushlerRu\PushlerClient;

$pushler = new PushlerClient(
    'key_your_app_key',        // Ключ приложения
    'secret_your_app_secret'   // Секретный ключ
);

Отправка событий

PHP
// Отправка в один канал
$pushler->trigger('user_123', 'notification', [
    'title' => 'Новый заказ',
    'message' => 'Ваш заказ #456 принят'
]);

// Отправка в несколько каналов
$pushler->trigger(
    ['user_1', 'user_2', 'user_3'],
    'broadcast',
    ['message' => 'Важное объявление!']
);

// Исключить отправителя
$pushler->trigger(
    'chat_room',
    'new_message',
    ['text' => 'Привет!'],
    $socketId // socket_id отправителя
);

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

Для приватных и presence каналов необходимо реализовать endpoint авторизации на вашем бэкенде:

PHP (Laravel)
// routes/api.php
Route::post('/pushler/auth', function (Request $request) {
    $channelName = $request->input('channel_name');
    $socketId = $request->input('socket_id');
    
    // Проверьте права доступа пользователя!
    if (!auth()->check()) {
        return response()->json(['error' => 'Unauthorized'], 403);
    }
    
    // Для presence каналов
    if (str_starts_with($channelName, 'presence-')) {
        return $pushler->authorizePresenceChannel($channelName, $socketId, [
            'user_id' => (string) auth()->id(),
            'user_info' => [
                'name' => auth()->user()->name
            ]
        ]);
    }
    
    // Для приватных каналов
    return $pushler->authorizeChannel($channelName, $socketId);
})->middleware('auth');

Типы каналов

Pushler поддерживает три типа каналов для разных сценариев использования.

Тип Префикс Авторизация Применение
Публичный Не требуется Новости, объявления
Приватный private- HMAC подпись Личные уведомления
Presence presence- HMAC + user_data Чаты, онлайн-статусы

Публичные каналы

Любой клиент может подписаться на публичный канал без авторизации. Идеально для открытой информации: новостей, курсов валют, спортивных результатов.

JavaScript
// Подписка на публичный канал
const news = pushler.subscribe('public-news');

news.on('breaking', (data) => {
  alert('Срочная новость: ' + data.title);
});

Приватные каналы

Для подписки на приватный канал клиент должен получить подпись с вашего бэкенда. Это позволяет контролировать, кто имеет доступ к каналу.

Важно

Секретный ключ app_secret никогда не должен попадать в клиентский код! Подпись генерируется только на сервере.

JavaScript
// Подписка на приватный канал с автоматической авторизацией
const privateChannel = pushler.subscribe('private-user-123', {
  autoAuth: true // SDK автоматически запросит подпись с apiUrl
});

privateChannel.on('notification', (data) => {
  console.log('Личное уведомление:', data);
});

Presence каналы

Presence каналы позволяют отслеживать, какие пользователи сейчас подключены к каналу. Идеально для чатов, совместного редактирования, игр.

JavaScript
const chatRoom = pushler.subscribe('presence-chat-room', {
  autoAuth: true,
  user: {
    id: 'user-123',
    name: 'Иван Иванов'
  }
});

// Список пользователей в канале
const members = chatRoom.getPresenceData();

// Кто-то присоединился
chatRoom.on('user-joined', (user) => {
  console.log(user.name + ' вошёл в чат');
});

// Кто-то вышел
chatRoom.on('user-left', (user) => {
  console.log(user.name + ' покинул чат');
});

Аутентификация API

Все запросы к API подписываются с помощью HMAC-SHA256. SDK делает это автоматически.

Заголовки запроса

Заголовок Описание
X-Pushler-Key Ваш ключ приложения
X-Pushler-Timestamp Unix timestamp запроса
X-Pushler-Signature HMAC-SHA256 подпись

Формула подписи

Pseudo
signature = HMAC-SHA256(request_body + timestamp, app_secret)
Защита от replay атак

Timestamp должен быть не старше 5 минут. Запросы с устаревшим timestamp будут отклонены.

Отправка событий

POST /api/messages/send

Отправка события в канал.

JSON
{
  "channel": "user_123",
  "event": "notification",
  "data": {
    "title": "Новое сообщение",
    "text": "Привет!"
  }
}

POST /api/messages/broadcast

Отправка события в несколько каналов.

JSON
{
  "channels": ["user_1", "user_2", "user_3"],
  "event": "broadcast",
  "data": {
    "message": "Важное объявление!"
  }
}

Коды ошибок

HTTP код Описание
401 Неверная подпись или ключ приложения
403 Приложение неактивно
404 Приложение не найдено
422 Ошибка валидации данных
429 Превышен лимит запросов

Интеграция без SDK

Если для вашего языка программирования нет SDK, или вам нужен полный контроль — используйте протокол напрямую.

WebSocket протокол

Pushler использует стандартный WebSocket с JSON-сообщениями.

Подключение

URL
wss://ws.pushler.ru/app/{your_app_key}

Формат сообщений

Все сообщения — JSON-объекты с полями event и data:

JSON
{
  "event": "event_name",
  "data": { ... },
  "channel": "channel_name"  // опционально
}

Системные события сервера

Событие Описание Данные
pushler:connection_established Соединение установлено { "socket_id": "abc123.456" }
pushler:subscription_succeeded Подписка на канал успешна { "channel": "my-channel" }
pushler:auth_success Авторизация канала успешна { "channel": "private-..." }
pushler:auth_error Ошибка авторизации { "error": "Invalid signature" }
pushler:error Общая ошибка { "message": "...", "code": 4001 }

Команды клиента

Подписка на публичный канал:

JSON → Server
{
  "event": "pushler:subscribe",
  "data": {
    "channel": "your_app_key:news",
    "app_key": "your_app_key"
  }
}

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

JSON → Server
{
  "event": "pushler:auth",
  "data": {
    "app_key": "your_app_key",
    "channel": "your_app_key:private-user-123",
    "socket_id": "abc123.456",
    "signature": "hmac_sha256_signature"
  }
}

Отписка от канала:

JSON → Server
{
  "event": "pushler:unsubscribe",
  "data": {
    "channel": "your_app_key:news"
  }
}

Пример на чистом JavaScript

JavaScript
const APP_KEY = 'your_app_key';
const ws = new WebSocket(`wss://ws.pushler.ru/app/${APP_KEY}`);

let socketId = null;

ws.onopen = () => {
  console.log('WebSocket connected');
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  
  switch (msg.event) {
    case 'pushler:connection_established':
      socketId = msg.data.socket_id;
      console.log('Connected, socket_id:', socketId);
      
      // Подписываемся на публичный канал
      ws.send(JSON.stringify({
        event: 'pushler:subscribe',
        data: {
          channel: APP_KEY + ':notifications',
          app_key: APP_KEY
        }
      }));
      break;
      
    case 'pushler:subscription_succeeded':
      console.log('Subscribed to:', msg.channel);
      break;
      
    default:
      // Обычное событие канала
      console.log('Event:', msg.event, 'Data:', msg.data);
  }
};

ws.onclose = () => {
  console.log('Disconnected');
};

ws.onerror = (error) => {
  console.error('WebSocket error:', error);
};

Пример на Python

Python
import asyncio
import json
import websockets

APP_KEY = 'your_app_key'

async def connect():
    uri = f'wss://ws.pushler.ru/app/{APP_KEY}'
    
    async with websockets.connect(uri) as ws:
        async for message in ws:
            msg = json.loads(message)
            
            if msg['event'] == 'pushler:connection_established':
                socket_id = msg['data']['socket_id']
                print(f'Connected: {socket_id}')
                
                # Подписка на канал
                await ws.send(json.dumps({
                    'event': 'pushler:subscribe',
                    'data': {
                        'channel': f'{APP_KEY}:news',
                        'app_key': APP_KEY
                    }
                }))
            else:
                print(f'Event: {msg}')

asyncio.run(connect())

Пример на Go

Go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "github.com/gorilla/websocket"
)

const appKey = "your_app_key"

type Message struct {
    Event   string                 `json:"event"`
    Data    map[string]interface{} `json:"data"`
    Channel string                 `json:"channel,omitempty"`
}

func main() {
    url := fmt.Sprintf("wss://ws.pushler.ru/app/%s", appKey)
    conn, _, err := websocket.DefaultDialer.Dial(url, nil)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    for {
        _, message, err := conn.ReadMessage()
        if err != nil {
            log.Println("read error:", err)
            return
        }

        var msg Message
        json.Unmarshal(message, &msg)

        if msg.Event == "pushler:connection_established" {
            fmt.Println("Connected:", msg.Data["socket_id"])
            
            // Подписка
            sub := Message{
                Event: "pushler:subscribe",
                Data: map[string]interface{}{
                    "channel": appKey + ":news",
                    "app_key": appKey,
                },
            }
            conn.WriteJSON(sub)
        } else {
            fmt.Printf("Event: %s, Data: %v\n", msg.Event, msg.Data)
        }
    }
}

Пример на Java

Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.util.concurrent.CompletionStage;
import com.google.gson.Gson;
import com.google.gson.JsonObject;

public class PushlerClient {
    private static final String APP_KEY = "your_app_key";
    private String socketId;
    private WebSocket webSocket;
    private final Gson gson = new Gson();

    public void connect() {
        HttpClient client = HttpClient.newHttpClient();
        
        webSocket = client.newWebSocketBuilder()
            .buildAsync(
                URI.create("wss://ws.pushler.ru/app/" + APP_KEY),
                new WebSocket.Listener() {
                    
                    @Override
                    public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
                        JsonObject msg = gson.fromJson(data.toString(), JsonObject.class);
                        String event = msg.get("event").getAsString();
                        
                        if (event.equals("pushler:connection_established")) {
                            socketId = msg.getAsJsonObject("data")
                                .get("socket_id").getAsString();
                            System.out.println("Connected: " + socketId);
                            
                            // Подписка на канал
                            subscribe("notifications");
                        } else {
                            System.out.println("Event: " + event + ", Data: " + msg.get("data"));
                        }
                        
                        return WebSocket.Listener.super.onText(ws, data, last);
                    }
                    
                    @Override
                    public void onError(WebSocket ws, Throwable error) {
                        System.err.println("Error: " + error.getMessage());
                    }
                }
            ).join();
    }
    
    public void subscribe(String channel) {
        JsonObject data = new JsonObject();
        data.addProperty("channel", APP_KEY + ":" + channel);
        data.addProperty("app_key", APP_KEY);
        
        JsonObject msg = new JsonObject();
        msg.addProperty("event", "pushler:subscribe");
        msg.add("data", data);
        
        webSocket.sendText(gson.toJson(msg), true);
    }
    
    public static void main(String[] args) {
        PushlerClient client = new PushlerClient();
        client.connect();
        
        // Держим приложение активным
        try { Thread.sleep(Long.MAX_VALUE); } 
        catch (InterruptedException e) { }
    }
}

HTTP API для отправки событий

Для отправки событий с сервера используйте HTTP API.

Endpoint

HTTP
POST https://api.pushler.ru/messages/send

Заголовки

Заголовок Описание
Content-Type application/json
X-Pushler-Key Ваш app_key
X-Pushler-Timestamp Unix timestamp (секунды)
X-Pushler-Signature HMAC-SHA256 подпись

Тело запроса

JSON
{
  "channel": "notifications",
  "event": "new-message",
  "data": {
    "title": "Привет!",
    "body": "Это тестовое сообщение"
  }
}

Пример на cURL

Bash
APP_KEY="your_app_key"
APP_SECRET="your_app_secret"
TIMESTAMP=$(date +%s)
BODY='{"channel":"news","event":"update","data":{"message":"Hello!"}}'

# Генерация подписи
SIGNATURE=$(echo -n "${BODY}${TIMESTAMP}" | openssl dgst -sha256 -hmac "${APP_SECRET}" | awk '{print $2}')

curl -X POST https://api.pushler.ru/messages/send \
  -H "Content-Type: application/json" \
  -H "X-Pushler-Key: ${APP_KEY}" \
  -H "X-Pushler-Timestamp: ${TIMESTAMP}" \
  -H "X-Pushler-Signature: ${SIGNATURE}" \
  -d "${BODY}"

Подпись запросов

Все запросы к API подписываются HMAC-SHA256 для защиты от подделки.

Алгоритм

Pseudo
signature = HMAC-SHA256(request_body + timestamp, app_secret)
Важно

Timestamp должен быть не старше 5 минут. Это защищает от replay-атак.

Реализация на разных языках

Python:

Python
import hmac
import hashlib
import time
import json
import requests

def send_event(channel, event, data):
    app_key = 'your_app_key'
    app_secret = 'your_app_secret'
    
    timestamp = str(int(time.time()))
    body = json.dumps({
        'channel': channel,
        'event': event,
        'data': data
    }, separators=(',', ':'))
    
    # Генерация подписи
    message = (body + timestamp).encode('utf-8')
    signature = hmac.new(
        app_secret.encode('utf-8'),
        message,
        hashlib.sha256
    ).hexdigest()
    
    response = requests.post(
        'https://api.pushler.ru/messages/send',
        headers={
            'Content-Type': 'application/json',
            'X-Pushler-Key': app_key,
            'X-Pushler-Timestamp': timestamp,
            'X-Pushler-Signature': signature
        },
        data=body
    )
    
    return response.json()

# Использование
send_event('notifications', 'new-order', {'order_id': 123})

Node.js:

JavaScript
const crypto = require('crypto');

async function sendEvent(channel, event, data) {
  const appKey = 'your_app_key';
  const appSecret = 'your_app_secret';
  
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const body = JSON.stringify({ channel, event, data });
  
  // Генерация подписи
  const signature = crypto
    .createHmac('sha256', appSecret)
    .update(body + timestamp)
    .digest('hex');
  
  const response = await fetch('https://api.pushler.ru/messages/send', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Pushler-Key': appKey,
      'X-Pushler-Timestamp': timestamp,
      'X-Pushler-Signature': signature
    },
    body: body
  });
  
  return response.json();
}

// Использование
sendEvent('notifications', 'new-order', { order_id: 123 });

Go:

Go
package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "net/http"
    "strconv"
    "time"
)

func sendEvent(channel, event string, data interface{}) error {
    appKey := "your_app_key"
    appSecret := "your_app_secret"
    
    timestamp := strconv.FormatInt(time.Now().Unix(), 10)
    
    payload := map[string]interface{}{
        "channel": channel,
        "event":   event,
        "data":    data,
    }
    body, _ := json.Marshal(payload)
    
    // Генерация подписи
    h := hmac.New(sha256.New, []byte(appSecret))
    h.Write(append(body, []byte(timestamp)...))
    signature := hex.EncodeToString(h.Sum(nil))
    
    req, _ := http.NewRequest("POST", "https://api.pushler.ru/messages/send", bytes.NewBuffer(body))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-Pushler-Key", appKey)
    req.Header.Set("X-Pushler-Timestamp", timestamp)
    req.Header.Set("X-Pushler-Signature", signature)
    
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    fmt.Println("Status:", resp.Status)
    return nil
}

Ruby:

Ruby
require 'openssl'
require 'json'
require 'net/http'
require 'uri'

def send_event(channel, event, data)
  app_key = 'your_app_key'
  app_secret = 'your_app_secret'
  
  timestamp = Time.now.to_i.to_s
  body = { channel: channel, event: event, data: data }.to_json
  
  # Генерация подписи
  signature = OpenSSL::HMAC.hexdigest('SHA256', app_secret, body + timestamp)
  
  uri = URI('https://api.pushler.ru/messages/send')
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  
  request = Net::HTTP::Post.new(uri)
  request['Content-Type'] = 'application/json'
  request['X-Pushler-Key'] = app_key
  request['X-Pushler-Timestamp'] = timestamp
  request['X-Pushler-Signature'] = signature
  request.body = body
  
  response = http.request(request)
  JSON.parse(response.body)
end

# Использование
send_event('notifications', 'new-order', { order_id: 123 })

Java:

Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import com.google.gson.Gson;
import java.util.Map;

public class PushlerAPI {
    private static final String APP_KEY = "your_app_key";
    private static final String APP_SECRET = "your_app_secret";
    private static final String API_URL = "https://api.pushler.ru/messages/send";
    private final Gson gson = new Gson();
    private final HttpClient client = HttpClient.newHttpClient();

    public String sendEvent(String channel, String event, Object data) throws Exception {
        String timestamp = String.valueOf(Instant.now().getEpochSecond());
        
        Map<String, Object> payload = Map.of(
            "channel", channel,
            "event", event,
            "data", data
        );
        String body = gson.toJson(payload);
        
        // Генерация подписи
        String signature = hmacSha256(body + timestamp, APP_SECRET);
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(API_URL))
            .header("Content-Type", "application/json")
            .header("X-Pushler-Key", APP_KEY)
            .header("X-Pushler-Timestamp", timestamp)
            .header("X-Pushler-Signature", signature)
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        
        HttpResponse<String> response = client.send(
            request, 
            HttpResponse.BodyHandlers.ofString()
        );
        
        return response.body();
    }
    
    private String hmacSha256(String data, String secret) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec keySpec = new SecretKeySpec(
            secret.getBytes(StandardCharsets.UTF_8), 
            "HmacSHA256"
        );
        mac.init(keySpec);
        byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        
        StringBuilder hex = new StringBuilder();
        for (byte b : hmacBytes) {
            hex.append(String.format("%02x", b));
        }
        return hex.toString();
    }
    
    public static void main(String[] args) throws Exception {
        PushlerAPI api = new PushlerAPI();
        String result = api.sendEvent(
            "notifications", 
            "new-order", 
            Map.of("order_id", 123)
        );
        System.out.println(result);
    }
}

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

Для подписки на приватные и presence каналы клиент должен получить подпись с вашего бэкенда.

Процесс авторизации

  1. Клиент подключается к WebSocket и получает socket_id
  2. Клиент отправляет socket_id + channel_name на ваш бэкенд
  3. Бэкенд проверяет права пользователя
  4. Бэкенд генерирует подпись и возвращает клиенту
  5. Клиент отправляет подпись на WebSocket сервер

Формула подписи канала

Pseudo
// Для приватных каналов
string_to_sign = socket_id + ":" + channel_name
signature = HMAC-SHA256(string_to_sign, app_secret)

// Для presence каналов
user_data = JSON.stringify({ id: "user_id", name: "User Name" })
string_to_sign = socket_id + ":" + channel_name + ":" + user_data
signature = HMAC-SHA256(string_to_sign, app_secret)

Endpoint авторизации (ваш бэкенд)

Python (Flask)
from flask import Flask, request, jsonify
import hmac
import hashlib
import json

app = Flask(__name__)

APP_SECRET = 'your_app_secret'

@app.route('/pushler/auth', methods=['POST'])
def pushler_auth():
    # Проверка авторизации пользователя
    user = get_current_user()  # Ваша логика аутентификации
    if not user:
        return jsonify({'error': 'Unauthorized'}), 403
    
    socket_id = request.json.get('socket_id')
    channel_name = request.json.get('channel_name')
    
    # Проверка доступа к каналу
    if not user_can_access_channel(user, channel_name):
        return jsonify({'error': 'Forbidden'}), 403
    
    # Генерация подписи
    if channel_name.startswith('presence-'):
        user_data = json.dumps({
            'id': str(user.id),
            'name': user.name
        }, separators=(',', ':'))
        string_to_sign = f'{socket_id}:{channel_name}:{user_data}'
    else:
        string_to_sign = f'{socket_id}:{channel_name}'
    
    signature = hmac.new(
        APP_SECRET.encode('utf-8'),
        string_to_sign.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    return jsonify({'signature': signature})

Java (Spring Boot):

Java
import org.springframework.web.bind.annotation.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Map;

@RestController
public class PushlerAuthController {
    
    private static final String APP_SECRET = "your_app_secret";
    
    @PostMapping("/pushler/auth")
    public Map<String, String> auth(@RequestBody AuthRequest request) {
        // Получаем текущего пользователя (Spring Security)
        User user = getCurrentUser();
        if (user == null) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN);
        }
        
        String stringToSign;
        if (request.channelName.startsWith("presence-")) {
            // Для presence каналов добавляем user_data
            String userData = String.format(
                "{\"id\":\"%s\",\"name\":\"%s\"}",
                user.getId(), user.getName()
            );
            stringToSign = request.socketId + ":" 
                + request.channelName + ":" + userData;
        } else {
            stringToSign = request.socketId + ":" + request.channelName;
        }
        
        String signature = hmacSha256(stringToSign, APP_SECRET);
        return Map.of("signature", signature);
    }
    
    private String hmacSha256(String data, String secret) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8), 
                "HmacSHA256"
            ));
            byte[] bytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
            StringBuilder hex = new StringBuilder();
            for (byte b : bytes) {
                hex.append(String.format("%02x", b));
            }
            return hex.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

record AuthRequest(
    @JsonProperty("socket_id") String socketId,
    @JsonProperty("channel_name") String channelName
) {}

Клиент запрашивает авторизацию

JavaScript
async function subscribeToPrivateChannel(channelName) {
  // 1. Получаем подпись с бэкенда
  const authResponse = await fetch('/pushler/auth', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      socket_id: socketId,
      channel_name: channelName
    })
  });
  
  const { signature } = await authResponse.json();
  
  // 2. Отправляем авторизацию на WebSocket
  ws.send(JSON.stringify({
    event: 'pushler:auth',
    data: {
      app_key: APP_KEY,
      channel: APP_KEY + ':' + channelName,
      socket_id: socketId,
      signature: signature
    }
  }));
}

Python клиент:

Python
import asyncio
import json
import requests
import websockets

APP_KEY = 'your_app_key'
AUTH_URL = 'https://your-backend.com/pushler/auth'

async def subscribe_private():
    uri = f'wss://ws.pushler.ru/app/{APP_KEY}'
    
    async with websockets.connect(uri) as ws:
        # Ждём connection_established
        msg = json.loads(await ws.recv())
        socket_id = msg['data']['socket_id']
        print(f'Connected: {socket_id}')
        
        channel_name = 'private-user-123'
        
        # 1. Получаем подпись с бэкенда
        auth_response = requests.post(AUTH_URL, json={
            'socket_id': socket_id,
            'channel_name': channel_name
        })
        signature = auth_response.json()['signature']
        
        # 2. Отправляем авторизацию
        await ws.send(json.dumps({
            'event': 'pushler:auth',
            'data': {
                'app_key': APP_KEY,
                'channel': f'{APP_KEY}:{channel_name}',
                'socket_id': socket_id,
                'signature': signature
            }
        }))
        
        # Слушаем события
        async for message in ws:
            msg = json.loads(message)
            print(f'Event: {msg}')

asyncio.run(subscribe_private())
Формат имени канала

Все каналы автоматически получают префикс {app_key}:. Например, канал private-user-123 становится your_app_key:private-user-123.

Пример: Чат

Реализация простого чата с отображением онлайн-пользователей.

Клиентская часть (JavaScript)

JavaScript
const pushler = new PushlerClient({
  appKey: 'your_app_key',
  apiUrl: '/api'
});

pushler.on('connected', () => {
  // Подписка на чат-комнату
  const chat = pushler.subscribe('presence-room-1', {
    autoAuth: true,
    user: { id: userId, name: userName }
  });
  
  // Новое сообщение
  chat.on('message', (msg) => {
    addMessage(msg.user, msg.text);
  });
  
  // Пользователь печатает
  chat.on('typing', (data) => {
    showTypingIndicator(data.user);
  });
  
  // Обновление списка онлайн
  chat.on('user-joined', updateOnlineList);
  chat.on('user-left', updateOnlineList);
});

Серверная часть (PHP)

PHP
// Отправка сообщения в чат
public function sendMessage(Request $request)
{
    $message = Message::create([
        'room_id' => $request->room_id,
        'user_id' => auth()->id(),
        'text' => $request->text
    ]);
    
    $this->pushler->trigger(
        'presence-room-' . $request->room_id,
        'message',
        [
            'id' => $message->id,
            'user' => auth()->user()->name,
            'text' => $message->text,
            'time' => $message->created_at->toISOString()
        ]
    );
    
    return response()->json($message);
}

Пример: Уведомления

Система push-уведомлений для пользователей.

PHP
// Уведомление одному пользователю
$pushler->trigger('private-user-' . $userId, 'notification', [
    'type' => 'order',
    'title' => 'Заказ отправлен',
    'message' => 'Ваш заказ #123 передан в доставку',
    'url' => '/orders/123',
    'icon' => 'truck'
]);

// Уведомление всем администраторам
$adminChannels = User::where('role', 'admin')
    ->pluck('id')
    ->map(fn($id) => 'private-user-' . $id)
    ->toArray();

$pushler->trigger($adminChannels, 'admin-alert', [
    'type' => 'warning',
    'message' => 'Требуется проверка заказа #456'
]);
JavaScript
// Получение уведомлений на клиенте
const userChannel = pushler.subscribe('private-user-' + userId, {
  autoAuth: true
});

userChannel.on('notification', (data) => {
  // Показать toast-уведомление
  showToast({
    title: data.title,
    message: data.message,
    icon: data.icon,
    onClick: () => navigate(data.url)
  });
  
  // Обновить счётчик непрочитанных
  updateBadge();
});

Пример: Live-дашборд

Обновление статистики и метрик в реальном времени — курсы валют, аналитика, мониторинг.

Серверная часть (PHP)

PHP
// Публикация обновлений метрик (вызывается по расписанию)
class MetricsPublisher
{
    public function publishStats()
    {
        $stats = [
            'orders_today' => Order::whereDate('created_at', today())->count(),
            'revenue_today' => Order::whereDate('created_at', today())->sum('total'),
            'active_users' => User::where('last_seen', '>', now()->subMinutes(5))->count(),
            'updated_at' => now()->toISOString()
        ];
        
        // Отправляем в публичный канал для всех менеджеров
        $this->pushler->trigger('dashboard-stats', 'stats-updated', $stats);
    }
    
    public function publishCurrencyRates()
    {
        $rates = [
            'USD' => ['value' => 92.50, 'change' => +0.25],
            'EUR' => ['value' => 100.80, 'change' => -0.15],
            'CNY' => ['value' => 12.75, 'change' => +0.05],
        ];
        
        $this->pushler->trigger('currency-rates', 'rates-updated', $rates);
    }
}

Клиентская часть (JavaScript)

JavaScript
// Подписка на обновления статистики
const statsChannel = pushler.subscribe('dashboard-stats');

statsChannel.on('stats-updated', (stats) => {
  // Обновляем карточки с анимацией
  animateValue('#orders-today', stats.orders_today);
  animateValue('#revenue-today', stats.revenue_today, { prefix: '₽' });
  animateValue('#active-users', stats.active_users);
  
  // Показываем время последнего обновления
  document.querySelector('#last-update').textContent = 
    new Date(stats.updated_at).toLocaleTimeString();
});

// Подписка на курсы валют
const ratesChannel = pushler.subscribe('currency-rates');

ratesChannel.on('rates-updated', (rates) => {
  Object.entries(rates).forEach(([currency, data]) => {
    const el = document.querySelector(`#rate-${currency}`);
    el.querySelector('.value').textContent = data.value.toFixed(2);
    
    // Подсветка изменения
    const changeEl = el.querySelector('.change');
    changeEl.textContent = (data.change > 0 ? '+' : '') + data.change;
    changeEl.className = 'change ' + (data.change > 0 ? 'up' : 'down');
  });
});

Пример: Отслеживание заказа

Обновление статуса заказа в реальном времени для покупателей и менеджеров.

Серверная часть (PHP)

PHP
class OrderService
{
    public function updateStatus(Order $order, string $status)
    {
        $order->update(['status' => $status]);
        
        $statusInfo = [
            'order_id' => $order->id,
            'status' => $status,
            'status_label' => $this->getStatusLabel($status),
            'updated_at' => now()->toISOString(),
            'timeline' => $this->getTimeline($order)
        ];
        
        // Уведомляем покупателя
        $this->pushler->trigger(
            'private-user-' . $order->user_id,
            'order-status-changed',
            $statusInfo
        );
        
        // Уведомляем менеджеров
        $this->pushler->trigger(
            'orders-management',
            'order-updated',
            $statusInfo
        );
    }
    
    public function updateDeliveryLocation(Order $order, float $lat, float $lng)
    {
        // Обновление геолокации курьера
        $this->pushler->trigger(
            'private-order-' . $order->id,
            'courier-location',
            [
                'lat' => $lat,
                'lng' => $lng,
                'eta' => $this->calculateETA($lat, $lng, $order)
            ]
        );
    }
}

Клиентская часть (JavaScript)

JavaScript
// Отслеживание статуса заказа
const userChannel = pushler.subscribe('private-user-' + userId, {
  autoAuth: true
});

userChannel.on('order-status-changed', (data) => {
  // Обновляем статус на странице
  updateOrderStatus(data.order_id, data.status, data.status_label);
  
  // Обновляем таймлайн
  renderTimeline(data.timeline);
  
  // Push-уведомление если вкладка не активна
  if (document.hidden && 'Notification' in window) {
    new Notification('Статус заказа обновлён', {
      body: data.status_label,
      icon: '/icons/order.png'
    });
  }
});

// Отслеживание курьера на карте
const orderChannel = pushler.subscribe('private-order-' + orderId, {
  autoAuth: true
});

orderChannel.on('courier-location', (data) => {
  // Обновляем маркер курьера на карте
  courierMarker.setLatLng([data.lat, data.lng]);
  
  // Показываем ETA
  document.querySelector('#eta').textContent = 
    `Примерное время: ${data.eta} мин`;
});

Пример: Совместное редактирование

Реализация коллаборативного редактирования документов в стиле Google Docs.

Клиентская часть (JavaScript)

JavaScript
const docChannel = pushler.subscribe('presence-document-' + documentId, {
  autoAuth: true,
  user: {
    id: currentUser.id,
    name: currentUser.name,
    avatar: currentUser.avatar,
    color: getRandomColor() // Уникальный цвет курсора
  }
});

// Получение списка участников
docChannel.on('channel_subscribed', () => {
  const members = docChannel.getPresenceData();
  renderCollaborators(members);
});

// Кто-то присоединился
docChannel.on('user-joined', (user) => {
  addCollaborator(user);
  showToast(`${user.name} открыл документ`);
});

// Отслеживание курсора других пользователей
docChannel.on('cursor-move', (data) => {
  updateRemoteCursor(data.userId, data.position, data.color);
});

// Получение изменений текста
docChannel.on('text-change', (delta) => {
  // Применяем изменения к редактору (например, Quill)
  editor.updateContents(delta.ops, 'api');
});

// Отправка своих изменений
editor.on('text-change', (delta, oldDelta, source) => {
  if (source !== 'user') return;
  
  // Отправляем изменения на сервер
  fetch('/api/documents/' + documentId + '/changes', {
    method: 'POST',
    body: JSON.stringify({ delta: delta.ops })
  });
});

// Отслеживание позиции курсора
editor.on('selection-change', (range) => {
  if (range) {
    fetch('/api/documents/' + documentId + '/cursor', {
      method: 'POST',
      body: JSON.stringify({ position: range.index })
    });
  }
});

Серверная часть (PHP)

PHP
class DocumentController
{
    public function saveChange(Request $request, Document $document)
    {
        // Сохраняем изменения
        $document->applyDelta($request->delta);
        
        // Рассылаем всем участникам (кроме автора)
        $this->pushler->trigger(
            'presence-document-' . $document->id,
            'text-change',
            [
                'ops' => $request->delta,
                'userId' => auth()->id()
            ],
            $request->header('X-Socket-ID') // Исключаем автора
        );
    }
    
    public function updateCursor(Request $request, Document $document)
    {
        $user = auth()->user();
        
        $this->pushler->trigger(
            'presence-document-' . $document->id,
            'cursor-move',
            [
                'userId' => $user->id,
                'position' => $request->position,
                'color' => $user->cursor_color
            ],
            $request->header('X-Socket-ID')
        );
    }
}

Пример: Онлайн-аукцион

Реализация аукциона с ставками в реальном времени и обратным отсчётом.

Серверная часть (PHP)

PHP
class AuctionService
{
    public function placeBid(Auction $auction, User $user, float $amount)
    {
        // Проверка валидности ставки
        if ($amount <= $auction->current_bid) {
            throw new InvalidBidException('Ставка должна быть выше текущей');
        }
        
        // Создаём ставку
        $bid = Bid::create([
            'auction_id' => $auction->id,
            'user_id' => $user->id,
            'amount' => $amount
        ]);
        
        $auction->update(['current_bid' => $amount]);
        
        // Продлеваем аукцион если ставка в последнюю минуту
        if ($auction->ends_at->diffInSeconds(now()) < 60) {
            $auction->update([
                'ends_at' => $auction->ends_at->addMinutes(2)
            ]);
        }
        
        // Уведомляем всех участников
        $this->pushler->trigger(
            'auction-' . $auction->id,
            'new-bid',
            [
                'bid_id' => $bid->id,
                'amount' => $amount,
                'bidder' => $user->display_name,
                'ends_at' => $auction->ends_at->toISOString(),
                'bid_count' => $auction->bids()->count()
            ]
        );
        
        // Уведомляем предыдущего лидера что его перебили
        if ($previousBid = $auction->previousLeadingBid()) {
            $this->pushler->trigger(
                'private-user-' . $previousBid->user_id,
                'outbid',
                [
                    'auction_id' => $auction->id,
                    'auction_title' => $auction->title,
                    'new_amount' => $amount
                ]
            );
        }
        
        return $bid;
    }
}

Клиентская часть (JavaScript)

JavaScript
// Подписка на аукцион
const auctionChannel = pushler.subscribe('auction-' + auctionId);
let countdownInterval;

auctionChannel.on('new-bid', (data) => {
  // Обновляем текущую ставку с анимацией
  animateBidUpdate(data.amount);
  
  // Добавляем в историю ставок
  addBidToHistory({
    bidder: data.bidder,
    amount: data.amount,
    time: new Date()
  });
  
  // Обновляем обратный отсчёт
  updateCountdown(new Date(data.ends_at));
  
  // Показываем уведомление
  showToast(`${data.bidder} сделал ставку ₽${data.amount.toLocaleString()}`);
});

// Уведомление о перебитой ставке (приватный канал)
const userChannel = pushler.subscribe('private-user-' + userId, {
  autoAuth: true
});

userChannel.on('outbid', (data) => {
  showAlert({
    type: 'warning',
    title: 'Вашу ставку перебили!',
    message: `Новая ставка: ₽${data.new_amount.toLocaleString()}`,
    actions: [
      { label: 'Повысить ставку', href: `/auctions/${data.auction_id}` }
    ]
  });
});

// Обратный отсчёт
function updateCountdown(endsAt) {
  clearInterval(countdownInterval);
  
  countdownInterval = setInterval(() => {
    const diff = endsAt - new Date();
    
    if (diff <= 0) {
      clearInterval(countdownInterval);
      showAuctionEnded();
      return;
    }
    
    const hours = Math.floor(diff / 3600000);
    const minutes = Math.floor((diff % 3600000) / 60000);
    const seconds = Math.floor((diff % 60000) / 1000);
    
    document.querySelector('#countdown').textContent = 
      `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
    
    // Подсветка последней минуты
    if (diff < 60000) {
      document.querySelector('#countdown').classList.add('urgent');
    }
  }, 1000);
}