openvibers/openvibe.chat Commits
Elements: a chat command reaches Live on the existing chat-message effect (#533)
Changes
@@@ -1514,6 +1514,20 @@ class ChatServer {const parts = text.trim().split(/\s+/);const cmd = parts[0].toLowerCase();++ // A !command may be an Element trigger (Elements, stage 1). Commands bypass the normal chat++ // line path, so this is the one place they reach Live's existing chat-message effect; Live runs++ // an element only if its library listens for that command, and a signed-in person is required.++ if (client.channelUserId && client.user?.id) {++ try {++ ctx.effects.chatMessage({++ element: true, element_command: true,++ stream_id: client.streamId || null, channel_user_id: client.channelUserId,++ user_id: client.user.id, username: client.user.display_name || client.user.username,++ message: text,++ }).catch(() => { /* the element trigger is not worth a user-visible error */ });++ } catch { /* non-critical */ }++ }++// Arena: !hype / !beef / !arena (Live server/arena/arena-chat.js) — chat can only hype; the Arena// is pure mic. Live runs the command. When Live delivers through Chat's ingress (LIVE_CHAT_INGRESS)// the sender-only replies come back in the effect response and this socket sends them; on the old@@@ -138,6 +138,29 @@ t('chat into a stream room: room delivery, global cross-feed, persistence, outboassert.strictEqual(reaction.body.powerchat_chat.externalChatterId, `u${alice.id}`);});++t('a chat command is marked for Elements on the existing chat-message effect', async () => {++ const carol = h.addUser('carol', { subject: 'usr_01J9CCCCCCCCCCCCCCCCCCCCCC' });++ const ws = await h.ws({ ip: '198.51.100.22', token: carol.token, stream: streamId });++ ws.sendJson({ type: 'join', streamId, token: carol.token });++ await ws.next((m) => m.type === 'auth');++ ws.sendJson({ type: 'chat', message: '!dance please' });++ let fx = null;++ for (let i = 0; i < 60 && !fx; i++) {++ fx = h.live.effects.find((e) => e.name === 'chat-message' && e.body.message === '!dance please');++ if (!fx) await h.sleep(25);++ }++ assert.ok(fx, 'the command reaches Live on the one chat-message effect');++ assert.strictEqual(fx.body.element_command, true);++ assert.strictEqual(fx.body.element, true);++ assert.strictEqual(fx.body.channel_user_id, streamer.id);++ assert.strictEqual(fx.body.user_id, carol.id);++ assert.strictEqual(fx.body.award, undefined, 'a command does not carry the coin bonus');++ // A normal line goes the ordinary path; its effect is not an element command.++ const plain = h.live.effects.find((e) => e.name === 'chat-message' && e.body.message === 'hello stream');++ assert.strictEqual(plain.body.element_command, undefined, 'a normal line is not an element trigger');++ ws.close();++});++t('a message that could not be saved is not broadcast; the sender is told', async () => {const orig = h.db.saveChatMessage;h.db.saveChatMessage = () => { throw new Error('disk full'); };
@@ -1514,6 +1514,20 @@ class ChatServer {
@@ -1514,6 +1514,20 @@ class ChatServer {
const parts = text.trim().split(/\s+/);
const parts = text.trim().split(/\s+/);
const cmd = parts[0].toLowerCase();
const cmd = parts[0].toLowerCase();
+ // A !command may be an Element trigger (Elements, stage 1). Commands bypass the normal chat
+ // line path, so this is the one place they reach Live's existing chat-message effect; Live runs
+ // an element only if its library listens for that command, and a signed-in person is required.
+ if (client.channelUserId && client.user?.id) {
+ try {
+ ctx.effects.chatMessage({
+ element: true, element_command: true,
+ stream_id: client.streamId || null, channel_user_id: client.channelUserId,
+ user_id: client.user.id, username: client.user.display_name || client.user.username,
+ message: text,
+ }).catch(() => { /* the element trigger is not worth a user-visible error */ });
+ } catch { /* non-critical */ }
+ }
+
// Arena: !hype / !beef / !arena (Live server/arena/arena-chat.js) — chat can only hype; the Arena
// Arena: !hype / !beef / !arena (Live server/arena/arena-chat.js) — chat can only hype; the Arena
// is pure mic. Live runs the command. When Live delivers through Chat's ingress (LIVE_CHAT_INGRESS)
// is pure mic. Live runs the command. When Live delivers through Chat's ingress (LIVE_CHAT_INGRESS)
// the sender-only replies come back in the effect response and this socket sends them; on the old
// the sender-only replies come back in the effect response and this socket sends them; on the old
@@ -138,6 +138,29 @@ t('chat into a stream room: room delivery, global cross-feed, persistence, outbo
@@ -138,6 +138,29 @@ t('chat into a stream room: room delivery, global cross-feed, persistence, outbo
assert.strictEqual(reaction.body.powerchat_chat.externalChatterId, `u${alice.id}`);
assert.strictEqual(reaction.body.powerchat_chat.externalChatterId, `u${alice.id}`);
});
});
+t('a chat command is marked for Elements on the existing chat-message effect', async () => {
+ const carol = h.addUser('carol', { subject: 'usr_01J9CCCCCCCCCCCCCCCCCCCCCC' });
+ const ws = await h.ws({ ip: '198.51.100.22', token: carol.token, stream: streamId });
+ ws.sendJson({ type: 'join', streamId, token: carol.token });
+ await ws.next((m) => m.type === 'auth');
+ ws.sendJson({ type: 'chat', message: '!dance please' });
+ let fx = null;
+ for (let i = 0; i < 60 && !fx; i++) {
+ fx = h.live.effects.find((e) => e.name === 'chat-message' && e.body.message === '!dance please');
+ if (!fx) await h.sleep(25);
+ }
+ assert.ok(fx, 'the command reaches Live on the one chat-message effect');
+ assert.strictEqual(fx.body.element_command, true);
+ assert.strictEqual(fx.body.element, true);
+ assert.strictEqual(fx.body.channel_user_id, streamer.id);
+ assert.strictEqual(fx.body.user_id, carol.id);
+ assert.strictEqual(fx.body.award, undefined, 'a command does not carry the coin bonus');
+ // A normal line goes the ordinary path; its effect is not an element command.
+ const plain = h.live.effects.find((e) => e.name === 'chat-message' && e.body.message === 'hello stream');
+ assert.strictEqual(plain.body.element_command, undefined, 'a normal line is not an element trigger');
+ ws.close();
+});
+
t('a message that could not be saved is not broadcast; the sender is told', async () => {
t('a message that could not be saved is not broadcast; the sender is told', async () => {
const orig = h.db.saveChatMessage;
const orig = h.db.saveChatMessage;
h.db.saveChatMessage = () => { throw new Error('disk full'); };
h.db.saveChatMessage = () => { throw new Error('disk full'); };