From bfcb423cdf6d4d59a1882924e727f9f9ef64d5b7 Mon Sep 17 00:00:00 2001 From: Finn Date: Thu, 30 Sep 2021 00:39:54 -0700 Subject: [PATCH] add support for errors in the protocol --- protocol.json | 1352 ++++++++++++++++++++++-- signald/client-protocol/protocol.go | 13 + signald/client-protocol/v0/structs.go | 8 +- signald/client-protocol/v1/errors.go | 462 ++++++++ signald/client-protocol/v1/requests.go | 124 ++- signald/client-protocol/v1/structs.go | 104 +- signald/signald.go | 36 +- tools/generator/errors.go.tmpl | 37 + tools/generator/main.go | 72 +- tools/generator/requests.go.tmpl | 9 +- 10 files changed, 2007 insertions(+), 210 deletions(-) create mode 100644 signald/client-protocol/protocol.go create mode 100644 signald/client-protocol/v1/errors.go create mode 100644 tools/generator/errors.go.tmpl diff --git a/protocol.json b/protocol.json index f416485..1905083 100644 --- a/protocol.json +++ b/protocol.json @@ -2,9 +2,9 @@ "doc_version": "v1", "version": { "name": "signald", - "version": "0.14.1+git2021-08-27r6dd96a17.28", + "version": "0.14.1-48-7d927883", "branch": "main", - "commit": "6dd96a17c921766b0d284165e571476744ac2829" + "commit": "7d92788343f34c08634abfeda06045ae13e18670" }, "info": "This document describes objects that may be used when communicating with signald.", "types": { @@ -203,6 +203,11 @@ "list": true, "type": "JsonMention", "version": "v1" + }, + "previews": { + "list": true, + "type": "JsonPreview", + "version": "v1" } } }, @@ -219,6 +224,84 @@ } } }, + "NoSuchAccountError": { + "fields": { + "account": { + "type": "String" + }, + "message": { + "type": "String" + } + }, + "error": true + }, + "ServerNotFoundError": { + "fields": { + "uuid": { + "type": "String" + }, + "message": { + "type": "String" + } + }, + "error": true + }, + "InvalidProxyError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, + "NoSendPermissionError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, + "InvalidAttachmentError": { + "fields": { + "filename": { + "type": "String" + }, + "message": { + "type": "String" + } + }, + "error": true + }, + "InternalError": { + "fields": { + "exceptions": { + "list": true, + "type": "String" + }, + "message": { + "type": "String" + } + }, + "doc": "an internal error in signald has occured.", + "error": true + }, + "UnknownGroupError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, + "InvalidRecipientError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, "ReactRequest": { "fields": { "username": { @@ -256,7 +339,7 @@ }, "version": { "type": "String", - "example": "\"0.14.1+git2021-08-27r6dd96a17.28\"" + "example": "\"0.14.1-48-7d927883\"" }, "branch": { "type": "String", @@ -264,7 +347,7 @@ }, "commit": { "type": "String", - "example": "\"6dd96a17c921766b0d284165e571476744ac2829\"" + "example": "\"7d92788343f34c08634abfeda06045ae13e18670\"" } } }, @@ -345,10 +428,22 @@ "type": "GroupMember", "version": "v1", "doc": "detailed pending member list" + }, + "announcements": { + "type": "String", + "doc": "indicates if the group is an announcements group. Only admins are allowed to send messages to announcements groups. Options are UNKNOWN, ENABLED or DISABLED" } }, "doc": "Information about a Signal group" }, + "OwnProfileKeyDoesNotExistError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, "ApproveMembershipRequest": { "fields": { "account": { @@ -372,6 +467,14 @@ }, "doc": "approve a request to join a group" }, + "GroupVerificationError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, "GetGroupRequest": { "fields": { "account": { @@ -390,7 +493,15 @@ "doc": "the latest known revision, default value (-1) forces fetch from server" } }, - "doc": "Query the server for the latest state of a known group. If no account in signald is a member of the group (anymore), an error with error_type: 'UnknownGroupException' is returned." + "doc": "Query the server for the latest state of a known group. If no account in signald is a member of the group (anymore), an error with error_type: 'UnknownGroupError' is returned." + }, + "InvalidGroupStateError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true }, "GetLinkedDevicesRequest": { "fields": { @@ -462,6 +573,30 @@ } } }, + "InvalidRequestError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, + "InvalidInviteURIError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, + "GroupNotActiveError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, "RemoveLinkedDeviceRequest": { "fields": { "account": { @@ -532,6 +667,10 @@ "resetLink": { "type": "boolean", "doc": "regenerate the group link password, invalidating the old one" + }, + "announcements": { + "type": "String", + "doc": "ENABLED to only allow admins to post messages, DISABLED to allow anyone to post" } }, "doc": "modify a group. Note that only one modification action may be performed at once" @@ -582,6 +721,14 @@ } } }, + "InvalidBase64Error": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, "ResolveAddressRequest": { "fields": { "account": { @@ -597,7 +744,7 @@ "required": true } }, - "doc": "Resolve a partial JsonAddress with only a number or UUID to one with both. Anywhere that signald accepts a JsonAddress will except a partial, this is a convenience function for client authors, mostly because signald doesn't resolve all the partials it returns" + "doc": "Resolve a partial JsonAddress with only a number or UUID to one with both. Anywhere that signald accepts a JsonAddress will except a partial, this is a convenience function for client authors, mostly because signald doesn't resolve all the partials it returns." }, "JsonAddress": { "fields": { @@ -641,6 +788,21 @@ } } }, + "UntrustedIdentityError": { + "fields": { + "identifier": { + "type": "String" + }, + "message": { + "type": "String" + }, + "identity_key": { + "type": "IdentityKey", + "version": "v1" + } + }, + "error": true + }, "GetProfileRequest": { "fields": { "account": { @@ -706,6 +868,14 @@ }, "doc": "Information about a Signal user" }, + "ProfileUnavailableError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, "ListGroupsRequest": { "fields": { "account": { @@ -783,6 +953,14 @@ } } }, + "NoKnownUUIDError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, "LeaveGroupRequest": { "fields": { "account": { @@ -836,6 +1014,10 @@ "version": "v1", "doc": "The address of this account" }, + "pending": { + "type": "Boolean", + "doc": "indicates the account has not completed registration" + }, "device_id": { "type": "int", "doc": "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." @@ -847,6 +1029,25 @@ }, "doc": "A local account in signald" }, + "NoSuchSessionError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, + "UserAlreadyExistsError": { + "fields": { + "uuid": { + "type": "UUID" + }, + "message": { + "type": "String" + } + }, + "error": true + }, "AddLinkedDeviceRequest": { "fields": { "account": { @@ -887,6 +1088,17 @@ }, "doc": "begin the account registration process by requesting a phone number verification code. when the code is received, submit it with a verify request" }, + "CaptchaRequiredError": { + "fields": { + "more": { + "type": "String" + }, + "message": { + "type": "String" + } + }, + "error": true + }, "VerifyRequest": { "fields": { "account": { @@ -904,6 +1116,33 @@ }, "doc": "verify an account's phone number with a code after registering, completing the account creation process" }, + "AccountHasNoKeysError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, + "AccountAlreadyVerifiedError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, + "AccountLockedError": { + "fields": { + "more": { + "type": "String" + }, + "message": { + "type": "String" + } + }, + "error": true + }, "GetIdentitiesRequest": { "fields": { "account": { @@ -966,6 +1205,30 @@ }, "doc": "Trust another user's safety number using either the QR code data or the safety number text" }, + "FingerprintVersionMismatchError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, + "UnknownIdentityKeyError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, + "InvalidFingerprintError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, "DeleteAccountRequest": { "fields": { "account": { @@ -1008,6 +1271,14 @@ }, "doc": "send a typing started or stopped message" }, + "InvalidGroupError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, "ResetSessionRequest": { "fields": { "account": { @@ -1085,6 +1356,14 @@ }, "doc": "Get information about a group from a signal.group link" }, + "GroupLinkNotActiveError": { + "fields": { + "message": { + "type": "String" + } + }, + "error": true + }, "UpdateContactRequest": { "fields": { "account": { @@ -1288,6 +1567,29 @@ } } }, + "RefuseMembershipRequest": { + "fields": { + "account": { + "type": "String", + "doc": "The account to interact with", + "example": "\"+12024561414\"", + "required": true + }, + "members": { + "list": true, + "type": "JsonAddress", + "version": "v1", + "doc": "list of requesting members to refuse", + "required": true + }, + "group_id": { + "type": "String", + "example": "\"EdSqI90cS0UomDpgUXOlCoObWvQOXlH5G3Z2d3f4ayE=\"", + "required": true + } + }, + "doc": "deny a request to join a group" + }, "JsonDataMessage": { "fields": { "timestamp": { @@ -1298,7 +1600,7 @@ "attachments": { "list": true, "type": "JsonAttachment", - "version": "v0", + "version": "v1", "doc": "files attached to the incoming message" }, "body": { @@ -1340,7 +1642,7 @@ "previews": { "list": true, "type": "JsonPreview", - "version": "v0", + "version": "v1", "doc": "if the incoming message has a link preview, information about that preview will be here" }, "sticker": { @@ -1387,14 +1689,14 @@ }, "contacts": { "type": "JsonAttachment", - "version": "v0" + "version": "v1" }, "contactsComplete": { "type": "boolean" }, "groups": { "type": "JsonAttachment", - "version": "v0" + "version": "v1" }, "blockedList": { "type": "JsonBlockedListMessage", @@ -1544,6 +1846,28 @@ } } }, + "JsonPreview": { + "fields": { + "url": { + "type": "String" + }, + "title": { + "type": "String" + }, + "description": { + "type": "String" + }, + "date": { + "type": "long" + }, + "attachment": { + "type": "JsonAttachment", + "version": "v1", + "doc": "an optional image file attached to the preview" + } + }, + "doc": "metadata about one of the links in a message" + }, "JsonSendMessageResult": { "fields": { "address": { @@ -1551,8 +1875,8 @@ "version": "v1" }, "success": { - "type": "Success", - "version": "v0" + "type": "SendSuccess", + "version": "v1" }, "networkFailure": { "type": "boolean", @@ -1662,19 +1986,6 @@ }, "doc": "information about a legacy group" }, - "Capabilities": { - "fields": { - "gv2": { - "type": "boolean" - }, - "storage": { - "type": "boolean" - }, - "gv1-migration": { - "type": "boolean" - } - } - }, "IdentityKey": { "fields": { "added": { @@ -1695,6 +2006,19 @@ } } }, + "Capabilities": { + "fields": { + "gv2": { + "type": "boolean" + }, + "storage": { + "type": "boolean" + }, + "gv1-migration": { + "type": "boolean" + } + } + }, "Server": { "fields": { "uuid": { @@ -1705,7 +2029,8 @@ "type": "String" }, "ca": { - "type": "String" + "type": "String", + "doc": "base64 encoded trust store, password must be 'whisper'" }, "service_url": { "type": "String" @@ -1729,7 +2054,25 @@ "doc": "base64 encoded ZKGROUP_SERVER_PUBLIC_PARAMS value" }, "unidentified_sender_root": { + "type": "String", + "doc": "base64 encoded" + }, + "key_backup_service_name": { "type": "String" + }, + "key_backup_service_id": { + "type": "String", + "doc": "base64 encoded" + }, + "key_backup_mrenclave": { + "type": "String" + }, + "cds_mrenclave": { + "type": "String" + }, + "ias_ca": { + "type": "String", + "doc": "base64 encoded trust store, password must be 'whisper'" } }, "doc": "a Signal server" @@ -1762,6 +2105,53 @@ }, "doc": "A remote config (feature flag) entry." }, + "JsonAttachment": { + "fields": { + "contentType": { + "type": "String" + }, + "id": { + "type": "String" + }, + "size": { + "type": "int" + }, + "storedFilename": { + "type": "String", + "doc": "when receiving, the path that file has been downloaded to" + }, + "filename": { + "type": "String", + "doc": "when sending, the path to the local file to upload" + }, + "customFilename": { + "type": "String", + "doc": "the original name of the file" + }, + "caption": { + "type": "String" + }, + "width": { + "type": "int" + }, + "height": { + "type": "int" + }, + "voiceNote": { + "type": "boolean" + }, + "key": { + "type": "String" + }, + "digest": { + "type": "String" + }, + "blurhash": { + "type": "String" + } + }, + "doc": "represents a file attached to a message. When seding, only `filename` is required." + }, "RemoteDelete": { "fields": { "target_sent_timestamp": { @@ -1927,6 +2317,23 @@ } } }, + "SendSuccess": { + "fields": { + "unidentified": { + "type": "boolean" + }, + "needsSync": { + "type": "boolean" + }, + "duration": { + "type": "long" + }, + "devices": { + "list": true, + "type": "Integer" + } + } + }, "ServerCDN": { "fields": { "number": { @@ -1971,7 +2378,8 @@ "type": "String" }, "relay": { - "type": "String" + "type": "String", + "doc": "this field is no longer available and will never be populated" }, "timestamp": { "type": "long", @@ -2340,22 +2748,6 @@ } } }, - "JsonPreview": { - "fields": { - "url": { - "type": "String" - }, - "title": { - "type": "String" - }, - "attachment": { - "type": "JsonAttachment", - "version": "v0" - } - }, - "deprecated": true, - "removal_date": 1641027661 - }, "JsonSticker": { "fields": { "packID": { @@ -2603,6 +2995,22 @@ "deprecated": true, "removal_date": 1641027661 }, + "JsonPreview": { + "fields": { + "url": { + "type": "String" + }, + "title": { + "type": "String" + }, + "attachment": { + "type": "JsonAttachment", + "version": "v0" + } + }, + "deprecated": true, + "removal_date": 1641027661 + }, "JsonReaction": { "fields": { "emoji": { @@ -2777,19 +3185,6 @@ "deprecated": true, "removal_date": 1641027661 }, - "Success": { - "fields": { - "unidentified": { - "type": "boolean" - }, - "needsSync": { - "type": "boolean" - }, - "duration": { - "type": "long" - } - } - }, "Name": { "fields": { "display": { @@ -2872,12 +3267,61 @@ "v1": { "send": { "request": "SendRequest", - "response": "SendResponse" + "response": "SendResponse", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "NoSendPermissionError" + }, + { + "name": "InvalidAttachmentError" + }, + { + "name": "InternalError" + }, + { + "name": "UnknownGroupError" + }, + { + "name": "InvalidRecipientError" + } + ] }, "react": { "request": "ReactRequest", "response": "SendResponse", - "doc": "react to a previous message" + "doc": "react to a previous message", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "NoSendPermissionError" + }, + { + "name": "InternalError" + }, + { + "name": "InvalidRecipientError" + }, + { + "name": "UnknownGroupError" + } + ] }, "version": { "request": "VersionRequest", @@ -2886,182 +3330,894 @@ "accept_invitation": { "request": "AcceptInvitationRequest", "response": "JsonGroupV2Info", - "doc": "Accept a v2 group invitation. Note that you must have a profile name set to join groups." + "doc": "Accept a v2 group invitation. Note that you must have a profile name set to join groups.", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "OwnProfileKeyDoesNotExistError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "UnknownGroupError" + }, + { + "name": "InternalError" + } + ] }, "approve_membership": { "request": "ApproveMembershipRequest", "response": "JsonGroupV2Info", - "doc": "approve a request to join a group" + "doc": "approve a request to join a group", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "UnknownGroupError" + }, + { + "name": "InternalError" + }, + { + "name": "GroupVerificationError" + } + ] }, "get_group": { "request": "GetGroupRequest", "response": "JsonGroupV2Info", - "doc": "Query the server for the latest state of a known group. If no account in signald is a member of the group (anymore), an error with error_type: 'UnknownGroupException' is returned." + "doc": "Query the server for the latest state of a known group. If no account in signald is a member of the group (anymore), an error with error_type: 'UnknownGroupError' is returned.", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "UnknownGroupError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "InternalError" + }, + { + "name": "GroupVerificationError" + }, + { + "name": "InvalidGroupStateError" + } + ] }, "get_linked_devices": { "request": "GetLinkedDevicesRequest", "response": "LinkedDevices", - "doc": "list all linked devices on a Signal account" + "doc": "list all linked devices on a Signal account", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + } + ] }, "join_group": { "request": "JoinGroupRequest", "response": "JsonGroupJoinInfo", - "doc": "Join a group using the a signal.group URL. Note that you must have a profile name set to join groups." + "doc": "Join a group using the a signal.group URL. Note that you must have a profile name set to join groups.", + "errors": [ + { + "name": "InvalidRequestError" + }, + { + "name": "InvalidInviteURIError" + }, + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "OwnProfileKeyDoesNotExistError" + }, + { + "name": "GroupVerificationError" + }, + { + "name": "GroupNotActiveError" + }, + { + "name": "UnknownGroupError" + }, + { + "name": "InvalidGroupStateError" + } + ] }, "remove_linked_device": { "request": "RemoveLinkedDeviceRequest", - "doc": "Remove a linked device from the Signal account. Only allowed when the local device id is 1" + "doc": "Remove a linked device from the Signal account. Only allowed when the local device id is 1", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + } + ] }, "update_group": { "request": "UpdateGroupRequest", "response": "GroupInfo", - "doc": "modify a group. Note that only one modification action may be performed at once" + "doc": "modify a group. Note that only one modification action may be performed at once", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "UnknownGroupError" + }, + { + "name": "GroupVerificationError" + }, + { + "name": "InvalidRequestError" + } + ] }, "set_profile": { - "request": "SetProfile" + "request": "SetProfile", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "InvalidBase64Error" + } + ] }, "resolve_address": { "request": "ResolveAddressRequest", "response": "JsonAddress", - "doc": "Resolve a partial JsonAddress with only a number or UUID to one with both. Anywhere that signald accepts a JsonAddress will except a partial, this is a convenience function for client authors, mostly because signald doesn't resolve all the partials it returns" + "doc": "Resolve a partial JsonAddress with only a number or UUID to one with both. Anywhere that signald accepts a JsonAddress will except a partial, this is a convenience function for client authors, mostly because signald doesn't resolve all the partials it returns.", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "NoSuchAccountError" + } + ] }, "mark_read": { - "request": "MarkReadRequest" + "request": "MarkReadRequest", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "InternalError" + }, + { + "name": "UntrustedIdentityError" + } + ] }, "get_profile": { "request": "GetProfileRequest", "response": "Profile", - "doc": "Get all information available about a user" + "doc": "Get all information available about a user", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "ProfileUnavailableError" + } + ] }, "list_groups": { "request": "ListGroupsRequest", - "response": "GroupList" + "response": "GroupList", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + } + ] }, "list_contacts": { "request": "ListContactsRequest", - "response": "ProfileList" + "response": "ProfileList", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + } + ] }, "create_group": { "request": "CreateGroupRequest", - "response": "JsonGroupV2Info" + "response": "JsonGroupV2Info", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "OwnProfileKeyDoesNotExistError" + }, + { + "name": "NoKnownUUIDError" + }, + { + "name": "InvalidRequestError" + }, + { + "name": "GroupVerificationError" + }, + { + "name": "InvalidGroupStateError" + }, + { + "name": "UnknownGroupError" + } + ] }, "leave_group": { "request": "LeaveGroupRequest", - "response": "GroupInfo" + "response": "GroupInfo", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "InternalError" + }, + { + "name": "UnknownGroupError" + }, + { + "name": "GroupVerificationError" + } + ] }, "generate_linking_uri": { "request": "GenerateLinkingURIRequest", "response": "LinkingURI", - "doc": "Generate a linking URI. Typically this is QR encoded and scanned by the primary device. Submit the returned session_id with a finish_link request." + "doc": "Generate a linking URI. Typically this is QR encoded and scanned by the primary device. Submit the returned session_id with a finish_link request.", + "errors": [ + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + } + ] }, "finish_link": { "request": "FinishLinkRequest", "response": "Account", - "doc": "After a linking URI has been requested, finish_link must be called with the session_id provided with the URI. it will return information about the new account once the linking process is completed by the other device." + "doc": "After a linking URI has been requested, finish_link must be called with the session_id provided with the URI. it will return information about the new account once the linking process is completed by the other device.", + "errors": [ + { + "name": "NoSuchSessionError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "InternalError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "UserAlreadyExistsError" + } + ] }, "add_device": { "request": "AddLinkedDeviceRequest", - "doc": "Link a new device to a local Signal account" + "doc": "Link a new device to a local Signal account", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "InvalidRequestError", + "doc": "caused by syntax errors with the provided linking URI" + }, + { + "name": "InternalError" + } + ] }, "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" + "doc": "begin the account registration process by requesting a phone number verification code. when the code is received, submit it with a verify request", + "errors": [ + { + "name": "CaptchaRequiredError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + } + ] }, "verify": { "request": "VerifyRequest", "response": "Account", - "doc": "verify an account's phone number with a code after registering, completing the account creation process" + "doc": "verify an account's phone number with a code after registering, completing the account creation process", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "AccountHasNoKeysError" + }, + { + "name": "AccountAlreadyVerifiedError" + }, + { + "name": "AccountLockedError" + }, + { + "name": "NoSuchAccountError" + } + ] }, "get_identities": { "request": "GetIdentitiesRequest", "response": "IdentityKeyList", - "doc": "Get information about a known keys for a particular address" + "doc": "Get information about a known keys for a particular address", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + } + ] }, "trust": { "request": "TrustRequest", - "doc": "Trust another user's safety number using either the QR code data or the safety number text" + "doc": "Trust another user's safety number using either the QR code data or the safety number text", + "errors": [ + { + "name": "InvalidRequestError" + }, + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "FingerprintVersionMismatchError" + }, + { + "name": "InvalidBase64Error" + }, + { + "name": "UnknownIdentityKeyError" + }, + { + "name": "InvalidFingerprintError" + } + ] }, "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." + "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.", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + } + ] }, "typing": { "request": "TypingRequest", - "doc": "send a typing started or stopped message" + "doc": "send a typing started or stopped message", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "InvalidRecipientError" + }, + { + "name": "InvalidGroupError" + }, + { + "name": "UntrustedIdentityError" + }, + { + "name": "UnknownGroupError" + } + ] }, "reset_session": { "request": "ResetSessionRequest", "response": "SendResponse", - "doc": "reset a session with a particular user" + "doc": "reset a session with a particular user", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "InvalidRecipientError" + }, + { + "name": "NoSendPermissionError" + }, + { + "name": "UnknownGroupError" + } + ] }, "request_sync": { "request": "RequestSyncRequest", - "doc": "Request other devices on the account send us their group list, syncable config and contact list." + "doc": "Request other devices on the account send us their group list, syncable config and contact list.", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "UntrustedIdentityError" + } + ] }, "list_accounts": { "request": "ListAccountsRequest", "response": "AccountList", - "doc": "return all local accounts" + "doc": "return all local accounts", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "InternalError" + } + ] }, "group_link_info": { "request": "GroupLinkInfoRequest", "response": "JsonGroupJoinInfo", - "doc": "Get information about a group from a signal.group link" + "doc": "Get information about a group from a signal.group link", + "errors": [ + { + "name": "GroupLinkNotActiveError" + }, + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "InvalidRequestError" + }, + { + "name": "GroupVerificationError" + } + ] }, "update_contact": { "request": "UpdateContactRequest", "response": "Profile", - "doc": "update information about a local contact" + "doc": "update information about a local contact", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "InternalError" + } + ] }, "set_expiration": { "request": "SetExpirationRequest", "response": "SendResponse", - "doc": "Set the message expiration timer for a thread. Expiration must be specified in seconds, set to 0 to disable timer" + "doc": "Set the message expiration timer for a thread. Expiration must be specified in seconds, set to 0 to disable timer", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "UnknownGroupError" + }, + { + "name": "GroupVerificationError" + } + ] }, "set_device_name": { "request": "SetDeviceNameRequest", - "doc": "set this device's name. This will show up on the mobile device on the same account under " + "doc": "set this device's name. This will show up on the mobile device on the same account under ", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + } + ] }, "get_all_identities": { "request": "GetAllIdentities", "response": "AllIdentityKeyList", - "doc": "get all known identity keys" + "doc": "get all known identity keys", + "errors": [ + { + "name": "InvalidProxyError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InternalError" + } + ] }, "subscribe": { "request": "SubscribeRequest", - "doc": "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." + "doc": "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.", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "InternalError" + } + ] }, "unsubscribe": { "request": "UnsubscribeRequest", - "doc": "See subscribe for more info" + "doc": "See subscribe for more info", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "InternalError" + } + ] }, "remote_delete": { "request": "RemoteDeleteRequest", "response": "SendResponse", - "doc": "delete a message previously sent" + "doc": "delete a message previously sent", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "InvalidRecipientError" + }, + { + "name": "NoSendPermissionError" + }, + { + "name": "UnknownGroupError" + } + ] }, "add_server": { "request": "AddServerRequest", "response": "String", - "doc": "add a new server to connect to. Returns the new server's UUID." + "doc": "add a new server to connect to. Returns the new server's UUID.", + "errors": [ + { + "name": "InvalidProxyError" + }, + { + "name": "InternalError" + } + ] }, "get_servers": { "request": "GetServersRequest", - "response": "ServerList" + "response": "ServerList", + "errors": [ + { + "name": "InternalError" + } + ] }, "delete_server": { - "request": "RemoveServerRequest" + "request": "RemoveServerRequest", + "errors": [ + { + "name": "InternalError" + } + ] }, "send_payment": { "request": "SendPaymentRequest", "response": "SendResponse", - "doc": "send a mobilecoin payment" + "doc": "send a mobilecoin payment", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + }, + { + "name": "InvalidBase64Error" + }, + { + "name": "InvalidRecipientError" + }, + { + "name": "UnknownGroupError" + }, + { + "name": "NoSendPermissionError" + } + ] }, "get_remote_config": { "request": "RemoteConfigRequest", "response": "RemoteConfigList", - "doc": "Retrieves the remote config (feature flags) from the server." + "doc": "Retrieves the remote config (feature flags) from the server.", + "errors": [ + { + "name": "InternalError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "NoSuchAccountError" + } + ] + }, + "refuse_membership": { + "request": "RefuseMembershipRequest", + "response": "JsonGroupV2Info", + "doc": "deny a request to join a group", + "errors": [ + { + "name": "NoSuchAccountError" + }, + { + "name": "ServerNotFoundError" + }, + { + "name": "InvalidProxyError" + }, + { + "name": "UnknownGroupError" + }, + { + "name": "GroupVerificationError" + }, + { + "name": "InternalError" + } + ] } } } diff --git a/signald/client-protocol/protocol.go b/signald/client-protocol/protocol.go new file mode 100644 index 0000000..d354c53 --- /dev/null +++ b/signald/client-protocol/protocol.go @@ -0,0 +1,13 @@ +package client_protocol + +import ( + "encoding/json" +) + +type BasicResponse struct { + ID string + Type string + ErrorType string + Error json.RawMessage + Data json.RawMessage +} diff --git a/signald/client-protocol/v0/structs.go b/signald/client-protocol/v0/structs.go index 481fc76..92de9ec 100644 --- a/signald/client-protocol/v0/structs.go +++ b/signald/client-protocol/v0/structs.go @@ -158,7 +158,7 @@ type JsonMessageEnvelope struct { HasLegacyMessage bool `json:"hasLegacyMessage,omitempty" yaml:"hasLegacyMessage,omitempty"` IsUnidentifiedSender bool `json:"isUnidentifiedSender,omitempty" yaml:"isUnidentifiedSender,omitempty"` Receipt *JsonReceiptMessage `json:"receipt,omitempty" yaml:"receipt,omitempty"` - Relay string `json:"relay,omitempty" yaml:"relay,omitempty"` + Relay string `json:"relay,omitempty" yaml:"relay,omitempty"` // this field is no longer available and will never be populated ServerDeliveredTimestamp int64 `json:"serverDeliveredTimestamp,omitempty" yaml:"serverDeliveredTimestamp,omitempty"` ServerTimestamp int64 `json:"serverTimestamp,omitempty" yaml:"serverTimestamp,omitempty"` Source *JsonAddress `json:"source,omitempty" yaml:"source,omitempty"` @@ -307,11 +307,5 @@ type SharedContact struct { Phone *Optional `json:"phone,omitempty" yaml:"phone,omitempty"` } -type Success struct { - Duration int64 `json:"duration,omitempty" yaml:"duration,omitempty"` - NeedsSync bool `json:"needsSync,omitempty" yaml:"needsSync,omitempty"` - Unidentified bool `json:"unidentified,omitempty" yaml:"unidentified,omitempty"` -} - type Type struct { } diff --git a/signald/client-protocol/v1/errors.go b/signald/client-protocol/v1/errors.go new file mode 100644 index 0000000..9778bd6 --- /dev/null +++ b/signald/client-protocol/v1/errors.go @@ -0,0 +1,462 @@ +package v1 + +import ( + "encoding/json" + "fmt" + + client_protocol "gitlab.com/signald/signald-go/signald/client-protocol" +) + +// DO NOT EDIT: this file is automatically generated by ./tools/generator in this repo + +func mkerr(response client_protocol.BasicResponse) error { + switch response.ErrorType { + case "AccountAlreadyVerifiedError": + result := AccountAlreadyVerifiedError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "AccountHasNoKeysError": + result := AccountHasNoKeysError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "AccountLockedError": + result := AccountLockedError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "CaptchaRequiredError": + result := CaptchaRequiredError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "FingerprintVersionMismatchError": + result := FingerprintVersionMismatchError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "GroupLinkNotActiveError": + result := GroupLinkNotActiveError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "GroupNotActiveError": + result := GroupNotActiveError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "GroupVerificationError": + result := GroupVerificationError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "InternalError": + result := InternalError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "InvalidAttachmentError": + result := InvalidAttachmentError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "InvalidBase64Error": + result := InvalidBase64Error{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "InvalidFingerprintError": + result := InvalidFingerprintError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "InvalidGroupError": + result := InvalidGroupError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "InvalidGroupStateError": + result := InvalidGroupStateError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "InvalidInviteURIError": + result := InvalidInviteURIError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "InvalidProxyError": + result := InvalidProxyError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "InvalidRecipientError": + result := InvalidRecipientError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "InvalidRequestError": + result := InvalidRequestError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "NoKnownUUIDError": + result := NoKnownUUIDError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "NoSendPermissionError": + result := NoSendPermissionError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "NoSuchAccountError": + result := NoSuchAccountError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "NoSuchSessionError": + result := NoSuchSessionError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "OwnProfileKeyDoesNotExistError": + result := OwnProfileKeyDoesNotExistError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "ProfileUnavailableError": + result := ProfileUnavailableError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "ServerNotFoundError": + result := ServerNotFoundError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "UnknownGroupError": + result := UnknownGroupError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "UnknownIdentityKeyError": + result := UnknownIdentityKeyError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "UntrustedIdentityError": + result := UntrustedIdentityError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + case "UserAlreadyExistsError": + result := UserAlreadyExistsError{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result + default: + return fmt.Errorf("unexpected response type from signald: %s: %s", response.ErrorType, string(response.Error)) + } +} + +type AccountAlreadyVerifiedError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e AccountAlreadyVerifiedError) Error() string { + return e.Message +} + +type AccountHasNoKeysError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e AccountHasNoKeysError) Error() string { + return e.Message +} + +type AccountLockedError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` + More string `json:"more,omitempty" yaml:"more,omitempty"` +} + +func (e AccountLockedError) Error() string { + return e.Message +} + +type CaptchaRequiredError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` + More string `json:"more,omitempty" yaml:"more,omitempty"` +} + +func (e CaptchaRequiredError) Error() string { + return e.Message +} + +type FingerprintVersionMismatchError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e FingerprintVersionMismatchError) Error() string { + return e.Message +} + +type GroupLinkNotActiveError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e GroupLinkNotActiveError) Error() string { + return e.Message +} + +type GroupNotActiveError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e GroupNotActiveError) Error() string { + return e.Message +} + +type GroupVerificationError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e GroupVerificationError) Error() string { + return e.Message +} + +// InternalError: an internal error in signald has occured. +type InternalError struct { + Exceptions []string `json:"exceptions,omitempty" yaml:"exceptions,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e InternalError) Error() string { + return e.Message +} + +type InvalidAttachmentError struct { + Filename string `json:"filename,omitempty" yaml:"filename,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e InvalidAttachmentError) Error() string { + return e.Message +} + +type InvalidBase64Error struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e InvalidBase64Error) Error() string { + return e.Message +} + +type InvalidFingerprintError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e InvalidFingerprintError) Error() string { + return e.Message +} + +type InvalidGroupError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e InvalidGroupError) Error() string { + return e.Message +} + +type InvalidGroupStateError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e InvalidGroupStateError) Error() string { + return e.Message +} + +type InvalidInviteURIError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e InvalidInviteURIError) Error() string { + return e.Message +} + +type InvalidProxyError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e InvalidProxyError) Error() string { + return e.Message +} + +type InvalidRecipientError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e InvalidRecipientError) Error() string { + return e.Message +} + +type InvalidRequestError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e InvalidRequestError) Error() string { + return e.Message +} + +type NoKnownUUIDError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e NoKnownUUIDError) Error() string { + return e.Message +} + +type NoSendPermissionError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e NoSendPermissionError) Error() string { + return e.Message +} + +type NoSuchAccountError struct { + Account string `json:"account,omitempty" yaml:"account,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e NoSuchAccountError) Error() string { + return e.Message +} + +type NoSuchSessionError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e NoSuchSessionError) Error() string { + return e.Message +} + +type OwnProfileKeyDoesNotExistError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e OwnProfileKeyDoesNotExistError) Error() string { + return e.Message +} + +type ProfileUnavailableError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e ProfileUnavailableError) Error() string { + return e.Message +} + +type ServerNotFoundError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` + UUID string `json:"uuid,omitempty" yaml:"uuid,omitempty"` +} + +func (e ServerNotFoundError) Error() string { + return e.Message +} + +type UnknownGroupError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e UnknownGroupError) Error() string { + return e.Message +} + +type UnknownIdentityKeyError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e UnknownIdentityKeyError) Error() string { + return e.Message +} + +type UntrustedIdentityError struct { + Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` + IdentityKey **IdentityKey `json:"identity_key,omitempty" yaml:"identity_key,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` +} + +func (e UntrustedIdentityError) Error() string { + return e.Message +} + +type UserAlreadyExistsError struct { + Message string `json:"message,omitempty" yaml:"message,omitempty"` + UUID string `json:"uuid,omitempty" yaml:"uuid,omitempty"` +} + +func (e UserAlreadyExistsError) Error() string { + return e.Message +} diff --git a/signald/client-protocol/v1/requests.go b/signald/client-protocol/v1/requests.go index e8de4d3..8d6f5b8 100644 --- a/signald/client-protocol/v1/requests.go +++ b/signald/client-protocol/v1/requests.go @@ -4,7 +4,6 @@ package v1 import ( "encoding/json" - "fmt" "log" "gitlab.com/signald/signald-go/signald" @@ -28,7 +27,7 @@ func (r *AcceptInvitationRequest) Submit(conn *signald.Signald) (response JsonGr rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -61,7 +60,7 @@ func (r *AddLinkedDeviceRequest) Submit(conn *signald.Signald) (err error) { rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -87,7 +86,7 @@ func (r *AddServerRequest) Submit(conn *signald.Signald) (response string, err e rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -120,7 +119,7 @@ func (r *ApproveMembershipRequest) Submit(conn *signald.Signald) (response JsonG rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -152,7 +151,7 @@ func (r *CreateGroupRequest) Submit(conn *signald.Signald) (response JsonGroupV2 rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -185,7 +184,7 @@ func (r *DeleteAccountRequest) Submit(conn *signald.Signald) (err error) { rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -210,7 +209,7 @@ func (r *RemoveServerRequest) Submit(conn *signald.Signald) (err error) { rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -236,7 +235,7 @@ func (r *FinishLinkRequest) Submit(conn *signald.Signald) (response Account, err rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -269,7 +268,7 @@ func (r *GenerateLinkingURIRequest) Submit(conn *signald.Signald) (response Link rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -302,7 +301,7 @@ func (r *GetAllIdentities) Submit(conn *signald.Signald) (response AllIdentityKe rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -317,7 +316,7 @@ func (r *GetAllIdentities) Submit(conn *signald.Signald) (response AllIdentityKe } -// Submit: Query the server for the latest state of a known group. If no account in signald is a member of the group (anymore), an error with error_type: 'UnknownGroupException' is returned. +// Submit: Query the server for the latest state of a known group. If no account in signald is a member of the group (anymore), an error with error_type: 'UnknownGroupError' is returned. func (r *GetGroupRequest) Submit(conn *signald.Signald) (response JsonGroupV2Info, err error) { r.Version = "v1" r.Type = "get_group" @@ -335,7 +334,7 @@ func (r *GetGroupRequest) Submit(conn *signald.Signald) (response JsonGroupV2Inf rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -368,7 +367,7 @@ func (r *GetIdentitiesRequest) Submit(conn *signald.Signald) (response IdentityK rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -401,7 +400,7 @@ func (r *GetLinkedDevicesRequest) Submit(conn *signald.Signald) (response Linked rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -434,7 +433,7 @@ func (r *GetProfileRequest) Submit(conn *signald.Signald) (response Profile, err rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -467,7 +466,7 @@ func (r *RemoteConfigRequest) Submit(conn *signald.Signald) (response RemoteConf rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -499,7 +498,7 @@ func (r *GetServersRequest) Submit(conn *signald.Signald) (response ServerList, rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -532,7 +531,7 @@ func (r *GroupLinkInfoRequest) Submit(conn *signald.Signald) (response JsonGroup rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -565,7 +564,7 @@ func (r *JoinGroupRequest) Submit(conn *signald.Signald) (response JsonGroupJoin rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -597,7 +596,7 @@ func (r *LeaveGroupRequest) Submit(conn *signald.Signald) (response GroupInfo, e rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -630,7 +629,7 @@ func (r *ListAccountsRequest) Submit(conn *signald.Signald) (response AccountLis rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -662,7 +661,7 @@ func (r *ListContactsRequest) Submit(conn *signald.Signald) (response ProfileLis rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -694,7 +693,7 @@ func (r *ListGroupsRequest) Submit(conn *signald.Signald) (response GroupList, e rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -726,7 +725,7 @@ func (r *MarkReadRequest) Submit(conn *signald.Signald) (err error) { rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -752,7 +751,40 @@ func (r *ReactRequest) Submit(conn *signald.Signald) (response SendResponse, err rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) + 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: deny a request to join a group +func (r *RefuseMembershipRequest) Submit(conn *signald.Signald) (response JsonGroupV2Info, err error) { + r.Version = "v1" + r.Type = "refuse_membership" + 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 } @@ -785,7 +817,7 @@ func (r *RegisterRequest) Submit(conn *signald.Signald) (response Account, err e rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -818,7 +850,7 @@ func (r *RemoteDeleteRequest) Submit(conn *signald.Signald) (response SendRespon rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -851,7 +883,7 @@ func (r *RemoveLinkedDeviceRequest) Submit(conn *signald.Signald) (err error) { rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -877,7 +909,7 @@ func (r *RequestSyncRequest) Submit(conn *signald.Signald) (err error) { rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -903,7 +935,7 @@ func (r *ResetSessionRequest) Submit(conn *signald.Signald) (response SendRespon rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -918,7 +950,7 @@ func (r *ResetSessionRequest) Submit(conn *signald.Signald) (response SendRespon } -// Submit: Resolve a partial JsonAddress with only a number or UUID to one with both. Anywhere that signald accepts a JsonAddress will except a partial, this is a convenience function for client authors, mostly because signald doesn't resolve all the partials it returns +// Submit: Resolve a partial JsonAddress with only a number or UUID to one with both. Anywhere that signald accepts a JsonAddress will except a partial, this is a convenience function for client authors, mostly because signald doesn't resolve all the partials it returns. func (r *ResolveAddressRequest) Submit(conn *signald.Signald) (response JsonAddress, err error) { r.Version = "v1" r.Type = "resolve_address" @@ -936,7 +968,7 @@ func (r *ResolveAddressRequest) Submit(conn *signald.Signald) (response JsonAddr rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -968,7 +1000,7 @@ func (r *SendRequest) Submit(conn *signald.Signald) (response SendResponse, err rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -1001,7 +1033,7 @@ func (r *SendPaymentRequest) Submit(conn *signald.Signald) (response SendRespons rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -1034,7 +1066,7 @@ func (r *SetDeviceNameRequest) Submit(conn *signald.Signald) (err error) { rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -1060,7 +1092,7 @@ func (r *SetExpirationRequest) Submit(conn *signald.Signald) (response SendRespo rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -1092,7 +1124,7 @@ func (r *SetProfile) Submit(conn *signald.Signald) (err error) { rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -1118,7 +1150,7 @@ func (r *SubscribeRequest) Submit(conn *signald.Signald) (err error) { rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -1144,7 +1176,7 @@ func (r *TrustRequest) Submit(conn *signald.Signald) (err error) { rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -1170,7 +1202,7 @@ func (r *TypingRequest) Submit(conn *signald.Signald) (err error) { rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -1196,7 +1228,7 @@ func (r *UnsubscribeRequest) Submit(conn *signald.Signald) (err error) { rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -1222,7 +1254,7 @@ func (r *UpdateContactRequest) Submit(conn *signald.Signald) (response Profile, rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -1255,7 +1287,7 @@ func (r *UpdateGroupRequest) Submit(conn *signald.Signald) (response GroupInfo, rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -1288,7 +1320,7 @@ func (r *VerifyRequest) Submit(conn *signald.Signald) (response Account, err err rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } @@ -1320,7 +1352,7 @@ func (r *VersionRequest) Submit(conn *signald.Signald) (response JsonVersionMess rawResponse := <-responseChannel if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) + err = mkerr(rawResponse) return } diff --git a/signald/client-protocol/v1/structs.go b/signald/client-protocol/v1/structs.go index 726126c..dd8a1c2 100644 --- a/signald/client-protocol/v1/structs.go +++ b/signald/client-protocol/v1/structs.go @@ -24,6 +24,7 @@ type Account struct { AccountId string `json:"account_id,omitempty" yaml:"account_id,omitempty"` // The primary identifier on the account, included with all requests to signald for this account. Previously called 'username' Address *JsonAddress `json:"address,omitempty" yaml:"address,omitempty"` // The address of this account 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. + Pending bool `json:"pending,omitempty" yaml:"pending,omitempty"` // indicates the account has not completed registration } type AccountList struct { @@ -132,7 +133,7 @@ type GetAllIdentities struct { Account string `json:"account,omitempty" yaml:"account,omitempty"` // The account to interact with } -// GetGroupRequest: Query the server for the latest state of a known group. If no account in signald is a member of the group (anymore), an error with error_type: 'UnknownGroupException' is returned. +// GetGroupRequest: Query the server for the latest state of a known group. If no account in signald is a member of the group (anymore), an error with error_type: 'UnknownGroupError' is returned. type GetGroupRequest struct { Request Account string `json:"account,omitempty" yaml:"account,omitempty"` // The account to interact with @@ -254,30 +255,47 @@ type JsonAddress struct { UUID string `json:"uuid,omitempty" yaml:"uuid,omitempty"` // A UUID, the unique identifier for a particular Signal account. } +// JsonAttachment: represents a file attached to a message. When seding, only `filename` is required. +type JsonAttachment struct { + Blurhash string `json:"blurhash,omitempty" yaml:"blurhash,omitempty"` + Caption string `json:"caption,omitempty" yaml:"caption,omitempty"` + ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` + CustomFilename string `json:"customFilename,omitempty" yaml:"customFilename,omitempty"` // the original name of the file + Digest string `json:"digest,omitempty" yaml:"digest,omitempty"` + Filename string `json:"filename,omitempty" yaml:"filename,omitempty"` // when sending, the path to the local file to upload + Height int32 `json:"height,omitempty" yaml:"height,omitempty"` + ID string `json:"id,omitempty" yaml:"id,omitempty"` + Key string `json:"key,omitempty" yaml:"key,omitempty"` + Size int32 `json:"size,omitempty" yaml:"size,omitempty"` + StoredFilename string `json:"storedFilename,omitempty" yaml:"storedFilename,omitempty"` // when receiving, the path that file has been downloaded to + VoiceNote bool `json:"voiceNote,omitempty" yaml:"voiceNote,omitempty"` + Width int32 `json:"width,omitempty" yaml:"width,omitempty"` +} + type JsonBlockedListMessage struct { Addresses []*JsonAddress `json:"addresses,omitempty" yaml:"addresses,omitempty"` GroupIds []string `json:"groupIds,omitempty" yaml:"groupIds,omitempty"` } type JsonDataMessage struct { - Attachments []*v0.JsonAttachment `json:"attachments,omitempty" yaml:"attachments,omitempty"` // files attached to the incoming message - Body string `json:"body,omitempty" yaml:"body,omitempty"` // the text body of the incoming message. - Contacts []*v0.SharedContact `json:"contacts,omitempty" yaml:"contacts,omitempty"` // if the incoming message has a shared contact, the contact's information will be here - EndSession bool `json:"endSession,omitempty" yaml:"endSession,omitempty"` - ExpiresInSeconds int32 `json:"expiresInSeconds,omitempty" yaml:"expiresInSeconds,omitempty"` // the expiry timer on the incoming message. Clients should delete records of the message within this number of seconds - Group *JsonGroupInfo `json:"group,omitempty" yaml:"group,omitempty"` // if the incoming message was sent to a v1 group, information about that group will be here - GroupV2 *JsonGroupV2Info `json:"groupV2,omitempty" yaml:"groupV2,omitempty"` // if the incoming message was sent to a v2 group, basic identifying information about that group will be here. If group information changes, JsonGroupV2Info.revision is incremented. If the group revision is higher than previously seen, a client can retrieve the group information by calling get_group. - GroupCallUpdate string `json:"group_call_update,omitempty" yaml:"group_call_update,omitempty"` // the eraId string from a group call message update - Mentions []*JsonMention `json:"mentions,omitempty" yaml:"mentions,omitempty"` // list of mentions in the message - Payment *Payment `json:"payment,omitempty" yaml:"payment,omitempty"` // details about the MobileCoin payment attached to the message, if present - Previews []*v0.JsonPreview `json:"previews,omitempty" yaml:"previews,omitempty"` // if the incoming message has a link preview, information about that preview will be here - ProfileKeyUpdate bool `json:"profileKeyUpdate,omitempty" yaml:"profileKeyUpdate,omitempty"` - Quote *JsonQuote `json:"quote,omitempty" yaml:"quote,omitempty"` // if the incoming message is a quote or reply to another message, this will contain information about that message - Reaction *JsonReaction `json:"reaction,omitempty" yaml:"reaction,omitempty"` // if the message adds or removes a reaction to another message, this will indicate what change is being made - RemoteDelete *RemoteDelete `json:"remoteDelete,omitempty" yaml:"remoteDelete,omitempty"` // if the inbound message is deleting a previously sent message, indicates which message should be deleted - Sticker *v0.JsonSticker `json:"sticker,omitempty" yaml:"sticker,omitempty"` // if the incoming message is a sticker, information about the sicker will be here - Timestamp int64 `json:"timestamp,omitempty" yaml:"timestamp,omitempty"` // 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. - ViewOnce bool `json:"viewOnce,omitempty" yaml:"viewOnce,omitempty"` // indicates the message is a view once message. View once messages typically include no body and a single image attachment. Official Signal clients will prevent the user from saving the image, and once the user has viewed the image once they will destroy the image. + Attachments []*JsonAttachment `json:"attachments,omitempty" yaml:"attachments,omitempty"` // files attached to the incoming message + Body string `json:"body,omitempty" yaml:"body,omitempty"` // the text body of the incoming message. + Contacts []*v0.SharedContact `json:"contacts,omitempty" yaml:"contacts,omitempty"` // if the incoming message has a shared contact, the contact's information will be here + EndSession bool `json:"endSession,omitempty" yaml:"endSession,omitempty"` + ExpiresInSeconds int32 `json:"expiresInSeconds,omitempty" yaml:"expiresInSeconds,omitempty"` // the expiry timer on the incoming message. Clients should delete records of the message within this number of seconds + Group *JsonGroupInfo `json:"group,omitempty" yaml:"group,omitempty"` // if the incoming message was sent to a v1 group, information about that group will be here + GroupV2 *JsonGroupV2Info `json:"groupV2,omitempty" yaml:"groupV2,omitempty"` // if the incoming message was sent to a v2 group, basic identifying information about that group will be here. If group information changes, JsonGroupV2Info.revision is incremented. If the group revision is higher than previously seen, a client can retrieve the group information by calling get_group. + GroupCallUpdate string `json:"group_call_update,omitempty" yaml:"group_call_update,omitempty"` // the eraId string from a group call message update + Mentions []*JsonMention `json:"mentions,omitempty" yaml:"mentions,omitempty"` // list of mentions in the message + Payment *Payment `json:"payment,omitempty" yaml:"payment,omitempty"` // details about the MobileCoin payment attached to the message, if present + Previews []*JsonPreview `json:"previews,omitempty" yaml:"previews,omitempty"` // if the incoming message has a link preview, information about that preview will be here + ProfileKeyUpdate bool `json:"profileKeyUpdate,omitempty" yaml:"profileKeyUpdate,omitempty"` + Quote *JsonQuote `json:"quote,omitempty" yaml:"quote,omitempty"` // if the incoming message is a quote or reply to another message, this will contain information about that message + Reaction *JsonReaction `json:"reaction,omitempty" yaml:"reaction,omitempty"` // if the message adds or removes a reaction to another message, this will indicate what change is being made + RemoteDelete *RemoteDelete `json:"remoteDelete,omitempty" yaml:"remoteDelete,omitempty"` // if the inbound message is deleting a previously sent message, indicates which message should be deleted + Sticker *v0.JsonSticker `json:"sticker,omitempty" yaml:"sticker,omitempty"` // if the incoming message is a sticker, information about the sicker will be here + Timestamp int64 `json:"timestamp,omitempty" yaml:"timestamp,omitempty"` // 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. + ViewOnce bool `json:"viewOnce,omitempty" yaml:"viewOnce,omitempty"` // indicates the message is a view once message. View once messages typically include no body and a single image attachment. Official Signal clients will prevent the user from saving the image, and once the user has viewed the image once they will destroy the image. } // JsonGroupInfo: information about a legacy group @@ -302,6 +320,7 @@ type JsonGroupJoinInfo struct { // JsonGroupV2Info: Information about a Signal group type JsonGroupV2Info struct { AccessControl *GroupAccessControl `json:"accessControl,omitempty" yaml:"accessControl,omitempty"` // current access control settings for this group + Announcements string `json:"announcements,omitempty" yaml:"announcements,omitempty"` // indicates if the group is an announcements group. Only admins are allowed to send messages to announcements groups. Options are UNKNOWN, ENABLED or DISABLED Avatar string `json:"avatar,omitempty" yaml:"avatar,omitempty"` // path to the group's avatar on local disk, if available Description string `json:"description,omitempty" yaml:"description,omitempty"` ID string `json:"id,omitempty" yaml:"id,omitempty"` @@ -349,6 +368,15 @@ type JsonMessageRequestResponseMessage struct { Type string `json:"type,omitempty" yaml:"type,omitempty"` } +// JsonPreview: metadata about one of the links in a message +type JsonPreview struct { + Attachment *JsonAttachment `json:"attachment,omitempty" yaml:"attachment,omitempty"` // an optional image file attached to the preview + Date int64 `json:"date,omitempty" yaml:"date,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Url string `json:"url,omitempty" yaml:"url,omitempty"` +} + // JsonQuote: A quote is a reply to a previous message. ID is the sent time of the message being replied to type JsonQuote struct { Attachments []*v0.JsonQuotedAttachment `json:"attachments,omitempty" yaml:"attachments,omitempty"` // list of files attached to the quoted message @@ -374,7 +402,7 @@ 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 *v0.Success `json:"success,omitempty" yaml:"success,omitempty"` + Success *SendSuccess `json:"success,omitempty" yaml:"success,omitempty"` UnregisteredFailure bool `json:"unregisteredFailure,omitempty" yaml:"unregisteredFailure,omitempty"` } @@ -390,10 +418,10 @@ type JsonSentTranscriptMessage struct { type JsonSyncMessage struct { BlockedList *JsonBlockedListMessage `json:"blockedList,omitempty" yaml:"blockedList,omitempty"` Configuration *v0.ConfigurationMessage `json:"configuration,omitempty" yaml:"configuration,omitempty"` - Contacts *v0.JsonAttachment `json:"contacts,omitempty" yaml:"contacts,omitempty"` + Contacts *JsonAttachment `json:"contacts,omitempty" yaml:"contacts,omitempty"` ContactsComplete bool `json:"contactsComplete,omitempty" yaml:"contactsComplete,omitempty"` FetchType string `json:"fetchType,omitempty" yaml:"fetchType,omitempty"` - Groups *v0.JsonAttachment `json:"groups,omitempty" yaml:"groups,omitempty"` + Groups *JsonAttachment `json:"groups,omitempty" yaml:"groups,omitempty"` MessageRequestResponse *JsonMessageRequestResponseMessage `json:"messageRequestResponse,omitempty" yaml:"messageRequestResponse,omitempty"` ReadMessages []*JsonReadMessage `json:"readMessages,omitempty" yaml:"readMessages,omitempty"` Request string `json:"request,omitempty" yaml:"request,omitempty"` @@ -514,6 +542,14 @@ type ReceiptMessage struct { When int64 `json:"when,omitempty" yaml:"when,omitempty"` } +// RefuseMembershipRequest: deny a request to join a group +type RefuseMembershipRequest struct { + Request + Account string `json:"account,omitempty" yaml:"account,omitempty"` // The account to interact with + GroupId string `json:"group_id,omitempty" yaml:"group_id,omitempty"` + Members []*JsonAddress `json:"members,omitempty" yaml:"members,omitempty"` // list of requesting members to refuse +} + // 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 @@ -582,7 +618,7 @@ type ResetSessionRequest struct { Timestamp int64 `json:"timestamp,omitempty" yaml:"timestamp,omitempty"` } -// ResolveAddressRequest: Resolve a partial JsonAddress with only a number or UUID to one with both. Anywhere that signald accepts a JsonAddress will except a partial, this is a convenience function for client authors, mostly because signald doesn't resolve all the partials it returns +// ResolveAddressRequest: Resolve a partial JsonAddress with only a number or UUID to one with both. Anywhere that signald accepts a JsonAddress will except a partial, this is a convenience function for client authors, mostly because signald doesn't resolve all the partials it returns. type ResolveAddressRequest struct { Request Account string `json:"account,omitempty" yaml:"account,omitempty"` // The signal account to use @@ -603,6 +639,7 @@ type SendRequest struct { Attachments []*v0.JsonAttachment `json:"attachments,omitempty" yaml:"attachments,omitempty"` Mentions []*JsonMention `json:"mentions,omitempty" yaml:"mentions,omitempty"` MessageBody string `json:"messageBody,omitempty" yaml:"messageBody,omitempty"` + Previews []*JsonPreview `json:"previews,omitempty" yaml:"previews,omitempty"` Quote *JsonQuote `json:"quote,omitempty" yaml:"quote,omitempty"` RecipientAddress *JsonAddress `json:"recipientAddress,omitempty" yaml:"recipientAddress,omitempty"` RecipientGroupID string `json:"recipientGroupId,omitempty" yaml:"recipientGroupId,omitempty"` @@ -615,18 +652,30 @@ type SendResponse struct { Timestamp int64 `json:"timestamp,omitempty" yaml:"timestamp,omitempty"` } +type SendSuccess struct { + Devices []int32 `json:"devices,omitempty" yaml:"devices,omitempty"` + Duration int64 `json:"duration,omitempty" yaml:"duration,omitempty"` + NeedsSync bool `json:"needsSync,omitempty" yaml:"needsSync,omitempty"` + Unidentified bool `json:"unidentified,omitempty" yaml:"unidentified,omitempty"` +} + // Server: a Signal server type Server struct { - Ca string `json:"ca,omitempty" yaml:"ca,omitempty"` + Ca string `json:"ca,omitempty" yaml:"ca,omitempty"` // base64 encoded trust store, password must be 'whisper' CdnUrls []*ServerCDN `json:"cdn_urls,omitempty" yaml:"cdn_urls,omitempty"` + CdsMrenclave string `json:"cds_mrenclave,omitempty" yaml:"cds_mrenclave,omitempty"` ContactDiscoveryUrl string `json:"contact_discovery_url,omitempty" yaml:"contact_discovery_url,omitempty"` + IasCa string `json:"ias_ca,omitempty" yaml:"ias_ca,omitempty"` // base64 encoded trust store, password must be 'whisper' + KeyBackupMrenclave string `json:"key_backup_mrenclave,omitempty" yaml:"key_backup_mrenclave,omitempty"` + KeyBackupServiceId string `json:"key_backup_service_id,omitempty" yaml:"key_backup_service_id,omitempty"` // base64 encoded + KeyBackupServiceName string `json:"key_backup_service_name,omitempty" yaml:"key_backup_service_name,omitempty"` KeyBackupUrl string `json:"key_backup_url,omitempty" yaml:"key_backup_url,omitempty"` Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"` ServiceUrl string `json:"service_url,omitempty" yaml:"service_url,omitempty"` StorageUrl string `json:"storage_url,omitempty" yaml:"storage_url,omitempty"` - UnidentifiedSenderRoot string `json:"unidentified_sender_root,omitempty" yaml:"unidentified_sender_root,omitempty"` - UUID string `json:"uuid,omitempty" yaml:"uuid,omitempty"` // A unique identifier for the server, referenced when adding accounts. Must be a valid UUID. Will be generated if not specified when creating. - ZkParam string `json:"zk_param,omitempty" yaml:"zk_param,omitempty"` // base64 encoded ZKGROUP_SERVER_PUBLIC_PARAMS value + UnidentifiedSenderRoot string `json:"unidentified_sender_root,omitempty" yaml:"unidentified_sender_root,omitempty"` // base64 encoded + UUID string `json:"uuid,omitempty" yaml:"uuid,omitempty"` // A unique identifier for the server, referenced when adding accounts. Must be a valid UUID. Will be generated if not specified when creating. + ZkParam string `json:"zk_param,omitempty" yaml:"zk_param,omitempty"` // base64 encoded ZKGROUP_SERVER_PUBLIC_PARAMS value } type ServerCDN struct { @@ -717,6 +766,7 @@ type UpdateGroupRequest struct { Request Account string `json:"account,omitempty" yaml:"account,omitempty"` // The identifier of the account to interact with AddMembers []*JsonAddress `json:"addMembers,omitempty" yaml:"addMembers,omitempty"` + Announcements string `json:"announcements,omitempty" yaml:"announcements,omitempty"` // ENABLED to only allow admins to post messages, DISABLED to allow anyone to post Avatar string `json:"avatar,omitempty" yaml:"avatar,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` // A new group description. Set to empty string to remove an existing description. GroupID string `json:"groupID,omitempty" yaml:"groupID,omitempty"` // the ID of the group to update diff --git a/signald/signald.go b/signald/signald.go index d215dde..c8a4897 100644 --- a/signald/signald.go +++ b/signald/signald.go @@ -17,8 +17,8 @@ package signald import ( "bytes" + "context" "encoding/json" - "fmt" "io" "log" "math/rand" @@ -28,6 +28,7 @@ import ( "strings" "time" + client_protocol "gitlab.com/signald/signald-go/signald/client-protocol" "gitlab.com/signald/signald-go/signald/client-protocol/v0" ) @@ -51,17 +52,10 @@ func init() { // Signald is a connection to a signald instance. type Signald struct { socket net.Conn - listeners map[string]chan BasicResponse + listeners map[string]chan client_protocol.BasicResponse SocketPath string } -type BasicResponse struct { - ID string - Type string - Error json.RawMessage - Data json.RawMessage -} - type UnexpectedError struct { Message string } @@ -91,10 +85,17 @@ func (s *Signald) Connect() error { } func (s *Signald) connect() error { - socket, err := net.Dial("unix", s.SocketPath) + var d net.Dialer + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + addr := net.UnixAddr{Name: s.SocketPath, Net: "unix"} + socket, err := d.DialContext(ctx, "unix", addr.String()) if err != nil { return err } + s.socket = socket return nil } @@ -146,13 +147,13 @@ func (s *Signald) RawRequest(request interface{}) error { return json.NewEncoder(s.socket).Encode(request) } -func (s *Signald) GetResponseListener(requestid string) chan BasicResponse { +func (s *Signald) GetResponseListener(requestid string) chan client_protocol.BasicResponse { if s.listeners == nil { - s.listeners = map[string]chan BasicResponse{} + s.listeners = map[string]chan client_protocol.BasicResponse{} } c, ok := s.listeners[requestid] if !ok { - c = make(chan BasicResponse) + c = make(chan client_protocol.BasicResponse) s.listeners[requestid] = c } return c @@ -167,7 +168,7 @@ func (s *Signald) CloseResponseListener(requestid string) { delete(s.listeners, requestid) } -func (s *Signald) readNext() (b BasicResponse, err error) { +func (s *Signald) readNext() (b client_protocol.BasicResponse, err error) { if debugSignaldIO { buffer := bytes.Buffer{} err = json.NewDecoder(io.TeeReader(s.socket, &buffer)).Decode(&b) @@ -181,10 +182,3 @@ func (s *Signald) readNext() (b BasicResponse, err error) { } return } - -func (b BasicResponse) GetError() error { - if b.Error == nil { - return nil - } - return fmt.Errorf("signald error: %s", string(b.Error)) -} diff --git a/tools/generator/errors.go.tmpl b/tools/generator/errors.go.tmpl new file mode 100644 index 0000000..69e2cdc --- /dev/null +++ b/tools/generator/errors.go.tmpl @@ -0,0 +1,37 @@ +package {{.Version}} + +import ( + "encoding/json" + "fmt" + + client_protocol "gitlab.com/signald/signald-go/signald/client-protocol" +) + +// DO NOT EDIT: this file is automatically generated by ./tools/generator in this repo + +func mkerr(response client_protocol.BasicResponse) error { + switch response.ErrorType { {{ range $structName, $type := .Types }} + case "{{ $structName }}": + result := {{ $structName }}{} + err := json.Unmarshal(response.Error, result) + if err != nil { + return err + } + return result{{ end }} + default: + return fmt.Errorf("unexpected response type from signald: %s: %s", response.ErrorType, string(response.Error)) + } +} + +{{ range $structName, $type := .Types }}{{if ne $type.Doc ""}}// {{$structName}}: {{$type.Doc}}{{end}} +type {{ $structName }} struct { +{{if $type.Request}} Request{{end}} +{{ range $fieldName, $field := $type.Fields }}{{ $field.FieldName }} {{if $field.List}}[]{{end}}{{ $field.Type }} `json:"{{$fieldName}},omitempty" yaml:"{{$fieldName}},omitempty"`{{if ne $field.Doc ""}} // {{$field.Doc}}{{end}} +{{ end }} +} + +func (e {{ $structName }}) Error() string { + return e.Message +} + +{{ end }} diff --git a/tools/generator/main.go b/tools/generator/main.go index 67503e4..a68a78a 100644 --- a/tools/generator/main.go +++ b/tools/generator/main.go @@ -19,6 +19,7 @@ type Type struct { Fields map[string]*DataType Request bool `json:"-"` Doc string + Error bool } type DataType struct { @@ -76,10 +77,12 @@ func (d *DataType) fixForVersion(field, version string) { } d.Type = response } else { - if d.Version == version || d.Version == "" { - d.Type = fmt.Sprintf("*%s", d.Type) - } else { - d.Type = fmt.Sprintf("*%s.%s", d.Version, d.Type) + if d.Version != "" { + if d.Version == version { + d.Type = fmt.Sprintf("*%s", d.Type) + } else { + d.Type = fmt.Sprintf("*%s.%s", d.Version, d.Type) + } } } @@ -145,7 +148,10 @@ func main() { } for version, types := range response.Types { - inputs := StructsTemplateInput{Version: version} + inputs := StructsTemplateInput{ + Version: version, + Types: make(map[string]*Type), + } for typeName, t := range types { for fieldName, field := range t.Fields { types[typeName].Fields[fieldName].fixForVersion(fieldName, version) @@ -162,8 +168,10 @@ func main() { } } } + if !t.Error { + inputs.Types[typeName] = t + } } - inputs.Types = types outputDir := fmt.Sprintf("signald/client-protocol/%s", version) err = os.MkdirAll(outputDir, os.ModePerm) if err != nil { @@ -185,4 +193,56 @@ func main() { } fmt.Println(outputFilename) } + + // errors + for version, types := range response.Types { + inputs := StructsTemplateInput{ + Version: version, + Types: make(map[string]*Type), + } + for typeName, t := range types { + for fieldName, field := range t.Fields { + types[typeName].Fields[fieldName].fixForVersion(fieldName, version) + if field.Version != "" && field.Version != version { + found := false + for _, v := range inputs.ImportVersions { + if v == field.Version { + found = true + break + } + } + if !found { + inputs.ImportVersions = append(inputs.ImportVersions, field.Version) + } + } + } + if t.Error { + inputs.Types[typeName] = t + } + } + if len(inputs.Types) == 0 { + continue + } + outputDir := fmt.Sprintf("signald/client-protocol/%s", version) + err = os.MkdirAll(outputDir, os.ModePerm) + if err != nil { + log.Fatal("Error creating", outputDir, err) + } + outputFilename := fmt.Sprintf("%s/%s", outputDir, "errors.go") + log.Println("Opening", outputFilename) + f, err := os.Create(outputFilename) + if err != nil { + log.Fatal(err, "\nfailed to open output file ", outputFilename) + } + err = tmpl.ExecuteTemplate(f, "errors.go.tmpl", inputs) + if err != nil { + log.Fatal(err, "\nfailed to render template") + } + err = exec.Command("gofmt", "-w", outputFilename).Run() + if err != nil { + log.Fatal(err, " error running gofmt on ", outputFilename) + } + fmt.Println(outputFilename) + } + } diff --git a/tools/generator/requests.go.tmpl b/tools/generator/requests.go.tmpl index 7f2967f..d706259 100644 --- a/tools/generator/requests.go.tmpl +++ b/tools/generator/requests.go.tmpl @@ -4,7 +4,6 @@ package {{.Version}} import ({{if .Responses}} "encoding/json" - "fmt" "log"{{end}} "gitlab.com/signald/signald-go/signald" @@ -33,10 +32,10 @@ func {{$action.FnName}}(conn *signald.Signald) ({{if ne $action.Response ""}}res defer conn.CloseResponseListener(r.ID) rawResponse := <- responseChannel - if rawResponse.Error != nil { - err = fmt.Errorf("signald error: %s", string(rawResponse.Error)) - return - } + if rawResponse.Error != nil { + err = mkerr(rawResponse) + return + } {{if ne $action.Response ""}} err = json.Unmarshal(rawResponse.Data, &response)