Skip to content

Commit b311ae2

Browse files
author
Micilini Roll
committed
phase 12: add bot hooks and automation events
1 parent dea3d04 commit b311ae2

20 files changed

Lines changed: 1020 additions & 3 deletions

File tree

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,38 @@ $pdo = PdoConnectionFactory::sqlite(__DIR__ . '/storage/phpsockets.sqlite');
151151

152152
The CLI migration command will be added in a future phase.
153153

154+
## Bot hooks
155+
156+
PHPSockets includes a lightweight bot hook layer.
157+
158+
Bots can listen to text messages and respond in the same conversation context.
159+
160+
Supported contexts:
161+
162+
- Global Room
163+
- Direct conversations
164+
- Private group rooms
165+
166+
Example:
167+
168+
```php
169+
$server->bots()->register(new EchoBot());
170+
```
171+
172+
Example command:
173+
174+
```txt
175+
/echo hello
176+
```
177+
178+
Response:
179+
180+
```txt
181+
hello
182+
```
183+
184+
Bots are intentionally simple in v1. They do not call external AI APIs or run asynchronous jobs.
185+
154186
## Emoji and small attachment support
155187

156188
The chat examples support a composer action button next to the message input.

examples/easy-chat/public/assets/app.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1044,11 +1044,13 @@ function addMessage(message) {
10441044
}
10451045

10461046
const isOwn = state.currentUser && message.fromUserId === state.currentUser.userId;
1047-
const sender = findDisplayName(message.fromUserId);
1047+
const sender = displayNameForMessage(message);
10481048
const createdAt = formatTime(message.createdAt);
1049+
const isBot = message.kind === 'bot' || Boolean(message.metadata && message.metadata.bot);
10491050

10501051
const row = document.createElement('div');
10511052
row.className = isOwn ? 'message-row is-own' : 'message-row';
1053+
row.classList.toggle('is-bot', isBot);
10521054
row.dataset.messageId = message.id;
10531055

10541056
const footer = document.createElement('div');
@@ -1157,6 +1159,14 @@ function findDisplayName(userId) {
11571159
return user.displayName;
11581160
}
11591161

