added REST API endpoints for Signal Polls

see #765
This commit is contained in:
Bernhard B 2026-01-24 17:37:20 +01:00
parent e5e21518a5
commit 7711ad5503
6 changed files with 1374 additions and 1 deletions

View File

@ -228,6 +228,29 @@ type DeviceLinkUriResponse struct {
DeviceLinkUri string `json:"device_link_uri"`
}
type CreatePollRequest struct {
Recipient string `json:"recipient" example:"<phone number> OR <username> OR <group id>"`
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:"<phone number> OR <username> OR <group id>"`
PollAuthor string `json:"poll_author" example:"<phone number> OR <uuid>"`
PollTimestamp string `json:"poll_timestamp" example:"1769271479"`
SelectedAnswers []int32 `json:"selected_answers" example:"1"`
}
type ClosePollRequest struct {
Recipient string `json:"recipient" example:"<phone number> OR <username> OR <group id>"`
PollTimestamp string `json:"poll_timestamp" example:"1769271479"`
}
type Api struct {
signalClient *client.SignalClient
wsMutex sync.Mutex
@ -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)
}

View File

@ -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
}
}

View File

@ -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"
},

View File

@ -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"
},

View File

@ -28,6 +28,15 @@ definitions:
type: string
type: array
type: object
api.ClosePollRequest:
properties:
poll_timestamp:
example: "1769271479"
type: string
recipient:
example: <phone number> OR <username> OR <group id>
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: <phone number> OR <username> OR <group id>
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: <phone number> OR <uuid>
type: string
poll_timestamp:
example: "1769271479"
type: string
recipient:
example: <phone number> OR <username> OR <group id>
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:

View File

@ -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")