Skip to content

Socket.IO Room Targeting

Advanced room management and targeting strategies for Socket.IO with Response Handler.

Room Management System

Room Creation and Management

javascript
const { Server } = require('socket.io');
const { quickSocketSetup } = require('response-handler');

class RoomManager {
  constructor() {
    this.rooms = new Map();
    this.userRooms = new Map(); // userId -> Set of roomIds
  }

  createRoom(roomId, options = {}) {
    const room = {
      id: roomId,
      name: options.name || roomId,
      description: options.description || '',
      createdAt: new Date(),
      createdBy: options.createdBy,
      isPrivate: options.isPrivate || false,
      maxUsers: options.maxUsers || 100,
      currentUsers: 0,
      settings: {
        allowFileSharing: options.allowFileSharing !== false,
        allowReactions: options.allowReactions !== false,
        messageHistory: options.messageHistory !== false,
        moderationEnabled: options.moderationEnabled || false,
      },
      metadata: options.metadata || {},
    };

    this.rooms.set(roomId, room);
    return room;
  }

  getRoom(roomId) {
    return this.rooms.get(roomId);
  }

  getAllRooms() {
    return Array.from(this.rooms.values());
  }

  getPublicRooms() {
    return Array.from(this.rooms.values()).filter((room) => !room.isPrivate);
  }

  deleteRoom(roomId) {
    this.rooms.delete(roomId);
  }

  addUserToRoom(userId, roomId) {
    if (!this.userRooms.has(userId)) {
      this.userRooms.set(userId, new Set());
    }
    this.userRooms.get(userId).add(roomId);
  }

  removeUserFromRoom(userId, roomId) {
    const userRooms = this.userRooms.get(userId);
    if (userRooms) {
      userRooms.delete(roomId);
      if (userRooms.size === 0) {
        this.userRooms.delete(userId);
      }
    }
  }

  getUserRooms(userId) {
    return this.userRooms.get(userId) || new Set();
  }
}

const roomManager = new RoomManager();

// Create default rooms
roomManager.createRoom('general', {
  name: 'General Chat',
  description: 'Main chat room for everyone',
  createdBy: 'system',
});

roomManager.createRoom('announcements', {
  name: 'Announcements',
  description: 'Important announcements only',
  createdBy: 'system',
  moderationEnabled: true,
});

module.exports = roomManager;

Room Operations

Joining Rooms

javascript
const io = new Server(server);
io.use(quickSocketSetup());

io.on('connection', (socket) => {
  // Join room event
  socket.on('room:join', async (data) => {
    try {
      const { roomId, password } = data;
      const user = socket.user;

      if (!user) {
        return socket.unauthorized({}, 'Authentication required');
      }

      // Validate room ID
      if (!roomId || typeof roomId !== 'string') {
        return socket.badRequest({ roomId, expectedType: 'string' }, 'Invalid room ID');
      }

      const room = roomManager.getRoom(roomId);
      if (!room) {
        return socket.notFound({ roomId }, 'Room not found');
      }

      // Check if room is private and user has permission
      if (room.isPrivate && !hasRoomAccess(user.id, roomId)) {
        return socket.forbidden(
          { roomId, reason: 'Private room' },
          'Access denied to private room',
        );
      }

      // Check password for password-protected rooms
      if (room.password && room.password !== password) {
        return socket.unauthorized(
          { roomId, reason: 'Incorrect password' },
          'Invalid room password',
        );
      }

      // Check room capacity
      const currentUsers = await getRoomUserCount(roomId);
      if (currentUsers >= room.maxUsers) {
        return socket.tooManyRequests(
          {
            roomId,
            currentUsers,
            maxUsers: room.maxUsers,
          },
          'Room is full',
        );
      }

      // Check if user is already in room
      if (socket.rooms.has(roomId)) {
        return socket.conflict({ roomId }, 'Already in this room');
      }

      // Join the room
      socket.join(roomId);
      roomManager.addUserToRoom(user.id, roomId);

      // Update room user count
      room.currentUsers = await getRoomUserCount(roomId);

      // Notify user
      socket.ok(
        {
          room: {
            id: room.id,
            name: room.name,
            description: room.description,
            currentUsers: room.currentUsers,
            maxUsers: room.maxUsers,
            settings: room.settings,
          },
          recentMessages: await getRecentMessages(roomId, 20),
        },
        `Joined ${room.name}`,
      );

      // Notify room members
      socket.to(roomId).emit('room:user_joined', {
        user: {
          id: user.id,
          username: user.username,
          avatar: user.avatar,
        },
        roomId,
        message: `${user.username} joined the room`,
        timestamp: new Date(),
      });

      // Send updated user list
      io.to(roomId).emit('room:users_updated', {
        roomId,
        users: await getRoomUsers(roomId),
        count: room.currentUsers,
      });
    } catch (error) {
      socket.error(error, 'Failed to join room');
    }
  });

  // Leave room event
  socket.on('room:leave', async (data) => {
    try {
      const { roomId } = data;
      const user = socket.user;

      if (!user) {
        return socket.unauthorized({}, 'Authentication required');
      }

      if (!roomId) {
        return socket.badRequest({ requiredFields: ['roomId'] }, 'Room ID is required');
      }

      const room = roomManager.getRoom(roomId);
      if (!room) {
        return socket.notFound({ roomId }, 'Room not found');
      }

      // Check if user is in room
      if (!socket.rooms.has(roomId)) {
        return socket.badRequest({ roomId }, 'Not in this room');
      }

      // Leave the room
      socket.leave(roomId);
      roomManager.removeUserFromRoom(user.id, roomId);

      // Update room user count
      room.currentUsers = await getRoomUserCount(roomId);

      // Notify user
      socket.ok({ roomId }, `Left ${room.name}`);

      // Notify room members
      socket.to(roomId).emit('room:user_left', {
        user: {
          id: user.id,
          username: user.username,
        },
        roomId,
        message: `${user.username} left the room`,
        timestamp: new Date(),
      });

      // Send updated user list
      socket.to(roomId).emit('room:users_updated', {
        roomId,
        users: await getRoomUsers(roomId),
        count: room.currentUsers,
      });
    } catch (error) {
      socket.error(error, 'Failed to leave room');
    }
  });
});

