Internet Message State Synchronization Protocol (IMSS)
This is a rough draft for a specification for generic state synchronization.
IMSS works primarily through three new headers:
Set-State, which sets a state value. This is closely inspired by the Set-Cookie header and follows a very similar pattern, although it is different in semantics and in flags.
State-CID, which identifies what the state should look like after all operations are applied, by using a hash.
Resync-State, which is used to indicate a message's involvement in state conflict resolution.
IMSS operates in two modes, message-level state and chat-level state. The key difference lies in flags. By default, replies (denoted with the In-Reply-To header) update the message-level state of the message they are replying to, and all other messages update the chat-level state.
The purpose of message-level state is to facilitate reactions and controlled third party edits (such as the buttons that Telegram bots can add to messages).
This protocol does not currently cover member-level state, which would allow third parties to write state about a particular member of a chat. This could be useful for a vanity "roles" system, for instance. Currently, we recommend putting what you can in the profile or in chat-level state instead.
IMSS state values MUST be stored in a way that attributes the member who set the state, in order to fulfill authorization requirements.
Set-State Header
The key-value pair is referred to as a "state field," identified by its key.
General syntax:
Set-State: <key>=<value> Set-State: <key>=<value>; <flag> Set-State: <key>=<value>; <option key>=<option value> Set-State: <key>=<value>; <option>; <option>
Of course, the "options" syntax (including flags) can be repeated as many times as needed.
Setting a state key to an empty value deletes it. The state field's removal MUST NOT affect future state or State-CID in any other way.
The character limitations for both keys and values remains the same as Set-Cookie. For keys, this is US-ASCII, without control characters, without separators, and without spaces, tabs, or any symbols from the set ( ) < > @ , ; : \ " / [ ] ? = { }. For values, this is US-ASCII without whitespace or any of the symbols from the set " , ; \. Values MAY be wrapped in double-quotes -- account for the case where they are not. To work around any of these limitations, use percent-encoding (this means that, in receiving state, you MUST accept percent-encoding). Values can also use base64; see below.
Available options for the Set-State header are:
Chat: this specifies that the state field being set is chat-level state in the chat the message is sent in.
Message=<message id>: this specifies that the state field being set is message-level state, as well as the message ID upon which to set the state.
Protect: tell user agents to "protect" the state from being updated by other senders. (TODO: There should be requirements for this.) If an unauthorized sender attempts to update this state field, it should be ignored, or a warning message should appear to other members of the chat.
Base64: tell user agents that they need to decode the value from Base64 in order to use it. The value may not be text in this case.
There was a FromSource here, but I decided that referring to the message in the value is probably a better idea.
State-CID Header
The value of this header consists of a base64-encoded SHA-256 hash of the DAG-CBOR representation of the entire relevant state, and a mention of which state it is relevant to.
Syntax:
State-CID: <hash>; Chat State-CID: <hash>; Message=<message id>
Chat and Message are the only two options available for State-CID, and exactly one of them MUST be specified. Their meanings are the same as above.
This header SHOULD be sent when setting state or sending messages which may use state in some way. It SHOULD NOT be sent with every message; it MAY be simply sent periodically.
When a State-CID header is received, the user agent SHOULD confirm that their state matches this CID. It does this by generating its own hash of the state using the below algorithm, and then comparing it with the given hash. If the hash does not match, state conflict resolution must begin.
To generate a State-CID hash:
1. Compile the state into string keys to string values.
2. Sort the keys lexicographically, using their decoded forms (decoded from base64 if the Base64 flag had been set).
TIP: while developing, if you find that hashes consistently don't match, make sure your keys and values are encoded in UTF-8.
3. Compile the sorted key-value pairs into minified JSON.
4. Hash it with SHA-256. Encode that with Base64.
State conflict resolution (the Resync-State Header)
If a device receives a State-CID which does not match the current state, it must first ask the sending party what the relevant state is. To do this, it sends an empty message including the Resync-State: Query header, along with any other headers that are needed to identify the chat or conversation, such as a References or In-Reply-To header. The user agent SHOULD send this message only to the sender that it is querying. To get message-level state instead of chat-level state, use Resync-State: Query; Message=<message id> instead, replacing <message id> as appropriate.
Upon receiving a message with Resync-State: Query, the user agent must answer with their entire relevant state as JSON. This should be the content of the message, with the correct Content-Type (application/json). This response also needs to include the Resync-State: Refresh header. The Message option SHOULD be used here as well for clarity, if syncing message-level state: Resync-State: Refresh; Message=<message id>
TIP: Implementations might find it convenient to re-use code between State-CID calculation and state conflict resolution. The JSON sent with Resync-State: Query does not need to be sorted or minified, but it might be convenient to reuse the code that does that.
The querying user agent MAY then use Set-State as normal, in the chat, in order to add any fields that the refreshed state did not have. It should include the Resync-State: Confirm header if it is doing this, and should exclude the state conflict resolution from the thread as it returns to the main chat. There is no message-level state equivalent, as this is handled in Set-State. TODO: is this a good idea? Especially considering by design there's no way to tell whether the missing keys have been deleted or not, and un-deleting them might be bad.
Do not consider the state conflict resolution messages to be part of the conversation (such as in References headers).
In summary, the Resync-State header might look like one of:
Resync-State: Query Resync-State: Query; Message=<message id> Resync-State: Refresh Resync-State: Refresh; Message=<message id> Resync-State: Confirm