added two new endpoints

* added endpoint to list a contact
* added endpoint to get a contact's avatar
This commit is contained in:
Bernhard B 2025-09-30 22:31:10 +02:00
parent 3a2b77b31f
commit 08cd2bd12c
6 changed files with 294 additions and 16 deletions

View File

@ -1014,7 +1014,7 @@ func (a *Api) GetGroupAvatar(c *gin.Context) {
}
groupId := c.Param("groupid")
groupAvatar, err := a.signalClient.GetGroupAvatar(number, groupId)
groupAvatar, err := a.signalClient.GetAvatar(number, groupId, client.GroupAvatar)
if err != nil {
switch err.(type) {
case *client.NotFoundError:
@ -2311,6 +2311,81 @@ func (a *Api) ListContacts(c *gin.Context) {
c.JSON(200, contacts)
}
// @Summary List Contact
// @Tags Contacts
// @Description List a specific contact.
// @Produce json
// @Success 200 {object} client.ListContactsResponse
// @Param number path string true "Registered Phone Number"
// @Router /v1/contacts/{number}/{uuid} [get]
func (a *Api) ListContact(c *gin.Context) {
number, err := url.PathUnescape(c.Param("number"))
if err != nil {
c.JSON(400, Error{Msg: "Couldn't process request - malformed number"})
return
}
if number == "" {
c.JSON(400, Error{Msg: "Couldn't process request - number missing"})
return
}
uuid := c.Param("uuid")
if uuid == "" {
c.JSON(400, Error{Msg: "Couldn't process request - uuid missing"})
return
}
contact, err := a.signalClient.ListContact(number, uuid)
if err != nil {
switch err.(type) {
case *client.NotFoundError:
c.JSON(404, Error{Msg: err.Error()})
return
default:
c.JSON(400, Error{Msg: err.Error()})
return
}
}
c.JSON(200, contact)
}
// @Summary Returns the avatar of a contact
// @Tags Contacts
// @Description Returns the avatar of a contact.
// @Produce json
// @Success 200 {string} string "Image"
// @Param number path string true "Registered Phone Number"
// @Router /v1/contacts/{number}/{uuid}/avatar [get]
func (a *Api) GetProfileAvatar(c *gin.Context) {
number, err := url.PathUnescape(c.Param("number"))
if err != nil {
c.JSON(400, Error{Msg: "Couldn't process request - malformed number"})
return
}
if number == "" {
c.JSON(400, Error{Msg: "Couldn't process request - number missing"})
return
}
uuid := c.Param("uuid")
if uuid == "" {
c.JSON(400, Error{Msg: "Couldn't process request - uuid missing"})
return
}
avatar, err := a.signalClient.GetAvatar(number, uuid, client.ProfileAvatar)
if err != nil {
c.JSON(400, Error{Msg: err.Error()})
return
}
mimeType := mimetype.Detect(avatar)
c.Data(200, mimeType.String(), avatar)
}
// @Summary Set Pin
// @Tags Accounts
// @Description Sets a new Signal Pin

View File

@ -29,6 +29,14 @@ const signalCliV2GroupError = "Cannot create a V2 group as self does not have a
const endpointNotSupportedInJsonRpcMode = "This endpoint is not supported in JSON-RPC mode."
type AvatarType int
const (
GroupAvatar AvatarType = iota + 1
ContactAvatar
ProfileAvatar
)
type GroupPermission int
const (
@ -1323,21 +1331,33 @@ func (s *SignalClient) GetGroup(number string, groupId string) (*GroupEntry, err
return nil, nil
}
func (s *SignalClient) GetGroupAvatar(number string, groupId string) ([]byte, error) {
func (s *SignalClient) GetAvatar(number string, id string, avatarType AvatarType) ([]byte, error) {
var err error
var rawData string
internalGroupId, err := ConvertGroupIdToInternalGroupId(groupId)
if err != nil {
return []byte{}, errors.New("Invalid group id")
if avatarType == GroupAvatar {
id, err = ConvertGroupIdToInternalGroupId(id)
if err != nil {
return []byte{}, errors.New("Invalid group id")
}
}
if s.signalCliMode == JsonRpc {
type Request struct {
GroupId string `json:"groupId"`
GroupId string `json:"groupId,omitempty"`
Contact string `json:"contact,omitempty"`
Profile string `json:"profile,omitempty"`
}
request := Request{GroupId: internalGroupId}
var request Request
if avatarType == GroupAvatar {
request.GroupId = id
} else if avatarType == ContactAvatar {
request.Contact = id
} else if avatarType == ProfileAvatar {
request.Profile = id
}
jsonRpc2Client, err := s.getJsonRpc2Client()
if err != nil {
@ -1351,8 +1371,21 @@ func (s *SignalClient) GetGroupAvatar(number string, groupId string) ([]byte, er
return []byte{}, err
}
} else {
rawData, err = s.cliClient.Execute(true, []string{"--config", s.signalCliConfig, "-o", "json", "-a", number, "getAvatar", "-g", internalGroupId}, "")
cmd := []string{"--config", s.signalCliConfig, "-o", "json", "-a", number, "getAvatar"}
if avatarType == GroupAvatar {
cmd = append(cmd, []string{"-g", id}...)
} else if avatarType == ContactAvatar {
cmd = append(cmd, []string{"--contact", id}...)
} else if avatarType == ProfileAvatar {
cmd = append(cmd, []string{"--profile", id}...)
}
rawData, err = s.cliClient.Execute(true, cmd, "")
if err != nil {
if strings.Contains(err.Error(), "Could not find avatar") {
return []byte{}, &NotFoundError{Description: "No avatar found."}
}
return []byte{}, err
}
}
@ -2528,6 +2561,21 @@ func (s *SignalClient) ListContacts(number string) ([]ListContactsResponse, erro
return resp, nil
}
func (s *SignalClient) ListContact(number string, uuid string) (ListContactsResponse, error) {
contacts, err := s.ListContacts(number)
if err != nil {
return ListContactsResponse{}, err
}
for _, contact := range contacts {
if contact.Uuid == uuid {
return contact, nil
}
}
return ListContactsResponse{}, &NotFoundError{Description: "No contact with that id (" + uuid + ") found"}
}
func (s *SignalClient) SetPin(number string, registrationLockPin string) error {
if s.signalCliMode == JsonRpc {
type Request struct {
@ -2544,11 +2592,10 @@ func (s *SignalClient) SetPin(number string, registrationLockPin string) error {
}
} else {
cmd := []string{"--config", s.signalCliConfig, "-o", "json", "-a", number, "setPin", registrationLockPin}
rawData, err := s.cliClient.Execute(true, cmd, "")
_, err := s.cliClient.Execute(true, cmd, "")
if err != nil {
return err
}
log.Info(string(rawData))
}
return nil
}

View File

@ -665,6 +665,64 @@ const docTemplate = `{
}
}
},
"/v1/contacts/{number}/{uuid}": {
"get": {
"description": "List a specific contact.",
"produces": [
"application/json"
],
"tags": [
"Contacts"
],
"summary": "List Contact",
"parameters": [
{
"type": "string",
"description": "Registered Phone Number",
"name": "number",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/client.ListContactsResponse"
}
}
}
}
},
"/v1/contacts/{number}/{uuid}/avatar": {
"get": {
"description": "Returns the avatar of a contact.",
"produces": [
"application/json"
],
"tags": [
"Contacts"
],
"summary": "Returns the avatar of a contact",
"parameters": [
{
"type": "string",
"description": "Registered Phone Number",
"name": "number",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Image",
"schema": {
"type": "string"
}
}
}
}
},
"/v1/devices/{number}": {
"get": {
"description": "List linked devices associated to this device.",

View File

@ -662,6 +662,64 @@
}
}
},
"/v1/contacts/{number}/{uuid}": {
"get": {
"description": "List a specific contact.",
"produces": [
"application/json"
],
"tags": [
"Contacts"
],
"summary": "List Contact",
"parameters": [
{
"type": "string",
"description": "Registered Phone Number",
"name": "number",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/client.ListContactsResponse"
}
}
}
}
},
"/v1/contacts/{number}/{uuid}/avatar": {
"get": {
"description": "Returns the avatar of a contact.",
"produces": [
"application/json"
],
"tags": [
"Contacts"
],
"summary": "Returns the avatar of a contact",
"parameters": [
{
"type": "string",
"description": "Registered Phone Number",
"name": "number",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Image",
"schema": {
"type": "string"
}
}
}
}
},
"/v1/devices/{number}": {
"get": {
"description": "List linked devices associated to this device.",

View File

@ -903,6 +903,44 @@ paths:
contact doesnt exist yet, it will be added.
tags:
- Contacts
/v1/contacts/{number}/{uuid}:
get:
description: List a specific contact.
parameters:
- description: Registered Phone Number
in: path
name: number
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/client.ListContactsResponse'
summary: List Contact
tags:
- Contacts
/v1/contacts/{number}/{uuid}/avatar:
get:
description: Returns the avatar of a contact.
parameters:
- description: Registered Phone Number
in: path
name: number
required: true
type: string
produces:
- application/json
responses:
"200":
description: Image
schema:
type: string
summary: Returns the avatar of a contact
tags:
- Contacts
/v1/contacts/{number}/sync:
post:
consumes:

View File

@ -3,6 +3,12 @@ package main
import (
"encoding/json"
"flag"
"io/ioutil"
"net/http"
"os"
"plugin"
"strconv"
"github.com/bbernhard/signal-cli-rest-api/api"
"github.com/bbernhard/signal-cli-rest-api/client"
docs "github.com/bbernhard/signal-cli-rest-api/docs"
@ -12,11 +18,6 @@ import (
log "github.com/sirupsen/logrus"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"io/ioutil"
"net/http"
"os"
"strconv"
"plugin"
)
// @title Signal Cli REST API
@ -69,7 +70,6 @@ func main() {
avatarTmpDir := flag.String("avatar-tmp-dir", "/tmp/", "Avatar tmp directory")
flag.Parse()
logLevel := utils.GetEnv("LOG_LEVEL", "")
if logLevel != "" {
err := utils.SetLogLevel(logLevel)
@ -305,6 +305,8 @@ func main() {
{
contacts.GET(":number", api.ListContacts)
contacts.PUT(":number", api.UpdateContact)
contacts.GET(":number/:uuid", api.ListContact)
contacts.GET(":number/:uuid/avatar", api.GetProfileAvatar)
contacts.POST(":number/sync", api.SendContacts)
}