From 7711ad5503f8cee37fac0170f0b11f20b24006e4 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Sat, 24 Jan 2026 17:37:20 +0100 Subject: [PATCH] added REST API endpoints for Signal Polls see #765 --- src/api/api.go | 204 ++++++++++++++++++++++++- src/client/client.go | 247 ++++++++++++++++++++++++++++++ src/docs/docs.go | 343 ++++++++++++++++++++++++++++++++++++++++++ src/docs/swagger.json | 343 ++++++++++++++++++++++++++++++++++++++++++ src/docs/swagger.yaml | 231 ++++++++++++++++++++++++++++ src/main.go | 7 + 6 files changed, 1374 insertions(+), 1 deletion(-) diff --git a/src/api/api.go b/src/api/api.go index 547cf04..07cc3bd 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -225,7 +225,30 @@ type DeleteLocalAccountDataRequest struct { } type DeviceLinkUriResponse struct { - DeviceLinkUri string `json:"device_link_uri"` + DeviceLinkUri string `json:"device_link_uri"` +} + +type CreatePollRequest struct { + Recipient string `json:"recipient" example:" OR OR "` + Question string `json:"question" example:"What's your favourite fruit?"` + Answers []string `json:"answers" example:"apple,banana,orange"` + AllowMultipleSelections *bool `json:"allow_multiple_selections" example:"true"` +} + +type CreatePollResponse struct { + Timestamp string `json:"timestamp" example:"1769271479"` +} + +type VoteRequest struct { + Recipient string `json:"recipient" example:" OR OR "` + PollAuthor string `json:"poll_author" example:" OR "` + PollTimestamp string `json:"poll_timestamp" example:"1769271479"` + SelectedAnswers []int32 `json:"selected_answers" example:"1"` +} + +type ClosePollRequest struct { + Recipient string `json:"recipient" example:" OR OR "` + PollTimestamp string `json:"poll_timestamp" example:"1769271479"` } type Api struct { @@ -2591,3 +2614,182 @@ func (a *Api) RemoteDelete(c *gin.Context) { } c.JSON(201, RemoteDeleteResponse{Timestamp: strconv.FormatInt(timestamp.Timestamp, 10)}) } + +// @Summary Create a new poll. +// @Tags Polls +// @Description Create a new poll +// @Accept json +// @Produce json +// @Success 201 {object} CreatePollResponse +// @Failure 400 {object} Error +// @Param number path string true "Registered Phone Number" +// @Param data body CreatePollRequest true "Type" +// @Router /v1/polls/{number} [post] +func (a *Api) CreatePoll(c *gin.Context) { + var req CreatePollRequest + err := c.BindJSON(&req) + if err != nil { + c.JSON(400, Error{Msg: "Couldn't process request - invalid request"}) + return + } + + 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 + } + + if req.Recipient == "" { + c.JSON(400, Error{Msg: "Couldn't process request - recipient missing"}) + return + } + + if req.Question == "" { + c.JSON(400, Error{Msg: "Couldn't process request - question missing"}) + return + } + + if len(req.Answers) == 0 { + c.JSON(400, Error{Msg: "Couldn't process request - answers missing"}) + return + } + + allowMultipleSelections := true + if req.AllowMultipleSelections != nil { + allowMultipleSelections = *req.AllowMultipleSelections + } + + timestamp, err := a.signalClient.CreatePoll(number, req.Recipient, req.Question, req.Answers, allowMultipleSelections) + if err != nil { + c.JSON(400, Error{Msg: err.Error()}) + return + } + c.JSON(201, CreatePollResponse{Timestamp: timestamp}) +} + +// @Summary Answer a poll. +// @Tags Polls +// @Description Answer a poll +// @Accept json +// @Produce json +// @Success 204 +// @Failure 400 {object} Error +// @Param number path string true "Registered Phone Number" +// @Param data body VoteRequest true "Type" +// @Router /v1/polls/{number}/vote [post] +func (a *Api) VoteInPoll(c *gin.Context) { + var req VoteRequest + err := c.BindJSON(&req) + if err != nil { + c.JSON(400, Error{Msg: "Couldn't process request - invalid request"}) + return + } + + 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 + } + + if req.PollTimestamp == "" { + c.JSON(400, Error{Msg: "Couldn't process request - poll_timestamp missing"}) + return + } + + if req.PollAuthor == "" { + c.JSON(400, Error{Msg: "Couldn't process request - poll_author missing"}) + return + } + + if len(req.SelectedAnswers) == 0 { + c.JSON(400, Error{Msg: "Couldn't process request - selected_answers missing"}) + return + } + + if req.Recipient == "" { + c.JSON(400, Error{Msg: "Couldn't process request - recipient missing"}) + return + } + + pollTimestamp, err := strconv.ParseInt(req.PollTimestamp, 10, 64) + if err != nil { + c.JSON(400, Error{Msg: "Couldn't process request - invalid timestamp"}) + return + } + + for _, index := range req.SelectedAnswers { + if index <= 0 { + c.JSON(400, Error{Msg: "Invalid index in selected_answers specified, index needs to be >= 1!"}) + return + } + } + + err = a.signalClient.VoteInPoll(number, req.Recipient, req.PollAuthor, pollTimestamp, req.SelectedAnswers) + if err != nil { + c.JSON(400, Error{Msg: err.Error()}) + return + } + + c.Status(204) +} + +// @Summary Close a poll. +// @Tags Polls +// @Description Close a poll +// @Accept json +// @Produce json +// @Success 204 +// @Failure 400 {object} Error +// @Param number path string true "Registered Phone Number" +// @Param data body ClosePollRequest true "Type" +// @Router /v1/polls/{number} [delete] +func (a *Api) ClosePoll(c *gin.Context) { + var req ClosePollRequest + err := c.BindJSON(&req) + if err != nil { + c.JSON(400, Error{Msg: "Couldn't process request - invalid request"}) + return + } + + 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 + } + + if req.PollTimestamp == "" { + c.JSON(400, Error{Msg: "Couldn't process request - poll_timestamp missing"}) + return + } + + if req.Recipient == "" { + c.JSON(400, Error{Msg: "Couldn't process request - recipient missing"}) + return + } + + pollTimestamp, err := strconv.ParseInt(req.PollTimestamp, 10, 64) + if err != nil { + c.JSON(400, Error{Msg: "Couldn't process request - invalid timestamp"}) + return + } + + err = a.signalClient.ClosePoll(number, req.Recipient, pollTimestamp) + if err != nil { + c.JSON(400, Error{Msg: err.Error()}) + return + } + + c.Status(204) +} diff --git a/src/client/client.go b/src/client/client.go index 6fba39b..84337ed 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -2800,3 +2800,250 @@ func (s *SignalClient) RemoteDelete(number string, recipient string, timestamp i return resp, err } } + +func (s *SignalClient) CreatePoll(number string, recipient string, question string, answers []string, allowMultipleSelections bool) (string, error) { + var err error + var rawData string + + type Response struct { + Timestamp int64 `json:"timestamp"` + } + + recp := recipient + recipientType, err := getRecipientType(recipient) + if err != nil { + return "", err + } + + if recipientType == ds.Group { + recp, err = ConvertGroupIdToInternalGroupId(recipient) + if err != nil { + return "", errors.New("Invalid group id") + } + } else if recipientType != ds.Number && recipientType != ds.Username { + return "", errors.New("Invalid recipient type") + } + + if s.signalCliMode == JsonRpc { + type Request struct { + Recipient string `json:"recipient,omitempty"` + GroupId string `json:"group-id,omitempty"` + Username string `json:"username,omitempty"` + Question string `json:"question"` + Option []string `json:"option"` + NoMulti bool `json:"no-multi"` + } + + req := Request{Question: question, Option: answers, NoMulti: !allowMultipleSelections} + + if recipientType == ds.Number { + req.Recipient = recp + } else if recipientType == ds.Group { + req.GroupId = recp + } else if recipientType == ds.Username { + req.Username = recp + } + + jsonRpc2Client, err := s.getJsonRpc2Client() + if err != nil { + return "", err + } + + rawData, err = jsonRpc2Client.getRaw("sendPollCreate", &number, req) + if err != nil { + return "", err + } + + } else { + cmd := []string{ + "--config", s.signalCliConfig, + "-a", number, + "-o", "json", + "sendPollCreate", + } + + if recipientType == ds.Number { + cmd = append(cmd, recp) + } else if recipientType == ds.Group { + cmd = append(cmd, "-g", recp) + } else if recipientType == ds.Username { + cmd = append(cmd, "-u", recp) + } + + cmd = append(cmd, "-q", question, "-o") + cmd = append(cmd, answers...) + + if !allowMultipleSelections { + cmd = append(cmd, "--no-multi") + } + + rawData, err = s.cliClient.Execute(true, cmd, "") + if err != nil { + return "", err + } + } + + var resp Response + err = json.Unmarshal([]byte(rawData), &resp) + if err != nil { + return "", errors.New("Couldn't process request - invalid signal-cli response") + } + + return strconv.FormatInt(resp.Timestamp, 10), nil +} + +func (s *SignalClient) VoteInPoll(number string, recipient string, pollAuthor string, pollTimestamp int64, selectedAnswers []int32) error { + var err error + + recp := recipient + recipientType, err := getRecipientType(recipient) + if err != nil { + return err + } + + if recipientType == ds.Group { + recp, err = ConvertGroupIdToInternalGroupId(recipient) + if err != nil { + return errors.New("Invalid group id") + } + } else if recipientType != ds.Number && recipientType != ds.Username { + return errors.New("Invalid recipient type") + } + + // the REST API requires the selected answers indexes to start at 1. + // signal-cli however starts with 0, so we need to correct them + signalCliSelectedAnswers := []int32{} + for _, selectedAnswer := range selectedAnswers { + signalCliSelectedAnswers = append(signalCliSelectedAnswers, selectedAnswer-1) + } + + if s.signalCliMode == JsonRpc { + type Request struct { + Recipient string `json:"recipient,omitempty"` + GroupId string `json:"group-id,omitempty"` + Username string `json:"username,omitempty"` + PollAuthor string `json:"poll-author"` + PollTimestamp int64 `json:"poll-timestamp"` + SelectedAnswers []int32 `json:"option"` + VoteCount int32 `json:"vote-count"` + } + req := Request{PollAuthor: pollAuthor, PollTimestamp: pollTimestamp, SelectedAnswers: signalCliSelectedAnswers, VoteCount: 1} + + if recipientType == ds.Number { + req.Recipient = recp + } else if recipientType == ds.Group { + req.GroupId = recp + } else if recipientType == ds.Username { + req.Username = recp + } + + jsonRpc2Client, err := s.getJsonRpc2Client() + if err != nil { + return err + } + + _, err = jsonRpc2Client.getRaw("sendPollVote", &number, req) + if err != nil { + return err + } + return nil + } else { + cmd := []string{ + "--config", s.signalCliConfig, + "-a", number, + "-o", "json", + "sendPollVote", + } + + if recipientType == ds.Number { + cmd = append(cmd, recp) + } else if recipientType == ds.Group { + cmd = append(cmd, "-g", recp) + } else if recipientType == ds.Username { + cmd = append(cmd, "-u", recp) + } + + cmd = append(cmd, "--poll-author", pollAuthor, "--poll-timestamp", strconv.FormatInt(pollTimestamp, 10), "--option") + for _, val := range signalCliSelectedAnswers { + cmd = append(cmd, strconv.Itoa(int(val))) + } + + cmd = append(cmd, "--vote-count", "1") + + _, err = s.cliClient.Execute(true, cmd, "") + if err != nil { + return err + } + return nil + } +} + +func (s *SignalClient) ClosePoll(number string, recipient string, pollTimestamp int64) error { + var err error + + recp := recipient + recipientType, err := getRecipientType(recipient) + if err != nil { + return err + } + + if recipientType == ds.Group { + recp, err = ConvertGroupIdToInternalGroupId(recipient) + if err != nil { + return errors.New("Invalid group id") + } + } else if recipientType != ds.Number && recipientType != ds.Username { + return errors.New("Invalid recipient type") + } + + if s.signalCliMode == JsonRpc { + type Request struct { + Recipient string `json:"recipient,omitempty"` + GroupId string `json:"group-id,omitempty"` + Username string `json:"username,omitempty"` + PollTimestamp int64 `json:"poll-timestamp"` + } + req := Request{PollTimestamp: pollTimestamp} + + if recipientType == ds.Number { + req.Recipient = recp + } else if recipientType == ds.Group { + req.GroupId = recp + } else if recipientType == ds.Username { + req.Username = recp + } + + jsonRpc2Client, err := s.getJsonRpc2Client() + if err != nil { + return err + } + + _, err = jsonRpc2Client.getRaw("sendPollTerminate", &number, req) + if err != nil { + return err + } + return nil + } else { + cmd := []string{ + "--config", s.signalCliConfig, + "-a", number, + "-o", "json", + "sendPollTerminate", + } + + if recipientType == ds.Number { + cmd = append(cmd, recp) + } else if recipientType == ds.Group { + cmd = append(cmd, "-g", recp) + } else if recipientType == ds.Username { + cmd = append(cmd, "-u", recp) + } + + cmd = append(cmd, "--poll-timestamp", strconv.FormatInt(pollTimestamp, 10)) + _, err = s.cliClient.Execute(true, cmd, "") + if err != nil { + return err + } + return nil + } +} diff --git a/src/docs/docs.go b/src/docs/docs.go index 414efc4..f4fdbf7 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -806,6 +806,85 @@ const docTemplate = `{ } } }, + "/v1/devices/{number}/local-data": { + "delete": { + "description": "Delete all local data for the specified account. Only use this after unregistering the account or after removing a linked device.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Devices" + ], + "summary": "Delete local account data", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Cleanup options", + "name": "data", + "in": "body", + "schema": { + "$ref": "#/definitions/api.DeleteLocalAccountDataRequest" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/devices/{number}/{deviceId}": { + "delete": { + "description": "Remove a linked device from the primary account.", + "tags": [ + "Devices" + ], + "summary": "Remove linked device", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Device ID from listDevices", + "name": "deviceId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, "/v1/groups/{number}": { "get": { "description": "List all Signal Groups.", @@ -1521,6 +1600,139 @@ const docTemplate = `{ } } }, + "/v1/polls/{number}": { + "post": { + "description": "Create a new poll", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Polls" + ], + "summary": "Create a new poll.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Type", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.CreatePollRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/api.CreatePollResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + }, + "delete": { + "description": "Close a poll", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Polls" + ], + "summary": "Close a poll.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Type", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.ClosePollRequest" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/polls/{number}/vote": { + "post": { + "description": "Answer a poll", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Polls" + ], + "summary": "Answer a poll.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Type", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.VoteRequest" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, "/v1/profiles/{number}": { "put": { "description": "Set your name and optional an avatar.", @@ -1606,6 +1818,41 @@ const docTemplate = `{ } } }, + "/v1/qrcodelink/raw": { + "get": { + "description": "Generate the deviceLinkUri string for linking without scanning a QR code.", + "produces": [ + "application/json" + ], + "tags": [ + "Devices" + ], + "summary": "Get raw device link URI", + "parameters": [ + { + "type": "string", + "description": "Device Name", + "name": "device_name", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.DeviceLinkUriResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, "/v1/reactions/{number}": { "post": { "description": "React to a message", @@ -2358,6 +2605,19 @@ const docTemplate = `{ } } }, + "api.ClosePollRequest": { + "type": "object", + "properties": { + "poll_timestamp": { + "type": "string", + "example": "1769271479" + }, + "recipient": { + "type": "string", + "example": "\u003cphone number\u003e OR \u003cusername\u003e OR \u003cgroup id\u003e" + } + } + }, "api.Configuration": { "type": "object", "properties": { @@ -2405,6 +2665,60 @@ const docTemplate = `{ } } }, + "api.CreatePollRequest": { + "type": "object", + "properties": { + "allow_multiple_selections": { + "type": "boolean", + "example": true + }, + "answers": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "apple", + "banana", + "orange" + ] + }, + "question": { + "type": "string", + "example": "What's your favourite fruit?" + }, + "recipient": { + "type": "string", + "example": "\u003cphone number\u003e OR \u003cusername\u003e OR \u003cgroup id\u003e" + } + } + }, + "api.CreatePollResponse": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "example": "1769271479" + } + } + }, + "api.DeleteLocalAccountDataRequest": { + "type": "object", + "properties": { + "ignore_registered": { + "type": "boolean", + "example": false + } + } + }, + "api.DeviceLinkUriResponse": { + "type": "object", + "properties": { + "device_link_uri": { + "type": "string" + } + } + }, "api.Error": { "type": "object", "properties": { @@ -2797,6 +3111,32 @@ const docTemplate = `{ } } }, + "api.VoteRequest": { + "type": "object", + "properties": { + "poll_author": { + "type": "string", + "example": "\u003cphone number\u003e OR \u003cuuid\u003e" + }, + "poll_timestamp": { + "type": "string", + "example": "1769271479" + }, + "recipient": { + "type": "string", + "example": "\u003cphone number\u003e OR \u003cusername\u003e OR \u003cgroup id\u003e" + }, + "selected_answers": { + "type": "array", + "items": { + "type": "integer" + }, + "example": [ + 1 + ] + } + } + }, "client.About": { "type": "object", "properties": { @@ -2963,6 +3303,9 @@ const docTemplate = `{ "creation_timestamp": { "type": "integer" }, + "id": { + "type": "integer" + }, "last_seen_timestamp": { "type": "integer" }, diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 0421024..e509ae1 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -803,6 +803,85 @@ } } }, + "/v1/devices/{number}/local-data": { + "delete": { + "description": "Delete all local data for the specified account. Only use this after unregistering the account or after removing a linked device.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Devices" + ], + "summary": "Delete local account data", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Cleanup options", + "name": "data", + "in": "body", + "schema": { + "$ref": "#/definitions/api.DeleteLocalAccountDataRequest" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/devices/{number}/{deviceId}": { + "delete": { + "description": "Remove a linked device from the primary account.", + "tags": [ + "Devices" + ], + "summary": "Remove linked device", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Device ID from listDevices", + "name": "deviceId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, "/v1/groups/{number}": { "get": { "description": "List all Signal Groups.", @@ -1518,6 +1597,139 @@ } } }, + "/v1/polls/{number}": { + "post": { + "description": "Create a new poll", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Polls" + ], + "summary": "Create a new poll.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Type", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.CreatePollRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/api.CreatePollResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + }, + "delete": { + "description": "Close a poll", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Polls" + ], + "summary": "Close a poll.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Type", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.ClosePollRequest" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/polls/{number}/vote": { + "post": { + "description": "Answer a poll", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Polls" + ], + "summary": "Answer a poll.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Type", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.VoteRequest" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, "/v1/profiles/{number}": { "put": { "description": "Set your name and optional an avatar.", @@ -1603,6 +1815,41 @@ } } }, + "/v1/qrcodelink/raw": { + "get": { + "description": "Generate the deviceLinkUri string for linking without scanning a QR code.", + "produces": [ + "application/json" + ], + "tags": [ + "Devices" + ], + "summary": "Get raw device link URI", + "parameters": [ + { + "type": "string", + "description": "Device Name", + "name": "device_name", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.DeviceLinkUriResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, "/v1/reactions/{number}": { "post": { "description": "React to a message", @@ -2355,6 +2602,19 @@ } } }, + "api.ClosePollRequest": { + "type": "object", + "properties": { + "poll_timestamp": { + "type": "string", + "example": "1769271479" + }, + "recipient": { + "type": "string", + "example": "\u003cphone number\u003e OR \u003cusername\u003e OR \u003cgroup id\u003e" + } + } + }, "api.Configuration": { "type": "object", "properties": { @@ -2402,6 +2662,60 @@ } } }, + "api.CreatePollRequest": { + "type": "object", + "properties": { + "allow_multiple_selections": { + "type": "boolean", + "example": true + }, + "answers": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "apple", + "banana", + "orange" + ] + }, + "question": { + "type": "string", + "example": "What's your favourite fruit?" + }, + "recipient": { + "type": "string", + "example": "\u003cphone number\u003e OR \u003cusername\u003e OR \u003cgroup id\u003e" + } + } + }, + "api.CreatePollResponse": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "example": "1769271479" + } + } + }, + "api.DeleteLocalAccountDataRequest": { + "type": "object", + "properties": { + "ignore_registered": { + "type": "boolean", + "example": false + } + } + }, + "api.DeviceLinkUriResponse": { + "type": "object", + "properties": { + "device_link_uri": { + "type": "string" + } + } + }, "api.Error": { "type": "object", "properties": { @@ -2794,6 +3108,32 @@ } } }, + "api.VoteRequest": { + "type": "object", + "properties": { + "poll_author": { + "type": "string", + "example": "\u003cphone number\u003e OR \u003cuuid\u003e" + }, + "poll_timestamp": { + "type": "string", + "example": "1769271479" + }, + "recipient": { + "type": "string", + "example": "\u003cphone number\u003e OR \u003cusername\u003e OR \u003cgroup id\u003e" + }, + "selected_answers": { + "type": "array", + "items": { + "type": "integer" + }, + "example": [ + 1 + ] + } + } + }, "client.About": { "type": "object", "properties": { @@ -2960,6 +3300,9 @@ "creation_timestamp": { "type": "integer" }, + "id": { + "type": "integer" + }, "last_seen_timestamp": { "type": "integer" }, diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 5f6e9ba..7e7ee54 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -28,6 +28,15 @@ definitions: type: string type: array type: object + api.ClosePollRequest: + properties: + poll_timestamp: + example: "1769271479" + type: string + recipient: + example: OR OR + type: string + type: object api.Configuration: properties: logging: @@ -59,6 +68,43 @@ definitions: id: type: string type: object + api.CreatePollRequest: + properties: + allow_multiple_selections: + example: true + type: boolean + answers: + example: + - apple + - banana + - orange + items: + type: string + type: array + question: + example: What's your favourite fruit? + type: string + recipient: + example: OR OR + type: string + type: object + api.CreatePollResponse: + properties: + timestamp: + example: "1769271479" + type: string + type: object + api.DeleteLocalAccountDataRequest: + properties: + ignore_registered: + example: false + type: boolean + type: object + api.DeviceLinkUriResponse: + properties: + device_link_uri: + type: string + type: object api.Error: properties: error: @@ -319,6 +365,24 @@ definitions: pin: type: string type: object + api.VoteRequest: + properties: + poll_author: + example: OR + type: string + poll_timestamp: + example: "1769271479" + type: string + recipient: + example: OR OR + type: string + selected_answers: + example: + - 1 + items: + type: integer + type: array + type: object client.About: properties: build: @@ -428,6 +492,8 @@ definitions: properties: creation_timestamp: type: integer + id: + type: integer last_seen_timestamp: type: integer name: @@ -1023,6 +1089,59 @@ paths: summary: Links another device to this device. tags: - Devices + /v1/devices/{number}/{deviceId}: + delete: + description: Remove a linked device from the primary account. + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + - description: Device ID from listDevices + in: path + name: deviceId + required: true + type: integer + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Remove linked device + tags: + - Devices + /v1/devices/{number}/local-data: + delete: + consumes: + - application/json + description: Delete all local data for the specified account. Only use this + after unregistering the account or after removing a linked device. + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + - description: Cleanup options + in: body + name: data + schema: + $ref: '#/definitions/api.DeleteLocalAccountDataRequest' + produces: + - application/json + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Delete local account data + tags: + - Devices /v1/groups/{number}: get: consumes: @@ -1501,6 +1620,94 @@ paths: summary: Trust Identity tags: - Identities + /v1/polls/{number}: + delete: + consumes: + - application/json + description: Close a poll + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + - description: Type + in: body + name: data + required: true + schema: + $ref: '#/definitions/api.ClosePollRequest' + produces: + - application/json + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Close a poll. + tags: + - Polls + post: + consumes: + - application/json + description: Create a new poll + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + - description: Type + in: body + name: data + required: true + schema: + $ref: '#/definitions/api.CreatePollRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/api.CreatePollResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Create a new poll. + tags: + - Polls + /v1/polls/{number}/vote: + post: + consumes: + - application/json + description: Answer a poll + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + - description: Type + in: body + name: data + required: true + schema: + $ref: '#/definitions/api.VoteRequest' + produces: + - application/json + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Answer a poll. + tags: + - Polls /v1/profiles/{number}: put: description: Set your name and optional an avatar. @@ -1557,6 +1764,30 @@ paths: summary: Link device and generate QR code. tags: - Devices + /v1/qrcodelink/raw: + get: + description: Generate the deviceLinkUri string for linking without scanning + a QR code. + parameters: + - description: Device Name + in: query + name: device_name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.DeviceLinkUriResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Get raw device link URI + tags: + - Devices /v1/reactions/{number}: delete: consumes: diff --git a/src/main.go b/src/main.go index 4fb0d89..68ac19c 100644 --- a/src/main.go +++ b/src/main.go @@ -313,6 +313,13 @@ func main() { contacts.POST(":number/sync", api.SendContacts) } + polls := v1.Group("/polls") + { + polls.POST(":number", api.CreatePoll) + polls.POST(":number/vote", api.VoteInPoll) + polls.DELETE(":number", api.ClosePoll) + } + if utils.GetEnv("ENABLE_PLUGINS", "false") == "true" { signalCliRestApiPluginSharedObjDir := utils.GetEnv("SIGNAL_CLI_REST_API_PLUGIN_SHARED_OBJ_DIR", "") sharedObj, err := plugin.Open(signalCliRestApiPluginSharedObjDir + "signal-cli-rest-api_plugin_loader.so")