Skip to content
This repository has been archived by the owner on Feb 18, 2021. It is now read-only.

Latest commit

 

History

History
198 lines (141 loc) · 9.51 KB

chat.md

File metadata and controls

198 lines (141 loc) · 9.51 KB

chat - Direct and Group Private Messaging

DRAFT

This is a lightweight chat protocol designed to encourage minimal interoperable communication support in future telehash based apps and devices. It is based the following principles:

  • learn from the experiences with XMPP
  • no central services or routing, is a full p2p mesh
  • decoupled from identity
  • real-time conversation focus, not optimized for archiving or ad-hoc async messaging
  • rich media support
  • multi-device aware
  • for individuals and small/private groups, not for large/persistent groupchat

A chat is a container of one or more messages from one or more participants. A participant is always a single hashname and has a profile, the first participant is called the leader. Messages are typically sent synchronously over a chat channel, and may also be sent asynchronously using boxes.

Message IDs

Every message is identified by a unique 8 byte SipHash digest ID that is generated by and specific to each participating hashname. These message IDs are string encoded in base32, for example: cn33wzacvwfya.

The first time any participant starts or joins a chat it must begin with a profile message that is the result of a chain of digests. The chain is created by using the first half of the participant's hashname (16 bytes) and starting with an initial random secret value (known only to the participant), then sequentially hashing the 8-byte digest outputs a large number of times (more than the potential number of messages to be sent in the chat). The final digest is the profile message ID for the participant and used for the lifetime of the chat.

All messages must have sequentially previous digests as their ID so that the recipient can verify continuity. A profile ID cannot be changed, they are immutable once joined and any detected mismatch must error and close the channel it was received on.

The first participant is always the leader and their generated profile ID also becomes the unique ID of the chat for all participants.

Profile Channel

Before any participant can join a chat they must connect to the leader and exchange their profile messages:

{
  "c":1,
  "seq":1,
  "type":"profile",
  "chat":"sgoomt3lqqkia",
  "profile":"cn33wzacvwfya"
}

The profile channel is a normal stream using lob encoding where the sender immediately sends their profile message on the stream and then ends it. If the sender already has the profile message of the recipient for the given chat it must include it as the profile value.

If/when the leader accepts the profile, they can either open a profile channel back and send their own profile (including the received profile id) or directly open a chat channel if the profile value was included. The participant when receiving the leader's profile message can then open a chat channel to start the normal messaging flow.

The leader validates the profile message and publishes it as the BODY of a join message to any other participants so that they can accept an incoming chat channel from the new participant as well.

The leader may also invite participants by initiating the first join channel to them, the chat id must match the streamed profile message id to be a valid invite. A participant can then send a profile channel back with that profile id and wait for the leader to initiate the chat channel once processed.

Chat Channel

The chat channel is reliable and the start request/response looks like:

{
  "c":1,
  "seq":1,
  "type":"chat",
  "chat":"sgoomt3lqqkia",
  "last":"cn33wzacvwfya"
}

The fields are defined as:

  • type - always chat
  • chat - (only sent in the request) the id of the chat, the leader's join id
  • last - the last message id the sender has seen from the recipient

The response is:

{ "c":1, "seq":1, "ack":1, "last":"k46demhj7b6ii" }

Once open, chat channels are a normal stream using lob encoding to carry individual synchronous messages bi-directionally and fragmented as needed.

Any messages newer than the given last values from either side are then streamed sequentially so that their IDs can be verified.

Every message has its own type JSON value, standard ones are defined below.

"type":"chat" - Content Messages

Each chat message is a LOB-encoded packet who's JSON object has these common fields:

  • id - (required) the unique message id as calculated by the sender
  • type - (required) "chat"
  • text - (required) plain text, optionally basic markdown
  • chat - (optional) the chat id, for use when sent async
  • state - (optional) senders current activity state [active, inactive, gone, composing, paused] based on XEP-0085
  • after - (required) the most recent message id in the chat the sender has seen, must be valid/known
  • at - (optional) epoch (in seconds, UTC)
  • refs - (optional) object, key:uri pairs, references
  • alts - (optional) object, key:string of alternate formats of the text content (rtf, xhtml, etc), if the value is a boolean true the alternate is attached as the BODY
{
  "type":"chat",
  "id":"k46demhj7b6ii",
  "at":1394162554,
  "after":"qgo32j67kbyjj",
  "text":"...markdown [ref][]...",
  "refs":{"ref":"uri:foo"}
}

