Merge branch 'groups'

This commit is contained in:
Bernhard B
2020-04-18 08:28:02 +02:00
4 changed files with 351 additions and 112 deletions

View File

@@ -75,4 +75,40 @@ Sample REST API calls:
```curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/receive/+431212131491291'``` ```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": "<group name>", "members": ["<member1>", "<member2>"]}' 'http://127.0.0.1:8080/v1/groups/<number>'```
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'```
* List groups
```curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups/<number>'```
e.g:
```curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups/+431212131491291'```
* 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/<number>/<group id>'```
e.g:
```curl -X DELETE -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups/+431212131491291/ckRzaEd4VmRzNnJaASAEsasa'```
* Send a message to a group
```curl -X POST -H "Content-Type: application/json" -d '{"message": "Hello World!", "number": "<number>", "recipients": ["<group id>"], "is_group": true}' 'http://127.0.0.1:8080/v1/send'```
e.g:
```curl -X POST -H "Content-Type: application/json" -d '{"message": "Hello World!", "number": "+431212131491291", "recipients": ["ckRzaEd4VmRzNnJaASAEsasa"], "is_group": true}' 'http://127.0.0.1:8080/v1/send'```
In case you need more functionality, please **file a ticket** or **create a PR** In case you need more functionality, please **file a ticket** or **create a PR**

View File

@@ -46,5 +46,33 @@ 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" 'http://127.0.0.1:8080/v1/register/+431212131491291/verify/123-456'```
## Sending messages to Signal Messenger groups
The `signal-cli-rest-api` docker container is also capable of sending messages to a Signal Messenger group.
Requirements:
* Home Assistant Version >= 0.109
* signal-cli-rest-api build-nr >= 2
The build number can be checked with: `curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/about`
* your phone number needs to be properly registered (see the "Register phone number" section above on how to do that)
A new Signal Messenger group can be created with the following REST API request:
```curl -X POST -H "Content-Type: application/json" -d '{"name": "<name of the group>", "members": ["<member1>", "<member2>"]}' 'http://127.0.0.1:8080/v1/groups/<number>'```
e.g:
This creates a new Signal Messenger group called `my group` with the members `+4354546464654` and `+4912812812121`.
```curl -X POST -H "Content-Type: application/json" -d '{"name": "my group", "members": ["+4354546464654", "+4912812812121"]}' 'http://127.0.0.1:8080/v1/groups/+431212131491291'```
Next, use the following endpoint to obtain the group id:
```curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups/<number>'```
The group id then needs to be added to the Home Assistant `configuration.yaml` file (see [here](https://www.home-assistant.io/integrations/signal_messenger/) for details)
# Troubleshooting # Troubleshooting
In case you've problems with the `signal-cli-rest-api` container, have a look [here](TROUBLESHOOTING.md) In case you've problems with the `signal-cli-rest-api` container, have a look [here](TROUBLESHOOTING.md)

View File

@@ -1,10 +1,11 @@
#!/bin/bash #!/bin/bash
while getopts v: option while getopts v:t: option
do do
case "${option}" case "${option}"
in in
v) VERSION=${OPTARG};; v) VERSION=${OPTARG};;
t) TAG=${OPTARG};;
esac esac
done done
@@ -14,8 +15,20 @@ then
exit 1 exit 1
fi fi
if [ -z "$TAG" ]
then
echo "Please provide a valid tag with the -t flag. e.g: -t stable (supported tags: dev, stable)"
exit 1
fi
if [[ "$TAG" != "dev" && "$TAG" != "stable" ]]; then
echo "Please use either dev or stable as tag"
exit 1
fi
echo "This will upload a new signal-cli-rest-api to dockerhub" echo "This will upload a new signal-cli-rest-api to dockerhub"
echo "Version: $VERSION" echo "Version: $VERSION"
echo "Tag: $TAG"
echo "" echo ""
read -r -p "Are you sure? [y/N] " response read -r -p "Are you sure? [y/N] " response
@@ -28,9 +41,17 @@ case "$response" in
docker buildx create --name multibuilder docker buildx create --name multibuilder
docker buildx use multibuilder docker buildx use multibuilder
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t bbernhard/signal-cli-rest-api:$VERSION . --push if [[ "$TAG" == "stable" ]]; then
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t bbernhard/signal-cli-rest-api:latest . --push docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t bbernhard/signal-cli-rest-api:$VERSION . --push
;; docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t bbernhard/signal-cli-rest-api:latest . --push
fi
if [[ "$TAG" == "dev" ]]; then
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t bbernhard/signal-cli-rest-api:${VERSION}-dev . --push
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t bbernhard/signal-cli-rest-api:latest-dev . --push
fi
;;
*) *)
echo "Aborting" echo "Aborting"
exit 1 exit 1

View File