Creating Custom Rooms

javascript
// Create room event
socket.on('room:create', async (data) => {
  try {
    const { name, description, isPrivate, maxUsers, password, settings } = data;
    const user = socket.user;

    if (!user) {
      return socket.unauthorized({}, 'Authentication required');
    }

    // Validation
    if (!name || name.trim().length < 3) {
      return socket.badRequest(
        {
          name,
          minLength: 3,
        },
        'Room name must be at least 3 characters',
      );
    }

    if (name.length > 50) {
      return socket.badRequest(
        {
          name,
          maxLength: 50,
        },
        'Room name too long',
      );
    }

    // Generate room ID
    const roomId = generateRoomId(name);

    // Check if room already exists
    if (roomManager.getRoom(roomId)) {
      return socket.conflict({ roomId, name }, 'Room with this name already exists');
    }

    // Check user permissions (can create rooms)
    if (!canCreateRoom(user)) {
      return socket.forbidden({ reason: 'Insufficient permissions' }, 'You cannot create rooms');
    }

    // Create room
    const room = roomManager.createRoom(roomId, {
      name: name.trim(),
      description: description?.trim() || '',
      isPrivate: isPrivate || false,
      maxUsers: Math.min(maxUsers || 50, 100),
      password: password?.trim() || null,
      createdBy: user.id,
      allowFileSharing: settings?.allowFileSharing !== false,
      allowReactions: settings?.allowReactions !== false,
      messageHistory: settings?.messageHistory !== false,
      moderationEnabled: settings?.moderationEnabled || false,
    });

    // Creator automatically joins
    socket.join(roomId);
    roomManager.addUserToRoom(user.id, roomId);
    room.currentUsers = 1;

    // Add creator as admin
    await addRoomAdmin(roomId, user.id);

    // Notify creator
    socket.created(
      {
        room: {
          id: room.id,
          name: room.name,
          description: room.description,
          isPrivate: room.isPrivate,
          maxUsers: room.maxUsers,
          currentUsers: room.currentUsers,
          settings: room.settings,
          role: 'admin',
        },
      },
      'Room created successfully',
    );

    // Broadcast new room to all users (if public)
    if (!room.isPrivate) {
      socket.broadcast.emit('room:created', {
        room: {
          id: room.id,
          name: room.name,
          description: room.description,
          currentUsers: room.currentUsers,
          maxUsers: room.maxUsers,
          createdBy: user.username,
        },
      });
    }
  } catch (error) {
    socket.error(error, 'Failed to create room');
  }
});

Advanced Room Targeting

Selective Broadcasting

javascript
// Broadcast to specific users in a room
function broadcastToUsersInRoom(io, roomId, userIds, event, data) {
  const sockets = io.sockets.adapter.rooms.get(roomId);

  if (sockets) {
    for (const socketId of sockets) {
      const socket = io.sockets.sockets.get(socketId);
      if (socket && socket.user && userIds.includes(socket.user.id)) {
        socket.emit(event, data);
      }
    }
  }
}

