Skip to content

0haris0/test-chat-app

Repository files navigation

Chat aplication (React, Node, Express and Redis)

Application for chat with others.

Technical Stack

  • Frontend - React, Socket.io
  • Backend - Node.js, Express.js, Redis

How it works?

Initialization:

First, we check for the existence of a key called total_users. If this key doesn't exist, we set up the Redis database with initial data.

Creating Sample Users:

We generate new user IDs incrementally using INCR total_users . Each user is associated with a username, and we store this data in a reference key, such as SET username:nick user:1. The remaining user data, including their username and a securely hashed password, is stored in a structured format.

Including Users in the Default "General" Room:

Every user is automatically included in the General room. To manage rooms specific to each user, we utilize a set that maintains room IDs.

Populating Private Conversations:

Private rooms are established as needed. For each user conversation, a unique room ID, like room:1:2, is created. Users are assigned to these private rooms using commands like SADD user:1:rooms 1:2 and SADD user:2:rooms 1:2. Messages within these rooms are stored in an organized set.

Filling the "General" Room with Messages:

Messages in the "General" room are organized within a sorted set labeled as room:0. We employ JSON to structure messages, simplifying the handling of message data in this demonstration application.

Here's a simplified version of the text with similar words:

User Registration

Redis primarily serves as a database for storing user and message data and facilitates message exchange among connected servers.

Data Storage:

  • Chat information is stored using various keys and data types.

    • User data resides in a hash set, with each user entry comprising the following:
      • username: a unique username
      • password: a securely hashed password
  • To access a user's hash set, employ the key format user:{userId}. Data is stored using the HSET command, where the key, field, and data are specified. User IDs are generated by incrementing the total_users key.

    • For instance, INCR total_users increments the total user count.
  • Usernames are stored as separate keys (username:{username}) to provide quick access to the corresponding user ID.

    • For example, SET username:Alex 4 associates the username "Alex" with the user ID 4.

Data Retrieval:

  • Fetching User Data: Utilize HGETALL user:{id} to retrieve all data associated with a specific user.

    • For example, HGETALL user:2 retrieves the data for the user with ID 2.
  • Online Users: To obtain a list of users who are currently online, use SMEMBERS online_users.

    • As an example, SMEMBERS online_users returns the IDs of online users.

Code Example: Preparing User Data in a Redis Hash Set

const usernameKey = makeUsernameKey(username);
/** Create user */
const hashedPassword = await bcrypt.hash(password, 10);
const nextId = await incr("total_users");
const userKey = `user:${nextId}`;
await set(usernameKey, userKey);
await hmset(userKey, ["username", username, "password", hashedPassword]);

/**
 * Each user has a set of rooms he is in
 * let's define the default ones
 */
await sadd(`user:${nextId}:rooms`, `${0}`); // Main room

Here's a simplified version of the text with different words while preserving the information:

Rooms

Data Storage:

Every user is associated with a set of rooms.

These rooms are organized collections of messages, where each message has a timestamp score. Each room also has a name linked to it.

  • The rooms that a user belongs to are stored in the format user:{userId}:rooms as a set of room IDs.

    • For instance, SADD user:Alex:rooms 1 adds user "Alex" to room 1.
  • You can set the name of a room using SET room:{roomId}:name {name}.

    • For example, SET room:1:name General assigns the name "General" to room 1.

Data Retrieval:

  • Obtaining Room Names: You can retrieve the name of a room using GET room:{roomId}:name.

    • For example, GET room:0:name should return "General."
  • Getting Room IDs for a User: To fetch the IDs of rooms that a user is a part of, use SMEMBERS user:{id}:rooms.

    • For instance, SMEMBERS user:2:rooms will provide the IDs of rooms for the user with ID 2.

Code Example: Retrieve All of My Rooms

const rooms = [];
for (let x = 0; x < roomIds.length; x++) {
  const roomId = roomIds[x];

  let name = await get(`room:${roomId}:name`);
  /** It's a room without a name, likey the one with private messages */
  if (!name) {
    /**
     * Make sure we don't add private rooms with empty messages
     * It's okay to add custom (named rooms)
     */
    const roomExists = await exists(`room:${roomId}`);
    if (!roomExists) {
      continue;
    }

    const userIds = roomId.split(":");
    if (userIds.length !== 2) {
      return res.sendStatus(400);
    }
    rooms.push({
      id: roomId,
      names: [
        await hmget(`user:${userIds[0]}`, "username"),
        await hmget(`user:${userIds[1]}`, "username"),
      ],
    });
  } else {
    rooms.push({
      id: roomId,
      names: [name]
    });
  }
}
return rooms;

