1
Fork 0
mirror of https://github.com/jech/galene.git synced 2024-11-22 08:35:57 +01:00

Client-side support for protocol version 2.

This does not yet support the new 'need-username' error.
This commit is contained in:
Juliusz Chroboczek 2022-12-16 17:54:46 +01:00
parent 397892d906
commit fae045fb61
3 changed files with 78 additions and 29 deletions

View file

@ -70,6 +70,14 @@ message types:
- `dest`, the client-id of the destination client; - `dest`, the client-id of the destination client;
- `privileged`, set by the server to indicate that the originating client - `privileged`, set by the server to indicate that the originating client
had the `op` privilege at the time when it sent the message. 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 ## Establishing and maintaining a connection
@ -81,15 +89,15 @@ start pipelining messages to the server.
```javascript ```javascript
{ {
type: 'handshake', type: 'handshake',
version: ["1"], version: ["2"],
id: id id: id
} }
``` ```
The version field contains an array of supported protocol versions; the The version field contains an array of supported protocol versions, in
client may announce multiple versions, but the server will always reply decreasing preference order; the client may announce multiple versions,
with a singleton. If the field `id` is absent, then the peer doesn't but the server will always reply with a singleton. If the field `id` is
originate streams. absent, then the peer doesn't originate streams.
A peer may, at any time, send a `ping` message. 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, username: username,
dest: dest-id, dest: dest-id,
privileged: boolean, privileged: boolean,
time: time,
noecho: false, noecho: false,
value: message 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` interpretation, the server only validates that the `source` and `username`
fields are authentic. The field `privileged` is set to true by the server 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 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 field `time` is the timestamp of the message, coded as a number in version
of its own message. 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 The `chathistory` message is similar to the `chat` message, but carries
a message taken from the chat history. Most clients should treat a message taken from the chat history. Most clients should treat

View file

@ -2520,7 +2520,7 @@ function gotFileTransferEvent(state, data) {
* @param {string} id * @param {string} id
* @param {string} dest * @param {string} dest
* @param {string} username * @param {string} username
* @param {number} time * @param {Date} time
* @param {boolean} privileged * @param {boolean} privileged
* @param {string} kind * @param {string} kind
* @param {any} message * @param {any} message
@ -2605,16 +2605,15 @@ function formatLines(lines) {
} }
/** /**
* @param {number} time * @param {Date} time
* @returns {string} * @returns {string}
*/ */
function formatTime(time) { function formatTime(time) {
let delta = Date.now() - time; let delta = Date.now() - time.getTime();
let date = new Date(time); let m = time.getMinutes();
let m = date.getMinutes();
if(delta > -30000) if(delta > -30000)
return date.getHours() + ':' + ((m < 10) ? '0' : '') + m; return time.getHours() + ':' + ((m < 10) ? '0' : '') + m;
return date.toLocaleString(); return time.toLocaleString();
} }
/** /**
@ -2622,7 +2621,7 @@ function formatTime(time) {
* @property {string} [nick] * @property {string} [nick]
* @property {string} [peerId] * @property {string} [peerId]
* @property {string} [dest] * @property {string} [dest]
* @property {number} [time] * @property {Date} [time]
*/ */
/** @type {lastMessage} */ /** @type {lastMessage} */
@ -2632,7 +2631,7 @@ let lastMessage = {};
* @param {string} peerId * @param {string} peerId
* @param {string} dest * @param {string} dest
* @param {string} nick * @param {string} nick
* @param {number} time * @param {Date} time
* @param {boolean} privileged * @param {boolean} privileged
* @param {boolean} history * @param {boolean} history
* @param {string} kind * @param {string} kind
@ -2662,7 +2661,7 @@ function addToChatbox(peerId, dest, nick, time, privileged, history, kind, messa
!time || !lastMessage.time) { !time || !lastMessage.time) {
doHeader = true; doHeader = true;
} else { } else {
let delta = time - lastMessage.time; let delta = time.getTime() - lastMessage.time.getTime();
doHeader = delta < 0 || delta > 60000; doHeader = delta < 0 || delta > 60000;
} }
@ -2725,7 +2724,7 @@ function addToChatbox(peerId, dest, nick, time, privileged, history, kind, messa
* @param {string} message * @param {string} message
*/ */
function localMessage(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() { function clearChat() {
@ -2960,7 +2959,7 @@ commands.msg = {
throw new Error(`Unknown user ${p[0]}`); throw new Error(`Unknown user ${p[0]}`);
serverConnection.chat('', id, p[1]); serverConnection.chat('', id, p[1]);
addToChatbox(serverConnection.id, id, serverConnection.username, addToChatbox(serverConnection.id, id, serverConnection.username,
Date.now(), false, false, '', p[1]); new Date(), false, false, '', p[1]);
} }
}; };

View file

@ -106,6 +106,12 @@ function ServerConnection() {
* @type {WebSocket} * @type {WebSocket}
*/ */
this.socket = null; this.socket = null;
/**
* The negotiated protocol version.
*
* @type {string}
*/
this.version = null;
/** /**
* The set of all up streams, indexed by their id. * 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. * 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; this.onchat = null;
/** /**
@ -199,7 +205,7 @@ function ServerConnection() {
* 'id' is non-null, 'privileged' indicates whether the message was * 'id' is non-null, 'privileged' indicates whether the message was
* sent by an operator. * 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; this.onusermessage = null;
/** /**
@ -225,6 +231,7 @@ function ServerConnection() {
* @property {string} type * @property {string} type
* @property {Array<string>} [version] * @property {Array<string>} [version]
* @property {string} [kind] * @property {string} [kind]
* @property {string} [error]
* @property {string} [id] * @property {string} [id]
* @property {string} [replace] * @property {string} [replace]
* @property {string} [source] * @property {string} [source]
@ -239,7 +246,7 @@ function ServerConnection() {
* @property {string} [group] * @property {string} [group]
* @property {unknown} [value] * @property {unknown} [value]
* @property {boolean} [noecho] * @property {boolean} [noecho]
* @property {number} [time] * @property {string|number} [time]
* @property {string} [sdp] * @property {string} [sdp]
* @property {RTCIceCandidate} [candidate] * @property {RTCIceCandidate} [candidate]
* @property {string} [label] * @property {string} [label]
@ -291,7 +298,7 @@ ServerConnection.prototype.connect = async function(url) {
this.socket.onopen = function(e) { this.socket.onopen = function(e) {
sc.send({ sc.send({
type: 'handshake', type: 'handshake',
version: ["1"], version: ["2", "1"],
id: sc.id, id: sc.id,
}); });
if(sc.onconnected) if(sc.onconnected)
@ -324,10 +331,23 @@ ServerConnection.prototype.connect = async function(url) {
this.socket.onmessage = function(e) { this.socket.onmessage = function(e) {
let m = JSON.parse(e.data); let m = JSON.parse(e.data);
switch(m.type) { switch(m.type) {
case 'handshake': case 'handshake': {
if(!m.version || !m.version.includes('1')) /** @type {string} */
console.warn(`Unexpected protocol version ${m.version}.`); 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; break;
}
case 'offer': case 'offer':
sc.gotOffer(m.id, m.label, m.source, m.username, sc.gotOffer(m.id, m.label, m.source, m.username,
m.sdp, m.replace); m.sdp, m.replace);
@ -419,8 +439,8 @@ ServerConnection.prototype.connect = async function(url) {
case 'chathistory': case 'chathistory':
if(sc.onchat) if(sc.onchat)
sc.onchat.call( sc.onchat.call(
sc, m.source, m.dest, m.username, m.time, m.privileged, sc, m.source, m.dest, m.username, parseTime(m.time),
m.type === 'chathistory', m.kind, m.value, m.privileged, m.type === 'chathistory', m.kind, m.value,
); );
break; break;
case 'usermessage': case 'usermessage':
@ -428,7 +448,7 @@ ServerConnection.prototype.connect = async function(url) {
sc.fileTransfer(m.source, m.username, m.value); sc.fileTransfer(m.source, m.username, m.value);
else if(sc.onusermessage) else if(sc.onusermessage)
sc.onusermessage.call( 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, m.privileged, m.kind, m.value,
); );
break; 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 * join requests to join a group. The onjoined callback will be called
* when we've effectively joined. * when we've effectively joined.