Compare commits

..

9 Commits
0.98 ... master

Author SHA1 Message Date
Bernhard B
5c6fd14944 improved error handling in jsonrpc2-helper 2026-03-22 22:18:16 +01:00
Bernhard B
6ca5ff1aee added expand query parameter to /groups endpoints
see #790
2026-03-21 21:38:49 +01:00
Bernhard B
f142e8089c added new json-rpc-native mode 2026-03-21 19:49:26 +01:00
Bernhard B
d45b906aa9 regenerated swagger documentation 2026-03-21 19:48:10 +01:00
Bernhard B
5435b12e81 Revert "added query parameter 'use_only_uuid_as_identifier' to group(s) GET endpoint"
This reverts commit 44ce4fe83d964f39d6677fa46186bc1126fd069c.
2026-03-19 17:24:30 +01:00
Bernhard B
07260c0811 fixed bug in json-rpc webhook
* only post messages that were received via 'receive'

see #813
2026-03-14 15:54:49 +01:00
Bernhard B
eeacb488ad updated signal-cli-native version 2026-03-13 22:42:36 +01:00
Bernhard B
baca954dcc updated swagger documentation 2026-03-13 21:24:15 +01:00
Bernhard B
44ce4fe83d added query parameter 'use_only_uuid_as_identifier' to group(s) GET endpoint
see #790
2026-03-13 21:18:28 +01:00
11 changed files with 182 additions and 58 deletions

View File

@ -1,6 +1,6 @@
ARG SIGNAL_CLI_VERSION=0.14.1 ARG SIGNAL_CLI_VERSION=0.14.1
ARG LIBSIGNAL_CLIENT_VERSION=0.87.4 ARG LIBSIGNAL_CLIENT_VERSION=0.87.4
ARG SIGNAL_CLI_NATIVE_PACKAGE_VERSION=0.14.1+morph027+1 ARG SIGNAL_CLI_NATIVE_PACKAGE_VERSION=0.14.1+morph027+2
ARG SWAG_VERSION=1.16.4 ARG SWAG_VERSION=1.16.4
ARG GRAALVM_VERSION=25.0.2 ARG GRAALVM_VERSION=25.0.2

View File

@ -58,13 +58,14 @@ The `signal-cli-rest-api` supports three different modes of execution, which can
* **`normal` Mode: (Default)** The `signal-cli` executable is invoked for every REST API request. Being a Java application, each REST call requires a new startup of the JVM (Java Virtual Machine), increasing the latency and hence leading to the slowest mode of operation. * **`normal` Mode: (Default)** The `signal-cli` executable is invoked for every REST API request. Being a Java application, each REST call requires a new startup of the JVM (Java Virtual Machine), increasing the latency and hence leading to the slowest mode of operation.
* **`native` Mode:** A precompiled binary `signal-cli-native` (using GraalVM) is used for every REST API request. This results in a much lower latency & memory usage on each call. On the `armv7` platform this mode is not available and falls back to `normal`. The native mode may also be less stable, due to the experimental state of GraalVM compiler. * **`native` Mode:** A precompiled binary `signal-cli-native` (using GraalVM) is used for every REST API request. This results in a much lower latency & memory usage on each call. On the `armv7` platform this mode is not available and falls back to `normal`. The native mode may also be less stable, due to the experimental state of GraalVM compiler.
* `json-rpc` Mode: A single, JVM-based `signal-cli` instance is spawned as daemon process. This mode is usually the fastest, but requires more memory as the JVM keeps running. * `json-rpc` Mode: A single, JVM-based `signal-cli` instance is spawned as daemon process. This mode is usually the fastest, but requires more memory as the JVM keeps running.
* `json-rpc-native` Mode: Uses the `signal-cli-native` binary and starts it in daemon mode (this mode basically combines the advantages of the `native` mode and the `json-rpc` mode).
| mode | speed | resident memory usage | | mode | speed | resident memory usage |
| ---------: | :------------------------------------------------------- | :-------------------- | | ---------: | :------------------------------------------------------- | :-------------------- |
| `normal` | :heavy_check_mark: | normal | | `normal` | :heavy_check_mark: | normal |
| `native` | :heavy_check_mark: :heavy_check_mark: | normal | | `native` | :heavy_check_mark: :heavy_check_mark: | normal |
| `json-rpc` | :heavy_check_mark: :heavy_check_mark: :heavy_check_mark: | increased | | `json-rpc` | :heavy_check_mark: :heavy_check_mark: :heavy_check_mark: | increased |
| `json-rpc-native` | :heavy_check_mark: :heavy_check_mark: :heavy_check_mark: :heavy_check_mark: | normal |
**Example of running `signal-cli-rest` in `native` mode** **Example of running `signal-cli-rest` in `native` mode**

