Документация Pushler.ru
Pushler.ru — это надежный WebSocket-сервис для мгновенной доставки сообщений в реальном времени. Простая интеграция, высокая производительность, масштабируемость.
Быстрый старт
Интеграция Pushler занимает всего несколько минут. Следуйте этим шагам:
Создайте приложение
Зарегистрируйтесь в панели управления и создайте новое приложение. Вы получите ключи app_key и app_secret.
Подключите SDK на клиенте
Добавьте JavaScript SDK на ваш сайт:
<!-- Подключение 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 для отправки событий:
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
npm install @pushler/js
CDN
<script src="https://cdn.jsdelivr.net/npm/@pushler/js/dist/pushler-ru.min.js"></script>
Инициализация
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 |
Максимум попыток переподключения |
События клиента
// Подключение установлено 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' });
Подписка на каналы
// Публичный канал 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.
Установка
npm install @pushler/vue
Vue 3.0+
Особенности
- 🎯 Composition API — полностью реактивный с
ref,reactive,computed - 🔄 Автопереподключение — автоматическое восстановление соединения
- 📦 Типизация — полная поддержка TypeScript
- 🔌 Vue Plugin — глобальная инициализация через
app.use() - 🎣 Composables —
usePushler,usePushlerChannel
Использование через Vue Plugin
// 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
<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
Для простых сценариев, когда нужен только один канал:
<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 предназначен для серверной интеграции: отправка событий, авторизация каналов, обработка вебхуков.
Установка
composer require pushler/php-sdk
packagist.org/packages/pushler/php-sdk
PHP >= 8.1, ext-curl
Инициализация
use PushlerRu\PushlerClient; $pushler = new PushlerClient( 'key_your_app_key', // Ключ приложения 'secret_your_app_secret' // Секретный ключ );
Отправка событий
// Отправка в один канал $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 авторизации на вашем бэкенде:
// 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 | Чаты, онлайн-статусы |
Публичные каналы
Любой клиент может подписаться на публичный канал без авторизации. Идеально для открытой информации: новостей, курсов валют, спортивных результатов.
// Подписка на публичный канал const news = pushler.subscribe('public-news'); news.on('breaking', (data) => { alert('Срочная новость: ' + data.title); });
Приватные каналы
Для подписки на приватный канал клиент должен получить подпись с вашего бэкенда. Это позволяет контролировать, кто имеет доступ к каналу.
Секретный ключ app_secret никогда не должен попадать в клиентский код! Подпись генерируется только на сервере.
// Подписка на приватный канал с автоматической авторизацией const privateChannel = pushler.subscribe('private-user-123', { autoAuth: true // SDK автоматически запросит подпись с apiUrl }); privateChannel.on('notification', (data) => { console.log('Личное уведомление:', data); });
Presence каналы
Presence каналы позволяют отслеживать, какие пользователи сейчас подключены к каналу. Идеально для чатов, совместного редактирования, игр.
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 подпись |
Формула подписи
signature = HMAC-SHA256(request_body + timestamp, app_secret)
Timestamp должен быть не старше 5 минут. Запросы с устаревшим timestamp будут отклонены.
Отправка событий
POST /api/messages/send
Отправка события в канал.
{
"channel": "user_123",
"event": "notification",
"data": {
"title": "Новое сообщение",
"text": "Привет!"
}
}
POST /api/messages/broadcast
Отправка события в несколько каналов.
{
"channels": ["user_1", "user_2", "user_3"],
"event": "broadcast",
"data": {
"message": "Важное объявление!"
}
}
Коды ошибок
| HTTP код | Описание |
|---|---|
401 |
Неверная подпись или ключ приложения |
403 |
Приложение неактивно |
404 |
Приложение не найдено |
422 |
Ошибка валидации данных |
429 |
Превышен лимит запросов |
Интеграция без SDK
Если для вашего языка программирования нет SDK, или вам нужен полный контроль — используйте протокол напрямую.
WebSocket протокол
Pushler использует стандартный WebSocket с JSON-сообщениями.
Подключение
wss://ws.pushler.ru/app/{your_app_key}
Формат сообщений
Все сообщения — JSON-объекты с полями event и data:
{
"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 } |
Команды клиента
Подписка на публичный канал:
{
"event": "pushler:subscribe",
"data": {
"channel": "your_app_key:news",
"app_key": "your_app_key"
}
}
Авторизация приватного канала:
{
"event": "pushler:auth",
"data": {
"app_key": "your_app_key",
"channel": "your_app_key:private-user-123",
"socket_id": "abc123.456",
"signature": "hmac_sha256_signature"
}
}
Отписка от канала:
{
"event": "pushler:unsubscribe",
"data": {
"channel": "your_app_key:news"
}
}
Пример на чистом 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
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
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
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
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 подпись |
Тело запроса
{
"channel": "notifications",
"event": "new-message",
"data": {
"title": "Привет!",
"body": "Это тестовое сообщение"
}
}
Пример на cURL
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 для защиты от подделки.
Алгоритм
signature = HMAC-SHA256(request_body + timestamp, app_secret)
Timestamp должен быть не старше 5 минут. Это защищает от replay-атак.
Реализация на разных языках
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:
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:
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:
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:
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 каналы клиент должен получить подпись с вашего бэкенда.
Процесс авторизации
- Клиент подключается к WebSocket и получает
socket_id - Клиент отправляет
socket_id+channel_nameна ваш бэкенд - Бэкенд проверяет права пользователя
- Бэкенд генерирует подпись и возвращает клиенту
- Клиент отправляет подпись на WebSocket сервер
Формула подписи канала
// Для приватных каналов 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 авторизации (ваш бэкенд)
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):
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 ) {}
Клиент запрашивает авторизацию
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 клиент:
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)
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)
// Отправка сообщения в чат 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-уведомлений для пользователей.
// Уведомление одному пользователю $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' ]);
// Получение уведомлений на клиенте 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)
// Публикация обновлений метрик (вызывается по расписанию) 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)
// Подписка на обновления статистики 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)
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)
// Отслеживание статуса заказа 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)
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)
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)
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)
// Подписка на аукцион 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); }