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 Morelink. - 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.
ChatPluginextendsFesti\Theme\Plugin\DisplayPlugin. - The
Usersplugin. The default users facade usesplugin\Users\UsersPlugin. - A
userstable. The tables of the plugin have foreign keys tousers.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:
- It adds the repository to the plugins directory as a git submodule.
- It writes a migration that adds the plugin to the
festi_pluginstable. - 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:
-
Create an empty migration.
./vendor/bin/festi-make-migration --path ./src/dashboard/ --dump ./dumps/ -
Copy the statements of
install/install.sqlinto the new migration file. - Remove the
INSERT INTO festi_pluginsstatement from the migration. Step 1 added this row. - Add the changes from Limits of the install dump to the migration.
-
Apply the migration.
./vendor/bin/festi-migrate --path ./src/dashboard/ --dump ./dumps/ -
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.jssends the message as a JSON body.addMessage()reads$_REQUEST.chat.jsdoes not add theareaparameter toget_url.onAjaxGetMessagesreads this parameter.chat.jsputs a message count in{offset}.onAjaxGetMessagesuses 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
publicprivacy 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:
- It writes the room data into
window.roomInfo. - It adds the
<div id="root">element. - It loads
main.css,main.jsandruntime-main.jsfromstatic/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)