// Broadcast to all except specific users
function broadcastToRoomExcept(io, roomId, excludeUserIds, event, data) {
  const sockets = io.sockets.adapter.rooms.get(roomId);

  if (sockets) {
    for (const socketId of sockets) {
      const socket = io.sockets.sockets.get(socketId);
      if (socket && socket.user && !excludeUserIds.includes(socket.user.id)) {
        socket.emit(event, data);
      }
    }
  }
}

// Role-based broadcasting
function broadcastToRole(io, roomId, role, event, data) {
  const sockets = io.sockets.adapter.rooms.get(roomId);

  if (sockets) {
    for (const socketId of sockets) {
      const socket = io.sockets.sockets.get(socketId);
      if (socket && socket.user && socket.user.role === role) {
        socket.emit(event, data);
      }
    }
  }
}

// Usage examples
socket.on('admin:announcement', (data) => {
  const { roomId, message } = data;

  if (!isRoomAdmin(socket.user.id, roomId)) {
    return socket.forbidden({}, 'Admin privileges required');
  }

  // Send to all users in room
  io.to(roomId).emit('announcement', {
    message,
    from: 'admin',
    timestamp: new Date(),
  });

  socket.ok({}, 'Announcement sent');
});

Room-Specific Message Types

javascript
// Enhanced message sending with room targeting
socket.on('message:send', async (data) => {
  try {
    const { roomId, content, type, target } = data;
    const user = socket.user;

    // Standard validation...

    const message = {
      id: generateMessageId(),
      roomId,
      userId: user.id,
      username: user.username,
      content,
      type,
      timestamp: new Date(),
    };

    // Handle different targeting strategies
    switch (target?.type) {
      case 'all':
        // Send to all users in room
        io.to(roomId).emit('message:received', message);
        break;

      case 'role':
        // Send to users with specific role
        broadcastToRole(io, roomId, target.role, 'message:received', message);
        break;

      case 'users':
        // Send to specific users
        broadcastToUsersInRoom(io, roomId, target.userIds, 'message:received', message);
        break;

      case 'except':
        // Send to all except specific users
        broadcastToRoomExcept(io, roomId, target.excludeUserIds, 'message:received', message);
        break;

      default:
        // Default: send to all
        io.to(roomId).emit('message:received', message);
    }

    socket.ok({ messageId: message.id }, 'Message sent');
  } catch (error) {
    socket.error(error, 'Failed to send message');
  }
});

Room Permissions and Moderation

Permission System

javascript
// Room permissions
const ROOM_PERMISSIONS = {
  SEND_MESSAGES: 'send_messages',
  DELETE_MESSAGES: 'delete_messages',
  KICK_USERS: 'kick_users',
  BAN_USERS: 'ban_users',
  MANAGE_ROOM: 'manage_room',
  INVITE_USERS: 'invite_users',
};

// Check room permission
function hasRoomPermission(userId, roomId, permission) {
  const userRole = getRoomUserRole(userId, roomId);
  const permissions = getRolePermissions(userRole);
  return permissions.includes(permission);
}

// Kick user from room
socket.on('room:kick', async (data) => {
  try {
    const { roomId, userId, reason } = data;
    const moderator = socket.user;

    if (!moderator) {
      return socket.unauthorized({}, 'Authentication required');
    }

    // Check permissions
    if (!hasRoomPermission(moderator.id, roomId, ROOM_PERMISSIONS.KICK_USERS)) {
      return socket.forbidden(
        {
          permission: ROOM_PERMISSIONS.KICK_USERS,
          userRole: getRoomUserRole(moderator.id, roomId),
        },
        'Insufficient permissions to kick users',
      );
    }

    // Find target user's socket
    const targetSocket = findUserSocket(io, userId);
    if (!targetSocket) {
      return socket.notFound({ userId }, 'User not found or not online');
    }

    // Check if user is in the room
    if (!targetSocket.rooms.has(roomId)) {
      return socket.badRequest({ userId, roomId }, 'User is not in this room');
    }

    // Cannot kick admins or users with higher role
    const targetRole = getRoomUserRole(userId, roomId);
    const moderatorRole = getRoomUserRole(moderator.id, roomId);

    if (getRoleLevel(targetRole) >= getRoleLevel(moderatorRole)) {
      return socket.forbidden(
        {
          targetRole,
          moderatorRole,
        },
        'Cannot kick user with equal or higher privileges',
      );
    }

    // Kick user from room
    targetSocket.leave(roomId);
    roomManager.removeUserFromRoom(userId, roomId);

    // Notify kicked user
    targetSocket.emit('room:kicked', {
      roomId,
      reason: reason || 'No reason provided',
      kickedBy: moderator.username,
      timestamp: new Date(),
    });

    // Notify room
    io.to(roomId).emit('room:user_kicked', {
      userId,
      username: targetSocket.user?.username,
      kickedBy: moderator.username,
      reason,
      timestamp: new Date(),
    });

    // Log moderation action
    await logModerationAction({
      type: 'kick',
      roomId,
      targetUserId: userId,
      moderatorId: moderator.id,
      reason,
      timestamp: new Date(),
    });

    socket.ok(
      {
        userId,
        roomId,
        reason,
      },
      'User kicked successfully',
    );
  } catch (error) {
    socket.error(error, 'Failed to kick user');
  }
});

