📋 Что получится
Любой бэкенд: Presence-каналы работают с любым языком. Примеры на PHP, но концепция универсальна.
Сейчас онлайн 5
Алиса Иванова
Борис Петров
Вера Сидорова (отошла)
Геннадий Козлов
Дарья Новикова
- Список онлайн-пользователей в реальном времени
- Мгновенное обновление при входе/выходе
- Информация о пользователях (имя, аватар, статус)
- Счётчик онлайн
🏗️ Как работают Presence-каналы
1
Пользователь подписывается на presence-канал
↓
2
Сервер авторизует и возвращает данные пользователя
↓
3
Pushler добавляет пользователя в список присутствия
↓
4
Всем участникам приходит событие member_added
↓
5
При отключении — событие member_removed
Ключевые события:
pushler:subscription_succeeded— успешная подписка (содержит список всех)pushler:member_added— новый участник вошёлpushler:member_removed— участник вышел
💻 Код сервера
Сервер авторизует пользователей для presence-каналов. Примеры на PHP — принцип одинаков для всех языков.
Установка PHP SDK
Bash
composer require pushler/php-sdk
Авторизация Presence-канала
PHP
<?php
// auth.php — авторизация каналов Pushler
require_once 'vendor/autoload.php';
use PushlerRu\PushlerClient;
// Инициализация Pushler
$pushler = new PushlerClient(
'key_ваш_ключ', // App Key
'secret_ваш_секрет' // App Secret
);
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;
}
// Проверяем доступ к каналу
if (!canAccessChannel($userId, $channelName)) {
http_response_code(403);
echo json_encode(['error' => 'Forbidden']);
exit;
}
// Для presence-каналов передаём данные пользователя
if (str_starts_with($channelName, 'presence-')) {
$user = getUserById($userId);
$auth = $pushler->authorizePresenceChannel(
$channelName,
$socketId,
[
'user_id' => (string) $userId, // Обязательно строка!
'user_info' => [
'name' => $user['name'],
'avatar' => $user['avatar'],
'email' => $user['email'],
'role' => $user['role'] ?? 'user'
]
]
);
echo json_encode($auth);
exit;
}
// Для приватных каналов
$auth = $pushler->authorizeChannel($channelName, $socketId);
echo json_encode($auth);
// ============================================
// Функция проверки доступа к каналу
// ============================================
function canAccessChannel(int $userId, string $channelName): bool
{
// Presence-канал чата — проверяем участие
if (preg_match('/^presence-chat-(\d+)$/', $channelName, $matches)) {
$chatId = (int) $matches[1];
return userHasAccessToChat($userId, $chatId);
}
// Presence-канал документа — проверяем права
if (preg_match('/^presence-document-(\d+)$/', $channelName, $matches)) {
$documentId = (int) $matches[1];
return userCanViewDocument($userId, $documentId);
}
// Общий канал приложения — всем авторизованным
if ($channelName === 'presence-app') {
return true;
}
// Приватный канал пользователя — только владельцу
if (preg_match('/^private-user-(\d+)$/', $channelName, $matches)) {
return (int) $matches[1] === $userId;
}
return false;
}
Отправка событий в Presence-канал
PHP
<?php
// notify_room.php — отправить уведомление всем в комнате
require_once 'vendor/autoload.php';
use PushlerRu\PushlerClient;
$pushler = new PushlerClient('key_ваш_ключ', 'secret_ваш_секрет');
$roomId = (int) $_POST['room_id'];
$message = $_POST['message'];
// Отправляем в presence-канал комнаты
$pushler->trigger(
"presence-room-{$roomId}",
'announcement',
[
'message' => $message,
'timestamp' => date('c')
]
);
echo json_encode(['success' => true]);
🌐 Код клиента (JavaScript)
Vanilla JavaScript — класс OnlineUsers
JavaScript
/**
* Класс для отслеживания онлайн-пользователей через Presence-канал
*/
class OnlineUsers {
constructor(pushler, channelName) {
this.pushler = pushler;
this.channelName = channelName;
this.members = new Map();
this.channel = null;
// Callbacks
this.onMembersChange = null;
this.onMemberJoined = null;
this.onMemberLeft = null;
}
async connect() {
return new Promise((resolve, reject) => {
this.channel = this.pushler.subscribe(this.channelName);
// Получили список всех участников при подписке
this.channel.on('pushler:subscription_succeeded', (data) => {
this.members.clear();
if (data.members) {
Object.entries(data.members).forEach(([id, info]) => {
this.members.set(id, info);
});
}
this.notifyChange();
resolve(this.getMembersList());
});
// Новый участник вошёл
this.channel.on('pushler:member_added', (member) => {
this.members.set(member.id, member.info);
this.notifyChange();
if (this.onMemberJoined) {
this.onMemberJoined(member);
}
});
// Участник вышел
this.channel.on('pushler:member_removed', (member) => {
this.members.delete(member.id);
this.notifyChange();
if (this.onMemberLeft) {
this.onMemberLeft(member);
}
});
// Ошибка подписки
this.channel.on('pushler:subscription_error', (error) => {
reject(error);
});
});
}
getMembersList() {
return Array.from(this.members.entries()).map(([id, info]) => ({
id,
...info
}));
}
getMembersCount() {
return this.members.size;
}
isMember(userId) {
return this.members.has(String(userId));
}
notifyChange() {
if (this.onMembersChange) {
this.onMembersChange(this.getMembersList());
}
}
disconnect() {
if (this.channel) {
this.channel.unsubscribe();
this.channel = null;
}
this.members.clear();
}
}
// === Использование ===
const pushler = new PushlerClient({
appKey: 'key_ваш_ключ',
authEndpoint: '/auth.php'
});
pushler.on('connected', async () => {
const onlineUsers = new OnlineUsers(pushler, 'presence-chat-123');
onlineUsers.onMembersChange = (members) => {
updateOnlineList(members);
updateOnlineCount(members.length);
};
onlineUsers.onMemberJoined = (member) => {
showNotification(`${member.info.name} присоединился`);
};
onlineUsers.onMemberLeft = (member) => {
showNotification(`${member.info.name} вышел`);
};
await onlineUsers.connect();
});
function updateOnlineList(members) {
const container = document.getElementById('online-list');
container.innerHTML = members.map(member => `
<div class="online-user">
<img src="${member.avatar || '/default-avatar.png'}" alt="" />
<span class="name">${escapeHtml(member.name)}</span>
<span class="status-dot online"></span>
</div>
`).join('');
}
function updateOnlineCount(count) {
document.getElementById('online-count').textContent = count;
}
Vue 3 Composable
JavaScript
// composables/usePresence.js
import { ref, computed, onMounted, onUnmounted, shallowRef } from 'vue';
export function usePresence(pushler, channelName) {
const members = shallowRef(new Map());
const isConnected = ref(false);
const error = ref(null);
let channel = null;
const membersList = computed(() =>
Array.from(members.value.entries()).map(([id, info]) => ({
id,
...info
}))
);
const membersCount = computed(() => members.value.size);
const isMember = (userId) => members.value.has(String(userId));
function connect() {
return new Promise((resolve, reject) => {
channel = pushler.subscribe(channelName);
channel.on('pushler:subscription_succeeded', (data) => {
const newMembers = new Map();
if (data.members) {
Object.entries(data.members).forEach(([id, info]) => {
newMembers.set(id, info);
});
}
members.value = newMembers;
isConnected.value = true;
resolve(membersList.value);
});
channel.on('pushler:member_added', (member) => {
const newMembers = new Map(members.value);
newMembers.set(member.id, member.info);
members.value = newMembers;
});
channel.on('pushler:member_removed', (member) => {
const newMembers = new Map(members.value);
newMembers.delete(member.id);
members.value = newMembers;
});
channel.on('pushler:subscription_error', (err) => {
error.value = err;
reject(err);
});
});
}
function disconnect() {
channel?.unsubscribe();
members.value = new Map();
isConnected.value = false;
}
onMounted(() => {
if (pushler.connectionState === 'connected') {
connect();
} else {
pushler.on('connected', connect);
}
});
onUnmounted(disconnect);
return { members: membersList, count: membersCount, isConnected, error, isMember };
}
Vue компонент онлайн-пользователей
Vue
<!-- OnlineUsers.vue -->
<template>
<div class="online-users">
<div class="header">
<h3>Сейчас онлайн</h3>
<span class="count">{{ presence.count.value }}</span>
</div>
<TransitionGroup name="list" tag="div" class="users-list">
<div
v-for="member in presence.members.value"
:key="member.id"
class="user"
>
<div class="avatar-wrapper">
<img :src="member.avatar || '/default-avatar.png'" :alt="member.name" />
<span class="status-dot" :class="member.status || 'online'"></span>
</div>
<div class="info">
<div class="name">{{ member.name }}</div>
<div v-if="member.role" class="role">{{ member.role }}</div>
</div>
</div>
</TransitionGroup>
<div v-if="presence.members.value.length === 0" class="empty">
Никого нет онлайн
</div>
</div>
</template>
<script setup>
import { usePresence } from './composables/usePresence';
const props = defineProps({
pushler: { type: Object, required: true },
channelName: { type: String, required: true }
});
const presence = usePresence(props.pushler, props.channelName);
</script>
🎨 Продвинутые сценарии
Статус пользователя (away/busy)
JavaScript
// Клиент отправляет обновление статуса
async function updateStatus(status) {
await fetch('/update_status.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `status=${status}` // 'online' | 'away' | 'busy'
});
}
// Автоматическое определение "отошёл"
let idleTimeout;
document.addEventListener('mousemove', resetIdle);
document.addEventListener('keydown', resetIdle);
function resetIdle() {
clearTimeout(idleTimeout);
updateStatus('online');
idleTimeout = setTimeout(() => {
updateStatus('away');
}, 5 * 60 * 1000); // 5 минут
}
Подсчёт просмотров страницы
JavaScript
// Сколько человек сейчас смотрят страницу
const pushler = new PushlerClient({
appKey: 'key_ваш_ключ',
authEndpoint: '/auth.php'
});
pushler.on('connected', () => {
const channel = pushler.subscribe(`presence-page-${pageId}`);
channel.on('pushler:subscription_succeeded', (data) => {
const count = Object.keys(data.members || {}).length;
document.getElementById('viewers').textContent = `👁️ ${count} смотрят`;
});
channel.on('pushler:member_added', () => updateViewersCount(1));
channel.on('pushler:member_removed', () => updateViewersCount(-1));
});
⚠️ Типичные ошибки
1. Неверное имя канала
JavaScript
// ❌ Неправильно — без префикса presence-
pushler.subscribe('chat-room-123');
// ✅ Правильно — с префиксом
pushler.subscribe('presence-chat-room-123');
2. Не передаём user_info при авторизации
PHP
// ❌ Неправильно — нет данных пользователя
$auth = $pushler->authorizePresenceChannel($channelName, $socketId, [
'user_id' => $userId
// user_info отсутствует!
]);
// ✅ Правильно — передаём информацию
$auth = $pushler->authorizePresenceChannel($channelName, $socketId, [
'user_id' => (string) $userId,
'user_info' => [
'name' => $user['name'],
'avatar' => $user['avatar']
]
]);
3. user_id должен быть строкой
PHP
// ❌ Может вызвать проблемы
'user_id' => $userId // integer
// ✅ Правильно — явно приводим к строке
'user_id' => (string) $userId
Готовы попробовать?
Создайте бесплатный аккаунт и начните интеграцию за пару минут