openvibers/openvibe.chat Commits
Chat: robot commands name the viewer (Network subject) so Live can send them to OpenVibe.Bot as that person; Live's refusals are shown in chat (#84)
Changes
@@@ -1449,14 +1449,20 @@ class ChatServer {}}-- /** Send a command to the streamer's hardware client (Live). Silent when the streamer is unknown. */-- _hardwareCommand(ws, stream, command, client) {-- ctx.effects.hardwareCommand(stream.user_id, command, client.user?.display_name || `anon${client.anonId || ''}`).then((r) => {-- if (r && r.ok) return;-- if (r && r.reason === 'no_hardware') this.sendTo(ws, { type: 'system', message: 'No hardware client connected.' });-- }).catch((err) => {-- this.sendTo(ws, { type: 'system', message: err.message || 'Media command failed.' });-- });++ /**++ * A robot command for the channel's OpenVibe.Bot robot, through Live (it forwards to Bot as this viewer, plan T15).++ * The viewer is named by Network subject (none when not signed in: Bot only takes commands from people). Live's++ * refusal comes back as a sentence for this viewer alone; a quiet refusal (a cooldown) says nothing.++ */++ async _hardwareCommand(ws, stream, command, client) {++ try {++ const subject = client.user ? await this._subjectOfUser(client.user) : null;++ const r = await ctx.effects.hardwareCommand(stream.user_id, command, client.user?.display_name || `anon${client.anonId || ''}`,++ { subject, userId: client.user?.id || null });++ if (r && !r.ok && r.message) this.sendTo(ws, { type: 'system', message: r.message });++ } catch (err) {++ this.sendTo(ws, { type: 'system', message: err.message || 'The robot command failed.' });++ }}/**@@@ -919,7 +919,9 @@ const effects = {// bridge path the body carries none and Live's sendToConn answers the connection itself.arenaCommand: (client, cmd, parts) => effect('arena-command', { client: clientHandle(client), cmd, parts }),mediaQueue: async (op, args) => await effect('media-queue', { op, ...args }),-- hardwareCommand: async (streamerUserId, command, fromUser) => await effect('hardware', { streamer_user_id: streamerUserId, command, from_user: fromUser }),++ 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 } : {}),++ }),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),@@@ -260,7 +260,8 @@ async function boot({ env = {} } = {}) {if (body.op === 'state') return send(200, { state: live.mediaState });if (body.op === 'skip') return send(200, { ended: live.mediaState.now_playing, next: live.mediaState.queue[0] || null });return send(400, { error: 'unknown op' });-- case 'hardware': return send(200, { ok: false, reason: 'no_hardware' });++ // 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 '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); }@@@ -329,7 +329,9 @@ t('media commands go to Live’s queue with the same replies', async () => {await aliceWs.next((x) => x.type === 'system' && x.message === 'Only the streamer or a moderator can skip media.');await h.sleep(1100);aliceWs.sendJson({ type: 'chat', message: '!forward' });-- await aliceWs.next((x) => x.type === 'system' && x.message === 'No hardware client connected.');++ // The robot command goes to Live with the viewer's Network subject (Live forwards it to OpenVibe.Bot as them),++ // and Live's refusal is shown to the viewer as it said it.++ await aliceWs.next((x) => x.type === 'system' && x.message === `No robot here (asked as usr_01J9AAAAAAAAAAAAAAAAAAAAAA, user ${alice.id}).`);});t('upgrade guards: foreign origin refused, banned address refused (admins exempt)', async () => {
@@ -1449,14 +1449,20 @@ class ChatServer {
@@ -1449,14 +1449,20 @@ class ChatServer {
}
}
}
}
- /** Send a command to the streamer's hardware client (Live). Silent when the streamer is unknown. */
- _hardwareCommand(ws, stream, command, client) {
- ctx.effects.hardwareCommand(stream.user_id, command, client.user?.display_name || `anon${client.anonId || ''}`).then((r) => {
- if (r && r.ok) return;
- if (r && r.reason === 'no_hardware') this.sendTo(ws, { type: 'system', message: 'No hardware client connected.' });
- }).catch((err) => {
- this.sendTo(ws, { type: 'system', message: err.message || 'Media command failed.' });
- });
+ /**
+ * A robot command for the channel's OpenVibe.Bot robot, through Live (it forwards to Bot as this viewer, plan T15).
+ * The viewer is named by Network subject (none when not signed in: Bot only takes commands from people). Live's
+ * refusal comes back as a sentence for this viewer alone; a quiet refusal (a cooldown) says nothing.
+ */
+ async _hardwareCommand(ws, stream, command, client) {
+ try {
+ const subject = client.user ? await this._subjectOfUser(client.user) : null;
+ const r = await ctx.effects.hardwareCommand(stream.user_id, command, client.user?.display_name || `anon${client.anonId || ''}`,
+ { subject, userId: client.user?.id || null });
+ if (r && !r.ok && r.message) this.sendTo(ws, { type: 'system', message: r.message });
+ } catch (err) {
+ this.sendTo(ws, { type: 'system', message: err.message || 'The robot command failed.' });
+ }
}
}
/**
/**
@@ -919,7 +919,9 @@ const effects = {
@@ -919,7 +919,9 @@ const effects = {
// bridge path the body carries none and Live's sendToConn answers the connection itself.
// bridge path the body carries none and Live's sendToConn answers the connection itself.
arenaCommand: (client, cmd, parts) => effect('arena-command', { client: clientHandle(client), cmd, parts }),
arenaCommand: (client, cmd, parts) => effect('arena-command', { client: clientHandle(client), cmd, parts }),
mediaQueue: async (op, args) => await effect('media-queue', { op, ...args }),
mediaQueue: async (op, args) => await effect('media-queue', { op, ...args }),
- hardwareCommand: async (streamerUserId, command, fromUser) => await effect('hardware', { streamer_user_id: streamerUserId, command, from_user: fromUser }),
+ 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 } : {}),
+ }),
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),
@@ -260,7 +260,8 @@ async function boot({ env = {} } = {}) {
@@ -260,7 +260,8 @@ async function boot({ env = {} } = {}) {
if (body.op === 'state') return send(200, { state: live.mediaState });
if (body.op === 'state') return send(200, { state: live.mediaState });
if (body.op === 'skip') return send(200, { ended: live.mediaState.now_playing, next: live.mediaState.queue[0] || null });
if (body.op === 'skip') return send(200, { ended: live.mediaState.now_playing, next: live.mediaState.queue[0] || null });
return send(400, { error: 'unknown op' });
return send(400, { error: 'unknown op' });
- case 'hardware': return send(200, { ok: false, reason: 'no_hardware' });
+ // 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 '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); }
@@ -329,7 +329,9 @@ t('media commands go to Live’s queue with the same replies', async () => {
@@ -329,7 +329,9 @@ t('media commands go to Live’s queue with the same replies', async () => {
await aliceWs.next((x) => x.type === 'system' && x.message === 'Only the streamer or a moderator can skip media.');
await aliceWs.next((x) => x.type === 'system' && x.message === 'Only the streamer or a moderator can skip media.');
await h.sleep(1100);
await h.sleep(1100);
aliceWs.sendJson({ type: 'chat', message: '!forward' });
aliceWs.sendJson({ type: 'chat', message: '!forward' });
- await aliceWs.next((x) => x.type === 'system' && x.message === 'No hardware client connected.');
+ // The robot command goes to Live with the viewer's Network subject (Live forwards it to OpenVibe.Bot as them),
+ // and Live's refusal is shown to the viewer as it said it.
+ await aliceWs.next((x) => x.type === 'system' && x.message === `No robot here (asked as usr_01J9AAAAAAAAAAAAAAAAAAAAAA, user ${alice.id}).`);
});
});
t('upgrade guards: foreign origin refused, banned address refused (admins exempt)', async () => {
t('upgrade guards: foreign origin refused, banned address refused (admins exempt)', async () => {