From fae045fb611e24693a0e918046b05f09a8958abb Mon Sep 17 00:00:00 2001 From: Juliusz Chroboczek Date: Fri, 16 Dec 2022 17:54:46 +0100 Subject: [PATCH] Client-side support for protocol version 2. This does not yet support the new 'need-username' error. --- README.PROTOCOL | 25 ++++++++++++++------ static/galene.js | 23 +++++++++--------- static/protocol.js | 59 ++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 78 insertions(+), 29 deletions(-) diff --git a/README.PROTOCOL b/README.PROTOCOL index 2fe9b5c..c212dde 100644 --- a/README.PROTOCOL +++ b/README.PROTOCOL @@ -70,6 +70,14 @@ message types: - `dest`, the client-id of the destination client; - `privileged`, set by the server to indicate that the originating client had the `op` privilege at the time when it sent the message. + - `value`, the value of the message (which can be of any type). + +There are two kinds of errors. Unsolicited errors are sent using messages +of type `usermessage` of kind `error` or `warning`. Errors sent in reply +to a message use the same type as the usual reply, but with a specific +kind (such as `fail`). In either case, the field `value` contains +a human-readable error message, while the field `error`, if present, +contains a stable, program-readable identifier for the error. ## Establishing and maintaining a connection @@ -81,15 +89,15 @@ start pipelining messages to the server. ```javascript { type: 'handshake', - version: ["1"], + version: ["2"], id: id } ``` -The version field contains an array of supported protocol versions; the -client may announce multiple versions, but the server will always reply -with a singleton. If the field `id` is absent, then the peer doesn't -originate streams. +The version field contains an array of supported protocol versions, in +decreasing preference order; the client may announce multiple versions, +but the server will always reply with a singleton. If the field `id` is +absent, then the peer doesn't originate streams. A peer may, at any time, send a `ping` message. @@ -302,6 +310,7 @@ A chat message may be sent using a `chat` message. username: username, dest: dest-id, privileged: boolean, + time: time, noecho: false, value: message } @@ -313,8 +322,10 @@ originated by the server. The message is forwarded by the server without interpretation, the server only validates that the `source` and `username` fields are authentic. The field `privileged` is set to true by the server if the message was originated by a client with the `op` permission. The -field `noecho` is set by the client if it doesn't wish to receive a copy -of its own message. +field `time` is the timestamp of the message, coded as a number in version +1 of the protocol, and as a string in ISO 8601 format in later versions. +The field `noecho` is set by the client if it doesn't wish to receive +a copy of its own message. The `chathistory` message is similar to the `chat` message, but carries a message taken from the chat history. Most clients should treat diff --git a/static/galene.js b/static/galene.js index 1c8021e..d958268 100644 --- a/static/galene.js +++ b/static/galene.js @@ -2520,7 +2520,7 @@ function gotFileTransferEvent(state, data) { * @param {string} id * @param {string} dest * @param {string} username - * @param {number} time + * @param {Date} time * @param {boolean} privileged * @param {string} kind * @param {any} message @@ -2605,16 +2605,15 @@ function formatLines(lines) { } /** - * @param {number} time + * @param {Date} time * @returns {string} */ function formatTime(time) { - let delta = Date.now() - time; - let date = new Date(time); - let m = date.getMinutes(); + let delta = Date.now() - time.getTime(); + let m = time.getMinutes(); if(delta > -30000) - return date.getHours() + ':' + ((m < 10) ? '0' : '') + m; - return date.toLocaleString(); + return time.getHours() + ':' + ((m < 10) ? '0' : '') + m; + return time.toLocaleString(); } /** @@ -2622,7 +2621,7 @@ function formatTime(time) { * @property {string} [nick] * @property {string} [peerId] * @property {string} [dest] - * @property {number} [time] + * @property {Date} [time] */ /** @type {lastMessage} */ @@ -2632,7 +2631,7 @@ let lastMessage = {}; * @param {string} peerId * @param {string} dest * @param {string} nick - * @param {number} time + * @param {Date} time * @param {boolean} privileged * @param {boolean} history * @param {string} kind @@ -2662,7 +2661,7 @@ function addToChatbox(peerId, dest, nick, time, privileged, history, kind, messa !time || !lastMessage.time) { doHeader = true; } else { - let delta = time - lastMessage.time; + let delta = time.getTime() - lastMessage.time.getTime(); doHeader = delta < 0 || delta > 60000; } @@ -2725,7 +2724,7 @@ function addToChatbox(peerId, dest, nick, time, privileged, history, kind, messa * @param {string} message */ function localMessage(message) { - return addToChatbox(null, null, null, Date.now(), false, false, '', message); + return addToChatbox(null, null, null, new Date(), false, false, '', message); } function clearChat() { @@ -2960,7 +2959,7 @@ commands.msg = { throw new Error(`Unknown user ${p[0]}`); serverConnection.chat('', id, p[1]); addToChatbox(serverConnection.id, id, serverConnection.username, - Date.now(), false, false, '', p[1]); + new Date(), false, false, '', p[1]); } }; diff --git a/static/protocol.js b/static/protocol.js index bcc6b32..b557491 100644 --- a/static/protocol.js +++ b/static/protocol.js @@ -106,6 +106,12 @@ function ServerConnection() { * @type {WebSocket} */ this.socket = null; + /** + * The negotiated protocol version. + * + * @type {string} + */ + this.version = null; /** * The set of all up streams, indexed by their id. * @@ -187,7 +193,7 @@ function ServerConnection() { /** * onchat is called whenever a new chat message is received. * - * @type {(this: ServerConnection, id: string, dest: string, username: string, time: number, privileged: boolean, history: boolean, kind: string, message: unknown) => void} + * @type {(this: ServerConnection, id: string, dest: string, username: string, time: Date, privileged: boolean, history: boolean, kind: string, message: unknown) => void} */ this.onchat = null; /** @@ -199,7 +205,7 @@ function ServerConnection() { * 'id' is non-null, 'privileged' indicates whether the message was * sent by an operator. * - * @type {(this: ServerConnection, id: string, dest: string, username: string, time: number, privileged: boolean, kind: string, message: unknown) => void} + * @type {(this: ServerConnection, id: string, dest: string, username: string, time: Date, privileged: boolean, kind: string, message: unknown) => void} */ this.onusermessage = null; /** @@ -225,6 +231,7 @@ function ServerConnection() { * @property {string} type * @property {Array} [version] * @property {string} [kind] + * @property {string} [error] * @property {string} [id] * @property {string} [replace] * @property {string} [source] @@ -239,7 +246,7 @@ function ServerConnection() { * @property {string} [group] * @property {unknown} [value] * @property {boolean} [noecho] - * @property {number} [time] + * @property {string|number} [time] * @property {string} [sdp] * @property {RTCIceCandidate} [candidate] * @property {string} [label] @@ -291,7 +298,7 @@ ServerConnection.prototype.connect = async function(url) { this.socket.onopen = function(e) { sc.send({ type: 'handshake', - version: ["1"], + version: ["2", "1"], id: sc.id, }); if(sc.onconnected) @@ -324,10 +331,23 @@ ServerConnection.prototype.connect = async function(url) { this.socket.onmessage = function(e) { let m = JSON.parse(e.data); switch(m.type) { - case 'handshake': - if(!m.version || !m.version.includes('1')) - console.warn(`Unexpected protocol version ${m.version}.`); + case 'handshake': { + /** @type {string} */ + let v; + if(!m.version || !(m.version instanceof Array) || + m.version.length < 1 || typeof(m.version[0]) !== 'string') { + v = null; + } else { + v = m.version[0]; + } + if(v === "1" || v === "2") { + sc.version = v; + } else { + console.warn(`Unknown protocol version ${v || m.version}`); + sc.version = "1" + } break; + } case 'offer': sc.gotOffer(m.id, m.label, m.source, m.username, m.sdp, m.replace); @@ -419,8 +439,8 @@ ServerConnection.prototype.connect = async function(url) { case 'chathistory': if(sc.onchat) sc.onchat.call( - sc, m.source, m.dest, m.username, m.time, m.privileged, - m.type === 'chathistory', m.kind, m.value, + sc, m.source, m.dest, m.username, parseTime(m.time), + m.privileged, m.type === 'chathistory', m.kind, m.value, ); break; case 'usermessage': @@ -428,7 +448,7 @@ ServerConnection.prototype.connect = async function(url) { sc.fileTransfer(m.source, m.username, m.value); else if(sc.onusermessage) sc.onusermessage.call( - sc, m.source, m.dest, m.username, m.time, + sc, m.source, m.dest, m.username, parseTime(m.time), m.privileged, m.kind, m.value, ); break; @@ -448,6 +468,25 @@ ServerConnection.prototype.connect = async function(url) { }); }; +/** + * Protocol version 1 uses integers for dates, later versions use dates in + * ISO 8601 format. This function takes a date in either format and + * returns a Date object. + * + * @param {string|number} value + * @returns {Date} + */ +function parseTime(value) { + if(!value) + return null; + try { + return new Date(value); + } catch(e) { + console.warn(`Couldn't parse ${value}:`, e); + return null; + } +} + /** * join requests to join a group. The onjoined callback will be called * when we've effectively joined.