diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 51ab474..a8b6c29 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,25 +2,27 @@ stages: - build lint: - image: nixery.dev/shell/go/golangci-lint + image: nixery.dev/shell/diffutils/go/golangci-lint stage: build before_script: - cp /share/go/bin/go /bin && mkdir /tmp # fix weirdness from nixery image - - mkdir -p /go/src/git.callpipe.com/finn/signald-go - - cp -r * /go/src/git.callpipe.com/finn/signald-go - - cd /go/src/git.callpipe.com/finn/signald-go + - mkdir -p /go/src/src/gitlab.com/signald/signald-go + - cp -r * /go/src/src/gitlab.com/signald/signald-go + - cd /go/src/src/gitlab.com/signald/signald-go script: - golangci-lint run + - go mod tidy + - diff --color=always go.mod "${CI_PROJECT_DIR}/go.mod" + - diff --color=always go.sum "${CI_PROJECT_DIR}/go.sum" build: stage: build image: golang:latest before_script: - - mkdir -p /go/src/git.callpipe.com/finn/signald-go - - cp -r * /go/src/git.callpipe.com/finn/signald-go - - cd /go/src/git.callpipe.com/finn/signald-go + - mkdir -p /go/src/gitlab.com/signald/signald-go + - cp -r * /go/src/gitlab.com/signald/signald-go + - cd /go/src/gitlab.com/signald/signald-go script: - - go get ./... # TODO: Improve how dependencies are handled - go build -o "${CI_PROJECT_DIR}/signald-cli" ./cmd/signald-cli artifacts: paths: diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f95d203 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +signald-cli: signald/client-protocol + go build -o signald-cli ./cmd/signald-cli + +protocol.json: + echo '{"type": "protocol", "version": "v1alpha1"}' | nc -q0 -U /var/run/signald/signald.sock | jq 'select(.type == "protocol").data' > protocol.json + +signald/client-protocol: protocol.json tools/generator/* + go run ./tools/generator < protocol.json + +clean: + rm -rf protocol.json + rm -rf signald/client-protocol/* \ No newline at end of file diff --git a/cmd/signald-cli/cmd/link.go b/cmd/signald-cli/cmd/link.go index 1bc2ee3..2f6c3bd 100644 --- a/cmd/signald-cli/cmd/link.go +++ b/cmd/signald-cli/cmd/link.go @@ -24,7 +24,7 @@ import ( "github.com/mdp/qrterminal" "github.com/spf13/cobra" - "git.callpipe.com/finn/signald-go/signald" + "gitlab.com/signald/signald-go/signald/client-protocol/v0" ) var uriOrQR bool @@ -36,7 +36,7 @@ var linkCmd = &cobra.Command{ Long: `Get a URI or QR code to link to an existing Signal account`, Run: func(cmd *cobra.Command, args []string) { requestID := fmt.Sprint("signald-cli-", rand.Intn(1000)) - err := s.SendRequest(signald.Request{ + err := s.RawRequest(v0.LegacyRequest{ Type: "link", ID: requestID, }) @@ -44,7 +44,7 @@ var linkCmd = &cobra.Command{ log.Fatal("error sending request: ", err) } - c := make(chan signald.Response) + c := make(chan v0.LegacyResponse) go s.Listen(c) for { message := <-c diff --git a/cmd/signald-cli/cmd/listAccounts.go b/cmd/signald-cli/cmd/listAccounts.go index 1b5b5f8..265cf06 100644 --- a/cmd/signald-cli/cmd/listAccounts.go +++ b/cmd/signald-cli/cmd/listAccounts.go @@ -22,7 +22,7 @@ import ( "github.com/spf13/cobra" - "git.callpipe.com/finn/signald-go/signald" + "gitlab.com/signald/signald-go/signald/client-protocol/v0" ) // listAccountsCmd represents the listAccounts command @@ -32,7 +32,7 @@ var listAccountsCmd = &cobra.Command{ Long: `Prints a list of all users to stdout.`, Run: func(cmd *cobra.Command, args []string) { requestID := fmt.Sprint("signald-cli-", rand.Intn(1000)) - err := s.SendRequest(signald.Request{ + err := s.RawRequest(v0.LegacyRequest{ Type: "list_accounts", ID: requestID, }) @@ -40,7 +40,7 @@ var listAccountsCmd = &cobra.Command{ log.Fatal("error sending request: ", err) } - c := make(chan signald.Response) + c := make(chan v0.LegacyResponse) go s.Listen(c) for { message := <-c diff --git a/cmd/signald-cli/cmd/listGroups.go b/cmd/signald-cli/cmd/listGroups.go index 7e6f934..d0c8e5f 100644 --- a/cmd/signald-cli/cmd/listGroups.go +++ b/cmd/signald-cli/cmd/listGroups.go @@ -22,7 +22,7 @@ import ( "github.com/spf13/cobra" - "git.callpipe.com/finn/signald-go/signald" + "gitlab.com/signald/signald-go/signald/client-protocol/v0" ) // listGroupsCmd represents the listGroups command @@ -32,7 +32,7 @@ var listGroupsCmd = &cobra.Command{ Long: `Prints a list of all groups the user is in to stdout.`, Run: func(cmd *cobra.Command, args []string) { requestID := fmt.Sprint("signald-cli-", rand.Intn(1000)) - err := s.SendRequest(signald.Request{ + err := s.RawRequest(v0.LegacyRequest{ Type: "list_groups", Username: username, ID: requestID, @@ -41,7 +41,7 @@ var listGroupsCmd = &cobra.Command{ log.Fatal("error sending request: ", err) } - c := make(chan signald.Response) + c := make(chan v0.LegacyResponse) go s.Listen(c) for { message := <-c diff --git a/cmd/signald-cli/cmd/root.go b/cmd/signald-cli/cmd/root.go index 0d2c569..6dd8b2d 100644 --- a/cmd/signald-cli/cmd/root.go +++ b/cmd/signald-cli/cmd/root.go @@ -23,7 +23,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "git.callpipe.com/finn/signald-go/signald" + "gitlab.com/signald/signald-go/signald" ) var cfgFile string diff --git a/cmd/signald-cli/cmd/send.go b/cmd/signald-cli/cmd/send.go index 9f93bc2..33d964a 100644 --- a/cmd/signald-cli/cmd/send.go +++ b/cmd/signald-cli/cmd/send.go @@ -16,13 +16,14 @@ package cmd import ( + "encoding/json" "log" + "os" "github.com/spf13/cobra" - "time" - "git.callpipe.com/finn/signald-go/signald" - "git.callpipe.com/finn/signald-go/signald/client-protocol/v1" + "gitlab.com/signald/signald-go/signald/client-protocol/v0" + "gitlab.com/signald/signald-go/signald/client-protocol/v1" ) var ( @@ -39,13 +40,10 @@ var sendCmd = &cobra.Command{ Short: "send a message to another user or group", Long: `send a message to another user or group on Signal`, Run: func(cmd *cobra.Command, args []string) { - request := signald.Request{ - Type: "send", - Username: username, - } + request := v1.SendRequest{Username: username} if toUser != "" { - request.RecipientAddress = v1.JsonAddress{Number: toUser} + request.RecipientAddress = &v1.JsonAddress{Number: toUser} } else if toGroup != "" { request.RecipientGroupID = toGroup } else { @@ -57,25 +55,16 @@ var sendCmd = &cobra.Command{ } if attachment != "" { - request.AttachmentFilenames = []string{attachment} + request.Attachments = []*v0.JsonAttachment{{Filename: attachment}} } - err := s.SendRequest(request) + go s.Listen(nil) + response, err := request.Submit(s) if err != nil { - log.Fatal("error sending request: ", err) + log.Fatal("error submitting request to signald: ", err) } - - timeout := 10 - - // Wait for the response - c := make(chan signald.Response) - - go s.Listen(c) - select { - case <-c: - log.Println("Ok.") - case <-time.After(1 * time.Second): - // But timeout after a while - log.Fatalf("Timeout after %d seconds\n", timeout) + err = json.NewEncoder(os.Stdout).Encode(response) + if err != nil { + log.Fatal("error encoding output ", err) } }, } diff --git a/cmd/signald-cli/cmd/version.go b/cmd/signald-cli/cmd/version.go new file mode 100644 index 0000000..542f2a3 --- /dev/null +++ b/cmd/signald-cli/cmd/version.go @@ -0,0 +1,49 @@ +// Copyright © 2018 Finn Herzfeld +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package cmd + +import ( + "encoding/json" + "log" + "os" + + "github.com/spf13/cobra" + + "gitlab.com/signald/signald-go/signald/client-protocol/v1" +) + +// versionCmd represents the version command +var versionCmd = &cobra.Command{ + Use: "version", + Short: "print the signald version", + Long: `print the signald version`, + Run: func(cmd *cobra.Command, args []string) { + go s.Listen(nil) + r := v1.VersionRequest{} + response, err := r.Submit(s) + if err != nil { + log.Fatal(err) + } + err = json.NewEncoder(os.Stdout).Encode(response) + if err != nil { + log.Fatal("error encoding output ", err) + } + }, +} + +func init() { + RootCmd.AddCommand(versionCmd) +} diff --git a/cmd/signald-cli/main.go b/cmd/signald-cli/main.go index 1a197d3..c3d5ba1 100644 --- a/cmd/signald-cli/main.go +++ b/cmd/signald-cli/main.go @@ -15,7 +15,7 @@ package main -import "git.callpipe.com/finn/signald-go/cmd/signald-cli/cmd" +import "gitlab.com/signald/signald-go/cmd/signald-cli/cmd" func main() { cmd.Execute() diff --git a/go.mod b/go.mod index ac735c0..6c0bc1f 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,14 @@ -module git.callpipe.com/finn/signald-go +module gitlab.com/signald/signald-go go 1.14 require ( + github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/mdp/qrterminal v1.0.1 github.com/spf13/cobra v1.0.0 github.com/spf13/viper v1.7.0 + github.com/stretchr/testify v1.4.0 // indirect + golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index 32ffbed..21bc510 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -184,6 +186,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -256,6 +260,9 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -306,6 +313,8 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -315,6 +324,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/protocol.json b/protocol.json new file mode 100644 index 0000000..60d9868 --- /dev/null +++ b/protocol.json @@ -0,0 +1,945 @@ +{ + "doc_version": "v1alpha1", + "version": { + "name": "signald", + "version": "0.10.0+git2020-12-06rcea5cb72.51", + "branch": "refactor-client-request-handling", + "commit": "cea5cb720355afdca460aa30257bdd1317494c7b" + }, + "info": "This document describes objects that may be used when communicating with signald. If this document lacks something you need to generate a client, please open an issue (https://gitlab.com/thefinn93/signald/-/issues/new). This is an initial proposal for the format, and I expect to change it before finalizing it. If it workswell, I hope to slowly move things out of the legacy request types.", + "types": { + "v1": { + "JsonMessageEnvelope": { + "fields": { + "username": { + "type": "String" + }, + "uuid": { + "type": "String" + }, + "source": { + "type": "JsonAddress", + "version": "v1" + }, + "sourceDevice": { + "type": "int" + }, + "type": { + "type": "String" + }, + "relay": { + "type": "String" + }, + "timestamp": { + "type": "long" + }, + "timestampISO": { + "type": "String" + }, + "serverTimestamp": { + "type": "long" + }, + "serverDeliveredTimestamp": { + "type": "long" + }, + "hasLegacyMessage": { + "type": "boolean" + }, + "hasContent": { + "type": "boolean" + }, + "isUnidentifiedSender": { + "type": "boolean" + }, + "dataMessage": { + "type": "JsonDataMessage", + "version": "v1" + }, + "syncMessage": { + "type": "JsonSyncMessage", + "version": "v1" + }, + "callMessage": { + "type": "JsonCallMessage", + "version": "v0" + }, + "receipt": { + "type": "JsonReceiptMessage", + "version": "v0" + }, + "typing": { + "type": "JsonTypingMessage", + "version": "v0" + } + } + }, + "SendRequest": { + "fields": { + "username": { + "type": "String" + }, + "recipientAddress": { + "type": "JsonAddress", + "version": "v1" + }, + "recipientGroupId": { + "type": "String" + }, + "messageBody": { + "type": "String" + }, + "attachments": { + "list": true, + "type": "JsonAttachment", + "version": "v0" + }, + "quote": { + "type": "JsonQuote", + "version": "v1" + }, + "timestamp": { + "type": "Long" + }, + "mentions": { + "list": true, + "type": "JsonMention", + "version": "v1" + } + } + }, + "SendResponse": { + "fields": { + "results": { + "list": true, + "type": "JsonSendMessageResult", + "version": "v1" + }, + "timestamp": { + "type": "long" + } + } + }, + "ReactRequest": { + "fields": { + "username": { + "type": "String" + }, + "recipientAddress": { + "type": "JsonAddress", + "version": "v1" + }, + "recipientGroupId": { + "type": "String" + }, + "reaction": { + "type": "JsonReaction", + "version": "v1" + }, + "timestamp": { + "type": "long" + } + }, + "doc": "react to a previous message" + }, + "VersionRequest": { + "fields": {} + }, + "JsonVersionMessage": { + "fields": { + "name": { + "type": "String" + }, + "version": { + "type": "String" + }, + "branch": { + "type": "String" + }, + "commit": { + "type": "String" + } + } + }, + "JsonAddress": { + "fields": { + "number": { + "type": "String", + "doc": "An e164 phone number, starting with +. Currently the only available user-facing Signal identifier." + }, + "uuid": { + "type": "UUID", + "doc": "A UUID, the unique identifier for a particular Signal account." + }, + "relay": { + "type": "String" + } + } + }, + "JsonDataMessage": { + "fields": { + "timestamp": { + "type": "long", + "doc": "the (unix) 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." + }, + "attachments": { + "list": true, + "type": "JsonAttachment", + "version": "v0", + "doc": "files attached to the incoming message" + }, + "body": { + "type": "String", + "doc": "the text body of the incoming message." + }, + "group": { + "type": "JsonGroupInfo", + "version": "v1", + "doc": "if the incoming message was sent to a v1 group, information about that group will be here" + }, + "groupV2": { + "type": "JsonGroupV2Info", + "version": "v1", + "doc": "is the incoming message was sent to a v2 group, basic identifying information about that group will be here. For full information, use list_groups" + }, + "endSession": { + "type": "boolean" + }, + "expiresInSeconds": { + "type": "int", + "doc": "the expiry timer on the incoming message. Clients should delete records of the message within this number of seconds" + }, + "profileKeyUpdate": { + "type": "boolean" + }, + "quote": { + "type": "JsonQuote", + "version": "v1", + "doc": "if the incoming message is a quote or reply to another message, this will contain information about that message" + }, + "contacts": { + "list": true, + "type": "SharedContact", + "version": "v0", + "doc": "if the incoming message has a shared contact, the contact's information will be here" + }, + "previews": { + "list": true, + "type": "JsonPreview", + "version": "v0", + "doc": "if the incoming message has a link preview, information about that preview will be here" + }, + "sticker": { + "type": "JsonSticker", + "version": "v0", + "doc": "if the incoming message is a sticker, information about the sicker will be here" + }, + "viewOnce": { + "type": "boolean", + "doc": "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." + }, + "reaction": { + "type": "JsonReaction", + "version": "v1", + "doc": "if the message adds or removes a reaction to another message, this will indicate what change is being made" + }, + "remoteDelete": { + "type": "RemoteDelete", + "version": "v0", + "doc": "if the inbound message is deleting a previously sent message, indicates which message should be deleted" + } + } + }, + "JsonSyncMessage": { + "fields": { + "sent": { + "type": "JsonSentTranscriptMessage", + "version": "v1" + }, + "contacts": { + "type": "JsonAttachment", + "version": "v0" + }, + "contactsComplete": { + "type": "boolean" + }, + "groups": { + "type": "JsonAttachment", + "version": "v0" + }, + "blockedList": { + "type": "JsonBlockedListMessage", + "version": "v1" + }, + "request": { + "type": "String" + }, + "readMessages": { + "list": true, + "type": "JsonReadMessage", + "version": "v1" + }, + "viewOnceOpen": { + "type": "JsonViewOnceOpenMessage", + "version": "v1" + }, + "verified": { + "type": "JsonVerifiedMessage", + "version": "v1" + }, + "configuration": { + "type": "ConfigurationMessage", + "version": "v0" + }, + "stickerPackOperations": { + "list": true, + "type": "JsonStickerPackOperationMessage", + "version": "v0" + }, + "fetchType": { + "type": "String" + }, + "messageRequestResponse": { + "type": "JsonMessageRequestResponseMessage", + "version": "v1" + } + } + }, + "JsonQuote": { + "fields": { + "id": { + "type": "long" + }, + "author": { + "type": "JsonAddress", + "version": "v1" + }, + "text": { + "type": "String" + }, + "attachments": { + "list": true, + "type": "JsonQuotedAttachment", + "version": "v0" + }, + "mentions": { + "list": true, + "type": "Mention", + "version": "v0" + } + }, + "doc": "A quote is a reply to a previous message. ID is the sent time of the message being replied to" + }, + "JsonMention": { + "fields": { + "uuid": { + "type": "String" + }, + "start": { + "type": "int" + }, + "length": { + "type": "int" + } + } + }, + "JsonSendMessageResult": { + "fields": { + "address": { + "type": "JsonAddress", + "version": "v1" + }, + "success": { + "type": "Success", + "version": "v0" + }, + "networkFailure": { + "type": "boolean" + }, + "unregisteredFailure": { + "type": "boolean" + }, + "identityFailure": { + "type": "String" + } + } + }, + "JsonReaction": { + "fields": { + "emoji": { + "type": "String", + "doc": "the emoji to react with" + }, + "remove": { + "type": "boolean", + "doc": "set to true to remove the reaction. requires emoji be set to previously reacted emoji" + }, + "targetAuthor": { + "type": "JsonAddress", + "version": "v1", + "doc": "the author of the message being reacted to" + }, + "targetSentTimestamp": { + "type": "long", + "doc": "the client timestamp of the message being reacted to" + } + } + }, + "JsonGroupInfo": { + "fields": { + "groupId": { + "type": "String" + }, + "members": { + "list": true, + "type": "JsonAddress", + "version": "v1" + }, + "name": { + "type": "String" + }, + "type": { + "type": "String" + }, + "avatarId": { + "type": "long" + } + } + }, + "JsonGroupV2Info": { + "fields": { + "id": { + "type": "String" + }, + "masterKey": { + "type": "String" + }, + "revision": { + "type": "int" + }, + "title": { + "type": "String" + }, + "timer": { + "type": "int" + }, + "members": { + "list": true, + "type": "JsonAddress", + "version": "v1" + }, + "pendingMembers": { + "list": true, + "type": "JsonAddress", + "version": "v1" + }, + "requestingMembers": { + "list": true, + "type": "JsonAddress", + "version": "v1" + }, + "inviteLinkPassword": { + "type": "String" + } + } + }, + "JsonSentTranscriptMessage": { + "fields": { + "destination": { + "type": "JsonAddress", + "version": "v1" + }, + "timestamp": { + "type": "long" + }, + "expirationStartTimestamp": { + "type": "long" + }, + "message": { + "type": "JsonDataMessage", + "version": "v1" + }, + "unidentifiedStatus": { + "type": "Map" + }, + "isRecipientUpdate": { + "type": "boolean" + } + } + }, + "JsonBlockedListMessage": { + "fields": { + "addresses": { + "list": true, + "type": "JsonAddress", + "version": "v1" + }, + "groupIds": { + "list": true, + "type": "String" + } + } + }, + "JsonReadMessage": { + "fields": { + "sender": { + "type": "JsonAddress", + "version": "v1" + }, + "timestamp": { + "type": "long" + } + } + }, + "JsonViewOnceOpenMessage": { + "fields": { + "sender": { + "type": "JsonAddress", + "version": "v1" + }, + "timestamp": { + "type": "long" + } + } + }, + "JsonVerifiedMessage": { + "fields": { + "destination": { + "type": "JsonAddress", + "version": "v1" + }, + "identityKey": { + "type": "String" + }, + "verified": { + "type": "String" + }, + "timestamp": { + "type": "long" + } + } + }, + "JsonMessageRequestResponseMessage": { + "fields": { + "person": { + "type": "JsonAddress", + "version": "v1" + }, + "groupId": { + "type": "String" + }, + "type": { + "type": "String" + } + } + } + }, + "v0": { + "JsonAccountList": { + "fields": { + "accounts": { + "list": true, + "type": "JsonAccount", + "version": "v0" + } + } + }, + "JsonCallMessage": { + "fields": { + "offerMessage": { + "type": "OfferMessage", + "version": "v0" + }, + "answerMessage": { + "type": "AnswerMessage", + "version": "v0" + }, + "busyMessage": { + "type": "BusyMessage", + "version": "v0" + }, + "hangupMessage": { + "type": "HangupMessage", + "version": "v0" + }, + "iceUpdateMessages": { + "list": true, + "type": "IceUpdateMessage", + "version": "v0" + }, + "destinationDeviceId": { + "type": "int" + }, + "isMultiRing": { + "type": "boolean" + } + } + }, + "JsonReceiptMessage": { + "fields": { + "type": { + "type": "String" + }, + "timestamps": { + "list": true, + "type": "Long" + }, + "when": { + "type": "long" + } + } + }, + "JsonTypingMessage": { + "fields": { + "action": { + "type": "String" + }, + "timestamp": { + "type": "long" + }, + "groupId": { + "type": "String" + } + } + }, + "JsonAccount": { + "fields": { + "deviceId": { + "type": "int" + }, + "username": { + "type": "String" + }, + "filename": { + "type": "String" + }, + "uuid": { + "type": "String" + }, + "registered": { + "type": "boolean" + }, + "has_keys": { + "type": "boolean" + }, + "subscribed": { + "type": "boolean" + } + } + }, + "JsonAttachment": { + "fields": { + "contentType": { + "type": "String" + }, + "id": { + "type": "String" + }, + "size": { + "type": "int" + }, + "storedFilename": { + "type": "String" + }, + "filename": { + "type": "String" + }, + "customFilename": { + "type": "String" + }, + "caption": { + "type": "String" + }, + "width": { + "type": "int" + }, + "height": { + "type": "int" + }, + "voiceNote": { + "type": "boolean" + }, + "key": { + "type": "String" + }, + "digest": { + "type": "String" + }, + "blurhash": { + "type": "String" + } + } + }, + "SharedContact": { + "fields": { + "name": { + "type": "Name", + "version": "v0" + }, + "avatar": { + "type": "Optional", + "version": "v0" + }, + "phone": { + "type": "Optional", + "version": "v0" + }, + "email": { + "type": "Optional", + "version": "v0" + }, + "address": { + "type": "Optional", + "version": "v0" + }, + "organization": { + "type": "Optional", + "version": "v0" + } + } + }, + "JsonPreview": { + "fields": { + "url": { + "type": "String" + }, + "title": { + "type": "String" + }, + "attachment": { + "type": "JsonAttachment", + "version": "v0" + } + } + }, + "JsonSticker": { + "fields": { + "packID": { + "type": "String" + }, + "packKey": { + "type": "String" + }, + "stickerID": { + "type": "int" + }, + "attachment": { + "type": "JsonAttachment", + "version": "v0" + } + } + }, + "RemoteDelete": { + "fields": { + "targetSentTimestamp": { + "type": "long" + } + } + }, + "ConfigurationMessage": { + "fields": { + "readReceipts": { + "type": "Optional", + "version": "v0" + }, + "unidentifiedDeliveryIndicators": { + "type": "Optional", + "version": "v0" + }, + "typingIndicators": { + "type": "Optional", + "version": "v0" + }, + "linkPreviews": { + "type": "Optional", + "version": "v0" + } + } + }, + "JsonStickerPackOperationMessage": { + "fields": { + "packID": { + "type": "String" + }, + "packKey": { + "type": "String" + }, + "type": { + "type": "String" + } + } + }, + "OfferMessage": { + "fields": { + "id": { + "type": "long" + }, + "sdp": { + "type": "String" + }, + "type": { + "type": "Type", + "version": "v0" + }, + "opaque": { + "type": "String" + } + } + }, + "AnswerMessage": { + "fields": { + "id": { + "type": "long" + }, + "sdp": { + "type": "String" + }, + "opaque": { + "type": "String" + } + } + }, + "BusyMessage": { + "fields": { + "id": { + "type": "long" + } + } + }, + "HangupMessage": { + "fields": { + "id": { + "type": "long" + }, + "type": { + "type": "Type", + "version": "v0" + }, + "deviceId": { + "type": "int" + }, + "legacy": { + "type": "boolean" + } + } + }, + "IceUpdateMessage": { + "fields": { + "id": { + "type": "long" + }, + "opaque": { + "type": "String" + }, + "sdp": { + "type": "String" + } + } + }, + "JsonQuotedAttachment": { + "fields": { + "contentType": { + "type": "String" + }, + "fileName": { + "type": "String" + }, + "thumbnail": { + "type": "JsonAttachment", + "version": "v0" + } + } + }, + "Mention": { + "fields": { + "uuid": { + "type": "UUID" + }, + "start": { + "type": "int" + }, + "length": { + "type": "int" + } + } + }, + "Success": { + "fields": { + "unidentified": { + "type": "boolean" + }, + "needsSync": { + "type": "boolean" + }, + "duration": { + "type": "long" + } + } + }, + "Name": { + "fields": { + "display": { + "type": "Optional", + "version": "v0" + }, + "given": { + "type": "Optional", + "version": "v0" + }, + "family": { + "type": "Optional", + "version": "v0" + }, + "prefix": { + "type": "Optional", + "version": "v0" + }, + "suffix": { + "type": "Optional", + "version": "v0" + }, + "middle": { + "type": "Optional", + "version": "v0" + } + } + }, + "Optional": { + "fields": { + "present": { + "type": "boolean" + } + } + }, + "Type": { + "fields": {} + } + }, + "v1alpha1": { + "ProtocolRequest": { + "fields": {} + } + } + }, + "actions": { + "v1": { + "send": { + "request": "SendRequest", + "response": "SendResponse" + }, + "react": { + "request": "ReactRequest", + "response": "SendResponse", + "doc": "react to a previous message" + }, + "version": { + "request": "VersionRequest", + "response": "JsonVersionMessage" + } + }, + "v1alpha1": { + "protocol": { + "request": "ProtocolRequest" + } + } + } +} diff --git a/signald/signaldrequest.go b/signald/client-protocol/v0/signaldrequest.go similarity index 80% rename from signald/signaldrequest.go rename to signald/client-protocol/v0/signaldrequest.go index b81d474..8c841e9 100644 --- a/signald/signaldrequest.go +++ b/signald/client-protocol/v0/signaldrequest.go @@ -13,19 +13,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package signald - -import ( - "git.callpipe.com/finn/signald-go/signald/client-protocol/v1" -) +package v0 // Request represents a message sent to signald -type Request struct { +type LegacyRequest struct { Type string `json:"type"` ID string `json:"id,omitempty"` Username string `json:"username,omitempty"` MessageBody string `json:"messageBody,omitempty"` - RecipientAddress v1.JsonAddress `json:"recipientAddress,omitempty"` + RecipientAddress JsonAddress `json:"recipientAddress,omitempty"` RecipientGroupID string `json:"recipientGroupId,omitempty"` Voice bool `json:"voice,omitempty"` Code string `json:"code,omitempty"` @@ -38,11 +34,7 @@ type Request struct { Avatar string `json:"avatar,omitempty"` } -type JsonAttachment struct { - Filename string `json:"filename"` - Caption string `json:"caption"` - Width int `json:"width"` - Height int `json:"height"` - VoiceNote bool `json:"voiceNote"` - Preview bool `json:"preview"` +type JsonAddress struct { + Number string + UUID string } diff --git a/signald/signaldresponse.go b/signald/client-protocol/v0/signaldresponse.go similarity index 67% rename from signald/signaldresponse.go rename to signald/client-protocol/v0/signaldresponse.go index 064ec6c..c57d922 100644 --- a/signald/signaldresponse.go +++ b/signald/client-protocol/v0/signaldresponse.go @@ -1,30 +1,25 @@ -package signald +package v0 -import ( - "git.callpipe.com/finn/signald-go/signald/client-protocol/v1" -) - -// Response is a response to a request to signald, or a new inbound message -type Response struct { +type LegacyResponse struct { ID string - Data ResponseData + Data LegacyResponseData Type string } // ResponseData is where most of the data in the response is stored. -type ResponseData struct { +type LegacyResponseData struct { Groups []Group Accounts []Account URI string DataMessage DataMessage Message string Username string - Source v1.JsonAddress + Source JsonAddress SourceDevice int Type string IsReceipt bool - Timestamp float64 - ServerTimestamp float64 + Timestamp int64 + ServerTimestamp int64 } // Group represents a group in signal @@ -47,10 +42,10 @@ type Account struct { // DataMessage is the main component of incoming text messages type DataMessage struct { - Timestamp float64 - Message string - ExpiresInSeconds float64 - GroupInfo IncomingGroupInfo + Timestamp int64 + Body string + ExpiresInSeconds int64 + GroupInfo IncomingGroupInfo `json:"group"` } // IncomingGroupInfo is information about a particular group diff --git a/signald/client-protocol/v0/structs.go b/signald/client-protocol/v0/structs.go new file mode 100644 index 0000000..288339c --- /dev/null +++ b/signald/client-protocol/v0/structs.go @@ -0,0 +1,164 @@ +package v0 + +// DO NOT EDIT: this file is automatically generated by ./tools/generator in this repo + +type Request struct { + ID string `json:"id"` + Version string `json:"version"` + Type string `json:"type"` +} + +type AnswerMessage struct { + ID int64 `json:"id,omitempty"` + Opaque string `json:"opaque,omitempty"` + Sdp string `json:"sdp,omitempty"` +} + +type BusyMessage struct { + ID int64 `json:"id,omitempty"` +} + +type ConfigurationMessage struct { + LinkPreviews *Optional `json:"linkPreviews,omitempty"` + ReadReceipts *Optional `json:"readReceipts,omitempty"` + TypingIndicators *Optional `json:"typingIndicators,omitempty"` + UnidentifiedDeliveryIndicators *Optional `json:"unidentifiedDeliveryIndicators,omitempty"` +} + +type HangupMessage struct { + DeviceId int32 `json:"deviceId,omitempty"` + ID int64 `json:"id,omitempty"` + Legacy bool `json:"legacy,omitempty"` + Type *Type `json:"type,omitempty"` +} + +type IceUpdateMessage struct { + ID int64 `json:"id,omitempty"` + Opaque string `json:"opaque,omitempty"` + Sdp string `json:"sdp,omitempty"` +} + +type JsonAccount struct { + DeviceId int32 `json:"deviceId,omitempty"` + Filename string `json:"filename,omitempty"` + Has_keys bool `json:"has_keys,omitempty"` + Registered bool `json:"registered,omitempty"` + Subscribed bool `json:"subscribed,omitempty"` + Username string `json:"username,omitempty"` + UUID string `json:"uuid,omitempty"` +} + +type JsonAccountList struct { + Accounts []*JsonAccount `json:"accounts,omitempty"` +} + +type JsonAttachment struct { + Blurhash string `json:"blurhash,omitempty"` + Caption string `json:"caption,omitempty"` + ContentType string `json:"contentType,omitempty"` + CustomFilename string `json:"customFilename,omitempty"` + Digest string `json:"digest,omitempty"` + Filename string `json:"filename,omitempty"` + Height int32 `json:"height,omitempty"` + ID string `json:"id,omitempty"` + Key string `json:"key,omitempty"` + Size int32 `json:"size,omitempty"` + StoredFilename string `json:"storedFilename,omitempty"` + VoiceNote bool `json:"voiceNote,omitempty"` + Width int32 `json:"width,omitempty"` +} + +type JsonCallMessage struct { + AnswerMessage *AnswerMessage `json:"answerMessage,omitempty"` + BusyMessage *BusyMessage `json:"busyMessage,omitempty"` + DestinationDeviceId int32 `json:"destinationDeviceId,omitempty"` + HangupMessage *HangupMessage `json:"hangupMessage,omitempty"` + IceUpdateMessages []*IceUpdateMessage `json:"iceUpdateMessages,omitempty"` + IsMultiRing bool `json:"isMultiRing,omitempty"` + OfferMessage *OfferMessage `json:"offerMessage,omitempty"` +} + +type JsonPreview struct { + Attachment *JsonAttachment `json:"attachment,omitempty"` + Title string `json:"title,omitempty"` + Url string `json:"url,omitempty"` +} + +type JsonQuotedAttachment struct { + ContentType string `json:"contentType,omitempty"` + FileName string `json:"fileName,omitempty"` + Thumbnail *JsonAttachment `json:"thumbnail,omitempty"` +} + +type JsonReceiptMessage struct { + Timestamps []int64 `json:"timestamps,omitempty"` + Type string `json:"type,omitempty"` + When int64 `json:"when,omitempty"` +} + +type JsonSticker struct { + Attachment *JsonAttachment `json:"attachment,omitempty"` + PackID string `json:"packID,omitempty"` + PackKey string `json:"packKey,omitempty"` + StickerID int32 `json:"stickerID,omitempty"` +} + +type JsonStickerPackOperationMessage struct { + PackID string `json:"packID,omitempty"` + PackKey string `json:"packKey,omitempty"` + Type string `json:"type,omitempty"` +} + +type JsonTypingMessage struct { + Action string `json:"action,omitempty"` + GroupId string `json:"groupId,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` +} + +type Mention struct { + Length int32 `json:"length,omitempty"` + Start int32 `json:"start,omitempty"` + UUID string `json:"uuid,omitempty"` +} + +type Name struct { + Display *Optional `json:"display,omitempty"` + Family *Optional `json:"family,omitempty"` + Given *Optional `json:"given,omitempty"` + Middle *Optional `json:"middle,omitempty"` + Prefix *Optional `json:"prefix,omitempty"` + Suffix *Optional `json:"suffix,omitempty"` +} + +type OfferMessage struct { + ID int64 `json:"id,omitempty"` + Opaque string `json:"opaque,omitempty"` + Sdp string `json:"sdp,omitempty"` + Type *Type `json:"type,omitempty"` +} + +type Optional struct { + Present bool `json:"present,omitempty"` +} + +type RemoteDelete struct { + TargetSentTimestamp int64 `json:"targetSentTimestamp,omitempty"` +} + +type SharedContact struct { + Address *Optional `json:"address,omitempty"` + Avatar *Optional `json:"avatar,omitempty"` + Email *Optional `json:"email,omitempty"` + Name *Name `json:"name,omitempty"` + Organization *Optional `json:"organization,omitempty"` + Phone *Optional `json:"phone,omitempty"` +} + +type Success struct { + Duration int64 `json:"duration,omitempty"` + NeedsSync bool `json:"needsSync,omitempty"` + Unidentified bool `json:"unidentified,omitempty"` +} + +type Type struct { +} diff --git a/signald/client-protocol/v1/requests.go b/signald/client-protocol/v1/requests.go new file mode 100644 index 0000000..f648cc9 --- /dev/null +++ b/signald/client-protocol/v1/requests.go @@ -0,0 +1,121 @@ +package v1 + +// DO NOT EDIT: this file is automatically generated by ./tools/generator in this repo + +import ( + "encoding/json" + "fmt" + "log" + "math/rand" + + "gitlab.com/signald/signald-go/signald" +) + +// Submit: react to a previous message +func (r *ReactRequest) Submit(conn *signald.Signald) (response SendResponse, err error) { + r.Version = "v1" + r.Type = "react" + if r.ID == "" { + r.ID = generateID() + } + + err = conn.RawRequest(r) + if err != nil { + log.Println("signald-go: error submitting request to signald") + return response, err + } + + 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 response, err + } + + return response, nil + +} + +func (r *SendRequest) Submit(conn *signald.Signald) (response SendResponse, err error) { + r.Version = "v1" + r.Type = "send" + if r.ID == "" { + r.ID = generateID() + } + + err = conn.RawRequest(r) + if err != nil { + log.Println("signald-go: error submitting request to signald") + return response, err + } + + 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 response, err + } + + return response, nil + +} + +func (r *VersionRequest) Submit(conn *signald.Signald) (response JsonVersionMessage, err error) { + r.Version = "v1" + r.Type = "version" + if r.ID == "" { + r.ID = generateID() + } + + err = conn.RawRequest(r) + if err != nil { + log.Println("signald-go: error submitting request to signald") + return response, err + } + + 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 response, err + } + + return response, nil + +} + +const idsize = 10 + +var charset = []rune("abcdefghijklmnopqrstuvwxyz0123456789") + +func generateID() string { + id := make([]rune, idsize) + for i := range id { + id[i] = charset[rand.Intn(len(charset))] + } + return string(id) +} diff --git a/signald/client-protocol/v1/structs.go b/signald/client-protocol/v1/structs.go new file mode 100644 index 0000000..88659fd --- /dev/null +++ b/signald/client-protocol/v1/structs.go @@ -0,0 +1,199 @@ +package v1 + +// DO NOT EDIT: this file is automatically generated by ./tools/generator in this repo + +import ( + "gitlab.com/signald/signald-go/signald/client-protocol/v0" +) + +type Request struct { + ID string `json:"id"` + Version string `json:"version"` + Type string `json:"type"` +} + +type JsonAddress struct { + Number string `json:"number,omitempty"` // An e164 phone number, starting with +. Currently the only available user-facing Signal identifier. + Relay string `json:"relay,omitempty"` + UUID string `json:"uuid,omitempty"` // A UUID, the unique identifier for a particular Signal account. +} + +type JsonBlockedListMessage struct { + Addresses []*JsonAddress `json:"addresses,omitempty"` + GroupIds []string `json:"groupIds,omitempty"` +} + +type JsonDataMessage struct { + Attachments []*v0.JsonAttachment `json:"attachments,omitempty"` // files attached to the incoming message + Body string `json:"body,omitempty"` // the text body of the incoming message. + Contacts []*v0.SharedContact `json:"contacts,omitempty"` // if the incoming message has a shared contact, the contact's information will be here + EndSession bool `json:"endSession,omitempty"` + ExpiresInSeconds int32 `json:"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"` // if the incoming message was sent to a v1 group, information about that group will be here + GroupV2 *JsonGroupV2Info `json:"groupV2,omitempty"` // is the incoming message was sent to a v2 group, basic identifying information about that group will be here. For full information, use list_groups + Previews []*v0.JsonPreview `json:"previews,omitempty"` // if the incoming message has a link preview, information about that preview will be here + ProfileKeyUpdate bool `json:"profileKeyUpdate,omitempty"` + Quote *JsonQuote `json:"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"` // if the message adds or removes a reaction to another message, this will indicate what change is being made + RemoteDelete *v0.RemoteDelete `json:"remoteDelete,omitempty"` // if the inbound message is deleting a previously sent message, indicates which message should be deleted + Sticker *v0.JsonSticker `json:"sticker,omitempty"` // if the incoming message is a sticker, information about the sicker will be here + Timestamp int64 `json:"timestamp,omitempty"` // the (unix) 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"` // 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. +} + +type JsonGroupInfo struct { + AvatarId int64 `json:"avatarId,omitempty"` + GroupId string `json:"groupId,omitempty"` + Members []*JsonAddress `json:"members,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` +} + +type JsonGroupV2Info struct { + ID string `json:"id,omitempty"` + InviteLinkPassword string `json:"inviteLinkPassword,omitempty"` + MasterKey string `json:"masterKey,omitempty"` + Members []*JsonAddress `json:"members,omitempty"` + PendingMembers []*JsonAddress `json:"pendingMembers,omitempty"` + RequestingMembers []*JsonAddress `json:"requestingMembers,omitempty"` + Revision int32 `json:"revision,omitempty"` + Timer int32 `json:"timer,omitempty"` + Title string `json:"title,omitempty"` +} + +type JsonMention struct { + Length int32 `json:"length,omitempty"` + Start int32 `json:"start,omitempty"` + UUID string `json:"uuid,omitempty"` +} + +type JsonMessageEnvelope struct { + CallMessage *v0.JsonCallMessage `json:"callMessage,omitempty"` + DataMessage *JsonDataMessage `json:"dataMessage,omitempty"` + HasContent bool `json:"hasContent,omitempty"` + HasLegacyMessage bool `json:"hasLegacyMessage,omitempty"` + IsUnidentifiedSender bool `json:"isUnidentifiedSender,omitempty"` + Receipt *v0.JsonReceiptMessage `json:"receipt,omitempty"` + Relay string `json:"relay,omitempty"` + ServerDeliveredTimestamp int64 `json:"serverDeliveredTimestamp,omitempty"` + ServerTimestamp int64 `json:"serverTimestamp,omitempty"` + Source *JsonAddress `json:"source,omitempty"` + SourceDevice int32 `json:"sourceDevice,omitempty"` + SyncMessage *JsonSyncMessage `json:"syncMessage,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + TimestampISO string `json:"timestampISO,omitempty"` + Type string `json:"type,omitempty"` + Typing *v0.JsonTypingMessage `json:"typing,omitempty"` + Username string `json:"username,omitempty"` + UUID string `json:"uuid,omitempty"` +} + +type JsonMessageRequestResponseMessage struct { + GroupId string `json:"groupId,omitempty"` + Person *JsonAddress `json:"person,omitempty"` + Type string `json:"type,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"` + Author *JsonAddress `json:"author,omitempty"` + ID int64 `json:"id,omitempty"` + Mentions []*v0.Mention `json:"mentions,omitempty"` + Text string `json:"text,omitempty"` +} + +type JsonReaction struct { + Emoji string `json:"emoji,omitempty"` // the emoji to react with + Remove bool `json:"remove,omitempty"` // set to true to remove the reaction. requires emoji be set to previously reacted emoji + TargetAuthor *JsonAddress `json:"targetAuthor,omitempty"` // the author of the message being reacted to + TargetSentTimestamp int64 `json:"targetSentTimestamp,omitempty"` // the client timestamp of the message being reacted to +} + +type JsonReadMessage struct { + Sender *JsonAddress `json:"sender,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` +} + +type JsonSendMessageResult struct { + Address *JsonAddress `json:"address,omitempty"` + IdentityFailure string `json:"identityFailure,omitempty"` + NetworkFailure bool `json:"networkFailure,omitempty"` + Success *v0.Success `json:"success,omitempty"` + UnregisteredFailure bool `json:"unregisteredFailure,omitempty"` +} + +type JsonSentTranscriptMessage struct { + Destination *JsonAddress `json:"destination,omitempty"` + ExpirationStartTimestamp int64 `json:"expirationStartTimestamp,omitempty"` + IsRecipientUpdate bool `json:"isRecipientUpdate,omitempty"` + Message *JsonDataMessage `json:"message,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + UnidentifiedStatus map[string]string `json:"unidentifiedStatus,omitempty"` +} + +type JsonSyncMessage struct { + BlockedList *JsonBlockedListMessage `json:"blockedList,omitempty"` + Configuration *v0.ConfigurationMessage `json:"configuration,omitempty"` + Contacts *v0.JsonAttachment `json:"contacts,omitempty"` + ContactsComplete bool `json:"contactsComplete,omitempty"` + FetchType string `json:"fetchType,omitempty"` + Groups *v0.JsonAttachment `json:"groups,omitempty"` + MessageRequestResponse *JsonMessageRequestResponseMessage `json:"messageRequestResponse,omitempty"` + ReadMessages []*JsonReadMessage `json:"readMessages,omitempty"` + Request string `json:"request,omitempty"` + Sent *JsonSentTranscriptMessage `json:"sent,omitempty"` + StickerPackOperations []*v0.JsonStickerPackOperationMessage `json:"stickerPackOperations,omitempty"` + Verified *JsonVerifiedMessage `json:"verified,omitempty"` + ViewOnceOpen *JsonViewOnceOpenMessage `json:"viewOnceOpen,omitempty"` +} + +type JsonVerifiedMessage struct { + Destination *JsonAddress `json:"destination,omitempty"` + IdentityKey string `json:"identityKey,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + Verified string `json:"verified,omitempty"` +} + +type JsonVersionMessage struct { + Branch string `json:"branch,omitempty"` + Commit string `json:"commit,omitempty"` + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` +} + +type JsonViewOnceOpenMessage struct { + Sender *JsonAddress `json:"sender,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` +} + +// ReactRequest: react to a previous message +type ReactRequest struct { + Request + Reaction *JsonReaction `json:"reaction,omitempty"` + RecipientAddress *JsonAddress `json:"recipientAddress,omitempty"` + RecipientGroupID string `json:"recipientGroupId,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + Username string `json:"username,omitempty"` +} + +type SendRequest struct { + Request + Attachments []*v0.JsonAttachment `json:"attachments,omitempty"` + Mentions []*JsonMention `json:"mentions,omitempty"` + MessageBody string `json:"messageBody,omitempty"` + Quote *JsonQuote `json:"quote,omitempty"` + RecipientAddress *JsonAddress `json:"recipientAddress,omitempty"` + RecipientGroupID string `json:"recipientGroupId,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + Username string `json:"username,omitempty"` +} + +type SendResponse struct { + Results []*JsonSendMessageResult `json:"results,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` +} + +type VersionRequest struct { + Request +} diff --git a/signald/client-protocol/v1/types.go b/signald/client-protocol/v1/types.go deleted file mode 100644 index 70596d8..0000000 --- a/signald/client-protocol/v1/types.go +++ /dev/null @@ -1,44 +0,0 @@ -package v1 - -// JsonAddress is a signal user's contact information. a phone number, UUID or both -type JsonAddress struct { - UUID string `json:"uuid,omitempty"` - Number string `json:"number"` -} - -type JsonMessageRequestResponseMessage struct { - Person JsonAddress `json:"person"` - GroupID string `json:"groupId"` - Type string `json:"type"` -} - -type JsonReaction struct { - Emoji string `json:"emoji"` - Remove bool `json:"remove"` - TargetAuthor JsonAddress `json:"targetAuthor"` - TargetSentTimestamp uint64 `json:"targetSentTimestamp"` -} - -type JsonReadMessage struct { - Sender JsonAddress `json:"sender"` - Timestamp uint64 `json:"timestamp"` -} - -type JsonSendMessageResult struct { - Address JsonAddress `json:"address"` - Success Success `json:"success"` - NetworkFailure bool `json:"networkFailure"` - UnregisteredFailure bool `json:"unregisteredFailure"` - IdentityFailure string `json:"identityFailure"` -} - -type Success struct { - Unidentified bool `json:"unidentified"` - NeedsSync bool `json:"needsSync"` -} - -type RequestValidationFailure struct { - ValidationResults []string `json:"validationResults"` - Type string `json:"type"` - Message string `json:"message"` -} diff --git a/signald/client-protocol/v1alpha1/requests.go b/signald/client-protocol/v1alpha1/requests.go new file mode 100644 index 0000000..b2bd38a --- /dev/null +++ b/signald/client-protocol/v1alpha1/requests.go @@ -0,0 +1,32 @@ +package v1alpha1 + +// DO NOT EDIT: this file is automatically generated by ./tools/generator in this repo + +import ( + "math/rand" + + "gitlab.com/signald/signald-go/signald" +) + +func (r *ProtocolRequest) Submit(conn *signald.Signald) error { + r.Version = "v1alpha1" + r.Type = "protocol" + if r.ID == "" { + r.ID = generateID() + } + + return conn.RawRequest(r) + +} + +const idsize = 10 + +var charset = []rune("abcdefghijklmnopqrstuvwxyz0123456789") + +func generateID() string { + id := make([]rune, idsize) + for i := range id { + id[i] = charset[rand.Intn(len(charset))] + } + return string(id) +} diff --git a/signald/client-protocol/v1alpha1/structs.go b/signald/client-protocol/v1alpha1/structs.go new file mode 100644 index 0000000..2fdf815 --- /dev/null +++ b/signald/client-protocol/v1alpha1/structs.go @@ -0,0 +1,13 @@ +package v1alpha1 + +// DO NOT EDIT: this file is automatically generated by ./tools/generator in this repo + +type Request struct { + ID string `json:"id"` + Version string `json:"version"` + Type string `json:"type"` +} + +type ProtocolRequest struct { + Request +} diff --git a/signald/signald.go b/signald/signald.go index b7f0848..7df4561 100644 --- a/signald/signald.go +++ b/signald/signald.go @@ -1,4 +1,4 @@ -// Copyright © 2018 Finn Herzfeld +// Copyright © 2020 Finn Herzfeld // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -16,17 +16,39 @@ package signald import ( + "bytes" "encoding/json" + "io" "log" "net" + "os" + "strconv" + + "gitlab.com/signald/signald-go/signald/client-protocol/v0" +) + +var ( + debugSignaldIO, _ = strconv.ParseBool(os.Getenv("DEBUG_SIGNALD_IO")) ) // Signald is a connection to a signald instance. type Signald struct { socket net.Conn + listeners map[string]chan BasicResponse SocketPath string } +type BasicResponse struct { + ID string + Type string + Error json.RawMessage + Data json.RawMessage +} + +type UnexpectedError struct { + Message string +} + // Connect connects to the signad socket func (s *Signald) Connect() error { if s.SocketPath == "" { @@ -37,32 +59,86 @@ func (s *Signald) Connect() error { return err } s.socket = socket - log.Print("Connected to signald socket ", socket.RemoteAddr().String()) + log.Println("signald-go: Connected to signald socket ", socket.RemoteAddr().String()) return nil } // Listen listens for events from signald -func (s *Signald) Listen(c chan Response) { - // we create a decoder that reads directly from the socket - d := json.NewDecoder(s.socket) - - var msg Response - +func (s *Signald) Listen(c chan v0.LegacyResponse) { for { - if err := d.Decode(&msg); err != nil { - log.Println("error decoding message from signald:", err) + msg, err := s.readNext() + if err == io.EOF { + log.Println("signald-go: socket disconnected!") + return + } + + if msg.Type == "unexpected_error" { + var errorResponse UnexpectedError + if err := json.Unmarshal(msg.Data, &errorResponse); err != nil { + log.Println("signald-go: Error unmarshaling error response:", err.Error()) + continue + } + log.Println("signald-go: Unexpected error", errorResponse.Message) + continue + } + + if subscribers, ok := s.listeners[msg.ID]; ok { + subscribers <- msg + } + + if c != nil { + legacyResponse := v0.LegacyResponse{ + ID: msg.ID, + Type: msg.Type, + } + _ = json.Unmarshal(msg.Data, &legacyResponse.Data) + c <- legacyResponse } - c <- msg } } -// SendRequest sends a request to signald. Mostly used interally. -func (s *Signald) SendRequest(request Request) error { - b, err := json.Marshal(request) - if err != nil { - return err +func (s *Signald) RawRequest(request interface{}) error { + if debugSignaldIO { + buffer := bytes.Buffer{} + if err := json.NewEncoder(&buffer).Encode(request); err == nil { + log.Println("[to signald]", buffer.String()) + } } - log.Print("Sending ", string(b)) - e := json.NewEncoder(s.socket) - return e.Encode(request) + return json.NewEncoder(s.socket).Encode(request) +} + +func (s *Signald) GetResponseListener(requestid string) chan BasicResponse { + if s.listeners == nil { + s.listeners = map[string]chan BasicResponse{} + } + c, ok := s.listeners[requestid] + if !ok { + c = make(chan BasicResponse) + s.listeners[requestid] = c + } + return c +} + +func (s *Signald) CloseResponseListener(requestid string) { + listener, ok := s.listeners[requestid] + if !ok { + return + } + close(listener) + delete(s.listeners, requestid) +} + +func (s *Signald) readNext() (b BasicResponse, err error) { + if debugSignaldIO { + buffer := bytes.Buffer{} + err = json.NewDecoder(io.TeeReader(s.socket, &buffer)).Decode(&b) + log.Println("[from signald]", buffer.String()) + } else { + err = json.NewDecoder(s.socket).Decode(&b) + } + if err != nil { + log.Println("signald-go: error decoding message from signald:", err) + return + } + return } diff --git a/tools/generator/main.go b/tools/generator/main.go new file mode 100644 index 0000000..03dac27 --- /dev/null +++ b/tools/generator/main.go @@ -0,0 +1,179 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + "os/exec" + "strings" + "text/template" +) + +type Protocol struct { + Types map[string]map[string]*Type + Actions map[string]map[string]*Action +} + +type Type struct { + Fields map[string]*DataType + Request bool `json:"-"` + Doc string +} + +type DataType struct { + List bool + Type string + Version string + FieldName string + Doc string +} + +type Action struct { + FnName string + Request string + Response string + Doc string +} + +type StructsTemplateInput struct { + Types map[string]*Type + Version string + ImportVersions []string +} + +type ActionsTemplateInput struct { + Actions map[string]*Action + Version string + Responses bool +} + +var typeMap = map[string]string{ + "int": "int32", + "Integer": "int32", + "Boolean": "bool", + "long": "int64", + "Long": "int64", + "UUID": "string", + "boolean": "bool", + "String": "string", + "Map": "map[string]string", // TODO: make signald print the actual key and value types +} + +var fieldNameMap = map[string]string{ + "id": "ID", + "recipientGroupId": "RecipientGroupID", + "uuid": "UUID", +} + +func (d *DataType) fixForVersion(field, version string) { + response, ok := typeMap[d.Type] + if ok { + if d.Type == "byte" && d.List { + d.List = false + } + 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) + } + } + + fieldName, ok := fieldNameMap[field] + if ok { + d.FieldName = fieldName + } else { + d.FieldName = strings.Title(field) + } +} + +func main() { + var response Protocol + err := json.NewDecoder(os.Stdin).Decode(&response) + if err != nil { + log.Fatal(err, "\nError parsing stdin") + } + + tmpl, err := template.ParseGlob("tools/generator/*.tmpl") + if err != nil { + log.Fatal(err, "\nError parsing templates from tools/generator/*.tmpl") + } + + for version, actions := range response.Actions { + inputs := ActionsTemplateInput{Version: version, Responses: false} + for action, a := range actions { + actions[action].FnName = strings.Title(action) + if a.Request != "" { + response.Types[version][a.Request].Request = true + } + if a.Response != "" { + inputs.Responses = true + } + } + inputs.Actions = actions + 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, "requests.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, "requests.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) + } + + for version, types := range response.Types { + inputs := StructsTemplateInput{Version: version} + 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) + } + } + } + } + inputs.Types = types + 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, "structs.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, "structs.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 new file mode 100644 index 0000000..962d499 --- /dev/null +++ b/tools/generator/requests.go.tmpl @@ -0,0 +1,67 @@ +package {{.Version}} + +// DO NOT EDIT: this file is automatically generated by ./tools/generator in this repo + +import ({{if .Responses}} + "encoding/json" + "fmt" + "log"{{end}} + "math/rand" + + "gitlab.com/signald/signald-go/signald" +) + +{{range $type, $action := .Actions}} +{{if ne $action.Request ""}} +{{if ne $action.Doc ""}}// Submit: {{$action.Doc}}{{end}} +func (r *{{$action.Request}}) Submit(conn *signald.Signald) ({{if ne $action.Response ""}}response {{$action.Response}}, err {{end}}error) { + r.Version = "{{$.Version}}" +{{else}} +{{if ne $action.Doc ""}}// {{$action.FnName}}: {{$action.Doc}}{{end}} +func {{$action.FnName}}(conn *signald.Signald) ({{if ne $action.Response ""}}response {{$action.Response}}, {{end}}err error) { + r := Request{Version: "{{.Version}}"} +{{end}} r.Type = "{{$type}}" + if(r.ID == "") { + r.ID = generateID() + } + +{{if ne $action.Response ""}} + err = conn.RawRequest(r) + if err != nil { + log.Println("signald-go: error submitting request to signald") + return response, err + } + + 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 response, err + } + + return response, nil +{{else}} + return conn.RawRequest(r) +{{end}} +} +{{end}} + + +const idsize = 10 +var charset = []rune("abcdefghijklmnopqrstuvwxyz0123456789") + +func generateID() string { + id := make([]rune, idsize) + for i := range id { + id[i]= charset[rand.Intn(len(charset))] + } + return string(id) +} diff --git a/tools/generator/structs.go.tmpl b/tools/generator/structs.go.tmpl new file mode 100644 index 0000000..884bbc5 --- /dev/null +++ b/tools/generator/structs.go.tmpl @@ -0,0 +1,21 @@ +package {{.Version}} + +// DO NOT EDIT: this file is automatically generated by ./tools/generator in this repo +{{if gt (.ImportVersions | len) 0}} +import ( +{{range $version := .ImportVersions}} + "gitlab.com/signald/signald-go/signald/client-protocol/{{$version}}"{{end}} +) +{{end}} + +type Request struct { + ID string `json:"id"` + Version string `json:"version"` + Type string `json:"type"` +} +{{ 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"`{{if ne $field.Doc ""}} // {{$field.Doc}}{{end}} +{{ end }}} +{{ end }}