Festi Chat Plugin

The Chat plugin adds chats to a Festi project. It has two parts:

  • Area chat. A message list and a form that you put on a page of your plugin. The browser gets new messages at a fixed interval.
  • Chat room. A page for one room with text chat and video calls. The VirtualRoom ReactJS client shows this page.

Each message belongs to an area. An area is a string of a maximum of 32 characters, for example task_15. A chat room also has an area. Thus the two parts use the same message store.

Functions

  • Show the last messages of an area.
  • Send a message.
  • Get new messages at an interval. The default interval is 5 seconds.
  • Load more messages with the Read More link.
  • Keep chat rooms, room members and the last message that each user read.
  • Keep WebRTC peer data and a log of user actions for the rooms.
  • Send events when a message or a room changes.

Requirements

  • PHP 8.0 or later. The source uses constructor property promotion.
  • Festi Framework with the Theme package. ChatPlugin extends Festi\Theme\Plugin\DisplayPlugin.
  • The Users plugin. The default users facade uses plugin\Users\UsersPlugin.
  • A users table. The tables of the plugin have foreign keys to users.id.
  • MySQL. The install dump has MySQL statements only.
  • jQuery and Jimbo.lang() on the page that shows the area chat.

Install

The CLI commands on this page were not run for this page.

The plugin directory must have the name Chat. The namespace of the plugin is plugin\Chat. The repository has no init.php file.

1. Add the plugin to the project

Use the Plugin Manager of the Festi CLI. Replace the paths with the paths of your project.

./vendor/bin/festi-plugin --mode install --path ./src/dashboard/ --dump ./dumps/ \
    --plugin Chat --repository [email protected]:FestiPlugins/php_festi_plugin_chat.git

The command does these steps:

  1. It adds the repository to the plugins directory as a git submodule.
  2. It writes a migration that adds the plugin to the festi_plugins table.
  3. It applies the migrations of the project.

2. Add the tables and the URL rules

The Plugin Manager reads the file install/install.<database type>.sql. This repository has only install/install.sql. Thus the command in step 1 does not create the tables.

Add the tables and the URL rules with a migration of the project:

  1. Create an empty migration.

    ./vendor/bin/festi-make-migration --path ./src/dashboard/ --dump ./dumps/
  2. Copy the statements of install/install.sql into the new migration file.

  3. Remove the INSERT INTO festi_plugins statement from the migration. Step 1 added this row.
  4. Add the changes from Limits of the install dump to the migration.
  5. Apply the migration.

    ./vendor/bin/festi-migrate --path ./src/dashboard/ --dump ./dumps/
  6. Commit the submodule and the migration files together.

Limits of the install dump

install/install.sql does not agree with the source in these points. Correct them in the migration of your project.

Item Dump Source
Messages table chat chats (MessagesObject::TABLE_CHATS)
Table chat_room_users Not in the dump OnlineUsersObject reads id_room, id_user, cdate, mdate
Table chat_areas2users Not in the dump AreaToUsersObject reads id, area, id_user, id_last_read_message
Columns of chats_rooms Not in the dump type, last_activity_date, shared_id, type_by_members_count
Columns of chat_room_peers Not in the dump id_target, type, is_initiator
Column of chats_rooms2users Not in the dump cdate
URL rule ~^/chat/message/send/$~ Method onAjaxSendMessage ChatPlugin does not have this method
URL rules for the chat room Only ~^/api/chats/([0-9]+)/action/$~ See URL rules

URL rules

Festi routes a URL with the rows of festi_url_rules and festi_url_rules2areas. All rules of the plugin use the backend area.

Rules in install/install.sql:

Pattern Method Function
~^/chat/message/send/$~ onAjaxSendMessage Send a message. The method is not in ChatPlugin. Add it in your plugin.
~^/chat/messages/get/([0-9]+)/$~ onAjaxGetMessages Get more messages (Read More).
~^/chat/messages/get/new/([0-9]+)/$~ onAjaxGetNewMessages Get the new messages of other users.
~^/api/chats/([0-9]+)/action/$~ onJsonUserAction Write a user action to the log.

Methods of ChatPlugin that have no rule in the dump:

Pattern in the source Method Function
~^/chats/([0-9]+)/$~ onDisplayRoom Show the page of a chat room.
~^/api/chats/([0-9]+)/join/$~ onJsonJoinRoom Add the current user to the room. Return the other users online.
~^/api/chats/([0-9]+)/peer/$~ onJsonAddPeer Save peer data for the room.
~^/api/chats/([0-9]+)/video/host/$~ onJsonGetRoomHost Return the host peer and the peer list of the room.
No pattern in the source onJsonGetRoomPeers Return the peers that have the current user as the target.

festi-plugin --mode url adds a new method to the plugin class. Do not use it for a method that the plugin has. Add the rule to a migration file and apply it with festi-migrate. This example was not run.

INSERT INTO `festi_url_rules` (`plugin`, `pattern`, `method`)
SELECT 'Chat', '~^/chats/([0-9]+)/$~', 'onDisplayRoom'
FROM DUAL
WHERE NOT EXISTS (
    SELECT 1 FROM `festi_url_rules`
    WHERE `plugin` = 'Chat' AND `method` = 'onDisplayRoom'
);