View File

@ -27,7 +27,7 @@ cap_prefix="-cap_"
caps="$cap_prefix$(seq -s ",$cap_prefix" 0 $(cat /proc/sys/kernel/cap_last_cap))" caps="$cap_prefix$(seq -s ",$cap_prefix" 0 $(cat /proc/sys/kernel/cap_last_cap))"
# TODO: check mode # TODO: check mode
if [ "$MODE" = "json-rpc" ] if [ "$MODE" = "json-rpc" ] || [ "$MODE" = "json-rpc-native" ]
then then
/usr/bin/jsonrpc2-helper /usr/bin/jsonrpc2-helper
if [ -n "$JAVA_OPTS" ] ; then if [ -n "$JAVA_OPTS" ] ; then

View File

@ -1024,6 +1024,7 @@ func (a *Api) RemoveAdminsFromGroup(c *gin.Context) {
// @Success 200 {object} []client.GroupEntry // @Success 200 {object} []client.GroupEntry
// @Failure 400 {object} Error // @Failure 400 {object} Error
// @Param number path string true "Registered Phone Number" // @Param number path string true "Registered Phone Number"
// @Param expand query bool false "Expand the response to show more details (default: false)"
// @Router /v1/groups/{number} [get] // @Router /v1/groups/{number} [get]
func (a *Api) GetGroups(c *gin.Context) { func (a *Api) GetGroups(c *gin.Context) {
number, err := url.PathUnescape(c.Param("number")) number, err := url.PathUnescape(c.Param("number"))
@ -1032,12 +1033,26 @@ func (a *Api) GetGroups(c *gin.Context) {
return return
} }
groups, err := a.signalClient.GetGroups(number) expand := c.DefaultQuery("expand", "false")
if err != nil { if expand != "true" && expand != "false" {
c.JSON(400, Error{Msg: err.Error()}) c.JSON(400, Error{Msg: "Couldn't process request - expand parameter needs to be either 'true' or 'false'"})
return return
} }
var groups any
if StringToBool(expand) {
groups, err = a.signalClient.GetGroupsExpanded(number)
if err != nil {
c.JSON(400, Error{Msg: err.Error()})
return
}
} else {
groups, err = a.signalClient.GetGroups(number)
if err != nil {
c.JSON(400, Error{Msg: err.Error()})
return
}
}
c.JSON(200, groups) c.JSON(200, groups)
} }
@ -1050,6 +1065,7 @@ func (a *Api) GetGroups(c *gin.Context) {
// @Failure 400 {object} Error // @Failure 400 {object} Error
// @Param number path string true "Registered Phone Number" // @Param number path string true "Registered Phone Number"
// @Param groupid path string true "Group ID" // @Param groupid path string true "Group ID"
// @Param expand query bool false "Expand the response to show more details (default: false)"
// @Router /v1/groups/{number}/{groupid} [get] // @Router /v1/groups/{number}/{groupid} [get]
func (a *Api) GetGroup(c *gin.Context) { func (a *Api) GetGroup(c *gin.Context) {
number, err := url.PathUnescape(c.Param("number")) number, err := url.PathUnescape(c.Param("number"))
@ -1059,12 +1075,27 @@ func (a *Api) GetGroup(c *gin.Context) {
} }
groupId := c.Param("groupid") groupId := c.Param("groupid")
groupEntry, err := a.signalClient.GetGroup(number, groupId) expand := c.DefaultQuery("expand", "false")
if err != nil { if expand != "true" && expand != "false" {
c.JSON(400, Error{Msg: err.Error()}) c.JSON(400, Error{Msg: "Couldn't process request - expand parameter needs to be either 'true' or 'false'"})
return return
} }
var groupEntry any
if StringToBool(expand) {
groupEntry, err = a.signalClient.GetGroupExpanded(number, groupId)
if err != nil {
c.JSON(400, Error{Msg: err.Error()})
return
}
} else {
groupEntry, err = a.signalClient.GetGroup(number, groupId)
if err != nil {
c.JSON(400, Error{Msg: err.Error()})
return
}
}
if groupEntry != nil { if groupEntry != nil {
c.JSON(200, groupEntry) c.JSON(200, groupEntry)
} else { } else {

View File

@ -126,6 +126,30 @@ type GroupEntry struct {
Permissions ds.GroupPermissions `json:"permissions"` Permissions ds.GroupPermissions `json:"permissions"`
} }
type GroupMember struct {
Number string `json:"number"`
Uuid string `json:"uuid"`
}
type GroupAdmin struct {
Number string `json:"number"`
Uuid string `json:"uuid"`
}
type ExpandedGroupEntry struct {
Name string `json:"name"`
Description string `json:"description"`
Id string `json:"id"`
InternalId string `json:"internal_id"`
Members []GroupMember `json:"members"`
Blocked bool `json:"blocked"`
PendingInvites []GroupMember `json:"pending_invites"`
PendingRequests []GroupMember `json:"pending_requests"`
InviteLink string `json:"invite_link"`
Admins []GroupAdmin `json:"admins"`
Permissions ds.GroupPermissions `json:"permissions"`
}
type IdentityEntry struct { type IdentityEntry struct {
Number string `json:"number"` Number string `json:"number"`
Status string `json:"status"` Status string `json:"status"`
@ -135,31 +159,21 @@ type IdentityEntry struct {
Uuid string `json:"uuid"` Uuid string `json:"uuid"`
} }
type SignalCliGroupMember struct {
Number string `json:"number"`
Uuid string `json:"uuid"`
}
type SignalCliGroupAdmin struct {
Number string `json:"number"`
Uuid string `json:"uuid"`
}
type SignalCliGroupEntry struct { type SignalCliGroupEntry struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Id string `json:"id"` Id string `json:"id"`
IsMember bool `json:"isMember"` IsMember bool `json:"isMember"`
IsBlocked bool `json:"isBlocked"` IsBlocked bool `json:"isBlocked"`
Members []SignalCliGroupMember `json:"members"` Members []GroupMember `json:"members"`
PendingMembers []SignalCliGroupMember `json:"pendingMembers"` PendingMembers []GroupMember `json:"pendingMembers"`
RequestingMembers []SignalCliGroupMember `json:"requestingMembers"` RequestingMembers []GroupMember `json:"requestingMembers"`
GroupInviteLink string `json:"groupInviteLink"` GroupInviteLink string `json:"groupInviteLink"`
Admins []SignalCliGroupAdmin `json:"admins"` Admins []GroupAdmin `json:"admins"`
Uuid string `json:"uuid"` Uuid string `json:"uuid"`
PermissionEditDetails string `json:"permissionEditDetails"` PermissionEditDetails string `json:"permissionEditDetails"`
PermissionAddMember string `json:"permissionAddMember"` PermissionAddMember string `json:"permissionAddMember"`
PermissionSendMessage string `json:"permissionSendMessage"` PermissionSendMessage string `json:"permissionSendMessage"`
} }
type SignalCliIdentityEntry struct { type SignalCliIdentityEntry struct {
@ -1300,8 +1314,8 @@ func (s *SignalClient) RemoveAdminsFromGroup(number string, groupId string, admi
return s.updateGroupAdmins(number, groupId, admins, false) return s.updateGroupAdmins(number, groupId, admins, false)
} }
func (s *SignalClient) GetGroups(number string) ([]GroupEntry, error) { func (s *SignalClient) GetGroupsExpanded(number string) ([]ExpandedGroupEntry, error) {
groupEntries := []GroupEntry{} groupEntries := []ExpandedGroupEntry{}
var signalCliGroupEntries []SignalCliGroupEntry var signalCliGroupEntries []SignalCliGroupEntry
var err error var err error
@ -1329,7 +1343,7 @@ func (s *SignalClient) GetGroups(number string) ([]GroupEntry, error) {
} }
for _, signalCliGroupEntry := range signalCliGroupEntries { for _, signalCliGroupEntry := range signalCliGroupEntries {
var groupEntry GroupEntry var groupEntry ExpandedGroupEntry
groupEntry.InternalId = signalCliGroupEntry.Id groupEntry.InternalId = signalCliGroupEntry.Id
groupEntry.Name = signalCliGroupEntry.Name groupEntry.Name = signalCliGroupEntry.Name
groupEntry.Id = convertInternalGroupIdToGroupId(signalCliGroupEntry.Id) groupEntry.Id = convertInternalGroupIdToGroupId(signalCliGroupEntry.Id)
@ -1338,9 +1352,32 @@ func (s *SignalClient) GetGroups(number string) ([]GroupEntry, error) {
groupEntry.Permissions.SendMessages = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionSendMessage) groupEntry.Permissions.SendMessages = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionSendMessage)
groupEntry.Permissions.EditGroup = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionSendMessage) groupEntry.Permissions.EditGroup = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionSendMessage)
groupEntry.Permissions.AddMembers = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionAddMember) groupEntry.Permissions.AddMembers = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionAddMember)
groupEntry.Members = signalCliGroupEntry.Members
groupEntry.PendingInvites = signalCliGroupEntry.PendingMembers
groupEntry.PendingRequests = signalCliGroupEntry.RequestingMembers
groupEntry.Admins = signalCliGroupEntry.Admins
groupEntry.InviteLink = signalCliGroupEntry.GroupInviteLink
groupEntries = append(groupEntries, groupEntry)
}
return groupEntries, nil
}
func (s *SignalClient) GetGroups(number string) ([]GroupEntry, error) {
expandedGroupEntries, err := s.GetGroupsExpanded(number)
if err != nil {
return []GroupEntry{}, err
}
groupEntries := []GroupEntry{}
for _, expandedGroupEntry := range expandedGroupEntries {
groupEntry := GroupEntry{InternalId: expandedGroupEntry.InternalId, Name: expandedGroupEntry.Name,
Id: expandedGroupEntry.Id, Blocked: expandedGroupEntry.Blocked, Description: expandedGroupEntry.Description,
Permissions: expandedGroupEntry.Permissions, InviteLink: expandedGroupEntry.InviteLink}
members := []string{} members := []string{}
for _, val := range signalCliGroupEntry.Members { for _, val := range expandedGroupEntry.Members {
identifier := val.Number identifier := val.Number
if identifier == "" { if identifier == "" {
identifier = val.Uuid identifier = val.Uuid
@ -1349,28 +1386,28 @@ func (s *SignalClient) GetGroups(number string) ([]GroupEntry, error) {
} }
groupEntry.Members = members groupEntry.Members = members
pendingMembers := []string{} pendingInvites := []string{}
for _, val := range signalCliGroupEntry.PendingMembers { for _, val := range expandedGroupEntry.PendingInvites {
identifier := val.Number identifier := val.Number
if identifier == "" { if identifier == "" {
identifier = val.Uuid identifier = val.Uuid
} }
pendingMembers = append(pendingMembers, identifier) pendingInvites = append(pendingInvites, identifier)
} }
groupEntry.PendingInvites = pendingMembers groupEntry.PendingInvites = pendingInvites
requestingMembers := []string{} pendingRequests := []string{}
for _, val := range signalCliGroupEntry.RequestingMembers { for _, val := range expandedGroupEntry.PendingRequests {
identifier := val.Number identifier := val.Number
if identifier == "" { if identifier == "" {
identifier = val.Uuid identifier = val.Uuid
} }
requestingMembers = append(requestingMembers, identifier) pendingRequests = append(pendingRequests, identifier)
} }
groupEntry.PendingRequests = requestingMembers groupEntry.PendingRequests = pendingRequests
admins := []string{} admins := []string{}
for _, val := range signalCliGroupEntry.Admins { for _, val := range expandedGroupEntry.Admins {
identifier := val.Number identifier := val.Number
if identifier == "" { if identifier == "" {
identifier = val.Uuid identifier = val.Uuid
@ -1379,8 +1416,6 @@ func (s *SignalClient) GetGroups(number string) ([]GroupEntry, error) {
} }
groupEntry.Admins = admins groupEntry.Admins = admins
groupEntry.InviteLink = signalCliGroupEntry.GroupInviteLink
groupEntries = append(groupEntries, groupEntry) groupEntries = append(groupEntries, groupEntry)
} }
@ -1404,6 +1439,23 @@ func (s *SignalClient) GetGroup(number string, groupId string) (*GroupEntry, err
return nil, nil return nil, nil
} }
func (s *SignalClient) GetGroupExpanded(number string, groupId string) (*ExpandedGroupEntry, error) {
groupEntry := ExpandedGroupEntry{}
groups, err := s.GetGroupsExpanded(number)
if err != nil {
return nil, err
}
for _, group := range groups {
if group.Id == groupId {
groupEntry = group
return &groupEntry, nil
}
}
return nil, nil
}
func (s *SignalClient) GetAvatar(number string, id string, avatarType AvatarType) ([]byte, error) { func (s *SignalClient) GetAvatar(number string, id string, avatarType AvatarType) ([]byte, error) {
var err error var err error
var rawData string var rawData string

View File

@ -233,13 +233,6 @@ func (r *JsonRpc2Client) ReceiveData(number string, receiveWebhookUrl string) {
} }
log.Debug("json-rpc received data: ", str) log.Debug("json-rpc received data: ", str)
if receiveWebhookUrl != "" {
err = postMessageToWebhook(receiveWebhookUrl, []byte(str))
if err != nil {
log.Error("Couldn't post data to webhook: ", err)
}
}
var resp1 JsonRpc2ReceivedMessage var resp1 JsonRpc2ReceivedMessage
json.Unmarshal([]byte(str), &resp1) json.Unmarshal([]byte(str), &resp1)
if resp1.Method == "receive" { if resp1.Method == "receive" {
@ -254,6 +247,13 @@ func (r *JsonRpc2Client) ReceiveData(number string, receiveWebhookUrl string) {
continue continue
} }
r.receivedMessagesMutex.Unlock() r.receivedMessagesMutex.Unlock()
if receiveWebhookUrl != "" {
err = postMessageToWebhook(receiveWebhookUrl, []byte(str))
if err != nil {
log.Error("Couldn't post data to webhook: ", err)
}
}
} }
var resp2 JsonRpc2MessageResponse var resp2 JsonRpc2MessageResponse

View File

@ -917,6 +917,12 @@ const docTemplate = `{
"name": "number", "name": "number",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "boolean",
"description": "Expand the response to show more details (default: false)",
"name": "expand",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -1010,6 +1016,12 @@ const docTemplate = `{
"name": "groupid", "name": "groupid",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "boolean",
"description": "Expand the response to show more details (default: false)",
"name": "expand",
"in": "query"
} }
], ],
"responses": { "responses": {

View File

@ -914,6 +914,12 @@
"name": "number", "name": "number",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "boolean",
"description": "Expand the response to show more details (default: false)",
"name": "expand",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -1007,6 +1013,12 @@
"name": "groupid", "name": "groupid",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "boolean",
"description": "Expand the response to show more details (default: false)",
"name": "expand",
"in": "query"
} }
], ],
"responses": { "responses": {

View File

@ -1163,6 +1163,10 @@ paths:
name: number name: number
required: true required: true
type: string type: string
- description: 'Expand the response to show more details (default: false)'
in: query
name: expand
type: boolean
produces: produces:
- application/json - application/json
responses: responses:
@ -1254,6 +1258,10 @@ paths:
name: groupid name: groupid
required: true required: true
type: string type: string
- description: 'Expand the response to show more details (default: false)'
in: query
name: expand
type: boolean
produces: produces:
- application/json - application/json
responses: responses:

View File

@ -123,7 +123,7 @@ func main() {
mode := utils.GetEnv("MODE", "normal") mode := utils.GetEnv("MODE", "normal")
if mode == "normal" { if mode == "normal" {
signalCliMode = client.Normal signalCliMode = client.Normal
} else if mode == "json-rpc" { } else if mode == "json-rpc" || mode == "json-rpc-native" {
signalCliMode = client.JsonRpc signalCliMode = client.JsonRpc
} else if mode == "native" { } else if mode == "native" {
signalCliMode = client.Native signalCliMode = client.Native

View File

@ -13,7 +13,7 @@ import (
const supervisorctlConfigTemplate = ` const supervisorctlConfigTemplate = `
[program:%s] [program:%s]
process_name=%s process_name=%s
command=signal-cli --output=json --config %s%s daemon %s%s%s%s --tcp 127.0.0.1:%d command=%s --output=json --config %s%s daemon %s%s%s%s --tcp 127.0.0.1:%d
autostart=true autostart=true
autorestart=true autorestart=true
startretries=10 startretries=10
@ -43,6 +43,14 @@ func main() {
jsonRpc2ClientConfig.AddEntry(utils.MULTI_ACCOUNT_NUMBER, utils.JsonRpc2ClientConfigEntry{TcpPort: tcpPort}) jsonRpc2ClientConfig.AddEntry(utils.MULTI_ACCOUNT_NUMBER, utils.JsonRpc2ClientConfigEntry{TcpPort: tcpPort})
signalCliBinary := "signal-cli"
signalMode := utils.GetEnv("MODE", "json-rpc")
if signalMode == "json-rpc-native" {
signalCliBinary = "signal-cli-native"
} else if signalMode != "json-rpc" {
log.Fatal("The mode needs to be either 'json-rpc' or 'json-rpc-native'")
}
signalCliIgnoreAttachments := "" signalCliIgnoreAttachments := ""
ignoreAttachments := utils.GetEnv("JSON_RPC_IGNORE_ATTACHMENTS", "") ignoreAttachments := utils.GetEnv("JSON_RPC_IGNORE_ATTACHMENTS", "")
if ignoreAttachments == "true" { if ignoreAttachments == "true" {
@ -91,7 +99,7 @@ func main() {
//write supervisorctl config //write supervisorctl config
supervisorctlConfigFilename := "/etc/supervisor/conf.d/" + "signal-cli-json-rpc-1.conf" supervisorctlConfigFilename := "/etc/supervisor/conf.d/" + "signal-cli-json-rpc-1.conf"
supervisorctlConfig := fmt.Sprintf(supervisorctlConfigTemplate, supervisorctlProgramName, supervisorctlProgramName, supervisorctlConfig := fmt.Sprintf(supervisorctlConfigTemplate, supervisorctlProgramName, supervisorctlProgramName, signalCliBinary,
signalCliConfigDir, trustNewIdentities, signalCliIgnoreAttachments, signalCliIgnoreStories, signalCliConfigDir, trustNewIdentities, signalCliIgnoreAttachments, signalCliIgnoreStories,
signalCliIgnoreAvatars, signalCliIgnoreStickers, tcpPort, signalCliIgnoreAvatars, signalCliIgnoreStickers, tcpPort,
supervisorctlProgramName, supervisorctlProgramName) supervisorctlProgramName, supervisorctlProgramName)