Pub/Sub

After initialization, a pub/sub subscription is created: SUBSCRIBE MESSAGES. At the same time, each server instance will run a listener on a message on this channel to receive real-time updates.

Again, for simplicity, each message is serialized to JSON, which we parse and then handle in the same manner, as WebSocket messages.

Pub/sub allows connecting multiple servers written in different platforms without taking into consideration the implementation detail of each server.

How the data is stored:

  • Messages are stored at room:{roomId} key in a sorted set (as mentioned above). They are added with ZADD room:{roomId} {timestamp} {message} command. Message is serialized to an app-specific JSON string.
    • E.g ZADD room:0 1617197047 { "From": "2", "Date": 1617197047, "Message": "Hello", "RoomId": "1:2"

How the data is accessed:

  • Get list of messages ZREVRANGE room:{roomId} {offset_start} {offset_end}.
    • E.g ZREVRANGE room:1:2 0 50 will return 50 messages with 0 offsets for the private room between users with IDs 1 and 2.

Code Example: Send Message

async (message) => {
  /** Make sure nothing illegal is sent here. */
  message = {
    ...message,
    message: sanitise(message.message)
  };
  /**
   * The user might be set as offline if he tried to access the chat from another tab, pinging by message
   * resets the user online status
   */
  await sadd("online_users", message.from);
  /** We've got a new message. Store it in db, then send back to the room. */
  const messageString = JSON.stringify(message);
  const roomKey = `room:${message.roomId}`;
  /**
   * It may be possible that the room is private and new, so it won't be shown on the other
   * user's screen, check if the roomKey exist. If not then broadcast message that the room is appeared
   */
  const isPrivate = !(await exists(`${roomKey}:name`));
  const roomHasMessages = await exists(roomKey);
  if (isPrivate && !roomHasMessages) {
    const ids = message.roomId.split(":");
    const msg = {
      id: message.roomId,
      names: [
        await hmget(`user:${ids[0]}`, "username"),
        await hmget(`user:${ids[1]}`, "username"),
      ],
    };
    publish("show.room", msg);
    socket.broadcast.emit(`show.room`, msg);
  }
  await zadd(roomKey, "" + message.date, messageString);
  publish("message", message);
  io.to(roomKey).emit("message", message);
}

Session handling

The chat server works as a basic REST API which involves keeping the session and handling the user state in the chat rooms (besides the WebSocket/real-time part).

When a WebSocket/real-time server is instantiated, which listens for the next events:

Connection. A new user is connected. At this point, a user ID is captured and saved to the session (which is cached in Redis). Note, that session caching is language/library-specific and it's used here purely for persistence and maintaining the state between server reloads.

A global set with online_users key is used for keeping the online state for each user. So on a new connection, a user ID is written to that set:

E.g. SADD online_users 1 (We add user with id 1 to the set online_users).

After that, a message is broadcasted to the clients to notify them that a new user is joined the chat.

Disconnect. It works similarly to the connection event, except we need to remove the user for online_users set and notify the clients: SREM online_users 1 (makes user with id 1 offline).

Message. A user sends a message, and it needs to be broadcasted to the other clients. The pub/sub allows us also to broadcast this message to all server instances which are connected to this Redis:

PUBLISH message "{'serverId': 4132, 'type':'message', 'data': {'from': 1, 'date': 1615480369, 'message': 'Hello', 'roomId': '1:2'}}"

Note we send additional data related to the type of the message and the server id. Server id is used to discard the messages by the server instance which sends them since it is connected to the same MESSAGES channel.

type field of the serialized JSON corresponds to the real-time method we use for real-time communication ( connect/disconnect/message).

data is method-specific information. In the example above it's related to the new message.

How the data is stored / accessed:

The session data is stored in Redis by utilizing the connect-redis client.

const session = require("express-session");
let RedisStore = require("connect-redis")(session);
const sessionMiddleware = session({
  store: new RedisStore({client: redisClient}),
  secret: "keyboard cat",
  saveUninitialized: true,
  resave: true,
});

How to run it locally?

Write in environment variable or Dockerfile actual connection to Redis:

   REDIS_ENDPOINT_URL = "Redis server URI"
   REDIS_PASSWORD = "Password to the server"

Run frontend

cd client
yarn install
yarn start

Run backend

yarn install
yarn start

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •