openvibers/openvibe.chat Commits
Arena on stream: !1, !2, !tie reach Live as chat effects (#240)
Changes
@@@ -1355,6 +1355,17 @@ class ChatServer {return;}++ if (cmd === '!1' || cmd === '!2' || cmd === '!tie') {++ if (!client.user?.id) { this.sendTo(ws, { type: 'system', message: 'Sign in to vote in Bench.' }); return; }++ if (!client.streamId) { this.sendTo(ws, { type: 'system', message: 'Join a live stream to vote in Bench.' }); return; }++ const choice = cmd === '!1' ? 'left' : cmd === '!2' ? 'right' : 'tie';++ try {++ const result = await ctx.effects.benchVote(client.streamId, client.user.id, choice);++ this.sendTo(ws, { type: 'system', message: result.message || 'Bench could not count that vote.' });++ } catch (err) { this.sendTo(ws, { type: 'system', message: 'Bench could not count that vote.' }); }++ return;++ }++if (cmd === '!gotti') {const username = client.user ? client.user.display_name : client.anonId;const coreUsername = client.user ? client.user.username : null;@@@ -4,7 +4,7 @@* Every piece of data Chat does not own (accounts and roles, streams and live state, channels,* channel moderation policy, bans and IP rules, follows, cosmetics and tags, site settings,* anon numbers) and every side effect chat triggers in Live (coins, AI viewers, arena, media-- * queue, hardware, pastes, translation, PowerChat, notifications, viewer counts, IP log) goes++ * queue, hardware, Bench votes, pastes, translation, PowerChat, notifications, viewer counts, IP log) goes* through this module. Nothing else in Chat talks to Live.** Transport: Live's internal endpoints, service token from OpenVibe.Network for audience@@@ -944,6 +944,7 @@ const effects = {hardwareCommand: async (streamerUserId, command, fromUser, { subject = null, userId = null } = {}) => await effect('hardware', {streamer_user_id: streamerUserId, command, from_user: fromUser, ...(subject ? { from_subject: subject } : {}), ...(userId ? { from_user_id: userId } : {}),}),++ benchVote: (streamId, userId, choice) => effect('bench-vote', { stream_id: streamId, viewer_user_id: userId, choice }),createPaste: (body) => effect('paste', body).then((r) => r.paste),translate: (text, channelUserId) => effect('translate', { text, channel_user_id: channelUserId }).then((r) => r.translation || null),notifyDm: (body) => fire('notify/dm', body),@@@ -262,6 +262,7 @@ async function boot({ env = {} } = {}) {return send(400, { error: 'unknown op' });// Live forwards a robot command to the channel's OpenVibe.Bot robot as the viewer: the stub says who it was asked as.case 'hardware': return send(200, { ok: false, reason: 'no_robot', message: `No robot here (asked as ${body.from_subject || 'nobody'}, user ${body.from_user_id || '-'}).` });++ case 'bench-vote': return send(200, { ok: true, message: 'Your Bench vote is counted.' });case 'ensure-channel': {let ch = [...live.channels.values()].find((c) => c.user_id === body.user_id);if (!ch) { ch = { id: 100 + live.channels.size, user_id: body.user_id, title: 'New Channel' }; live.channels.set(ch.id, ch); }@@@ -334,6 +334,27 @@ t('media commands go to Live’s queue with the same replies', async () => {await aliceWs.next((x) => x.type === 'system' && x.message === `No robot here (asked as usr_01J9AAAAAAAAAAAAAAAAAAAAAA, user ${alice.id}).`);});++t('Bench votes reach Live once with signed-in viewer and stream ids', async () => {++ await h.sleep(1100);++ // Bob is still timed out by the moderation test. Use a viewer with no moderation history.++ const viewer = h.addUser('benchvoter');++ await h.ctx.sync();++ const viewerWs = await h.ws({ ip: '198.51.100.75', token: viewer.token, stream: streamId });++ try {++ viewerWs.sendJson({ type: 'join', streamId, token: viewer.token });++ await viewerWs.next((m) => m.type === 'auth');++ const before = h.live.effects.length;++ aliceWs.sendJson({ type: 'chat', message: '!1' });++ await aliceWs.next((x) => x.type === 'system' && x.message === 'Your Bench vote is counted.');++ viewerWs.sendJson({ type: 'chat', message: '!tie' });++ await viewerWs.next((x) => x.type === 'system' && x.message === 'Your Bench vote is counted.');++ assert.deepStrictEqual(h.live.effects.slice(before).filter((e) => e.name === 'bench-vote').map((e) => e.body), [++ { stream_id: streamId, viewer_user_id: alice.id, choice: 'left' },++ { stream_id: streamId, viewer_user_id: viewer.id, choice: 'tie' },++ ]);++ } finally { viewerWs.close(); }++});++t('a !word that is not a channel sound is pressed as the robot button of that name, quietly', async () => {await h.sleep(1100);const before = h.live.effects.length;
@@ -1355,6 +1355,17 @@ class ChatServer {
@@ -1355,6 +1355,17 @@ class ChatServer {
return;
return;
}
}
+ if (cmd === '!1' || cmd === '!2' || cmd === '!tie') {
+ if (!client.user?.id) { this.sendTo(ws, { type: 'system', message: 'Sign in to vote in Bench.' }); return; }
+ if (!client.streamId) { this.sendTo(ws, { type: 'system', message: 'Join a live stream to vote in Bench.' }); return; }
+ const choice = cmd === '!1' ? 'left' : cmd === '!2' ? 'right' : 'tie';
+ try {
+ const result = await ctx.effects.benchVote(client.streamId, client.user.id, choice);
+ this.sendTo(ws, { type: 'system', message: result.message || 'Bench could not count that vote.' });
+ } catch (err) { this.sendTo(ws, { type: 'system', message: 'Bench could not count that vote.' }); }
+ return;
+ }
+
if (cmd === '!gotti') {
if (cmd === '!gotti') {
const username = client.user ? client.user.display_name : client.anonId;
const username = client.user ? client.user.display_name : client.anonId;
const coreUsername = client.user ? client.user.username : null;
const coreUsername = client.user ? client.user.username : null;
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
* Every piece of data Chat does not own (accounts and roles, streams and live state, channels,
* Every piece of data Chat does not own (accounts and roles, streams and live state, channels,
* channel moderation policy, bans and IP rules, follows, cosmetics and tags, site settings,
* channel moderation policy, bans and IP rules, follows, cosmetics and tags, site settings,
* anon numbers) and every side effect chat triggers in Live (coins, AI viewers, arena, media
* anon numbers) and every side effect chat triggers in Live (coins, AI viewers, arena, media
- * queue, hardware, pastes, translation, PowerChat, notifications, viewer counts, IP log) goes
+ * queue, hardware, Bench votes, pastes, translation, PowerChat, notifications, viewer counts, IP log) goes
* through this module. Nothing else in Chat talks to Live.
* through this module. Nothing else in Chat talks to Live.
*
*
* Transport: Live's internal endpoints, service token from OpenVibe.Network for audience
* Transport: Live's internal endpoints, service token from OpenVibe.Network for audience
@@ -944,6 +944,7 @@ const effects = {
@@ -944,6 +944,7 @@ const effects = {
hardwareCommand: async (streamerUserId, command, fromUser, { subject = null, userId = null } = {}) => await effect('hardware', {
hardwareCommand: async (streamerUserId, command, fromUser, { subject = null, userId = null } = {}) => await effect('hardware', {
streamer_user_id: streamerUserId, command, from_user: fromUser, ...(subject ? { from_subject: subject } : {}), ...(userId ? { from_user_id: userId } : {}),
streamer_user_id: streamerUserId, command, from_user: fromUser, ...(subject ? { from_subject: subject } : {}), ...(userId ? { from_user_id: userId } : {}),
}),
}),
+ benchVote: (streamId, userId, choice) => effect('bench-vote', { stream_id: streamId, viewer_user_id: userId, choice }),
createPaste: (body) => effect('paste', body).then((r) => r.paste),
createPaste: (body) => effect('paste', body).then((r) => r.paste),
translate: (text, channelUserId) => effect('translate', { text, channel_user_id: channelUserId }).then((r) => r.translation || null),
translate: (text, channelUserId) => effect('translate', { text, channel_user_id: channelUserId }).then((r) => r.translation || null),
notifyDm: (body) => fire('notify/dm', body),
notifyDm: (body) => fire('notify/dm', body),
@@ -262,6 +262,7 @@ async function boot({ env = {} } = {}) {
@@ -262,6 +262,7 @@ async function boot({ env = {} } = {}) {
return send(400, { error: 'unknown op' });
return send(400, { error: 'unknown op' });
// Live forwards a robot command to the channel's OpenVibe.Bot robot as the viewer: the stub says who it was asked as.
// Live forwards a robot command to the channel's OpenVibe.Bot robot as the viewer: the stub says who it was asked as.
case 'hardware': return send(200, { ok: false, reason: 'no_robot', message: `No robot here (asked as ${body.from_subject || 'nobody'}, user ${body.from_user_id || '-'}).` });
case 'hardware': return send(200, { ok: false, reason: 'no_robot', message: `No robot here (asked as ${body.from_subject || 'nobody'}, user ${body.from_user_id || '-'}).` });
+ case 'bench-vote': return send(200, { ok: true, message: 'Your Bench vote is counted.' });
case 'ensure-channel': {
case 'ensure-channel': {
let ch = [...live.channels.values()].find((c) => c.user_id === body.user_id);
let ch = [...live.channels.values()].find((c) => c.user_id === body.user_id);
if (!ch) { ch = { id: 100 + live.channels.size, user_id: body.user_id, title: 'New Channel' }; live.channels.set(ch.id, ch); }
if (!ch) { ch = { id: 100 + live.channels.size, user_id: body.user_id, title: 'New Channel' }; live.channels.set(ch.id, ch); }
@@ -334,6 +334,27 @@ t('media commands go to Live’s queue with the same replies', async () => {
@@ -334,6 +334,27 @@ t('media commands go to Live’s queue with the same replies', async () => {
await aliceWs.next((x) => x.type === 'system' && x.message === `No robot here (asked as usr_01J9AAAAAAAAAAAAAAAAAAAAAA, user ${alice.id}).`);
await aliceWs.next((x) => x.type === 'system' && x.message === `No robot here (asked as usr_01J9AAAAAAAAAAAAAAAAAAAAAA, user ${alice.id}).`);
});
});
+t('Bench votes reach Live once with signed-in viewer and stream ids', async () => {
+ await h.sleep(1100);
+ // Bob is still timed out by the moderation test. Use a viewer with no moderation history.
+ const viewer = h.addUser('benchvoter');
+ await h.ctx.sync();
+ const viewerWs = await h.ws({ ip: '198.51.100.75', token: viewer.token, stream: streamId });
+ try {
+ viewerWs.sendJson({ type: 'join', streamId, token: viewer.token });
+ await viewerWs.next((m) => m.type === 'auth');
+ const before = h.live.effects.length;
+ aliceWs.sendJson({ type: 'chat', message: '!1' });
+ await aliceWs.next((x) => x.type === 'system' && x.message === 'Your Bench vote is counted.');
+ viewerWs.sendJson({ type: 'chat', message: '!tie' });
+ await viewerWs.next((x) => x.type === 'system' && x.message === 'Your Bench vote is counted.');
+ assert.deepStrictEqual(h.live.effects.slice(before).filter((e) => e.name === 'bench-vote').map((e) => e.body), [
+ { stream_id: streamId, viewer_user_id: alice.id, choice: 'left' },
+ { stream_id: streamId, viewer_user_id: viewer.id, choice: 'tie' },
+ ]);
+ } finally { viewerWs.close(); }
+});
+
t('a !word that is not a channel sound is pressed as the robot button of that name, quietly', async () => {
t('a !word that is not a channel sound is pressed as the robot button of that name, quietly', async () => {
await h.sleep(1100);
await h.sleep(1100);
const before = h.live.effects.length;
const before = h.live.effects.length;