Room Settings Management

javascript
// Update room settings
socket.on('room:settings', async (data) => {
  try {
    const { roomId, settings } = data;
    const user = socket.user;

    if (!user) {
      return socket.unauthorized({}, 'Authentication required');
    }

    // Check permissions
    if (!hasRoomPermission(user.id, roomId, ROOM_PERMISSIONS.MANAGE_ROOM)) {
      return socket.forbidden(
        { permission: ROOM_PERMISSIONS.MANAGE_ROOM },
        'Insufficient permissions to manage room',
      );
    }

    const room = roomManager.getRoom(roomId);
    if (!room) {
      return socket.notFound({ roomId }, 'Room not found');
    }

    // Validate settings
    const validSettings = {
      allowFileSharing:
        typeof settings.allowFileSharing === 'boolean'
          ? settings.allowFileSharing
          : room.settings.allowFileSharing,
      allowReactions:
        typeof settings.allowReactions === 'boolean'
          ? settings.allowReactions
          : room.settings.allowReactions,
      messageHistory:
        typeof settings.messageHistory === 'boolean'
          ? settings.messageHistory
          : room.settings.messageHistory,
      moderationEnabled:
        typeof settings.moderationEnabled === 'boolean'
          ? settings.moderationEnabled
          : room.settings.moderationEnabled,
      slowMode: settings.slowMode
        ? Math.max(0, Math.min(settings.slowMode, 300))
        : room.settings.slowMode || 0,
    };

    // Update room settings
    room.settings = { ...room.settings, ...validSettings };
    await updateRoomSettings(roomId, room.settings);

    // Notify room members
    io.to(roomId).emit('room:settings_updated', {
      roomId,
      settings: room.settings,
      updatedBy: user.username,
      timestamp: new Date(),
    });

    socket.ok(
      {
        roomId,
        settings: room.settings,
      },
      'Room settings updated',
    );
  } catch (error) {
    socket.error(error, 'Failed to update room settings');
  }
});

Multi-Room User Management

User Room Status

javascript
// Get user's room list
socket.on('user:rooms', async () => {
  try {
    const user = socket.user;

    if (!user) {
      return socket.unauthorized({}, 'Authentication required');
    }

    const userRoomIds = roomManager.getUserRooms(user.id);
    const rooms = [];

    for (const roomId of userRoomIds) {
      const room = roomManager.getRoom(roomId);
      if (room) {
        const unreadCount = await getUnreadMessageCount(user.id, roomId);
        const lastMessage = await getLastMessage(roomId);

        rooms.push({
          id: room.id,
          name: room.name,
          description: room.description,
          isPrivate: room.isPrivate,
          currentUsers: room.currentUsers,
          maxUsers: room.maxUsers,
          unreadCount,
          lastMessage,
          userRole: getRoomUserRole(user.id, roomId),
          settings: room.settings
        });
      }
    }

    socket.ok({
      rooms,
      totalRooms: rooms.length
    }, 'User rooms retrieved');

  } catch (error) {
    socket.error(error, 'Failed to get user rooms');
  }
});

// Switch active room
socket.on('user:switch_room', (data) => {
  try {
    const { roomId } = data;
    const user = socket.user;

    if (!user) {
      return socket.unauthorized({}, 'Authentication required');
    }

    // Validate room exists and user is member
    if (!socket.rooms.has(roomId)) {
      return socket.forbidden(
        { roomId },
        'Not a member of this room'
      );
    }

    // Update user's active room
    socket.activeRoom = roomId;

    // Mark messages as read
    markMessagesAsRead(user.id, roomId);

    // Get room data
    const room = roomManager.getRoom(roomId);
    const recentMessages = await getRecentMessages(roomId, 50);
    const roomUsers = await getRoomUsers(roomId);

    socket.ok({
      room: {
        id: room.id,
        name: room.name,
        description: room.description,
        settings: room.settings,
        currentUsers: room.currentUsers
      },
      recentMessages,
      users: roomUsers
    }, `Switched to ${room.name}`);

  } catch (error) {
    socket.error(error, 'Failed to switch room');
  }
});

This comprehensive room targeting system provides sophisticated room management capabilities with proper permissions, moderation tools, and flexible broadcasting options.

Released under the ISC License.