Generate client for documented actions.

This commit is contained in:
Finn 2020-10-12 22:24:11 -07:00
parent 6fccb53b2a
commit ffaa25780a
24 changed files with 1965 additions and 137 deletions

View file

@ -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:

12
Makefile Normal file
View file

@ -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/*

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
}
},
}

View file

@ -0,0 +1,49 @@
// Copyright © 2018 Finn Herzfeld <finn@janky.solutions>
//
// 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 <http://www.gnu.org/licenses/>.
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)
}

View file

@ -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()

7
go.mod
View file

@ -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
)

11
go.sum
View file

@ -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=

945
protocol.json Normal file
View file

@ -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"
}
}
}
}

View file

@ -13,19 +13,15 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
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
}

View file

@ -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

View file

@ -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 {
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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"`
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -1,4 +1,4 @@
// Copyright © 2018 Finn Herzfeld <finn@janky.solutions>
// Copyright © 2020 Finn Herzfeld <finn@janky.solutions>
//
// 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
}

179
tools/generator/main.go Normal file
View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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 }}