diff --git a/src/api/api.go b/src/api/api.go index ffcfcd0..becf036 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -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 diff --git a/src/client/client.go b/src/client/client.go index 61050a5..6880e66 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -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 } diff --git a/src/docs/docs.go b/src/docs/docs.go index 380e0d4..414efc4 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -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.", diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 5fb4b44..0421024 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -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.", diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index d4420c6..5f6e9ba 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -903,6 +903,44 @@ paths: contact doesn’t 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: diff --git a/src/main.go b/src/main.go index 499b605..0303960 100644 --- a/src/main.go +++ b/src/main.go @@ -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) }