@@ -1,32 +1,61 @@
package main package main
import ( import (
log "github.com/sirupsen/logrus"
"github.com/satori/go.uuid"
"github.com/gin-gonic/gin"
"github.com/h2non/filetype"
"os/exec"
"time"
"errors"
"flag"
"bytes" "bytes"
"os"
"encoding/base64" "encoding/base64"
"encoding/json" "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" "strings"
"time"
) )
type GroupEntry struct {
Name string `json:"name"`
Id string `json:"id"`
InternalId string `json:"internal_id"`
Members []string `json:"members"`
Active bool `json:"active"`
Blocked bool `json:"blocked"`
}
func cleanupTmpFiles(paths []string) { func cleanupTmpFiles(paths []string) {
for _, path := range paths { for _, path := range paths {
os.Remove(path) os.Remove(path)
} }
} }
func send(c *gin.Context, attachmentTmpDir string, signalCliConfig string, number string, message string, recipients []string, base64Attachments []string) { func send(c *gin.Context, attachmentTmpDir string, signalCliConfig string, number string, message string,
recipients []string, base64Attachments []string, isGroup bool) {
cmd := []string{"--config", signalCliConfig, "-u", number, "send", "-m", message} cmd := []string{"--config", signalCliConfig, "-u", number, "send", "-m", message}
cmd = append(cmd, recipients...)
if len(recipients) == 0 {
c.JSON(400, gin.H{"error": "Please specify at least one recipient"})
return
}
if !isGroup {
cmd = append(cmd, recipients...)
} else {
if len(recipients) > 1 {
c.JSON(400, gin.H{"error": "More than one recipient is currently not allowed"})
return
}
groupId, err := base64.StdEncoding.DecodeString(recipients[0])
if err != nil {
c.JSON(400, gin.H{"error": "Invalid group id"})
return
}
cmd = append(cmd, []string{"-g", string(groupId)}...)
}
attachmentTmpPaths := []string{} attachmentTmpPaths := []string{}
for _, base64Attachment := range base64Attachments { for _, base64Attachment := range base64Attachments {
@@ -74,7 +103,7 @@ func send(c *gin.Context, attachmentTmpDir string, signalCliConfig string, numbe
if len(attachmentTmpPaths) > 0 { if len(attachmentTmpPaths) > 0 {
cmd = append(cmd, "-a") cmd = append(cmd, "-a")
cmd = append(cmd , attachmentTmpPaths...) cmd = append(cmd, attachmentTmpPaths...)
} }
_, err := runSignalCli(cmd) _, err := runSignalCli(cmd)
@@ -86,6 +115,66 @@ func send(c *gin.Context, attachmentTmpDir string, signalCliConfig string, numbe
c.JSON(201, nil) c.JSON(201, nil)
} }
func getGroups(number string, signalCliConfig string) ([]GroupEntry, error) {
groupEntries := []GroupEntry{}
out, err := runSignalCli([]string{"--config", signalCliConfig, "-u", number, "listGroups", "-d"})
if err != nil {
return groupEntries, err
}
lines := strings.Split(out, "\n")
for _, line := range lines {
var groupEntry GroupEntry
if line == "" {
continue
}
idIdx := strings.Index(line, " Name: ")
idPair := line[:idIdx]
groupEntry.InternalId = strings.TrimPrefix(idPair, "Id: ")
groupEntry.Id = base64.StdEncoding.EncodeToString([]byte(groupEntry.InternalId))
lineWithoutId := strings.TrimLeft(line[idIdx:], " ")
nameIdx := strings.Index(lineWithoutId, " Active: ")
namePair := lineWithoutId[:nameIdx]
groupEntry.Name = strings.TrimRight(strings.TrimPrefix(namePair, "Name: "), " ")
lineWithoutName := strings.TrimLeft(lineWithoutId[nameIdx:], " ")
activeIdx := strings.Index(lineWithoutName, " Blocked: ")
activePair := lineWithoutName[:activeIdx]
active := strings.TrimPrefix(activePair, "Active: ")
if active == "true" {
groupEntry.Active = true
} else {
groupEntry.Active = false
}
lineWithoutActive := strings.TrimLeft(lineWithoutName[activeIdx:], " ")
blockedIdx := strings.Index(lineWithoutActive, " Members: ")
blockedPair := lineWithoutActive[:blockedIdx]
blocked := strings.TrimPrefix(blockedPair, "Blocked: ")
if blocked == "true" {
groupEntry.Blocked = true
} else {
groupEntry.Blocked = false
}
lineWithoutBlocked := strings.TrimLeft(lineWithoutActive[blockedIdx:], " ")
membersPair := lineWithoutBlocked
members := strings.TrimPrefix(membersPair, "Members: ")
trimmedMembers := strings.TrimRight(strings.TrimLeft(members, "["), "]")
trimmedMembersList := strings.Split(trimmedMembers, ",")
for _, member := range trimmedMembersList {
groupEntry.Members = append(groupEntry.Members, strings.Trim(member, " "))
}
groupEntries = append(groupEntries, groupEntry)
}
return groupEntries, nil
}
func runSignalCli(args []string) (string, error) { func runSignalCli(args []string) (string, error) {
cmd := exec.Command("signal-cli", args...) cmd := exec.Command("signal-cli", args...)
var errBuffer bytes.Buffer var errBuffer bytes.Buffer
@@ -128,16 +217,17 @@ func main() {
router.GET("/v1/about", func(c *gin.Context) { router.GET("/v1/about", func(c *gin.Context) {
type About struct { type About struct {
SupportedApiVersions []string `json:"versions"` SupportedApiVersions []string `json:"versions"`
BuildNr int `json:"build"`
} }
about := About{SupportedApiVersions: []string{"v1", "v2"}} about := About{SupportedApiVersions: []string{"v1", "v2"}, BuildNr: 2}
c.JSON(200, about) c.JSON(200, about)
}) })
router.POST("/v1/register/:number", func(c *gin.Context) { router.POST("/v1/register/:number", func(c *gin.Context) {
number := c.Param("number") number := c.Param("number")
type Request struct{ type Request struct {
UseVoice bool `json:"use_voice"` UseVoice bool `json:"use_voice"`
} }
@@ -189,7 +279,6 @@ func main() {
return return
} }
_, err := runSignalCli([]string{"--config", *signalCliConfig, "-u", number, "verify", token}) _, err := runSignalCli([]string{"--config", *signalCliConfig, "-u", number, "verify", token})
if err != nil { if err != nil {
c.JSON(400, gin.H{"error": err.Error()}) c.JSON(400, gin.H{"error": err.Error()})
@@ -199,11 +288,12 @@ func main() {
}) })
router.POST("/v1/send", func(c *gin.Context) { router.POST("/v1/send", func(c *gin.Context) {
type Request struct{ type Request struct {
Number string `json:"number"` Number string `json:"number"`
Recipients []string `json:"recipients"` Recipients []string `json:"recipients"`
Message string `json:"message"` Message string `json:"message"`
Base64Attachment string `json:"base64_attachment"` Base64Attachment string `json:"base64_attachment"`
IsGroup bool `json:"is_group"`
} }
var req Request var req Request
err := c.BindJSON(&req) err := c.BindJSON(&req)
@@ -217,15 +307,16 @@ func main() {
base64Attachments = append(base64Attachments, req.Base64Attachment) base64Attachments = append(base64Attachments, req.Base64Attachment)
} }
send(c, *signalCliConfig, *signalCliConfig, req.Number, req.Message, req.Recipients, base64Attachments) send(c, *signalCliConfig, *signalCliConfig, req.Number, req.Message, req.Recipients, base64Attachments, req.IsGroup)
}) })
router.POST("/v2/send", func(c *gin.Context) { router.POST("/v2/send", func(c *gin.Context) {
type Request struct{ type Request struct {
Number string `json:"number"` Number string `json:"number"`
Recipients []string `json:"recipients"` Recipients []string `json:"recipients"`
Message string `json:"message"` Message string `json:"message"`
Base64Attachments []string `json:"base64_attachments"` Base64Attachments []string `json:"base64_attachments"`
IsGroup bool `json:"is_group"`
} }
var req Request var req Request
err := c.BindJSON(&req) err := c.BindJSON(&req)
@@ -235,7 +326,7 @@ func main() {
return return
} }
send(c, *attachmentTmpDir, *signalCliConfig, req.Number, req.Message, req.Recipients, req.Base64Attachments) send(c, *attachmentTmpDir, *signalCliConfig, req.Number, req.Message, req.Recipients, req.Base64Attachments, req.IsGroup)
}) })
router.GET("/v1/receive/:number", func(c *gin.Context) { router.GET("/v1/receive/:number", func(c *gin.Context) {
@@ -263,5 +354,68 @@ func main() {
c.String(200, jsonStr) c.String(200, jsonStr)
}) })
router.POST("/v1/groups/:number", func(c *gin.Context) {
number := c.Param("number")
type Request struct {
Name string `json:"name"`
Members []string `json:"members"`
}
var req Request
err := c.BindJSON(&req)
if err != nil {
c.JSON(400, "Couldn't process request - invalid request")
log.Error(err.Error())
return
}
cmd := []string{"--config", *signalCliConfig, "-u", number, "updateGroup", "-n", req.Name, "-m"}
cmd = append(cmd, req.Members...)
log.Info(cmd)
out, err := runSignalCli(cmd)
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
log.Info(out)
})
router.GET("/v1/groups/:number", func(c *gin.Context) {
number := c.Param("number")
groups, err := getGroups(number, *signalCliConfig)
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, groups)
})
router.DELETE("/v1/groups/:number/:groupid", func(c *gin.Context) {
base64EncodedGroupId := c.Param("groupid")
number := c.Param("number")
if base64EncodedGroupId == "" {
c.JSON(400, gin.H{"error": "Please specify a group id"})
return
}
groupId, err := base64.StdEncoding.DecodeString(base64EncodedGroupId)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid group id"})
return
}
_, err = runSignalCli([]string{"--config", *signalCliConfig, "-u", number, "quitGroup", "-g", string(groupId)})
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
})
router.Run() router.Run()
} }