1162+
function displayNameForMessage(message) {
1163+
if (message && message.metadata && message.metadata.botName) {
1164+
return message.metadata.botName;
1165+
}
1166+
1167+
return findDisplayName(message.fromUserId);
1168+
}
1169+
11601170
function formatTime(value) {
11611171
if (!value) {
11621172
return 'now';

examples/easy-chat/public/assets/style.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,25 @@ body {
368368
background: linear-gradient(135deg, rgba(37, 99, 235, 0.95), rgba(8, 145, 178, 0.95));
369369
}
370370

371+
.message-row.is-bot .message-bubble {
372+
border: 1px solid rgba(56, 189, 248, 0.28);
373+
background: rgba(14, 116, 144, 0.18);
374+
}
375+
376+
.message-row.is-bot .message-meta::before {
377+
content: "BOT";
378+
display: inline-flex;
379+
align-items: center;
380+
margin-right: 8px;
381+
padding: 2px 7px;
382+
border-radius: 999px;
383+
background: rgba(56, 189, 248, 0.18);
384+
color: #7dd3fc;
385+
font-size: 0.68rem;
386+
font-weight: 900;
387+
letter-spacing: 0.04em;
388+
}
389+
371390
.typing-indicator {
372391
min-height: 42px;
373392
padding: 0 24px 16px;

examples/medium-chat/public/assets/app.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1091,11 +1091,13 @@ function addMessage(message) {
10911091
}
10921092

10931093
const isOwn = state.currentUser && message.fromUserId === state.currentUser.userId;
1094-
const sender = findDisplayName(message.fromUserId);
1094+
const sender = displayNameForMessage(message);
10951095
const createdAt = formatTime(message.createdAt);
1096+
const isBot = message.kind === 'bot' || Boolean(message.metadata && message.metadata.bot);
10961097

10971098
const row = document.createElement('div');
10981099
row.className = isOwn ? 'message-row is-own' : 'message-row';
1100+
row.classList.toggle('is-bot', isBot);
10991101
row.dataset.messageId = message.id;
11001102

11011103
const footer = document.createElement('div');
@@ -1204,6 +1206,14 @@ function findDisplayName(userId) {
12041206
return user.displayName;
12051207
}
12061208

1209+
function displayNameForMessage(message) {
1210+
if (message && message.metadata && message.metadata.botName) {
1211+
return message.metadata.botName;
1212+
}
1213+
1214+
return findDisplayName(message.fromUserId);
1215+
}
1216+
12071217
function formatTime(value) {
12081218
if (!value) {
12091219
return 'now';

examples/medium-chat/public/assets/style.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,25 @@ body {
386386
background: linear-gradient(135deg, rgba(109, 40, 217, 0.96), rgba(8, 145, 178, 0.96));
387387
}
388388

389+
.message-row.is-bot .message-bubble {
390+
border: 1px solid rgba(56, 189, 248, 0.28);
391+
background: rgba(14, 116, 144, 0.18);
392+
}
393+
394+
.message-row.is-bot .message-meta::before {
395+
content: "BOT";
396+
display: inline-flex;
397+
align-items: center;
398+
margin-right: 8px;
399+
padding: 2px 7px;
400+
border-radius: 999px;
401+
background: rgba(56, 189, 248, 0.18);
402+
color: #7dd3fc;
403+
font-size: 0.68rem;
404+
font-weight: 900;
405+
letter-spacing: 0.04em;
406+
}
407+
389408
.typing-indicator {
390409
min-height: 42px;
391410
padding: 0 22px 16px;

examples/private-chat/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,23 @@ PrivateChat displays unread badges for Global Room, direct conversations and pri
112112

113113
Badges increase while a conversation is not open and reset when the conversation is opened.
114114

115+
## Bot commands
116+
117+
This example registers two simple bots:
118+
119+
```txt
120+
/help
121+
/echo <text>
122+
```
123+
124+
The bot response appears in the same conversation:
125+
126+
- Global Room
127+
- Direct conversation
128+
- Private group room
129+
130+
Bots only respond to text messages. File messages do not trigger bots.
131+
115132
## Storage note
116133

117134
This example still uses in-memory storage by default.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Micilini\PhpSockets\Examples\PrivateChat\Bots;
6+
7+
use Micilini\PhpSockets\Chat\Bot\BotContext;
8+
use Micilini\PhpSockets\Chat\Bot\BotResponse;
9+
use Micilini\PhpSockets\Contracts\BotInterface;
10+
11+
final class EchoBot implements BotInterface
12+
{
13+
public function name(): string
14+
{
15+
return 'Echo Bot';
16+
}
17+
18+
public function handle(BotContext $context): ?BotResponse
19+
{
20+
$text = trim($context->text());
21+
22+
if (!str_starts_with($text, '/echo ')) {
23+
return null;
24+
}
25+
26+
$message = trim(substr($text, 6));
27+
28+
if ($message === '') {
29+
return BotResponse::text('Usage: /echo <text>');
30+
}
31+
32+
return BotResponse::text($message);
33+
}
34+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Micilini\PhpSockets\Examples\PrivateChat\Bots;
6+
7+
use Micilini\PhpSockets\Chat\Bot\BotContext;
8+
use Micilini\PhpSockets\Chat\Bot\BotResponse;
9+
use Micilini\PhpSockets\Contracts\BotInterface;
10+
11+
final class HelpBot implements BotInterface
12+
{
13+
public function name(): string
14+
{
15+
return 'Help Bot';
16+
}
17+
18+
public function handle(BotContext $context): ?BotResponse
19+
{
20+
if (trim($context->text()) !== '/help') {
21+
return null;
22+
}
23+
24+
return BotResponse::text('Available commands: /help, /echo <text>');
25+
}
26+
}

examples/private-chat/public/assets/app.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -869,12 +869,24 @@ function conversationIdForMessage(message) {
869869
return 'global';
870870
}
871871

872+
if (message.roomId) {
873+
for (const conversation of state.conversations.values()) {
874+
if (conversation.roomId === message.roomId) {
875+
return conversation.id;
876+
}
877+
}
878+
}
879+
872880
const roomConversationId = groupConversationId(message.roomId);
873881

874882
if (state.conversations.has(roomConversationId)) {
875883
return roomConversationId;
876884
}
877885

886+
if (message.kind === 'bot' || (message.metadata && message.metadata.bot)) {
887+
return state.activeConversationId || 'global';
888+
}
889+
878890
if (state.currentUser && message.fromUserId !== state.currentUser.userId) {
879891
ensureDirectConversationFromUserId(message.fromUserId);
880892
return directConversationId(message.fromUserId);
@@ -1581,11 +1593,13 @@ function appendMessageElement(message) {
15811593
}
15821594

15831595
const isOwn = state.currentUser && message.fromUserId === state.currentUser.userId;
1584-
const sender = findDisplayName(message.fromUserId);
1596+
const sender = displayNameForMessage(message);
15851597
const createdAt = formatTime(message.createdAt);
1598+
const isBot = message.kind === 'bot' || Boolean(message.metadata && message.metadata.bot);
15861599

15871600
const row = document.createElement('div');
15881601
row.className = isOwn ? 'message-row is-own' : 'message-row';
1602+
row.classList.toggle('is-bot', isBot);
15891603
row.dataset.messageId = message.id;
15901604

15911605
const footer = document.createElement('div');
@@ -1762,6 +1776,14 @@ function findDisplayName(userId) {
17621776
return user.displayName;
17631777
}
17641778

1779+
function displayNameForMessage(message) {
1780+
if (message && message.metadata && message.metadata.botName) {
1781+
return message.metadata.botName;
1782+
}
1783+
1784+
return findDisplayName(message.fromUserId);
1785+
}
1786+
17651787
function formatTime(value) {
17661788
if (!value) {
17671789
return 'now';

examples/private-chat/public/assets/style.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,25 @@ body {
447447
background: linear-gradient(135deg, rgba(5, 150, 105, 0.96), rgba(37, 99, 235, 0.96));
448448
}
449449

450+
.message-row.is-bot .message-bubble {
451+
border: 1px solid rgba(56, 189, 248, 0.28);
452+
background: rgba(14, 116, 144, 0.18);
453+
}
454+
455+
.message-row.is-bot .message-meta::before {
456+
content: "BOT";
457+
display: inline-flex;
458+
align-items: center;
459+
margin-right: 8px;
460+
padding: 2px 7px;
461+
border-radius: 999px;
462+
background: rgba(56, 189, 248, 0.18);
463+
color: #7dd3fc;
464+
font-size: 0.68rem;
465+
font-weight: 900;
466+
letter-spacing: 0.04em;
467+
}
468+
450469
.typing-indicator {
451470
min-height: 42px;
452471
padding: 0 22px 16px;

0 commit comments

Comments
 (0)