INSERT INTO `festi_url_rules2areas` (`id_url_rule`, `area`)
SELECT rules.`id`, 'backend'
FROM `festi_url_rules` AS rules
WHERE rules.`plugin` = 'Chat'
    AND rules.`method` = 'onDisplayRoom'
    AND NOT EXISTS (
        SELECT 1 FROM `festi_url_rules2areas` AS areas
        WHERE areas.`id_url_rule` = rules.`id` AND areas.`area` = 'backend'
    );

Area chat

ChatPlugin::onFetch() returns the HTML of the chat for one area.

public function onFetch(IChatArea $chatArea, array $options = []): string

The method includes css/styles.css, js/chat.js and js/notify.min.js. It shows the last 10 messages of the area (ChatPlugin::DEFAULT_LIMIT_MESSAGES).

Options

Key Default Description
title Chat The title of the chat block.
interval 5 The interval in seconds between two requests for new messages.
send_url /chat/message/send/ The URL that receives a new message.
get_url /chat/messages/get/{offset}/ The URL of the Read More link.
refresh_url /chat/messages/get/new/{lastID}/ The URL that returns the new messages.

The constants for these keys are in IChatPlugin, for example IChatPlugin::OPTION_SEND_URL.

Set send_url, get_url and refresh_url if your project has its own logic for messages. Add a URL rule in your project for each URL that you set.

Placeholders

chat.js replaces the placeholders in the browser.

Placeholder Value
{offset} The number of messages that the page shows. It starts at 10 and increases by 10 for each Read More.
{lastID} The ID of the last message that the browser received.
let url = decodeURI(this.refreshUrl.replace("{lastID}", Chat.lastID));

Requests and responses

URL Request Response
send_url POST with a JSON body: message, area JSON. content is the HTML of the new message.
get_url GET JSON. content is the HTML of the messages. Empty content removes the Read More link.
refresh_url GET with the area parameter JSON. content is the HTML of the new messages. lastID is the ID of the last message.

onAjaxGetNewMessages and onAjaxGetMessages read the area request parameter. refresh_url does not return the messages of the current user.

Example

This example was not run.

use plugin\Chat\ChatPlugin;
use plugin\Chat\domain\ChatArea;

/**
 * Shows a task page with a chat.
 */
public function onDisplayTask(Response &$response, int $idTask): bool
{
    $chatPlugin = Core::getInstance()->getPluginInstance('Chat');
    assert($chatPlugin instanceof ChatPlugin);

    $chatArea = new ChatArea('task_'.$idTask);

    $options = [
        'send_url'    => $this->getUrl('/task/'.$idTask.'/chat/message/send/'),
        'get_url'     => $this->getUrl('/task/'.$idTask.'/chat/messages/get/{offset}/'),
        'refresh_url' => $this->getUrl('/task/'.$idTask.'/chat/messages/get/new/{lastID}/'),
    ];

    $response->content = $chatPlugin->onFetch($chatArea, $options);

    return true;
}

/**
 * Receives a message from the chat form.
 */
public function onAjaxSendMessage(Response &$response, int $idTask): bool
{
    $chatPlugin = Core::getInstance()->getPluginInstance('Chat');
    assert($chatPlugin instanceof ChatPlugin);

    $message = $chatPlugin->addMessage();

    $response->setType(Response::JSON);
    $response->content = $chatPlugin->fetchMessage($message);

    return true;
}

ChatPlugin::addMessage() reads message, area and id_author from $_REQUEST. The default value of id_author is the ID of the current user.

Known limits

  • onFetch() has this comment in the source: TODO: test & fix this method (use only for ajax mode).
  • chat.js sends the message as a JSON body. addMessage() reads $_REQUEST.
  • chat.js does not add the area parameter to get_url. onAjaxGetMessages reads this parameter.
  • chat.js puts a message count in {offset}. onAjaxGetMessages uses the value as a message ID.

Chat room

ChatPlugin::onDisplayRoom() shows the page of one room. It makes sure that the current user can open the room (doValidatePermission()):

  • The user must be logged in.
  • A room with the public privacy is open to each logged-in user.
  • Other rooms are open to the author and to the members of the room.

ChatPlugin::fetchRoom() returns the HTML of the room. Use it to put a room into your own page.

public function fetchRoom(IRoomEntity $room, array $options = []): string
use plugin\Chat\IChatPlugin;

$room = $chatPlugin->getRoomsFacade()->getByID($idRoom);
$chatPlugin->doValidatePermission($room);

$options = [
    IChatPlugin::OPTION_ENABLED_VIDEO_CHAT => true,
    IChatPlugin::OPTION_LANG               => 'en',
    IChatPlugin::OPTION_SIGNAL_SERVERS     => 'wss://signal.example.com',
];

$html = $chatPlugin->fetchRoom($room, $options);

This example was not run.

Options

