diff --git a/README.md b/README.md index 8dc1d98..7e6d749 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,15 @@ This project creates a small dockerized REST API around [signal-cli](https://github.com/AsamK/signal-cli). +At the moment, the following functionality is exposed via REST: -At the moment, the following functionality is exposed via REST: +- Register a number +- Verify the number using the code received via SMS +- Send message (+ attachment) to multiple recipients -* Register a number -* Verify the number using the code received via SMS -* Send message (+ attachment) to multiple recipients +## Examples - -## Examples - -Sample `docker-compose.yml`file: +Sample `docker-compose.yml`file: ``` version: "3" @@ -29,95 +27,104 @@ services: Sample REST API calls: -* Register a number (with SMS verification) +- Register a number (with SMS verification) -```curl -X POST -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/register/'``` +`curl -X POST -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/register/'` - e.g: - - ```curl -X POST -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/register/+431212131491291'``` +e.g: -* Register a number (with voice verification) +`curl -X POST -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/register/+431212131491291'` -```curl -X POST -H "Content-Type: application/json" --data '{"use_voice": true}' 'http://127.0.0.1:8080/v1/register/'``` +- Register a number (with voice verification) - e.g: - - ```curl -X POST -H "Content-Type: application/json" --data '{"use_voice": true}' 'http://127.0.0.1:8080/v1/register/+431212131491291'``` +`curl -X POST -H "Content-Type: application/json" --data '{"use_voice": true}' 'http://127.0.0.1:8080/v1/register/'` -* Verify the number using the code received via SMS/voice +e.g: - ```curl -X POST -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/register//verify/'``` +`curl -X POST -H "Content-Type: application/json" --data '{"use_voice": true}' 'http://127.0.0.1:8080/v1/register/+431212131491291'` - e.g: - - ```curl -X POST -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/register/+431212131491291/verify/123-456'``` +- Verify the number using the code received via SMS/voice -* Send a message to multiple recipients + `curl -X POST -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/register//verify/'` - ```curl -X POST -H "Content-Type: application/json" -d '{"message": "", "number": "", "recipients": ["", ""]}' 'http://127.0.0.1:8080/v2/send'``` + e.g: - e.g: + `curl -X POST -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/register/+431212131491291/verify/123-456'` - ```curl -X POST -H "Content-Type: application/json" -d '{"message": "Hello World!", "number": "+431212131491291", "recipients": ["+4354546464654", "+4912812812121"]}' 'http://127.0.0.1:8080/v2/send'``` +- Send a message to multiple recipients -* Send a message (+ base64 encoded attachment) to multiple recipients + `curl -X POST -H "Content-Type: application/json" -d '{"message": "", "number": "", "recipients": ["", ""]}' 'http://127.0.0.1:8080/v2/send'` - ```curl -X POST -H "Content-Type: application/json" -d '{"message": "", "base64_attachments": [""], "number": "", "recipients": ["", ""]}' 'http://127.0.0.1:8080/v2/send'``` + e.g: -* Send a message to a group + `curl -X POST -H "Content-Type: application/json" -d '{"message": "Hello World!", "number": "+431212131491291", "recipients": ["+4354546464654", "+4912812812121"]}' 'http://127.0.0.1:8080/v2/send'` -The group id can be obtained via the "List groups" REST call. +- Send a message (+ base64 encoded attachment) to multiple recipients - ```curl -X POST -H "Content-Type: application/json" -d '{"message": "", "number": "", "recipients": [""]}' 'http://127.0.0.1:8080/v2/send'``` + `curl -X POST -H "Content-Type: application/json" -d '{"message": "", "base64_attachments": [""], "number": "", "recipients": ["", ""]}' 'http://127.0.0.1:8080/v2/send'` - e.g: +- Send a message to a group - ```curl -X POST -H "Content-Type: application/json" -d '{"message": "Hello World!", "number": "+431212131491291", "recipients": ["group.ckRzaEd4VmRzNnJaASAEsasa", "+4912812812121"]}' 'http://127.0.0.1:8080/v2/send'``` + The group id can be obtained via the "List groups" REST call. -* Receive messages + `curl -X POST -H "Content-Type: application/json" -d '{"message": "", "number": "", "recipients": [""]}' 'http://127.0.0.1:8080/v2/send'` + + e.g: + + `curl -X POST -H "Content-Type: application/json" -d '{"message": "Hello World!", "number": "+431212131491291", "recipients": ["group.ckRzaEd4VmRzNnJaASAEsasa", "+4912812812121"]}' 'http://127.0.0.1:8080/v2/send'` + +- Receive messages Fetch all new messages in the inbox of the specified number. - ```curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/receive/'``` - - e.g: - - ```curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/receive/+431212131491291'``` - -* Create a new group - - Create a new group with the specified name and members. - - ```curl -X POST -H "Content-Type: application/json" -d '{"name": "", "members": ["", ""]}' 'http://127.0.0.1:8080/v1/groups/'``` + `curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/receive/'` e.g: - ```curl -X POST -H "Content-Type: application/json" -d '{"name": "my group", "members": ["+4354546464654", "+4912812812121"]}' 'http://127.0.0.1:8080/v1/groups/+431212131491291'``` + `curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/receive/+431212131491291'` -* List groups +- Create a new group - ```curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups/'``` + Create a new group with the specified name and members. + + `curl -X POST -H "Content-Type: application/json" -d '{"name": "", "members": ["", ""]}' 'http://127.0.0.1:8080/v1/groups/'` e.g: - ```curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups/+431212131491291'``` + `curl -X POST -H "Content-Type: application/json" -d '{"name": "my group", "members": ["+4354546464654", "+4912812812121"]}' 'http://127.0.0.1:8080/v1/groups/+431212131491291'` -* Delete a group +- List groups - Delete the group with the given group id. The group id can be obtained via the "List groups" REST call. + `curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups/'` - ```curl -X DELETE -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups//'``` + e.g: - e.g: + `curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups/+431212131491291'` - ```curl -X DELETE -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups/+431212131491291/ckRzaEd4VmRzNnJaASAEsasa'``` +- Delete a group + Delete the group with the given group id. The group id can be obtained via the "List groups" REST call. + `curl -X DELETE -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups//'` + e.g: + + `curl -X DELETE -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups/+431212131491291/ckRzaEd4VmRzNnJaASAEsasa'` + +- Link a device + + `curl -X POST -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/qrcodelink?'` + + e.g: + + `curl -X POST -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/qrcodelink?HomeAssistant'` + + This provides a QR-Code image. In case of an error a JSON object will be returned. + + Due to security reason of Signal, the provided QR-Code will change with each request. The following REST API endpoints are **deprecated and no longer maintained!** -```/v1/send``` +`/v1/send` In case you need more functionality, please **file a ticket** or **create a PR** diff --git a/src/main.go b/src/main.go index 4d5fb4f..9ddd4b9 100644 --- a/src/main.go +++ b/src/main.go @@ -1,19 +1,22 @@ package main import ( + "bufio" "bytes" "encoding/base64" "encoding/json" "errors" "flag" - "github.com/gin-gonic/gin" - "github.com/h2non/filetype" - "github.com/satori/go.uuid" - log "github.com/sirupsen/logrus" "os" "os/exec" "strings" "time" + + "github.com/gin-gonic/gin" + "github.com/h2non/filetype" + uuid "github.com/satori/go.uuid" + log "github.com/sirupsen/logrus" + qrcode "github.com/skip2/go-qrcode" ) const groupPrefix = "group." @@ -31,7 +34,6 @@ func convertInternalGroupIdToGroupId(internalId string) string { return groupPrefix + base64.StdEncoding.EncodeToString([]byte(internalId)) } - func getStringInBetween(str string, start string, end string) (result string) { i := strings.Index(str, start) if i == -1 { @@ -126,7 +128,7 @@ func send(c *gin.Context, attachmentTmpDir string, signalCliConfig string, numbe cmd = append(cmd, attachmentTmpPaths...) } - _, err := runSignalCli(cmd) + _, err := runSignalCli(true, cmd) if err != nil { cleanupTmpFiles(attachmentTmpPaths) c.JSON(400, gin.H{"error": err.Error()}) @@ -138,7 +140,7 @@ func send(c *gin.Context, attachmentTmpDir string, signalCliConfig string, numbe func getGroups(number string, signalCliConfig string) ([]GroupEntry, error) { groupEntries := []GroupEntry{} - out, err := runSignalCli([]string{"--config", signalCliConfig, "-u", number, "listGroups", "-d"}) + out, err := runSignalCli(true, []string{"--config", signalCliConfig, "-u", number, "listGroups", "-d"}) if err != nil { return groupEntries, err } @@ -195,35 +197,46 @@ func getGroups(number string, signalCliConfig string) ([]GroupEntry, error) { return groupEntries, nil } -func runSignalCli(args []string) (string, error) { +func runSignalCli(wait bool, args []string) (string, error) { cmd := exec.Command("signal-cli", args...) - var errBuffer bytes.Buffer - var outBuffer bytes.Buffer - cmd.Stderr = &errBuffer - cmd.Stdout = &outBuffer + if wait { + var errBuffer bytes.Buffer + var outBuffer bytes.Buffer + cmd.Stderr = &errBuffer + cmd.Stdout = &outBuffer - err := cmd.Start() - if err != nil { - return "", err - } - - done := make(chan error, 1) - go func() { - done <- cmd.Wait() - }() - select { - case <-time.After(60 * time.Second): - err := cmd.Process.Kill() + err := cmd.Start() if err != nil { return "", err } - return "", errors.New("process killed as timeout reached") - case err := <-done: - if err != nil { - return "", errors.New(errBuffer.String()) + + done := make(chan error, 1) + go func() { + done <- cmd.Wait() + }() + select { + case <-time.After(60 * time.Second): + err := cmd.Process.Kill() + if err != nil { + return "", err + } + return "", errors.New("process killed as timeout reached") + case err := <-done: + if err != nil { + return "", errors.New(errBuffer.String()) + } } + return outBuffer.String(), nil + } else { + stdout, err := cmd.StdoutPipe() + if err != nil { + return "", err + } + cmd.Start() + buf := bufio.NewReader(stdout) // Notice that this is not in a loop + line, _, _ := buf.ReadLine() + return string(line), nil } - return outBuffer.String(), nil } func main() { @@ -277,7 +290,7 @@ func main() { command = append(command, "--voice") } - _, err := runSignalCli(command) + _, err := runSignalCli(true, command) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return @@ -299,7 +312,7 @@ func main() { return } - _, err := runSignalCli([]string{"--config", *signalCliConfig, "-u", number, "verify", token}) + _, err := runSignalCli(true, []string{"--config", *signalCliConfig, "-u", number, "verify", token}) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return @@ -384,7 +397,7 @@ func main() { number := c.Param("number") command := []string{"--config", *signalCliConfig, "-u", number, "receive", "-t", "1", "--json"} - out, err := runSignalCli(command) + out, err := runSignalCli(true, command) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return @@ -424,12 +437,12 @@ func main() { cmd := []string{"--config", *signalCliConfig, "-u", number, "updateGroup", "-n", req.Name, "-m"} cmd = append(cmd, req.Members...) - out, err := runSignalCli(cmd) + out, err := runSignalCli(true, cmd) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } - + internalGroupId := getStringInBetween(out, `"`, `"`) c.JSON(201, gin.H{"id": convertInternalGroupIdToGroupId(internalGroupId)}) }) @@ -461,12 +474,43 @@ func main() { return } - _, err = runSignalCli([]string{"--config", *signalCliConfig, "-u", number, "quitGroup", "-g", string(groupId)}) + _, err = runSignalCli(true, []string{"--config", *signalCliConfig, "-u", number, "quitGroup", "-g", string(groupId)}) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } }) + router.GET("/v1/qrcodelink", func(c *gin.Context) { + deviceName := c.Query("device_name") + + if deviceName == "" { + c.JSON(400, gin.H{"error": "Please provide a name for the device"}) + return + } + + command := []string{"--config", *signalCliConfig, "link", "-n", deviceName} + + tsdeviceLink, err := runSignalCli(false, command) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + q, err := qrcode.New(string(tsdeviceLink), qrcode.Medium) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + } + + q.DisableBorder = true + var png []byte + png, err = q.PNG(256) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + } + + c.Data(200, "image/png", png) + }) + router.Run() }