diff --git a/README b/README index ab1c57f..5537f67 100644 --- a/README +++ b/README @@ -138,6 +138,7 @@ Typing a line starting with a slash `/` in the chat dialogue causes a command to be sent to the server. The following commands are available to all users: + - `/msg user text`: sends a private message; - `/me text`: sends a chat message starting with the sender's username; - `/leave`: equivalent to clicking the *Disconnect* button. - `/set var val`: sets the value of a configuration variable without any diff --git a/rtpconn/webclient.go b/rtpconn/webclient.go index 837e8b0..203f516 100644 --- a/rtpconn/webclient.go +++ b/rtpconn/webclient.go @@ -141,6 +141,7 @@ type clientMessage struct { Type string `json:"type"` Kind string `json:"kind,omitempty"` Id string `json:"id,omitempty"` + Dest string `json:"dest,omitempty"` Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` Permissions group.ClientPermissions `json:"permissions,omitempty"` @@ -158,14 +159,14 @@ func fromJSTime(tm uint64) time.Time { if tm == 0 { return time.Time{} } - return time.Unix(int64(tm)/1000, (int64(tm)%1000) * 1000000) + return time.Unix(int64(tm)/1000, (int64(tm)%1000)*1000000) } func toJSTime(tm time.Time) uint64 { if tm.Before(time.Unix(0, 0)) { return 0 } - return uint64((tm.Sub(time.Unix(0, 0)) + time.Millisecond / 2) / time.Millisecond) + return uint64((tm.Sub(time.Unix(0, 0)) + time.Millisecond/2) / time.Millisecond) } type closeMessage struct { @@ -292,7 +293,7 @@ func addDownConn(c *webClient, id string, remote conn.Up) (*rtpDownConnection, e return conn, err } -func addDownConnHelper(c *webClient, conn *rtpDownConnection, remote conn.Up) (error) { +func addDownConnHelper(c *webClient, conn *rtpDownConnection, remote conn.Up) error { c.mu.Lock() defer c.mu.Unlock() @@ -1048,23 +1049,38 @@ func handleClientMessage(c *webClient, m clientMessage) error { } case "chat": tm := toJSTime(time.Now()) - c.group.AddToChatHistory( - m.Id, m.Username, tm, m.Kind, m.Value, - ) + if m.Dest == "" { + c.group.AddToChatHistory( + m.Id, m.Username, tm, m.Kind, m.Value, + ) + } mm := clientMessage{ Type: "chat", Id: m.Id, + Dest: m.Dest, Username: m.Username, Time: tm, Kind: m.Kind, Value: m.Value, } - clients := c.group.GetClients(nil) - for _, cc := range clients { - cc, ok := cc.(*webClient) - if ok { - cc.write(mm) + if m.Dest == "" { + clients := c.group.GetClients(nil) + for _, cc := range clients { + ccc, ok := cc.(*webClient) + if ok { + ccc.write(mm) + } } + } else { + cc := c.group.GetClient(m.Dest) + if cc == nil { + return c.error(group.UserError("user unknown")) + } + ccc, ok := cc.(*webClient) + if !ok { + return c.error(group.UserError("this user doesn't chat")) + } + ccc.write(mm) } case "groupaction": switch m.Kind { diff --git a/static/protocol.js b/static/protocol.js index b8bc5c8..079d7d2 100644 --- a/static/protocol.js +++ b/static/protocol.js @@ -124,7 +124,7 @@ function ServerConnection() { /** * onchat is called whenever a new chat message is received. * - * @type {(this: ServerConnection, id: string, username: string, time: number, kind: string, message: string) => void} + * @type {(this: ServerConnection, id: string, dest: string, username: string, time: number, kind: string, message: string) => void} */ this.onchat = null; /** @@ -148,6 +148,7 @@ function ServerConnection() { * @property {string} type * @property {string} [kind] * @property {string} [id] + * @property {string} [dest] * @property {string} [username] * @property {string} [password] * @property {Object} [permissions] @@ -285,7 +286,7 @@ ServerConnection.prototype.connect = async function(url) { case 'chat': if(sc.onchat) sc.onchat.call( - sc, m.id, m.username, m.time, m.kind, m.value, + sc, m.id, m.dest, m.username, m.time, m.kind, m.value, ); break; case 'clearchat': @@ -428,10 +429,11 @@ ServerConnection.prototype.newUpStream = function(id) { * @param {string} kind - The kind of message, either "" or "me". * @param {string} message - The text of the message. */ -ServerConnection.prototype.chat = function(username, kind, message) { +ServerConnection.prototype.chat = function(username, kind, dest, message) { this.send({ type: 'chat', id: this.id, + dest: dest, username: username, kind: kind, value: message, diff --git a/static/sfu.css b/static/sfu.css index e4e458b..93229a9 100644 --- a/static/sfu.css +++ b/static/sfu.css @@ -297,6 +297,15 @@ textarea.form-reply { background: #ececec; } +.message-private { + background: white; +} + +.message-private .message-header:after { + content: "(private)"; + margin-left: 1em; +} + .message-system { font-size: 10px; background: #ececec; diff --git a/static/sfu.js b/static/sfu.js index 4587c30..f0b1475 100644 --- a/static/sfu.js +++ b/static/sfu.js @@ -1227,6 +1227,7 @@ function formatTime(time) { * @typedef {Object} lastMessage * @property {string} [nick] * @property {string} [peerId] + * @property {string} [dest] */ /** @type {lastMessage} */ @@ -1239,7 +1240,7 @@ let lastMessage = {}; * @param {string} kind * @param {string} message */ -function addToChatbox(peerId, nick, time, kind, message){ +function addToChatbox(peerId, dest, nick, time, kind, message) { let userpass = getUserPass(); let row = document.createElement('div'); row.classList.add('message-row'); @@ -1248,12 +1249,16 @@ function addToChatbox(peerId, nick, time, kind, message){ row.appendChild(container); if(!peerId) container.classList.add('message-system'); - else if(userpass.username === nick) { + if(userpass.username === nick) container.classList.add('message-sender'); - } + if(dest) + container.classList.add('message-private'); + if(kind !== 'me') { let p = formatLines(message.split('\n')); - if (lastMessage.nick !== nick || lastMessage.peerId !== peerId) { + if(lastMessage.nick !== nick || + lastMessage.peerId !== peerId || + lastMessage.dest !== (dest || null)) { let header = document.createElement('p'); let user = document.createElement('span'); user.textContent = nick; @@ -1272,6 +1277,7 @@ function addToChatbox(peerId, nick, time, kind, message){ container.appendChild(p); lastMessage.nick = nick; lastMessage.peerId = peerId; + lastMessage.dest = (dest || null); } else { let asterisk = document.createElement('span'); asterisk.textContent = '*'; @@ -1288,8 +1294,7 @@ function addToChatbox(peerId, nick, time, kind, message){ container.appendChild(user); container.appendChild(content); container.classList.add('message-me'); - delete(lastMessage.nick); - delete(lastMessage.peerId); + lastMessage = {}; } let box = document.getElementById('box'); @@ -1387,7 +1392,7 @@ function handleInput() { let s = ""; for(let key in settings) s = s + `${key}: ${JSON.stringify(settings[key])}\n` - addToChatbox(null, null, Date.now(), null, s); + addToChatbox(null, null, null, Date.now(), null, s); return; } let parsed = parseCommand(rest); @@ -1424,15 +1429,12 @@ function handleInput() { } serverConnection.groupAction(cmd.slice(1)); return; + case '/msg': case '/op': case '/unop': case '/kick': case '/present': case '/unpresent': { - if(!serverConnection.permissions.op) { - displayError("You're not an operator"); - return; - } let parsed = parseCommand(rest); let id; if(parsed[0] in users) { @@ -1449,7 +1451,18 @@ function handleInput() { displayError('Unknown user ' + parsed[0]); return; } - serverConnection.userAction(cmd.slice(1), id, parsed[1]); + if(cmd === '/msg') { + let username = getUsername(); + if(!username) { + displayError("Sorry, you're anonymous, you cannot chat"); + return; + } + serverConnection.chat(username, '', id, parsed[1]); + addToChatbox(serverConnection.id, + id, username, Date.now(), '', parsed[1]); + } else { + serverConnection.userAction(cmd.slice(1), id, parsed[1]); + } return; } default: @@ -1474,7 +1487,7 @@ function handleInput() { } try { - serverConnection.chat(username, me ? 'me' : '', message); + serverConnection.chat(username, me ? 'me' : '', '', message); } catch(e) { console.error(e); displayError(e);