Constant in IChatPlugin Key Default Description
OPTION_API_BASE_URL apiBaseUrl getUrl('/api/') The base URL of the chat API for the client.
OPTION_ENABLED_VIDEO_CHAT videochat false The room has a video chat.
OPTION_ALLOW_VIDEO_CHAT isAllowVideoChatInText false A text room shows the Call button.
OPTION_MESSAGES_PER_PAGE messagesLimit 10 The number of messages in one page.
OPTION_MESSAGE_MAX_LENGTH messageMaxLength 2000 The maximum length of a message.
OPTION_LANG lang [] The language of the client: en, ru, uk or kk. Always set it.
OPTION_TIME_FORMAT timeFormat HH:mm The time format of the messages.
OPTION_ROOM_NAME roomName null The name that the client shows for the room.
OPTION_ICE_SERVERS iceServers [] The ICE servers for WebRTC.
OPTION_SIGNAL_SERVERS signalingServerUrl '' The URL of the signalling server.
OPTION_PEER_SERVER peerServerConfig [] The options of the PeerJS server.
OPTION_SHARE_URL shareUrl false The URL to share the room.
OPTION_SHARE_ID shareID false The shared ID of the room.
OPTION_IS_DYNAMIC isDynamic false The client sends the name and the avatar of the user to the other users.

IChatPlugin also has OPTION_WITH_PAGE_HEAD and OPTION_IS_ANONYMOUS. ChatPlugin does not read them.

Settings of the system plugin

templates/room_chat.phtml reads two settings of the system plugin:

Setting Use
js_version The value of the v parameter in the URLs of main.css, main.js and runtime-main.js.
enabled_chat_logging A value that is not empty sets logEnabled to true for the client.

Room list

tblDefs/chats_rooms.xml is the DGS definition for the chats_rooms table. plugin\Chat\domain\RoomsStore is the store class for this DGS. It sets cdate and last_activity_date before an insert and sends EVENT_ON_CREATE_ROOM after it. The Join action of the DGS opens /chats/%id%/.

ChatPlugin has no method that shows this DGS. Add the method in your project.

VirtualRoom client

The source of the client is the repository reactjs_festi_plugin_chat_virtualroom. It is the git submodule static/virtualroom of this repository. The build of the client is in static/festi-virtualroom. This directory is in the repository.

templates/room_chat.phtml connects the plugin and the client:

  1. It writes the room data into window.roomInfo.
  2. It adds the <div id="root"> element.
  3. It loads main.css, main.js and runtime-main.js from static/festi-virtualroom.

The client sends its API requests to apiBaseUrl. ChatPlugin has methods for these paths of the client: chats/<room ID>/peer/, chats/<room ID>/join/ and chats/<room ID>/action/. ChatPlugin also has the method onJsonGetRoomPeers() for chats/<room ID>/peers/. The source has no URL rule for this method. The client also calls paths that ChatPlugin does not have: members/, messages/, messages/create/, messages/<ID>/remove/, messages/<ID>/read/ and user/<ID>/. Add these methods in your project. Use the facades of the plugin.

To build the client, see the README of the client repository.

Facades

ChatPlugin gives access to the data through facades.

Method Facade Data
getMessagesFacade() IMessagesFacade Messages of an area.
getRoomsFacade() IRoomsFacade Chat rooms.
getMembersFacade() IMembersFacade Members of a room.
getAreaToUsersFacade() IAreaToUsersFacade The last message that a user read, and unread counts.
getPeerToPeerFacade() IPeerToPeerFacade Peers and users online in a room.
getUsersActionsFacade() IUsersActionsFacade The log of user actions.
getUsersFacade() IUsersFacade Users of the project.

The default IUsersFacade is DefaultUsersFacade. It reads users through the Users plugin. To use your own user source, implement IUsersFacade and call setUsersFacade().

$chatPlugin->setUsersFacade(new ProjectUsersFacade());

Events

ChatPlugin dispatches these events. The constants are in IChatPlugin.

Constant Time Target
EVENT_ON_BEFORE_SEND_MESSAGE Before the plugin saves a message message (by reference)
EVENT_ON_MESSAGE_SENT After the plugin saves a message message
EVENT_ON_MESSAGE_DELETED After the plugin deletes a message message
EVENT_ON_CREATE_ROOM After the plugin creates a room The values of the room
EVENT_ON_USER_ACTION After the plugin writes a user action to the log userAction
$chatPlugin->addEventListener(
    IChatPlugin::EVENT_ON_MESSAGE_SENT,
    function (FestiEvent $event) {
        $message = $event->getTargetValueByKey('message');
        // ...
    }
);

Directory structure

ChatPlugin.php      # Plugin class: URL methods, fetch methods, facades
IChatPlugin.php     # Option and event constants, public interface
*Object.php         # Data access objects, one for each table
facades/            # Business logic for messages, rooms, members, peers, users
models/             # Entities: message, room, user, peer, user action
requests/           # Request objects that validate the input
domain/             # ChatArea, RoomsStore
tblDefs/            # DGS definition of chats_rooms
templates/          # chat.phtml (area chat), room_chat.phtml (chat room)
static/             # CSS and JS of the area chat, build of the VirtualRoom client
install/            # install.sql (MySQL)