From 924e9c3767d555e51922340e066c2d8da5751be0 Mon Sep 17 00:00:00 2001 From: Finn Date: Tue, 2 Nov 2021 21:26:30 -0700 Subject: [PATCH] bring protocol up to date --- protocol.json | 303 ++++++++++++++++++++----- signald/client-protocol/v1/errors.go | 69 +++++- signald/client-protocol/v1/requests.go | 25 ++ signald/client-protocol/v1/structs.go | 52 +++-- 4 files changed, 376 insertions(+), 73 deletions(-) diff --git a/protocol.json b/protocol.json index 1905083..68cb778 100644 --- a/protocol.json +++ b/protocol.json @@ -2,9 +2,9 @@ "doc_version": "v1", "version": { "name": "signald", - "version": "0.14.1-48-7d927883", + "version": "0.15.0-23-981b4409", "branch": "main", - "commit": "7d92788343f34c08634abfeda06045ae13e18670" + "commit": "981b44098da8ddd748832597d5f5bde019197902" }, "info": "This document describes objects that may be used when communicating with signald.", "types": { @@ -145,7 +145,20 @@ "type": "boolean" } }, - "doc": "indicates when the incoming connection to the signal server has started or stopped" + "doc": "prior attempt to indicate signald connectivity state. WebSocketConnectionState messages will be delivered at the same time as well as in other parts of the websocket lifecycle." + }, + "WebSocketConnectionState": { + "fields": { + "state": { + "type": "String", + "doc": "One of: DISCONNECTED, CONNECTING, CONNECTED, RECONNECTING, DISCONNECTING, AUTHENTICATION_FAILED, FAILED" + }, + "socket": { + "type": "String", + "doc": "One of: UNIDENTIFIED, IDENTIFIED" + } + }, + "doc": "indicates when the websocket connection state to the signal server has changed" }, "ClientMessageWrapper": { "fields": { @@ -164,9 +177,56 @@ "error": { "type": "Boolean", "doc": "true if the incoming message represents an error" + }, + "account": { + "type": "String", + "doc": "the account this message is from" } }, - "doc": "Wraps all incoming messages after a v1 subscribe request is issued" + "doc": "Wraps all incoming messages sent to the client after a v1 subscribe request is issued" + }, + "DuplicateMessageError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, + "ProtocolInvalidMessageError": { + "fields": { + "sender": { + "type": "String" + }, + "message": { + "type": "String" + }, + "sender_device": { + "type": "int" + }, + "content_hint": { + "type": "int" + }, + "group_id": { + "type": "String" + } + }, + "error": true + }, + "UntrustedIdentityError": { + "fields": { + "identifier": { + "type": "String" + }, + "message": { + "type": "String" + }, + "identity_key": { + "type": "IdentityKey", + "version": "v1" + } + }, + "error": true }, "SendRequest": { "fields": { @@ -208,6 +268,12 @@ "list": true, "type": "JsonPreview", "version": "v1" + }, + "members": { + "list": true, + "type": "JsonAddress", + "version": "v1", + "doc": "Optionally set to a sub-set of group members. Ignored if recipientGroupId isn't specified" } } }, @@ -283,7 +349,15 @@ "type": "String" } }, - "doc": "an internal error in signald has occured.", + "doc": "an internal error in signald has occurred. typically these are things that \"should never happen\" such as issues saving to the local disk, but it is also the default error type and may catch some things that should have their own error type. If you find tht your code is depending on the exception list for any particular behavior, please file an issue so we can pull those errors out to a separate error type: https://gitlab.com/signald/signald/-/issues/new", + "error": true + }, + "InvalidRequestError": { + "fields": { + "message": { + "type": "String" + } + }, "error": true }, "UnknownGroupError": { @@ -294,6 +368,14 @@ }, "error": true }, + "RateLimitError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, "InvalidRecipientError": { "fields": { "message": { @@ -324,6 +406,12 @@ }, "timestamp": { "type": "long" + }, + "members": { + "list": true, + "type": "JsonAddress", + "version": "v1", + "doc": "Optionally set to a sub-set of group members. Ignored if recipientGroupId isn't specified" } }, "doc": "react to a previous message" @@ -339,7 +427,7 @@ }, "version": { "type": "String", - "example": "\"0.14.1-48-7d927883\"" + "example": "\"0.15.0-23-981b4409\"" }, "branch": { "type": "String", @@ -347,7 +435,7 @@ }, "commit": { "type": "String", - "example": "\"7d92788343f34c08634abfeda06045ae13e18670\"" + "example": "\"981b44098da8ddd748832597d5f5bde019197902\"" } } }, @@ -573,14 +661,6 @@ } } }, - "InvalidRequestError": { - "fields": { - "message": { - "type": "String" - } - }, - "error": true - }, "InvalidInviteURIError": { "fields": { "message": { @@ -788,21 +868,6 @@ } } }, - "UntrustedIdentityError": { - "fields": { - "identifier": { - "type": "String" - }, - "message": { - "type": "String" - }, - "identity_key": { - "type": "IdentityKey", - "version": "v1" - } - }, - "error": true - }, "GetProfileRequest": { "fields": { "account": { @@ -1058,8 +1123,8 @@ }, "uri": { "type": "String", - "doc": "the tsdevice:/ uri provided (typically in qr code form) by the new device", - "example": "\"tsdevice:/?uuid=jAaZ5lxLfh7zVw5WELd6-Q&pub_key=BfFbjSwmAgpVJBXUdfmSgf61eX3a%2Bq9AoxAVpl1HUap9\"", + "doc": "the sgnl://linkdevice uri provided (typically in qr code form) by the new device", + "example": "\"sgnl://linkdevice?uuid=jAaZ5lxLfh7zVw5WELd6-Q&pub_key=BfFbjSwmAgpVJBXUdfmSgf61eX3a%2Bq9AoxAVpl1HUap9\"", "required": true } }, @@ -1489,6 +1554,12 @@ "timestamp": { "type": "long", "required": true + }, + "members": { + "list": true, + "type": "JsonAddress", + "version": "v1", + "doc": "Optionally set to a sub-set of group members. Ignored if group isn't specified" } }, "doc": "delete a message previously sent" @@ -1590,6 +1661,21 @@ }, "doc": "deny a request to join a group" }, + "SubmitChallengeRequest": { + "fields": { + "account": { + "type": "String", + "required": true + }, + "challenge": { + "type": "String", + "required": true + }, + "captcha_token": { + "type": "String" + } + } + }, "JsonDataMessage": { "fields": { "timestamp": { @@ -1795,6 +1881,26 @@ } } }, + "IdentityKey": { + "fields": { + "added": { + "type": "long", + "doc": "the first time this identity key was seen" + }, + "safety_number": { + "type": "String", + "example": "\"373453558586758076680580548714989751943247272727416091564451\"" + }, + "qr_code_data": { + "type": "String", + "doc": "base64-encoded QR code data" + }, + "trust_level": { + "type": "String", + "doc": "One of TRUSTED_UNVERIFIED, TRUSTED_VERIFIED or UNTRUSTED" + } + } + }, "JsonQuote": { "fields": { "id": { @@ -1888,6 +1994,10 @@ }, "identityFailure": { "type": "String" + }, + "proof_required_failure": { + "type": "ProofRequiredError", + "version": "v1" } } }, @@ -1986,26 +2096,6 @@ }, "doc": "information about a legacy group" }, - "IdentityKey": { - "fields": { - "added": { - "type": "long", - "doc": "the first time this identity key was seen" - }, - "safety_number": { - "type": "String", - "example": "\"373453558586758076680580548714989751943247272727416091564451\"" - }, - "qr_code_data": { - "type": "String", - "doc": "base64-encoded QR code data" - }, - "trust_level": { - "type": "String", - "doc": "One of TRUSTED_UNVERIFIED, TRUSTED_VERIFIED or UNTRUSTED" - } - } - }, "Capabilities": { "fields": { "gv2": { @@ -2334,6 +2424,26 @@ } } }, + "ProofRequiredError": { + "fields": { + "token": { + "type": "String" + }, + "options": { + "list": true, + "type": "String", + "doc": "possible list values are RECAPTCHA and PUSH_CHALLENGE" + }, + "message": { + "type": "String" + }, + "retry_after": { + "type": "long", + "doc": "value in seconds" + } + }, + "error": true + }, "ServerCDN": { "fields": { "number": { @@ -3287,9 +3397,15 @@ { "name": "InternalError" }, + { + "name": "InvalidRequestError" + }, { "name": "UnknownGroupError" }, + { + "name": "RateLimitError" + }, { "name": "InvalidRecipientError" } @@ -3320,6 +3436,12 @@ }, { "name": "UnknownGroupError" + }, + { + "name": "InvalidRequestError" + }, + { + "name": "RateLimitError" } ] }, @@ -3349,6 +3471,9 @@ }, { "name": "InternalError" + }, + { + "name": "InvalidRequestError" } ] }, @@ -3374,6 +3499,9 @@ }, { "name": "GroupVerificationError" + }, + { + "name": "InvalidRequestError" } ] }, @@ -3402,6 +3530,9 @@ }, { "name": "InvalidGroupStateError" + }, + { + "name": "InvalidRequestError" } ] }, @@ -3421,6 +3552,9 @@ }, { "name": "NoSuchAccountError" + }, + { + "name": "InvalidRequestError" } ] }, @@ -3479,6 +3613,9 @@ }, { "name": "NoSuchAccountError" + }, + { + "name": "InvalidRequestError" } ] }, @@ -3527,6 +3664,9 @@ }, { "name": "InvalidBase64Error" + }, + { + "name": "InvalidRequestError" } ] }, @@ -3600,6 +3740,9 @@ }, { "name": "NoSuchAccountError" + }, + { + "name": "InvalidRequestError" } ] }, @@ -3678,6 +3821,9 @@ }, { "name": "GroupVerificationError" + }, + { + "name": "InvalidRequestError" } ] }, @@ -3882,6 +4028,9 @@ }, { "name": "UnknownGroupError" + }, + { + "name": "InvalidRequestError" } ] }, @@ -3894,22 +4043,28 @@ "name": "InternalError" }, { - "name": "InvalidProxyError" + "name": "ServerNotFoundError" }, { - "name": "ServerNotFoundError" + "name": "InvalidProxyError" }, { "name": "NoSuchAccountError" }, { - "name": "InvalidRecipientError" + "name": "InvalidRequestError" }, { "name": "NoSendPermissionError" }, { "name": "UnknownGroupError" + }, + { + "name": "RateLimitError" + }, + { + "name": "InvalidRecipientError" } ] }, @@ -4016,6 +4171,9 @@ }, { "name": "GroupVerificationError" + }, + { + "name": "InvalidRequestError" } ] }, @@ -4111,6 +4269,12 @@ }, { "name": "UnknownGroupError" + }, + { + "name": "InvalidRequestError" + }, + { + "name": "RateLimitError" } ] }, @@ -4172,6 +4336,12 @@ }, { "name": "NoSendPermissionError" + }, + { + "name": "InvalidRequestError" + }, + { + "name": "RateLimitError" } ] }, @@ -4214,6 +4384,29 @@ { "name": "GroupVerificationError" }, + { + "name": "InternalError" + }, + { + "name": "InvalidRequestError" + } + ] + }, + "submit_challenge": { + "request": "SubmitChallengeRequest", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "InvalidRequestError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + }, { "name": "InternalError" } diff --git a/signald/client-protocol/v1/errors.go b/signald/client-protocol/v1/errors.go index 0cf7335..3081b52 100644 --- a/signald/client-protocol/v1/errors.go +++ b/signald/client-protocol/v1/errors.go @@ -39,6 +39,13 @@ func mkerr(response client_protocol.BasicResponse) error { return err } return result + case "DuplicateMessageError": + result := DuplicateMessageError{} + err := json.Unmarshal(response.Error, &result) + if err != nil { + return err + } + return result case "FingerprintVersionMismatchError": result := FingerprintVersionMismatchError{} err := json.Unmarshal(response.Error, &result) @@ -179,6 +186,27 @@ func mkerr(response client_protocol.BasicResponse) error { return err } return result + case "ProofRequiredError": + result := ProofRequiredError{} + err := json.Unmarshal(response.Error, &result) + if err != nil { + return err + } + return result + case "ProtocolInvalidMessageError": + result := ProtocolInvalidMessageError{} + err := json.Unmarshal(response.Error, &result) + if err != nil { + return err + } + return result + case "RateLimitError": + result := RateLimitError{} + err := json.Unmarshal(response.Error, &result) + if err != nil { + return err + } + return result case "ServerNotFoundError": result := ServerNotFoundError{} err := json.Unmarshal(response.Error, &result) @@ -253,6 +281,14 @@ func (e CaptchaRequiredError) Error() string { return e.Message } +type DuplicateMessageError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e DuplicateMessageError) Error() string { + return e.Message +} + type FingerprintVersionMismatchError struct { Message string `json:"message,omitempty" yaml:"message,omitempty"` } @@ -285,7 +321,7 @@ func (e GroupVerificationError) Error() string { return e.Message } -// InternalError: an internal error in signald has occured. +// InternalError: an internal error in signald has occurred. typically these are things that "should never happen" such as issues saving to the local disk, but it is also the default error type and may catch some things that should have their own error type. If you find tht your code is depending on the exception list for any particular behavior, please file an issue so we can pull those errors out to a separate error type: https://gitlab.com/signald/signald/-/issues/new type InternalError struct { Exceptions []string `json:"exceptions,omitempty" yaml:"exceptions,omitempty"` Message string `json:"message,omitempty" yaml:"message,omitempty"` @@ -417,6 +453,37 @@ func (e ProfileUnavailableError) Error() string { return e.Message } +type ProofRequiredError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` + Options []string `json:"options,omitempty" yaml:"options,omitempty"` // possible list values are RECAPTCHA and PUSH_CHALLENGE + RetryAfter int64 `json:"retry_after,omitempty" yaml:"retry_after,omitempty"` // value in seconds + Token string `json:"token,omitempty" yaml:"token,omitempty"` +} + +func (e ProofRequiredError) Error() string { + return e.Message +} + +type ProtocolInvalidMessageError struct { + ContentHint int32 `json:"content_hint,omitempty" yaml:"content_hint,omitempty"` + GroupId string `json:"group_id,omitempty" yaml:"group_id,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` + Sender string `json:"sender,omitempty" yaml:"sender,omitempty"` + SenderDevice int32 `json:"sender_device,omitempty" yaml:"sender_device,omitempty"` +} + +func (e ProtocolInvalidMessageError) Error() string { + return e.Message +} + +type RateLimitError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e RateLimitError) Error() string { + return e.Message +} + type ServerNotFoundError struct { Message string `json:"message,omitempty" yaml:"message,omitempty"` UUID string `json:"uuid,omitempty" yaml:"uuid,omitempty"` diff --git a/signald/client-protocol/v1/requests.go b/signald/client-protocol/v1/requests.go index 8d6f5b8..5a38921 100644 --- a/signald/client-protocol/v1/requests.go +++ b/signald/client-protocol/v1/requests.go @@ -1132,6 +1132,31 @@ func (r *SetProfile) Submit(conn *signald.Signald) (err error) { } +func (r *SubmitChallengeRequest) Submit(conn *signald.Signald) (err error) { + r.Version = "v1" + r.Type = "submit_challenge" + if r.ID == "" { + r.ID = signald.GenerateID() + } + err = conn.RawRequest(r) + if err != nil { + log.Println("signald-go: error submitting request to signald") + return + } + + responseChannel := conn.GetResponseListener(r.ID) + defer conn.CloseResponseListener(r.ID) + + rawResponse := <-responseChannel + if rawResponse.Error != nil { + err = mkerr(rawResponse) + return + } + + return err + +} + // Submit: receive incoming messages. After making a subscribe request, incoming messages will be sent to the client encoded as ClientMessageWrapper. Send an unsubscribe request or disconnect from the socket to stop receiving messages. func (r *SubscribeRequest) Submit(conn *signald.Signald) (err error) { r.Version = "v1" diff --git a/signald/client-protocol/v1/structs.go b/signald/client-protocol/v1/structs.go index dd8a1c2..38d30ea 100644 --- a/signald/client-protocol/v1/structs.go +++ b/signald/client-protocol/v1/structs.go @@ -35,7 +35,7 @@ type AccountList struct { type AddLinkedDeviceRequest struct { Request Account string `json:"account,omitempty" yaml:"account,omitempty"` // The account to interact with - Uri string `json:"uri,omitempty" yaml:"uri,omitempty"` // the tsdevice:/ uri provided (typically in qr code form) by the new device + Uri string `json:"uri,omitempty" yaml:"uri,omitempty"` // the sgnl://linkdevice uri provided (typically in qr code form) by the new device } // AddServerRequest: add a new server to connect to. Returns the new server's UUID. @@ -82,8 +82,9 @@ type Capabilities struct { Storage bool `json:"storage,omitempty" yaml:"storage,omitempty"` } -// ClientMessageWrapper: Wraps all incoming messages after a v1 subscribe request is issued +// ClientMessageWrapper: Wraps all incoming messages sent to the client after a v1 subscribe request is issued type ClientMessageWrapper struct { + Account string `json:"account,omitempty" yaml:"account,omitempty"` // the account this message is from Data interface{} `json:"data,omitempty" yaml:"data,omitempty"` // the incoming object. The structure will vary from message to message, see `type` and `version` fields Error bool `json:"error,omitempty" yaml:"error,omitempty"` // true if the incoming message represents an error Type string `json:"type,omitempty" yaml:"type,omitempty"` // the type of object to expect in the `data` field @@ -399,11 +400,12 @@ type JsonReadMessage struct { } type JsonSendMessageResult struct { - Address *JsonAddress `json:"address,omitempty" yaml:"address,omitempty"` - IdentityFailure string `json:"identityFailure,omitempty" yaml:"identityFailure,omitempty"` - NetworkFailure bool `json:"networkFailure,omitempty" yaml:"networkFailure,omitempty"` - Success *SendSuccess `json:"success,omitempty" yaml:"success,omitempty"` - UnregisteredFailure bool `json:"unregisteredFailure,omitempty" yaml:"unregisteredFailure,omitempty"` + Address *JsonAddress `json:"address,omitempty" yaml:"address,omitempty"` + IdentityFailure string `json:"identityFailure,omitempty" yaml:"identityFailure,omitempty"` + NetworkFailure bool `json:"networkFailure,omitempty" yaml:"networkFailure,omitempty"` + ProofRequiredFailure *ProofRequiredError `json:"proof_required_failure,omitempty" yaml:"proof_required_failure,omitempty"` + Success *SendSuccess `json:"success,omitempty" yaml:"success,omitempty"` + UnregisteredFailure bool `json:"unregisteredFailure,omitempty" yaml:"unregisteredFailure,omitempty"` } type JsonSentTranscriptMessage struct { @@ -481,7 +483,7 @@ type ListGroupsRequest struct { Account string `json:"account,omitempty" yaml:"account,omitempty"` } -// ListenerState: indicates when the incoming connection to the signal server has started or stopped +// ListenerState: prior attempt to indicate signald connectivity state. WebSocketConnectionState messages will be delivered at the same time as well as in other parts of the websocket lifecycle. type ListenerState struct { Connected bool `json:"connected,omitempty" yaml:"connected,omitempty"` } @@ -529,11 +531,12 @@ type ProfileList struct { // ReactRequest: react to a previous message type ReactRequest struct { Request - Reaction *JsonReaction `json:"reaction,omitempty" yaml:"reaction,omitempty"` - RecipientAddress *JsonAddress `json:"recipientAddress,omitempty" yaml:"recipientAddress,omitempty"` - RecipientGroupID string `json:"recipientGroupId,omitempty" yaml:"recipientGroupId,omitempty"` - Timestamp int64 `json:"timestamp,omitempty" yaml:"timestamp,omitempty"` - Username string `json:"username,omitempty" yaml:"username,omitempty"` + Members []*JsonAddress `json:"members,omitempty" yaml:"members,omitempty"` // Optionally set to a sub-set of group members. Ignored if recipientGroupId isn't specified + Reaction *JsonReaction `json:"reaction,omitempty" yaml:"reaction,omitempty"` + RecipientAddress *JsonAddress `json:"recipientAddress,omitempty" yaml:"recipientAddress,omitempty"` + RecipientGroupID string `json:"recipientGroupId,omitempty" yaml:"recipientGroupId,omitempty"` + Timestamp int64 `json:"timestamp,omitempty" yaml:"timestamp,omitempty"` + Username string `json:"username,omitempty" yaml:"username,omitempty"` } type ReceiptMessage struct { @@ -582,10 +585,11 @@ type RemoteDelete struct { // RemoteDeleteRequest: delete a message previously sent type RemoteDeleteRequest struct { Request - Account string `json:"account,omitempty" yaml:"account,omitempty"` // the account to use - Address *JsonAddress `json:"address,omitempty" yaml:"address,omitempty"` // the address to send the delete message to. should match address the message to be deleted was sent to. required if group is not set. - Group string `json:"group,omitempty" yaml:"group,omitempty"` // the group to send the delete message to. should match group the message to be deleted was sent to. required if address is not set. - Timestamp int64 `json:"timestamp,omitempty" yaml:"timestamp,omitempty"` + Account string `json:"account,omitempty" yaml:"account,omitempty"` // the account to use + Address *JsonAddress `json:"address,omitempty" yaml:"address,omitempty"` // the address to send the delete message to. should match address the message to be deleted was sent to. required if group is not set. + Group string `json:"group,omitempty" yaml:"group,omitempty"` // the group to send the delete message to. should match group the message to be deleted was sent to. required if address is not set. + Members []*JsonAddress `json:"members,omitempty" yaml:"members,omitempty"` // Optionally set to a sub-set of group members. Ignored if group isn't specified + Timestamp int64 `json:"timestamp,omitempty" yaml:"timestamp,omitempty"` } // RemoveLinkedDeviceRequest: Remove a linked device from the Signal account. Only allowed when the local device id is 1 @@ -637,6 +641,7 @@ type SendPaymentRequest struct { type SendRequest struct { Request Attachments []*v0.JsonAttachment `json:"attachments,omitempty" yaml:"attachments,omitempty"` + Members []*JsonAddress `json:"members,omitempty" yaml:"members,omitempty"` // Optionally set to a sub-set of group members. Ignored if recipientGroupId isn't specified Mentions []*JsonMention `json:"mentions,omitempty" yaml:"mentions,omitempty"` MessageBody string `json:"messageBody,omitempty" yaml:"messageBody,omitempty"` Previews []*JsonPreview `json:"previews,omitempty" yaml:"previews,omitempty"` @@ -713,6 +718,13 @@ type SetProfile struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` // New profile name. Set to empty string for no profile name } +type SubmitChallengeRequest struct { + Request + Account string `json:"account,omitempty" yaml:"account,omitempty"` + CaptchaToken string `json:"captcha_token,omitempty" yaml:"captcha_token,omitempty"` + Challenge string `json:"challenge,omitempty" yaml:"challenge,omitempty"` +} + // SubscribeRequest: receive incoming messages. After making a subscribe request, incoming messages will be sent to the client encoded as ClientMessageWrapper. Send an unsubscribe request or disconnect from the socket to stop receiving messages. type SubscribeRequest struct { Request @@ -788,3 +800,9 @@ type VerifyRequest struct { type VersionRequest struct { Request } + +// WebSocketConnectionState: indicates when the websocket connection state to the signal server has changed +type WebSocketConnectionState struct { + Socket string `json:"socket,omitempty" yaml:"socket,omitempty"` // One of: UNIDENTIFIED, IDENTIFIED + State string `json:"state,omitempty" yaml:"state,omitempty"` // One of: DISCONNECTED, CONNECTING, CONNECTED, RECONNECTING, DISCONNECTING, AUTHENTICATION_FAILED, FAILED +}