diff --git a/protocol.json b/protocol.json index 3205011..3d5e927 100644 --- a/protocol.json +++ b/protocol.json @@ -2,9 +2,9 @@ "doc_version": "v1", "version": { "name": "signald", - "version": "0.12.0+git2021-02-26r10a16d18.43", + "version": "0.12.0+git2021-03-18rd3ea4b49.57", "branch": "main", - "commit": "10a16d18fed05327ac723a0dc561bf2800115f6f" + "commit": "d3ea4b495025d555370ef3088208e9126f410e36" }, "info": "This document describes objects that may be used when communicating with signald.", "types": { @@ -34,7 +34,7 @@ }, "timestamp": { "type": "long", - "example": "1614362423771" + "example": "1615576442475" }, "timestampISO": { "type": "String" @@ -44,7 +44,7 @@ }, "serverDeliveredTimestamp": { "type": "long", - "example": "161436242377180" + "example": "161557644247580" }, "hasLegacyMessage": { "type": "boolean" @@ -124,7 +124,7 @@ }, "timestamp": { "type": "long", - "example": "1614362423771" + "example": "1615576442475" } } }, @@ -165,7 +165,7 @@ }, "version": { "type": "String", - "example": "\"0.12.0+git2021-02-26r10a16d18.43\"" + "example": "\"0.12.0+git2021-03-18rd3ea4b49.57\"" }, "branch": { "type": "String", @@ -173,7 +173,7 @@ }, "commit": { "type": "String", - "example": "\"10a16d18fed05327ac723a0dc561bf2800115f6f\"" + "example": "\"d3ea4b495025d555370ef3088208e9126f410e36\"" } } }, @@ -460,6 +460,12 @@ "type": "String", "doc": "Path to new profile avatar file, if the avatar should be updated", "example": "\"/tmp/image.jpg\"" + }, + "about": { + "type": "String" + }, + "emoji": { + "type": "String" } } }, @@ -514,7 +520,7 @@ "list": true, "type": "Long", "doc": "List of messages to mark as read", - "example": "1614362423771", + "example": "1615576442475", "required": true } } @@ -561,6 +567,12 @@ "type": "String", "doc": "color of the chat with this user" }, + "about": { + "type": "String" + }, + "emoji": { + "type": "String" + }, "profile_name": { "type": "String", "doc": "The user's Signal profile name" @@ -637,7 +649,8 @@ "members": { "list": true, "type": "JsonAddress", - "version": "v1" + "version": "v1", + "required": true }, "timer": { "type": "int", @@ -708,12 +721,138 @@ }, "doc": "A local account in signald" }, + "AddLinkedDeviceRequest": { + "fields": { + "account": { + "type": "String", + "doc": "The account to interact with", + "example": "\"+12024561414\"", + "required": true + }, + "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\"", + "required": true + } + }, + "doc": "Link a new device to a local Signal account" + }, + "RegisterRequest": { + "fields": { + "account": { + "type": "String", + "doc": "the e164 phone number to register with", + "example": "\"+12024561414\"", + "required": true + }, + "voice": { + "type": "boolean", + "doc": "set to true to request a voice call instead of an SMS for verification" + }, + "captcha": { + "type": "String", + "doc": "See https://gitlab.com/signald/signald/-/wikis/Captchas" + } + }, + "doc": "begin the account registration process by requesting a phone number verification code. when the code is received, submit it with a verify request" + }, + "VerifyRequest": { + "fields": { + "account": { + "type": "String", + "doc": "the e164 phone number being verified", + "example": "\"+12024561414\"", + "required": true + }, + "code": { + "type": "String", + "doc": "the verification code, dash (-) optional", + "example": "\"555555\"", + "required": true + } + }, + "doc": "verify an account's phone number with a code after registering, completing the account creation process" + }, + "GetIdentitiesRequest": { + "fields": { + "account": { + "type": "String", + "doc": "The account to interact with", + "example": "\"+12024561414\"", + "required": true + }, + "address": { + "type": "JsonAddress", + "version": "v1", + "doc": "address to get keys for", + "required": true + } + }, + "doc": "Get information about a known keys for a particular address" + }, + "IdentityKeyList": { + "fields": { + "address": { + "type": "JsonAddress", + "version": "v1" + }, + "identities": { + "list": true, + "type": "IdentityKey", + "version": "v1" + } + }, + "doc": "a list of identity keys associated with a particular address" + }, + "TrustRequest": { + "fields": { + "account": { + "type": "String", + "doc": "The account to interact with", + "example": "\"+12024561414\"", + "required": true + }, + "address": { + "type": "JsonAddress", + "version": "v1", + "doc": "The user to query identity keys for", + "required": true + }, + "safety_number": { + "type": "String", + "doc": "required if qr_code_data is absent" + }, + "qr_code_data": { + "type": "String", + "doc": "base64-encoded QR code data. required if safety_number is absent" + }, + "trust_level": { + "type": "String", + "doc": "One of TRUSTED_UNVERIFIED, TRUSTED_VERIFIED or UNTRUSTED", + "example": "\"TRUSTED_VERIFIED\"", + "required": true + } + }, + "doc": "Trust another user's safety number using either the QR code data or the safety number text" + }, + "DeleteAccountRequest": { + "fields": { + "account": { + "type": "String", + "doc": "The account to delete", + "example": "\"+12024561414\"", + "required": true + } + }, + "doc": "delete all account data signald has on disk, and optionally delete the account from the server as well. Note that this is not \"unlink\" and will delete the entire account, even from a linked device." + }, "JsonDataMessage": { "fields": { "timestamp": { "type": "long", "doc": "the timestamp that the message was sent at, according to the sender's device. This is used to uniquely identify this message for things like reactions and quotes.", - "example": "1614362423771" + "example": "1615576442475" }, "attachments": { "list": true, @@ -850,7 +989,7 @@ "id": { "type": "long", "doc": "the client timestamp of the message being quoted", - "example": "1614362423771" + "example": "1615576442475" }, "author": { "type": "JsonAddress", @@ -938,7 +1077,7 @@ "targetSentTimestamp": { "type": "long", "doc": "the client timestamp of the message being reacted to", - "example": "1614362423771" + "example": "1615576442475" } } }, @@ -1010,6 +1149,25 @@ } } }, + "IdentityKey": { + "fields": { + "added": { + "type": "long", + "doc": "the first time this identity key was seen" + }, + "safety_number": { + "type": "String" + }, + "qr_code_data": { + "type": "String", + "doc": "base64-encoded QR code data" + }, + "trust_level": { + "type": "String", + "doc": "One of TRUSTED_UNVERIFIED, TRUSTED_VERIFIED or UNTRUSTED" + } + } + }, "JsonSentTranscriptMessage": { "fields": { "destination": { @@ -1018,7 +1176,7 @@ }, "timestamp": { "type": "long", - "example": "1614362423771" + "example": "1615576442475" }, "expirationStartTimestamp": { "type": "long" @@ -1056,7 +1214,7 @@ }, "timestamp": { "type": "long", - "example": "1614362423771" + "example": "1615576442475" } } }, @@ -1068,7 +1226,7 @@ }, "timestamp": { "type": "long", - "example": "1614362423771" + "example": "1615576442475" } } }, @@ -1578,6 +1736,33 @@ "finish_link": { "request": "FinishLinkRequest", "response": "Account" + }, + "add_device": { + "request": "AddLinkedDeviceRequest", + "doc": "Link a new device to a local Signal account" + }, + "register": { + "request": "RegisterRequest", + "response": "Account", + "doc": "begin the account registration process by requesting a phone number verification code. when the code is received, submit it with a verify request" + }, + "verify": { + "request": "VerifyRequest", + "response": "Account", + "doc": "verify an account's phone number with a code after registering, completing the account creation process" + }, + "get_identities": { + "request": "GetIdentitiesRequest", + "response": "IdentityKeyList", + "doc": "Get information about a known keys for a particular address" + }, + "trust": { + "request": "TrustRequest", + "doc": "Trust another user's safety number using either the QR code data or the safety number text" + }, + "delete_account": { + "request": "DeleteAccountRequest", + "doc": "delete all account data signald has on disk, and optionally delete the account from the server as well. Note that this is not \"unlink\" and will delete the entire account, even from a linked device." } } } diff --git a/signald/client-protocol/v1/requests.go b/signald/client-protocol/v1/requests.go index 887a4c9..cac208a 100644 --- a/signald/client-protocol/v1/requests.go +++ b/signald/client-protocol/v1/requests.go @@ -43,6 +43,32 @@ func (r *AcceptInvitationRequest) Submit(conn *signald.Signald) (response JsonGr } +// Submit: Link a new device to a local Signal account +func (r *AddLinkedDeviceRequest) Submit(conn *signald.Signald) (err error) { + r.Version = "v1" + r.Type = "add_device" + 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 = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + return + } + + return err + +} + // Submit: approve a request to join a group func (r *ApproveMembershipRequest) Submit(conn *signald.Signald) (response JsonGroupV2Info, err error) { r.Version = "v1" @@ -108,6 +134,32 @@ func (r *CreateGroupRequest) Submit(conn *signald.Signald) (response JsonGroupV2 } +// Submit: delete all account data signald has on disk, and optionally delete the account from the server as well. Note that this is not "unlink" and will delete the entire account, even from a linked device. +func (r *DeleteAccountRequest) Submit(conn *signald.Signald) (err error) { + r.Version = "v1" + r.Type = "delete_account" + 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 = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + return + } + + return err + +} + func (r *FinishLinkRequest) Submit(conn *signald.Signald) (response Account, err error) { r.Version = "v1" r.Type = "finish_link" @@ -206,6 +258,39 @@ func (r *GetGroupRequest) Submit(conn *signald.Signald) (response JsonGroupV2Inf } +// Submit: Get information about a known keys for a particular address +func (r *GetIdentitiesRequest) Submit(conn *signald.Signald) (response IdentityKeyList, err error) { + r.Version = "v1" + r.Type = "get_identities" + 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 = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + return + } + + err = json.Unmarshal(rawResponse.Data, &response) + if err != nil { + rawResponseJson, _ := rawResponse.Data.MarshalJSON() + log.Println("signald-go: error unmarshalling response from signald of type", rawResponse.Type, string(rawResponseJson)) + return + } + + return response, nil + +} + // Submit: list all linked devices on a Signal account func (r *GetLinkedDevicesRequest) Submit(conn *signald.Signald) (response LinkedDevices, err error) { r.Version = "v1" @@ -459,6 +544,39 @@ func (r *ReactRequest) Submit(conn *signald.Signald) (response SendResponse, err } +// Submit: begin the account registration process by requesting a phone number verification code. when the code is received, submit it with a verify request +func (r *RegisterRequest) Submit(conn *signald.Signald) (response Account, err error) { + r.Version = "v1" + r.Type = "register" + 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 = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + return + } + + err = json.Unmarshal(rawResponse.Data, &response) + if err != nil { + rawResponseJson, _ := rawResponse.Data.MarshalJSON() + log.Println("signald-go: error unmarshalling response from signald of type", rawResponse.Type, string(rawResponseJson)) + return + } + + return response, nil + +} + // Submit: Remove a linked device from the Signal account. Only allowed when the local device id is 1 func (r *RemoveLinkedDeviceRequest) Submit(conn *signald.Signald) (err error) { r.Version = "v1" @@ -575,6 +693,32 @@ func (r *SetProfile) Submit(conn *signald.Signald) (err error) { } +// Submit: Trust another user's safety number using either the QR code data or the safety number text +func (r *TrustRequest) Submit(conn *signald.Signald) (err error) { + r.Version = "v1" + r.Type = "trust" + 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 = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + return + } + + return err + +} + // Submit: modify a group. Note that only one modification action may be preformed at once func (r *UpdateGroupRequest) Submit(conn *signald.Signald) (response GroupInfo, err error) { r.Version = "v1" @@ -608,6 +752,39 @@ func (r *UpdateGroupRequest) Submit(conn *signald.Signald) (response GroupInfo, } +// Submit: verify an account's phone number with a code after registering, completing the account creation process +func (r *VerifyRequest) Submit(conn *signald.Signald) (response Account, err error) { + r.Version = "v1" + r.Type = "verify" + 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 = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + return + } + + err = json.Unmarshal(rawResponse.Data, &response) + if err != nil { + rawResponseJson, _ := rawResponse.Data.MarshalJSON() + log.Println("signald-go: error unmarshalling response from signald of type", rawResponse.Type, string(rawResponseJson)) + return + } + + return response, nil + +} + func (r *VersionRequest) Submit(conn *signald.Signald) (response JsonVersionMessage, err error) { r.Version = "v1" r.Type = "version" diff --git a/signald/client-protocol/v1/structs.go b/signald/client-protocol/v1/structs.go index 9865e65..5c8066d 100644 --- a/signald/client-protocol/v1/structs.go +++ b/signald/client-protocol/v1/structs.go @@ -26,6 +26,13 @@ type Account struct { DeviceId int32 `json:"device_id,omitempty" yaml:"device_id,omitempty"` // The Signal device ID. Official Signal mobile clients (iPhone and Android) have device ID = 1, while linked devices such as Signal Desktop or Signal iPad have higher device IDs. } +// AddLinkedDeviceRequest: Link a new device to a local Signal account +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 +} + // ApproveMembershipRequest: approve a request to join a group type ApproveMembershipRequest struct { Request @@ -50,6 +57,12 @@ type CreateGroupRequest struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` } +// DeleteAccountRequest: delete all account data signald has on disk, and optionally delete the account from the server as well. Note that this is not "unlink" and will delete the entire account, even from a linked device. +type DeleteAccountRequest struct { + Request + Account string `json:"account,omitempty" yaml:"account,omitempty"` // The account to delete +} + type FinishLinkRequest struct { Request DeviceName string `json:"device_name,omitempty" yaml:"device_name,omitempty"` @@ -69,6 +82,13 @@ type GetGroupRequest struct { Revision int32 `json:"revision,omitempty" yaml:"revision,omitempty"` // the latest known revision, default value (-1) forces fetch from server } +// GetIdentitiesRequest: Get information about a known keys for a particular address +type GetIdentitiesRequest struct { + Request + Account string `json:"account,omitempty" yaml:"account,omitempty"` // The account to interact with + Address *JsonAddress `json:"address,omitempty" yaml:"address,omitempty"` // address to get keys for +} + // GetLinkedDevicesRequest: list all linked devices on a Signal account type GetLinkedDevicesRequest struct { Request @@ -107,6 +127,19 @@ type GroupMember struct { UUID string `json:"uuid,omitempty" yaml:"uuid,omitempty"` } +type IdentityKey struct { + Added int64 `json:"added,omitempty" yaml:"added,omitempty"` // the first time this identity key was seen + QrCodeData string `json:"qr_code_data,omitempty" yaml:"qr_code_data,omitempty"` // base64-encoded QR code data + SafetyNumber string `json:"safety_number,omitempty" yaml:"safety_number,omitempty"` + TrustLevel string `json:"trust_level,omitempty" yaml:"trust_level,omitempty"` // One of TRUSTED_UNVERIFIED, TRUSTED_VERIFIED or UNTRUSTED +} + +// IdentityKeyList: a list of identity keys associated with a particular address +type IdentityKeyList struct { + Address *JsonAddress `json:"address,omitempty" yaml:"address,omitempty"` + Identities []*IdentityKey `json:"identities,omitempty" yaml:"identities,omitempty"` +} + // JoinGroupRequest: Join a group using the a signal.group URL. Note that you must have a profile name set to join groups. type JoinGroupRequest struct { Request @@ -317,10 +350,12 @@ type MarkReadRequest struct { // Profile: Information about a Signal user type Profile struct { + About string `json:"about,omitempty" yaml:"about,omitempty"` Address *JsonAddress `json:"address,omitempty" yaml:"address,omitempty"` Avatar string `json:"avatar,omitempty" yaml:"avatar,omitempty"` // path to avatar on local disk Capabilities *Capabilities `json:"capabilities,omitempty" yaml:"capabilities,omitempty"` Color string `json:"color,omitempty" yaml:"color,omitempty"` // color of the chat with this user + Emoji string `json:"emoji,omitempty" yaml:"emoji,omitempty"` ExpirationTime int32 `json:"expiration_time,omitempty" yaml:"expiration_time,omitempty"` InboxPosition int32 `json:"inbox_position,omitempty" yaml:"inbox_position,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` // The user's name from local contact names if available, or if not in contact list their Signal profile name @@ -341,6 +376,14 @@ type ReactRequest struct { Username string `json:"username,omitempty" yaml:"username,omitempty"` } +// RegisterRequest: begin the account registration process by requesting a phone number verification code. when the code is received, submit it with a verify request +type RegisterRequest struct { + Request + Account string `json:"account,omitempty" yaml:"account,omitempty"` // the e164 phone number to register with + Captcha string `json:"captcha,omitempty" yaml:"captcha,omitempty"` // See https://gitlab.com/signald/signald/-/wikis/Captchas + Voice bool `json:"voice,omitempty" yaml:"voice,omitempty"` // set to true to request a voice call instead of an SMS for verification +} + // RemoveLinkedDeviceRequest: Remove a linked device from the Signal account. Only allowed when the local device id is 1 type RemoveLinkedDeviceRequest struct { Request @@ -374,9 +417,21 @@ type SendResponse struct { type SetProfile struct { Request + About string `json:"about,omitempty" yaml:"about,omitempty"` Account string `json:"account,omitempty" yaml:"account,omitempty"` // The phone number of the account to use AvatarFile string `json:"avatarFile,omitempty" yaml:"avatarFile,omitempty"` // Path to new profile avatar file, if the avatar should be updated - Name string `json:"name,omitempty" yaml:"name,omitempty"` // New profile name. Set to empty string for no profile name + Emoji string `json:"emoji,omitempty" yaml:"emoji,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` // New profile name. Set to empty string for no profile name +} + +// TrustRequest: Trust another user's safety number using either the QR code data or the safety number text +type TrustRequest struct { + Request + Account string `json:"account,omitempty" yaml:"account,omitempty"` // The account to interact with + Address *JsonAddress `json:"address,omitempty" yaml:"address,omitempty"` // The user to query identity keys for + QrCodeData string `json:"qr_code_data,omitempty" yaml:"qr_code_data,omitempty"` // base64-encoded QR code data. required if safety_number is absent + SafetyNumber string `json:"safety_number,omitempty" yaml:"safety_number,omitempty"` // required if qr_code_data is absent + TrustLevel string `json:"trust_level,omitempty" yaml:"trust_level,omitempty"` // One of TRUSTED_UNVERIFIED, TRUSTED_VERIFIED or UNTRUSTED } // UpdateGroupRequest: modify a group. Note that only one modification action may be preformed at once @@ -394,6 +449,13 @@ type UpdateGroupRequest struct { UpdateTimer int32 `json:"updateTimer,omitempty" yaml:"updateTimer,omitempty"` // update the group timer. } +// VerifyRequest: verify an account's phone number with a code after registering, completing the account creation process +type VerifyRequest struct { + Request + Account string `json:"account,omitempty" yaml:"account,omitempty"` // the e164 phone number being verified + Code string `json:"code,omitempty" yaml:"code,omitempty"` // the verification code, dash (-) optional +} + type VersionRequest struct { Request }