The id must be validated with the sender's join. There may be images embedded in the markdown and should be loaded if possible.

Chat messages should only be updated as long as there were no other messages sent yet after it, subsequent identical chat message ids replace previous ones. This can be displayed visually as either corrections/edited, or as-you-type live chat.

When a message text begins with "/me " the UI should display the message styled as an "action" coming from the sender.

"type":"profile" - Profile Messages

A profile message is only sent over the profile channel or attached to a join message. The leader and every participant must start with a profile message, its ID is always the highest digest of all message IDs from that participant.

  • type - (required) "profile"
  • id - (required) the the generated sequence digest id for the participant
  • text - (required) plain text visible name
  • at - (optional) epoch (in seconds, UTC)
  • refs - (optional) object, key:uri pairs, references
  • aka - (optional) array of other participant hashnames that are the same sending entity (to support multiple participating devices as one person)
{
  "id":"cbaccqcqiaqca",
  "type":"profile",
  "at":1394162554,
  "text":"Jeff Strongman",
  "refs":{"twitter":"http://twitter.com/strongman","email":"mailto:[email protected]","pic":"thtp:///profile/thumbnail.png","nick":"strongman"},
  "aka":["e5mwmtsvueumlqgo32j67kbyjjtk46demhj7b6iibnnq36fsylka"]
}

The profile text is the name for display, with optional profile pic url and nickname in the refs.

The aka is other hashnames that if joined must have the reciprocated hashname(s) in their own profile aka. Messages from any in the set should be visually displayed as from the same sender. Profiles with identical text/nick/pic (depending on what's displayed) and no matching aka should be modified visually so they are distinct (add a (2), etc).

The BODY may be a signed JWT that must contain the sender's hashname in the claims to be independently verified by the app.

Implementations may event this message type to the application to signal a join request from a new participant to an existing chat.

"type":"join" - Join Messages

A join message is required before any chat messages from any participant. It has two parts, the join message sent over the chat channel from the leader to announce a participant, and the profile message from the participant attached as the BODY.

  • type - (required) "join"
  • id - (required) the leader's next message id
  • from - (required) the participant's hashname
  • at - (optional) epoch (in seconds, UTC)
  • after - (required) last seen message id in the chat
{
  "id":"cbaccqcqiaqca",
  "type":"join",
  "from":"frnfke2szyna2vwkge6eubxtnkj46rtctqk7g7ewbvfiesycbjdq"
  "after":"qgo32j67kbyjj",
  "at":1394162554
}
BODY: {"type":"profile",...}

Upon receiving a join from the leader, all participants should attempt to initiate and accept streams with that hashname as well as make their presence visible.

The leader must always begin a new chat by joining their own profile as the first message.

"type":"ack" - Receipt Messages

The chat channel can carry ad-hoc receipt messages alongside chats. These messages have a "type":"ack" and are only sent from the recipient back to the sender/owner of a chat message. They only signal a current state change and are never stored, cached, or re-sent.

{
  "type":"ack",
  "id":"k46demhj7b6ii",
  "ack":"received"
}

Ack States:

  • received - message was received and processed/queued/notified
  • read - text was displayed
  • seen - any embedded references were displayed (media)
  • logged - chat was saved to external storage
  • referred - a reference was followed (clicked/opened)
  • saved - liked, bookmarked
  • copied - clipboard, pic was copied, message was forwarded

Ack messages can be updated anytime, all of the states should replace/update the last received one.

"type":"connect/disconnect" - Connectivity Signals

These message types are never transmitted and are reserved for local implementation signalling/eventing of the connection status of participants in a chat.