mirror of
https://github.com/aljazceru/signal-cli-rest-api.git
synced 2025-12-20 16:14:29 +01:00
Merge branch 'groups'
This commit is contained in:
36
README.md
36
README.md
@@ -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'```
|
||||
|
||||
* 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**
|
||||
|
||||
@@ -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'```
|
||||
|
||||
|
||||
## 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
|
||||
In case you've problems with the `signal-cli-rest-api` container, have a look [here](TROUBLESHOOTING.md)
|
||||
|
||||
23
publish.sh
23
publish.sh
@@ -1,10 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
while getopts v: option
|
||||
while getopts v:t: option
|
||||
do
|
||||
case "${option}"
|
||||
in
|
||||
v) VERSION=${OPTARG};;
|
||||
t) TAG=${OPTARG};;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -14,8 +15,20 @@ then
|
||||
exit 1
|
||||
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 "Version: $VERSION"
|
||||
echo "Tag: $TAG"
|
||||
echo ""
|
||||
|
||||
read -r -p "Are you sure? [y/N] " response
|
||||
@@ -28,8 +41,16 @@ case "$response" in
|
||||
docker buildx create --name multibuilder
|
||||
docker buildx use multibuilder
|
||||
|
||||
if [[ "$TAG" == "stable" ]]; then
|
||||
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"
|
||||
|
||||
190
src/main.go
190
src/main.go
@@ -1,32 +1,61 @@
|
||||
package main
|
||||
|
||||
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"
|
||||
"os"
|
||||
"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"
|
||||
)
|
||||
|
||||
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) {
|
||||
for _, path := range paths {
|
||||
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}
|
||||
|
||||
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{}
|
||||
for _, base64Attachment := range base64Attachments {
|
||||
@@ -74,7 +103,7 @@ func send(c *gin.Context, attachmentTmpDir string, signalCliConfig string, numbe
|
||||
|
||||
if len(attachmentTmpPaths) > 0 {
|
||||
cmd = append(cmd, "-a")
|
||||
cmd = append(cmd , attachmentTmpPaths...)
|
||||
cmd = append(cmd, attachmentTmpPaths...)
|
||||
}
|
||||
|
||||
_, err := runSignalCli(cmd)
|
||||
@@ -86,6 +115,66 @@ func send(c *gin.Context, attachmentTmpDir string, signalCliConfig string, numbe
|
||||
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) {
|
||||
cmd := exec.Command("signal-cli", args...)
|
||||
var errBuffer bytes.Buffer
|
||||
@@ -128,16 +217,17 @@ func main() {
|
||||
router.GET("/v1/about", func(c *gin.Context) {
|
||||
type About struct {
|
||||
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)
|
||||
})
|
||||
|
||||
router.POST("/v1/register/:number", func(c *gin.Context) {
|
||||
number := c.Param("number")
|
||||
|
||||
type Request struct{
|
||||
type Request struct {
|
||||
UseVoice bool `json:"use_voice"`
|
||||
}
|
||||
|
||||
@@ -189,7 +279,6 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
_, err := runSignalCli([]string{"--config", *signalCliConfig, "-u", number, "verify", token})
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
@@ -199,11 +288,12 @@ func main() {
|
||||
})
|
||||
|
||||
router.POST("/v1/send", func(c *gin.Context) {
|
||||
type Request struct{
|
||||
type Request struct {
|
||||
Number string `json:"number"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Message string `json:"message"`
|
||||
Base64Attachment string `json:"base64_attachment"`
|
||||
IsGroup bool `json:"is_group"`
|
||||
}
|
||||
var req Request
|
||||
err := c.BindJSON(&req)
|
||||
@@ -217,15 +307,16 @@ func main() {
|
||||
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) {
|
||||
type Request struct{
|
||||
type Request struct {
|
||||
Number string `json:"number"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Message string `json:"message"`
|
||||
Base64Attachments []string `json:"base64_attachments"`
|
||||
IsGroup bool `json:"is_group"`
|
||||
}
|
||||
var req Request
|
||||
err := c.BindJSON(&req)
|
||||
@@ -235,7 +326,7 @@ func main() {
|
||||
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) {
|
||||
@@ -263,5 +354,68 @@ func main() {
|
||||
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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user