🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Exemples Pusher
Exemples de communication en temps réel utilisant les API Pusher incluant canaux, événements, présence et authentification client
💻 Chat de Base avec Pusher javascript
🟢 simple
⭐⭐
Application de chat simple mais puissante utilisant les canaux publics et privés de Pusher avec authentification
⏱️ 20 min
🏷️ pusher, chat, client, real-time
Prerequisites:
Pusher account, JavaScript ES6+, HTTP basics
// Basic Real-time Chat Application using Pusher
// Client-side implementation
// Initialize Pusher with your app credentials
const pusher = new Pusher('your-pusher-app-key', {
cluster: 'your-pusher-cluster',
forceTLS: true,
authEndpoint: '/api/pusher/auth',
auth: {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
}
});
class ChatApp {
constructor() {
this.currentUser = null;
this.currentChannel = null;
this.messages = [];
this.users = new Map();
this.init();
}
async init() {
// Authenticate user
await this.authenticateUser();
// Connect to Pusher
this.connectToPusher();
// Setup UI event listeners
this.setupUIListeners();
// Subscribe to notifications channel
this.subscribeToNotifications();
}
async authenticateUser() {
try {
const response = await fetch('/api/user/me', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
});
if (response.ok) {
this.currentUser = await response.json();
} else {
// Redirect to login
window.location.href = '/login';
}
} catch (error) {
console.error('Authentication failed:', error);
window.location.href = '/login';
}
}
connectToPusher() {
// Bind to connection events
pusher.connection.bind('connected', () => {
console.log('Connected to Pusher');
this.updateConnectionStatus(true);
});
pusher.connection.bind('disconnected', () => {
console.log('Disconnected from Pusher');
this.updateConnectionStatus(false);
});
pusher.connection.bind('error', (err) => {
console.error('Pusher connection error:', err);
this.showErrorMessage('Connection error. Please refresh.');
});
}
// Subscribe to a chat room (private channel)
joinRoom(roomId) {
if (this.currentChannel) {
this.leaveCurrentRoom();
}
// Subscribe to private channel for the room
this.currentChannel = pusher.subscribe(`private-chat-${roomId}`);
// Bind to chat events
this.currentChannel.bind('pusher:subscription_succeeded', () => {
console.log(`Successfully joined room: ${roomId}`);
this.onRoomJoined(roomId);
});
this.currentChannel.bind('pusher:subscription_error', (err) => {
console.error('Failed to join room:', err);
this.showErrorMessage('Failed to join room');
});
// Listen for new messages
this.currentChannel.bind('new_message', (data) => {
this.addMessage(data);
});
// Listen for user events
this.currentChannel.bind('user_joined', (data) => {
this.onUserJoined(data);
});
this.currentChannel.bind('user_left', (data) => {
this.onUserLeft(data);
});
// Listen for typing indicators
this.currentChannel.bind('user_typing', (data) => {
this.onUserTyping(data);
});
this.currentChannel.bind('user_stop_typing', (data) => {
this.onUserStopTyping(data);
});
}
// Subscribe to presence channel for user presence
subscribeToPresence(roomId) {
const presenceChannel = pusher.subscribe(`presence-room-${roomId}`);
presenceChannel.bind('pusher:subscription_succeeded', (members) => {
console.log('Presence channel joined');
this.updateOnlineUsers(members.members);
});
presenceChannel.bind('pusher:member_added', (member) => {
this.onUserPresenceJoined(member);
});
presenceChannel.bind('pusher:member_removed', (member) => {
this.onUserPresenceLeft(member);
});
// Trigger presence event when joining
presenceChannel.trigger('client-joined', {
user: this.currentUser,
timestamp: new Date()
});
}
// Leave current room
leaveCurrentRoom() {
if (this.currentChannel) {
// Notify others we're leaving
this.currentChannel.trigger('client-leaving', {
user: this.currentUser,
timestamp: new Date()
});
pusher.unsubscribe(this.currentChannel.name);
this.currentChannel = null;
}
}
// Send a message
sendMessage(message, type = 'text') {
if (!this.currentChannel || !message.trim()) return;
const messageData = {
id: this.generateId(),
user: this.currentUser,
message: message.trim(),
type,
timestamp: new Date(),
roomId: this.getRoomId()
};
// Trigger client event (will be received by server and broadcast)
this.currentChannel.trigger('client-send_message', messageData);
// Optimistically add message to UI
this.addMessage(messageData);
}
// Send file
sendFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('roomId', this.getRoomId());
fetch('/api/upload', {
method: 'POST',
body: formData,
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
this.sendMessage({
type: 'file',
url: data.fileUrl,
fileName: file.name,
fileSize: file.size
}, 'file');
} else {
this.showErrorMessage('Failed to upload file');
}
})
.catch(error => {
console.error('File upload error:', error);
this.showErrorMessage('Failed to upload file');
});
}
// Start typing indicator
startTyping() {
if (this.currentChannel && !this.isTyping) {
this.isTyping = true;
this.currentChannel.trigger('client-typing', {
user: this.currentUser,
timestamp: new Date()
});
// Auto-stop typing after 3 seconds
clearTimeout(this.typingTimeout);
this.typingTimeout = setTimeout(() => this.stopTyping(), 3000);
}
}
// Stop typing indicator
stopTyping() {
if (this.currentChannel && this.isTyping) {
this.isTyping = false;
this.currentChannel.trigger('client-stop_typing', {
user: this.currentUser,
timestamp: new Date()
});
clearTimeout(this.typingTimeout);
}
}
// Subscribe to notifications
subscribeToNotifications() {
const notificationChannel = pusher.subscribe(`private-notifications-${this.currentUser.id}`);
notificationChannel.bind('new_notification', (data) => {
this.showNotification(data);
});
notificationChannel.bind('message_mention', (data) => {
this.showMentionNotification(data);
});
}
// Event handlers
onRoomJoined(roomId) {
this.messages = [];
this.updateMessagesList();
// Subscribe to presence for this room
this.subscribeToPresence(roomId);
// Load message history
this.loadMessageHistory(roomId);
}
onUserJoined(data) {
this.addSystemMessage(`${data.user.name} joined the chat`);
}
onUserLeft(data) {
this.addSystemMessage(`${data.user.name} left the chat`);
}
onUserTyping(data) {
this.updateTypingIndicator(data.user, true);
}
onUserStopTyping(data) {
this.updateTypingIndicator(data.user, false);
}
onUserPresenceJoined(member) {
this.users.set(member.id, member.info);
this.updateOnlineUsersList();
}
onUserPresenceLeft(member) {
this.users.delete(member.id);
this.updateOnlineUsersList();
}
// UI helpers
addMessage(messageData) {
this.messages.push(messageData);
this.updateMessagesList();
this.scrollToBottom();
// Play notification sound if message is not from current user
if (messageData.user.id !== this.currentUser.id) {
this.playNotificationSound();
}
}
addSystemMessage(text) {
const systemMessage = {
id: this.generateId(),
type: 'system',
message: text,
timestamp: new Date()
};
this.messages.push(systemMessage);
this.updateMessagesList();
this.scrollToBottom();
}
updateMessagesList() {
const container = document.getElementById('messages');
if (!container) return;
container.innerHTML = this.messages.map(msg => this.renderMessage(msg)).join('');
}
renderMessage(msg) {
if (msg.type === 'system') {
return `
<div class="system-message">
<span class="system-text">${msg.message}</span>
<span class="timestamp">${this.formatTime(msg.timestamp)}</span>
</div>
`;
}
const isOwnMessage = msg.user.id === this.currentUser.id;
const messageClass = isOwnMessage ? 'own-message' : 'other-message';
if (msg.type === 'file') {
return `
<div class="message ${messageClass}">
<div class="message-header">
<span class="username">${msg.user.name}</span>
<span class="timestamp">${this.formatTime(msg.timestamp)}</span>
</div>
<div class="file-message">
📎 <a href="${msg.message.url}" target="_blank">
${msg.message.fileName}
</a>
<span class="file-size">(${this.formatFileSize(msg.message.fileSize)})</span>
</div>
</div>
`;
}
return `
<div class="message ${messageClass}">
<div class="message-header">
<span class="username">${msg.user.name}</span>
<span class="timestamp">${this.formatTime(msg.timestamp)}</span>
</div>
<div class="message-content">${this.escapeHtml(msg.message)}</div>
</div>
`;
}
updateTypingIndicator(user, isTyping) {
const typingUsers = this.typingUsers || new Set();
if (isTyping) {
typingUsers.add(user.id);
} else {
typingUsers.delete(user.id);
}
this.typingUsers = typingUsers;
this.renderTypingIndicator();
}
renderTypingIndicator() {
const container = document.getElementById('typing-indicator');
if (!container) return;
if (this.typingUsers.size === 0) {
container.style.display = 'none';
return;
}
const typingNames = Array.from(this.typingUsers)
.map(userId => this.users.get(userId)?.name)
.filter(Boolean);
container.innerHTML = `
<div class="typing-indicator-content">
<span class="typing-dots"></span>
${typingNames.join(', ')} ${typingNames.length === 1 ? 'is' : 'are'} typing...
</div>
`;
container.style.display = 'block';
}
updateOnlineUsers(members) {
members.forEach((member, userId) => {
this.users.set(userId, member);
});
this.updateOnlineUsersList();
}
updateOnlineUsersList() {
const container = document.getElementById('online-users');
if (!container) return;
container.innerHTML = Array.from(this.users.values())
.map(user => `
<div class="online-user">
<div class="user-avatar" style="background-color: ${user.color}">
${user.name[0].toUpperCase()}
</div>
<span class="user-name">${user.name}</span>
</div>
`)
.join('');
}
setupUIListeners() {
// Message input
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
messageInput?.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage(messageInput.value);
messageInput.value = '';
} else {
this.startTyping();
}
});
messageInput?.addEventListener('blur', () => this.stopTyping());
sendButton?.addEventListener('click', () => {
this.sendMessage(messageInput.value);
messageInput.value = '';
});
// File upload
const fileInput = document.getElementById('file-input');
fileInput?.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
this.sendFile(file);
}
e.target.value = ''; // Reset input
});
// Room switching
const roomSelect = document.getElementById('room-select');
roomSelect?.addEventListener('change', (e) => {
this.joinRoom(e.target.value);
});
}
// Utility functions
generateId() {
return Math.random().toString(36).substr(2, 9);
}
formatTime(date) {
return new Date(date).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
scrollToBottom() {
const container = document.getElementById('messages-container');
if (container) {
container.scrollTop = container.scrollHeight;
}
}
updateConnectionStatus(connected) {
const indicator = document.getElementById('connection-status');
if (indicator) {
indicator.className = connected ? 'connected' : 'disconnected';
indicator.textContent = connected ? 'Connected' : 'Disconnected';
}
}
showNotification(data) {
// Show browser notification if permitted
if (Notification.permission === 'granted') {
new Notification(data.title, {
body: data.message,
icon: '/notification-icon.png',
tag: data.id
});
}
// Show in-app notification
this.showInAppNotification(data);
}
showMentionNotification(data) {
this.showNotification({
title: 'You were mentioned!',
message: `${data.message} in ${data.roomName}`,
type: 'mention',
roomId: data.roomId
});
}
showInAppNotification(data) {
// Implementation depends on your UI framework
console.log('Notification:', data);
}
showErrorMessage(message) {
alert(message); // Replace with better UI
}
playNotificationSound() {
// Play a subtle notification sound
const audio = new Audio('/notification.mp3');
audio.volume = 0.3;
audio.play().catch(() => {});
}
getRoomId() {
const roomSelect = document.getElementById('room-select');
return roomSelect?.value || 'general';
}
async loadMessageHistory(roomId) {
try {
const response = await fetch(`/api/messages/${roomId}`, {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
});
if (response.ok) {
const history = await response.json();
this.messages = history;
this.updateMessagesList();
this.scrollToBottom();
}
} catch (error) {
console.error('Failed to load message history:', error);
}
}
}
// Initialize the chat app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
// Request notification permission
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
// Initialize chat app
window.chatApp = new ChatApp();
});
export { ChatApp };
💻 Intégration Serveur Pusher javascript
🟡 intermediate
⭐⭐⭐⭐
Serveur Node.js complet avec intégration Pusher pour authentification, gestion des canaux et diffusion d'événements
⏱️ 30 min
🏷️ pusher, server, authentication, integration
Prerequisites:
Node.js, Express, Pusher account
// Server-side Pusher Integration with Node.js/Express
// Complete implementation with authentication, channels, and event broadcasting
const express = require('express');
const cors = require('cors');
const jwt = require('jsonwebtoken');
const Pusher = require('pusher');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const app = express();
const PORT = process.env.PORT || 3001;
// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Initialize Pusher
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_KEY,
secret: process.env.PUSHER_SECRET,
cluster: process.env.PUSHER_CLUSTER,
useTLS: true,
encrypted: true
});
// JWT Secret
const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key';
// File upload configuration
const upload = multer({
dest: 'uploads/',
limits: {
fileSize: 10 * 1024 * 1024 // 10MB limit
},
fileFilter: (req, file, cb) => {
// Allow common file types
const allowedTypes = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
'application/pdf', 'text/plain',
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'), false);
}
}
});
// In-memory storage (replace with database in production)
const users = new Map();
const rooms = new Map();
const messages = new Map();
const userSessions = new Map();
// Sample user data (in production, use a proper database)
const sampleUsers = [
{ id: '1', name: 'Alice', email: '[email protected]', role: 'user' },
{ id: '2', name: 'Bob', email: '[email protected]', role: 'user' },
{ id: '3', name: 'Charlie', email: '[email protected]', role: 'admin' }
];
sampleUsers.forEach(user => {
users.set(user.id, user);
});
// Authentication middleware
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.user = user;
next();
});
};
// Authentication routes
app.post('/api/login', (req, res) => {
const { email, password } = req.body;
// In production, verify against database with hashed passwords
const user = Array.from(users.values()).find(u => u.email === email);
if (!user || password !== 'password') { // Simplified for demo
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign(
{ userId: user.id, name: user.name, role: user.role },
JWT_SECRET,
{ expiresIn: '24h' }
);
// Generate a unique color for the user
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
const userColor = colors[user.id.charCodeAt(0) % colors.length];
res.json({
token,
user: {
...user,
color: userColor
}
});
});
app.get('/api/user/me', authenticateToken, (req, res) => {
const user = users.get(req.user.userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Generate color if not present
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
if (!user.color) {
user.color = colors[user.id.charCodeAt(0) % colors.length];
}
res.json(user);
});
// Pusher authentication endpoint
app.post('/api/pusher/auth', authenticateToken, (req, res) => {
const socketId = req.body.socket_id;
const channel = req.body.channel_name;
// Check if user has permission to access this channel
if (!hasChannelPermission(req.user, channel)) {
return res.status(403).json({ error: 'Permission denied' });
}
// Get presence data for presence channels
let presenceData = {};
if (channel.startsWith('presence-')) {
const user = users.get(req.user.userId);
presenceData = {
user_id: user.id,
user_info: {
name: user.name,
email: user.email,
role: user.role,
color: user.color
}
};
}
const auth = pusher.authenticate(socketId, channel, presenceData);
res.send(auth);
});
// Check channel permissions
function hasChannelPermission(user, channelName) {
// Public channels - everyone can access
if (channelName.startsWith('public-')) {
return true;
}
// Private channels - authenticated users can access
if (channelName.startsWith('private-')) {
return true;
}
// Presence channels - authenticated users can access
if (channelName.startsWith('presence-')) {
return true;
}
// Admin channels - only admin users can access
if (channelName.startsWith('private-admin-')) {
return user.role === 'admin';
}
// Notification channels - user can only access their own
if (channelName.startsWith('private-notifications-')) {
const userId = channelName.split('-')[2];
return userId === user.userId;
}
return false;
}
// Handle client events and broadcast them
app.post('/api/client-events', authenticateToken, (req, res) => {
const { channel_name, event_name, data } = req.body;
// Validate event name (must start with 'client-')
if (!event_name.startsWith('client-')) {
return res.status(400).json({ error: 'Invalid event name' });
}
// Check if user has permission to trigger event on this channel
if (!hasChannelPermission(req.user, channel_name)) {
return res.status(403).json({ error: 'Permission denied' });
}
try {
// Add user info to event data
const enrichedData = {
...data,
user: {
id: req.user.userId,
name: req.user.name,
role: req.user.role
},
server_timestamp: new Date().toISOString()
};
// Broadcast to channel (remove 'client-' prefix for actual event name)
const actualEventName = event_name.replace('client-', '');
pusher.trigger(channel_name, actualEventName, enrichedData);
// Handle specific events
if (actualEventName === 'send_message') {
saveMessage(enrichedData);
} else if (actualEventName === 'typing') {
handleTypingIndicator(enrichedData, true);
} else if (actualEventName === 'stop_typing') {
handleTypingIndicator(enrichedData, false);
}
res.json({ success: true });
} catch (error) {
console.error('Error broadcasting event:', error);
res.status(500).json({ error: 'Failed to broadcast event' });
}
});
// Save message to storage
function saveMessage(messageData) {
const roomId = messageData.roomId;
if (!messages.has(roomId)) {
messages.set(roomId, []);
}
const roomMessages = messages.get(roomId);
roomMessages.push(messageData);
// Limit message history
if (roomMessages.length > 1000) {
roomMessages.splice(0, roomMessages.length - 1000);
}
// Check for mentions and send notifications
const mentions = extractMentions(messageData.message);
if (mentions.length > 0) {
mentions.forEach(userId => {
sendMentionNotification(userId, messageData);
});
}
}
// Handle typing indicators
function handleTypingIndicator(data, isTyping) {
const typingData = {
user: data.user,
isTyping,
timestamp: data.timestamp
};
// Broadcast typing event to the channel
const eventName = isTyping ? 'user_typing' : 'user_stop_typing';
pusher.trigger(data.channel_name, eventName, typingData);
}
// Extract user mentions from message
function extractMentions(message) {
const mentionRegex = /@(\w+)/g;
const mentions = [];
let match;
while ((match = mentionRegex.exec(message)) !== null) {
const username = match[1];
const user = Array.from(users.values()).find(u =>
u.name.toLowerCase() === username.toLowerCase()
);
if (user) {
mentions.push(user.id);
}
}
return mentions;
}
// Send mention notification
function sendMentionNotification(userId, messageData) {
const notificationData = {
type: 'mention',
title: 'You were mentioned in a message',
message: `${messageData.user.name} mentioned you in a chat`,
roomId: messageData.roomId,
messageId: messageData.id,
timestamp: new Date().toISOString()
};
pusher.trigger(`private-notifications-${userId}`, 'message_mention', notificationData);
}
// Send general notification
function sendNotification(userId, notificationData) {
pusher.trigger(`private-notifications-${userId}`, 'new_notification', notificationData);
}
// File upload endpoint
app.post('/api/upload', authenticateToken, upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
try {
const file = req.file;
const roomId = req.body.roomId;
// Generate unique filename
const fileExtension = path.extname(file.originalname);
const fileName = `${Date.now()}-${file.originalname}`;
const filePath = path.join(__dirname, 'uploads', fileName);
// Move file to uploads directory
fs.renameSync(file.path, filePath);
// Create file URL
const fileUrl = `${req.protocol}://${req.get('host')}/uploads/${fileName}`;
// Create file message data
const fileMessage = {
id: Date.now().toString(),
type: 'file',
url: fileUrl,
fileName: file.originalname,
fileSize: file.size,
mimeType: file.mimetype
};
// Broadcast file message to room
const channelName = `private-chat-${roomId}`;
pusher.trigger(channelName, 'new_message', {
...fileMessage,
user: {
id: req.user.userId,
name: req.user.name,
role: req.user.role
},
timestamp: new Date().toISOString(),
roomId
});
res.json({
success: true,
fileUrl,
fileName: file.originalname,
fileSize: file.size
});
} catch (error) {
console.error('File upload error:', error);
res.status(500).json({ error: 'Failed to upload file' });
}
});
// Serve uploaded files
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Get message history for a room
app.get('/api/messages/:roomId', authenticateToken, (req, res) => {
const roomId = req.params.roomId;
const roomMessages = messages.get(roomId) || [];
// Return last 50 messages
const recentMessages = roomMessages.slice(-50);
res.json(recentMessages);
});
// Get available rooms
app.get('/api/rooms', authenticateToken, (req, res) => {
const roomList = Array.from(rooms.values()).map(room => ({
id: room.id,
name: room.name,
description: room.description,
memberCount: room.memberCount || 0,
isPrivate: room.isPrivate || false
}));
res.json(roomList);
});
// Create new room
app.post('/api/rooms', authenticateToken, (req, res) => {
const { name, description, isPrivate = false } = req.body;
if (!name) {
return res.status(400).json({ error: 'Room name is required' });
}
const roomId = generateId();
const newRoom = {
id: roomId,
name,
description,
isPrivate,
createdBy: req.user.userId,
createdAt: new Date(),
memberCount: 0
};
rooms.set(roomId, newRoom);
messages.set(roomId, []);
// Broadcast room creation (for admin channels)
pusher.trigger('private-admin-rooms', 'room_created', newRoom);
res.json(newRoom);
});
// Get online users in a room
app.get('/api/rooms/:roomId/users', authenticateToken, (req, res) => {
const roomId = req.params.roomId;
// This would typically come from Pusher's presence channels
// For now, return mock data
res.json([]);
});
// Send system announcement (admin only)
app.post('/api/announce', authenticateToken, (req, res) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
const { message, targetChannel = 'public-announcements' } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
const announcement = {
type: 'announcement',
message,
from: {
id: req.user.userId,
name: req.user.name
},
timestamp: new Date().toISOString()
};
pusher.trigger(targetChannel, 'announcement', announcement);
// Also send to all private notification channels for important announcements
if (targetChannel === 'public-announcements') {
Array.from(users.values()).forEach(user => {
sendNotification(user.id, {
type: 'announcement',
title: 'System Announcement',
message,
timestamp: announcement.timestamp
});
});
}
res.json({ success: true });
});
// Get Pusher stats
app.get('/api/stats', authenticateToken, (req, res) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
const stats = {
totalUsers: users.size,
totalRooms: rooms.size,
totalMessages: Array.from(messages.values())
.reduce((total, roomMessages) => total + roomMessages.length, 0),
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString()
};
res.json(stats);
});
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// Utility functions
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
// Error handling middleware
app.use((error, req, res, next) => {
console.error('Server error:', error);
res.status(500).json({ error: 'Internal server error' });
});
// Start server
app.listen(PORT, () => {
console.log(`Pusher server running on port ${PORT}`);
console.log(`Pusher app ID: ${process.env.PUSHER_APP_ID}`);
console.log(`Pusher cluster: ${process.env.PUSHER_CLUSTER}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
process.exit(0);
});
module.exports = app;
💻 React Hooks pour Pusher typescript
🔴 complex
⭐⭐⭐⭐
React hooks modernes pour intégration seamless avec Pusher avec support TypeScript et hooks personnalisés
⏱️ 35 min
🏷️ pusher, react, hooks, typescript
Prerequisites:
React, TypeScript, Pusher basics
// React Hooks for Pusher Integration
// Modern TypeScript hooks with state management and error handling
import { useState, useEffect, useCallback, useRef } from 'react';
import Pusher, { Channel, Members } from 'pusher-js';
// Type definitions
interface User {
id: string;
name: string;
email?: string;
role?: string;
color?: string;
avatar?: string;
}
interface Message {
id: string;
user: User;
message: string;
type: 'text' | 'file' | 'system';
timestamp: Date;
roomId?: string;
metadata?: Record<string, any>;
}
interface TypingUser {
id: string;
name: string;
color?: string;
lastSeen: Date;
}
interface NotificationSettings {
mentions: boolean;
replies: boolean;
systemMessages: boolean;
soundEnabled: boolean;
}
interface PusherConfig {
key: string;
cluster: string;
authEndpoint?: string;
forceTLS?: boolean;
}
// Custom hook for Pusher connection management
export const usePusher = (config: PusherConfig) => {
const [isConnected, setIsConnected] = useState(false);
const [connectionState, setConnectionState] = useState<string>('disconnected');
const [error, setError] = useState<string | null>(null);
const pusherRef = useRef<Pusher | null>(null);
useEffect(() => {
// Initialize Pusher
pusherRef.current = new Pusher(config.key, {
cluster: config.cluster,
authEndpoint: config.authEndpoint,
forceTLS: config.forceTLS ?? true,
auth: {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
}
});
// Connection event handlers
const handleConnected = () => {
setIsConnected(true);
setConnectionState('connected');
setError(null);
console.log('Connected to Pusher');
};
const handleDisconnected = () => {
setIsConnected(false);
setConnectionState('disconnected');
console.log('Disconnected from Pusher');
};
const handleError = (err: any) => {
setError(err.message || 'Connection error');
setConnectionState('error');
console.error('Pusher connection error:', err);
};
const handleConnecting = () => {
setConnectionState('connecting');
};
pusherRef.current.connection.bind('connected', handleConnected);
pusherRef.current.connection.bind('disconnected', handleDisconnected);
pusherRef.current.connection.bind('error', handleError);
pusherRef.current.connection.bind('connecting', handleConnecting);
return () => {
if (pusherRef.current) {
pusherRef.current.connection.unbind('connected', handleConnected);
pusherRef.current.connection.unbind('disconnected', handleDisconnected);
pusherRef.current.connection.unbind('error', handleError);
pusherRef.current.connection.unbind('connecting', handleConnecting);
pusherRef.current.disconnect();
}
};
}, [config]);
return {
pusher: pusherRef.current,
isConnected,
connectionState,
error
};
};
// Custom hook for channel subscription
export const useChannel = (
pusher: Pusher | null,
channelName: string,
options?: {
type?: 'public' | 'private' | 'presence';
onSubscribed?: () => void;
onSubscriptionError?: (error: any) => void;
}
) => {
const [channel, setChannel] = useState<Channel | null>(null);
const [isSubscribed, setIsSubscribed] = useState(false);
const [subscriptionError, setSubscriptionError] = useState<string | null>(null);
const channelRef = useRef<Channel | null>(null);
useEffect(() => {
if (!pusher || !channelName) return;
// Subscribe to channel
const fullChannelName = getFullChannelName(channelName, options?.type);
channelRef.current = pusher.subscribe(fullChannelName);
setChannel(channelRef.current);
// Bind to subscription events
const handleSubscribed = () => {
setIsSubscribed(true);
setSubscriptionError(null);
options?.onSubscribed?.();
};
const handleSubscriptionError = (err: any) => {
setSubscriptionError(err.message || 'Subscription error');
options?.onSubscriptionError?.(err);
};
channelRef.current.bind('pusher:subscription_succeeded', handleSubscribed);
channelRef.current.bind('pusher:subscription_error', handleSubscriptionError);
return () => {
if (channelRef.current) {
channelRef.current.unbind('pusher:subscription_succeeded', handleSubscribed);
channelRef.current.unbind('pusher:subscription_error', handleSubscriptionError);
pusher.unsubscribe(fullChannelName);
}
setChannel(null);
setIsSubscribed(false);
};
}, [pusher, channelName, options]);
return {
channel,
isSubscribed,
subscriptionError
};
};
// Custom hook for presence channel management
export const usePresenceChannel = (
pusher: Pusher | null,
channelName: string,
options?: {
onMemberJoined?: (member: any) => void;
onMemberLeft?: (member: any) => void;
}
) => {
const [members, setMembers] = useState<Map<string, any>>(new Map());
const [count, setCount] = useState(0);
const channelHook = useChannel(pusher, channelName, { type: 'presence' });
useEffect(() => {
if (!channelHook.channel) return;
const channel = channelHook.channel;
// Handle presence events
const handleSubscriptionSucceeded = (data: any) => {
const membersMap = new Map<string, any>();
data.members.forEach((member: any) => {
membersMap.set(member.id, member.info);
});
setMembers(membersMap);
setCount(data.count);
};
const handleMemberAdded = (member: any) => {
setMembers(prev => {
const newMembers = new Map(prev);
newMembers.set(member.id, member.info);
return newMembers;
});
setCount(prev => prev + 1);
options?.onMemberJoined?.(member);
};
const handleMemberRemoved = (member: any) => {
setMembers(prev => {
const newMembers = new Map(prev);
newMembers.delete(member.id);
return newMembers;
});
setCount(prev => prev - 1);
options?.onMemberLeft?.(member);
};
channel.bind('pusher:subscription_succeeded', handleSubscriptionSucceeded);
channel.bind('pusher:member_added', handleMemberAdded);
channel.bind('pusher:member_removed', handleMemberRemoved);
return () => {
channel.unbind('pusher:subscription_succeeded', handleSubscriptionSucceeded);
channel.unbind('pusher:member_added', handleMemberAdded);
channel.unbind('pusher:member_removed', handleMemberRemoved);
};
}, [channelHook.channel, options]);
return {
...channelHook,
members: Array.from(members.values()),
count,
getMember: (id: string) => members.get(id)
};
};
// Custom hook for real-time messaging
export const useMessaging = (
pusher: Pusher | null,
channelId: string,
options?: {
onNewMessage?: (message: Message) => void;
onTypingStart?: (user: TypingUser) => void;
onTypingStop?: (userId: string) => void;
onError?: (error: string) => void;
}
) => {
const [messages, setMessages] = useState<Message[]>([]);
const [typingUsers, setTypingUsers] = useState<Map<string, TypingUser>>(new Map());
const channelHook = useChannel(pusher, channelId, { type: 'private' });
// Send message function
const sendMessage = useCallback(async (content: string, type: 'text' | 'file' = 'text') => {
if (!channelHook.channel || !content.trim()) {
return false;
}
try {
const response = await fetch('/api/client-events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({
channel_name: channelHook.channel.name,
event_name: 'client-send_message',
data: {
message: content.trim(),
type,
timestamp: new Date().toISOString()
}
})
});
if (!response.ok) {
throw new Error('Failed to send message');
}
return true;
} catch (error) {
options?.onError?.(error instanceof Error ? error.message : 'Unknown error');
return false;
}
}, [channelHook.channel, options]);
// Send typing indicator
const sendTypingIndicator = useCallback((isTyping: boolean) => {
if (!channelHook.channel) return;
channelHook.channel.trigger(`client-${isTyping ? 'typing' : 'stop_typing'}`, {
timestamp: new Date().toISOString()
});
}, [channelHook.channel]);
// Handle channel events
useEffect(() => {
if (!channelHook.channel) return;
const channel = channelHook.channel;
// Listen for new messages
const handleNewMessage = (data: any) => {
const message: Message = {
id: data.id,
user: data.user,
message: data.message,
type: data.type || 'text',
timestamp: new Date(data.timestamp),
metadata: data.metadata
};
setMessages(prev => [...prev, message]);
options?.onNewMessage?.(message);
};
// Listen for typing indicators
const handleTypingStart = (data: any) => {
const typingUser: TypingUser = {
id: data.user.id,
name: data.user.name,
color: data.user.color,
lastSeen: new Date()
};
setTypingUsers(prev => {
const newMap = new Map(prev);
newMap.set(typingUser.id, typingUser);
return newMap;
});
options?.onTypingStart?.(typingUser);
};
const handleTypingStop = (data: any) => {
setTypingUsers(prev => {
const newMap = new Map(prev);
newMap.delete(data.user.id);
return newMap;
});
options?.onTypingStop?.(data.user.id);
};
channel.bind('new_message', handleNewMessage);
channel.bind('user_typing', handleTypingStart);
channel.bind('user_stop_typing', handleTypingStop);
return () => {
channel.unbind('new_message', handleNewMessage);
channel.unbind('user_typing', handleTypingStart);
channel.unbind('user_stop_typing', handleTypingStop);
};
}, [channelHook.channel, options]);
// Clean up old typing indicators
useEffect(() => {
const interval = setInterval(() => {
const now = new Date();
setTypingUsers(prev => {
const newMap = new Map(prev);
prev.forEach((user, id) => {
// Remove typing indicator if user hasn't typed in 3 seconds
if (now.getTime() - user.lastSeen.getTime() > 3000) {
newMap.delete(id);
}
});
return newMap;
});
}, 1000);
return () => clearInterval(interval);
}, []);
return {
...channelHook,
messages,
typingUsers: Array.from(typingUsers.values()),
sendMessage,
sendTypingIndicator
};
};
// Custom hook for notifications
export const useNotifications = (
pusher: Pusher | null,
userId: string,
settings: NotificationSettings = {
mentions: true,
replies: true,
systemMessages: true,
soundEnabled: true
}
) => {
const [notifications, setNotifications] = useState<any[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const channelHook = useChannel(pusher, `notifications-${userId}`, { type: 'private' });
// Mark notification as read
const markAsRead = useCallback((notificationId: string) => {
setNotifications(prev =>
prev.map(notif =>
notif.id === notificationId
? { ...notif, read: true }
: notif
)
);
// Update unread count
setUnreadCount(prev => Math.max(0, prev - 1));
}, []);
// Mark all as read
const markAllAsRead = useCallback(() => {
setNotifications(prev =>
prev.map(notif => ({ ...notif, read: true }))
);
setUnreadCount(0);
}, []);
// Clear notifications
const clearNotifications = useCallback(() => {
setNotifications([]);
setUnreadCount(0);
}, []);
// Listen for notifications
useEffect(() => {
if (!channelHook.channel) return;
const channel = channelHook.channel;
const handleNewNotification = (data: any) => {
// Check user preferences
if (data.type === 'mention' && !settings.mentions) return;
if (data.type === 'reply' && !settings.replies) return;
if (data.type === 'system' && !settings.systemMessages) return;
const notification = {
...data,
id: data.id || Date.now().toString(),
timestamp: new Date(data.timestamp),
read: false
};
setNotifications(prev => [notification, ...prev]);
setUnreadCount(prev => prev + 1);
// Play notification sound if enabled
if (settings.soundEnabled) {
playNotificationSound();
}
// Show browser notification
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(data.title, {
body: data.message,
icon: '/notification-icon.png',
tag: notification.id
});
}
};
channel.bind('new_notification', handleNewNotification);
channel.bind('message_mention', handleNewNotification);
return () => {
channel.unbind('new_notification', handleNewNotification);
channel.unbind('message_mention', handleNewNotification);
};
}, [channelHook.channel, settings]);
// Request notification permission
useEffect(() => {
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
}, []);
return {
...channelHook,
notifications,
unreadCount,
markAsRead,
markAllAsRead,
clearNotifications
};
};
// Utility functions
function getFullChannelName(channelName: string, type?: string): string {
switch (type) {
case 'private':
return `private-${channelName}`;
case 'presence':
return `presence-${channelName}`;
case 'public':
default:
return `public-${channelName}`;
}
}
function playNotificationSound() {
try {
const audio = new Audio('/notification.mp3');
audio.volume = 0.3;
audio.play().catch(() => {});
} catch (error) {
// Ignore audio errors
}
}
// Example usage component
export const ChatRoom: React.FC<{
roomId: string;
currentUser: User;
}> = ({ roomId, currentUser }) => {
const config: PusherConfig = {
key: process.env.REACT_APP_PUSHER_KEY!,
cluster: process.env.REACT_APP_PUSHER_CLUSTER!,
authEndpoint: '/api/pusher/auth'
};
const { pusher, isConnected } = usePusher(config);
const {
messages,
typingUsers,
sendMessage,
sendTypingIndicator
} = useMessaging(pusher, roomId, {
onNewMessage: (message) => {
console.log('New message:', message);
},
onError: (error) => {
console.error('Message error:', error);
}
});
const { members, count } = usePresenceChannel(pusher, roomId, {
onMemberJoined: (member) => {
console.log('Member joined:', member);
}
});
const [inputValue, setInputValue] = useState('');
const [isTyping, setIsTyping] = useState(false);
const handleSend = useCallback(() => {
if (sendMessage(inputValue)) {
setInputValue('');
setIsTyping(false);
}
}, [inputValue, sendMessage]);
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
if (!isTyping && e.target.value.trim()) {
setIsTyping(true);
sendTypingIndicator(true);
} else if (isTyping && !e.target.value.trim()) {
setIsTyping(false);
sendTypingIndicator(false);
}
}, [isTyping, sendTypingIndicator]);
return (
<div className="chat-room">
<div className="chat-header">
<h3>Room: {roomId}</h3>
<div className="connection-status">
{isConnected ? '🟢 Connected' : '🔴 Disconnected'}
</div>
<div className="online-count">
{count} users online
</div>
</div>
<div className="messages">
{messages.map(message => (
<div key={message.id} className="message">
<strong>{message.user.name}: </strong>
{message.message}
<small>{new Date(message.timestamp).toLocaleTimeString()}</small>
</div>
))}
</div>
{typingUsers.length > 0 && (
<div className="typing-indicator">
{typingUsers.map(user => user.name).join(', ')} is typing...
</div>
)}
<div className="message-input">
<input
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
placeholder="Type a message..."
disabled={!isConnected}
/>
<button onClick={handleSend} disabled={!isConnected || !inputValue.trim()}>
Send
</button>
</div>
<div className="online-users">
<h4>Online Users:</h4>
{members.map(member => (
<div key={member.id} className="user">
{member.name}
</div>
))}
</div>
</div>
);
};
export default {
usePusher,
useChannel,
usePresenceChannel,
useMessaging,
useNotifications,
ChatRoom
};