Compare commits

...

6 Commits

Author SHA1 Message Date
Hugo
15b23b7bdf
Merge 8ea7456812d6f8b53d11c2aa26f6a2645b83322f into 82d88c353a4305bb9467583c9fdfd64e2b8845f4 2026-01-28 14:52:12 +02:00
Bernhard B
82d88c353a fixed file location 2026-01-25 00:04:30 +01:00
Bernhard B
43b1704e66 updated signal-cli to v0.13.23 2026-01-24 20:56:42 +01:00
Bernhard B
7711ad5503 added REST API endpoints for Signal Polls
see #765
2026-01-24 17:37:20 +01:00
Hugo
8ea7456812
Add files via upload
added 2 API registrations :

/api/v2/sendalertmanager
/api/v2/sendgraylognotification
2024-12-24 08:45:50 +01:00
Hugo
95c14a5f2b
Add files via upload
Additional functions to process calls from Graylog or Grafana. Grafana uses the AlertManager structure, so all tools using this structure can use this.
2024-12-24 08:43:15 +01:00
11 changed files with 1596 additions and 4 deletions

View File

@ -1,6 +1,6 @@
ARG SIGNAL_CLI_VERSION=0.13.22
ARG LIBSIGNAL_CLIENT_VERSION=0.86.1
ARG SIGNAL_CLI_NATIVE_PACKAGE_VERSION=0.13.22+morph027+1
ARG SIGNAL_CLI_VERSION=0.13.23
ARG LIBSIGNAL_CLIENT_VERSION=0.86.12
ARG SIGNAL_CLI_NATIVE_PACKAGE_VERSION=0.13.23+morph027+1
ARG SWAG_VERSION=1.16.4
ARG GRAALVM_VERSION=21.0.0

View File

@ -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:"<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 {
@ -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)
}

211
src/api/graylogapi.go Normal file
View File

@ -0,0 +1,211 @@
package api
import (
"encoding/json"
"errors"
"strconv"
"strings"
"fmt"
_ "runtime/debug"
_ "github.com/yassinebenaid/godump"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/bbernhard/signal-cli-rest-api/client"
)
type AlertManagerNotification struct {
Receiver string `json:"receiver"`
Status string `json:"status"`
Alerts []Alert `json:"alerts"`
GroupLabels Labels `json:"groupLabels"`
CommonLabels Labels `json:"commonLabels"`
CommonAnnotations Annotations `json:"commonAnnotations"`
ExternalURL string `json:"externalURL"`
Version string `json:"version"`
GroupKey string `json:"groupKey"`
TruncatedAlerts int64 `json:"truncatedAlerts"`
OrgID int64 `json:"orgId"`
Title string `json:"title"`
State string `json:"state"`
Message string `json:"message"`
}
type Alert struct {
Status string `json:"status"`
Labels Labels `json:"labels"`
Annotations Annotations `json:"annotations"`
StartsAt string `json:"startsAt"`
EndsAt string `json:"endsAt"`
GeneratorURL string `json:"generatorURL"`
Fingerprint string `json:"fingerprint"`
SilenceURL string `json:"silenceURL"`
DashboardURL string `json:"dashboardURL"`
PanelURL string `json:"panelURL"`
Values interface{} `json:"values"`
ValueString string `json:"valueString"`
}
type Annotations struct {
Summary string `json:"summary"`
}
type Labels struct {
Alertname string `json:"alertname"`
Instance string `json:"instance"`
}
type GrafanaMessage struct {
Number string `json:"number"`
Recipients string `json:"recipients"`
Message string `json:"message"`
}
type GraylogNotification struct {
EventDefinitionID string `json:"event_definition_id"`
EventDefinitionType string `json:"event_definition_type"`
EventDefinitionTitle string `json:"event_definition_title"`
EventDefinitionDescription string `json:"event_definition_description"`
JobDefinitionID string `json:"job_definition_id"`
JobTriggerID string `json:"job_trigger_id"`
Event GraylogEvent `json:"event"`
Backlog []interface{} `json:"backlog"`
}
type GraylogEvent struct {
ID string `json:"id"`
EventDefinitionType string `json:"event_definition_type"`
EventDefinitionID string `json:"event_definition_id"`
OriginContext string `json:"origin_context"`
Timestamp string `json:"timestamp"`
TimestampProcessing string `json:"timestamp_processing"`
TimerangeStart interface{} `json:"timerange_start"`
TimerangeEnd interface{} `json:"timerange_end"`
Streams []string `json:"streams"`
SourceStreams []interface{} `json:"source_streams"`
Message string `json:"message"`
Source string `json:"source"`
KeyTuple []string `json:"key_tuple"`
Key string `json:"key"`
Priority int64 `json:"priority"`
Alert bool `json:"alert"`
Fields Fields `json:"fields"`
}
type Fields struct {
Recipients string `json:"recipients"`
FromNumber string `json:"fromnumber"`
Message string `json:"message"`
}
// @Summary Send a signal message.
// @Tags Messages
// @Description Send a signal message. Set the text_mode to 'styled' in case you want to add formatting to your text message. Styling Options: *italic text*, **bold text**, ~strikethrough text~.
// @Accept json
// @Produce json
// @Success 201 {object} SendMessageResponse
// @Failure 400 {object} SendMessageError
// @Param data body SendMessageV2 true "Input Data"
// @Router /v2/send [post]
func (a *Api) SendAlertManagerV2(c *gin.Context) {
var req AlertManagerNotification
var msg GrafanaMessage
base64Attachments := []string{}
err := c.BindJSON(&req)
if err != nil {
c.JSON(400, gin.H{"error": "Couldn't process request - invalid request"})
log.Error(err.Error())
return
}
//fmt.Printf(">>>%s\n",[]byte(req.Message))
// Unmarshal or Decode the JSON to the interface.
json.Unmarshal([]byte(req.Message), &msg)
// timestamp, err := a.signalClient.SendV1(msg.Number, msg.Message, msg.Recipients, base64Attachments, msg.IsGroup)
// if err != nil {
// c.JSON(400, Error{Msg: err.Error()})
// return
// }
// c.JSON(201, SendMessageResponse{Timestamp: strconv.FormatInt(timestamp.Timestamp, 10)})
data, err := a.signalClient.SendV2(msg.Number, msg.Message, strings.Split(msg.Recipients,","), base64Attachments, "", nil, nil, nil, nil, nil, nil, nil, nil)
if err != nil {
switch err.(type) {
case *client.RateLimitErrorType:
if rateLimitError, ok := err.(*client.RateLimitErrorType); ok {
extendedError := errors.New(err.Error() + ". Use the attached challenge tokens to lift the rate limit restrictions via the '/v1/accounts/{number}/rate-limit-challenge' endpoint.")
c.JSON(429, SendMessageError{Msg: extendedError.Error(), ChallengeTokens: rateLimitError.ChallengeTokens, Account: msg.Number})
return
} else {
c.JSON(400, Error{Msg: err.Error()})
return
}
default:
c.JSON(400, Error{Msg: err.Error()})
return
}
c.JSON(400, Error{Msg: err.Error()})
return
}
c.JSON(201, SendMessageResponse{Timestamp: strconv.FormatInt((*data)[0].Timestamp, 10)})
}
// @Summary Send a signal message.
// @Tags Messages
// @Description Send a signal message. Set the text_mode to 'styled' in case you want to add formatting to your text message. Styling Options: *italic text*, **bold text**, ~strikethrough text~.
// @Accept json
// @Produce json
// @Success 201 {object} SendMessageResponse
// @Failure 400 {object} SendMessageError
// @Param data body SendMessageV2 true "Input Data"
// @Router /v2/send [post]
func (a *Api) SendGraylogNotificationV2(c *gin.Context) {
var req GraylogNotification
base64Attachments := []string{}
// jsonData,err2 := io.ReadAll(c.Request.Body)
//if err2 != nil {
// log.Error(err2.Error())
//}
//fmt.Printf("<<<%s\n",jsonData)
err := c.BindJSON(&req)
if err != nil {
c.JSON(400, gin.H{"error": "Couldn't process request - invalid requestttttt"})
log.Error(err.Error())
fmt.Printf("<<<%s\n",c.Request.Body)
return
}
data, err := a.signalClient.SendV2(req.Event.Fields.FromNumber, req.Event.Fields.Message, strings.Split(req.Event.Fields.Recipients,","), base64Attachments, "", nil, nil, nil, nil, nil, nil, nil, nil)
if err != nil {
switch err.(type) {
case *client.RateLimitErrorType:
if rateLimitError, ok := err.(*client.RateLimitErrorType); ok {
extendedError := errors.New(err.Error() + ". Use the attached challenge tokens to lift the rate limit restrictions via the '/v1/accounts/{number}/rate-limit-challenge' endpoint.")
c.JSON(429, SendMessageError{Msg: extendedError.Error(), ChallengeTokens: rateLimitError.ChallengeTokens, Account: req.Event.Fields.FromNumber})
return
} else {
c.JSON(400, Error{Msg: err.Error()})
return
}
default:
c.JSON(400, Error{Msg: err.Error()})
return
}
c.JSON(400, Error{Msg: err.Error()})
return
}
c.JSON(201, SendMessageResponse{Timestamp: strconv.FormatInt((*data)[0].Timestamp, 10)})
}

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")
@ -359,6 +366,14 @@ func main() {
{
sendV2.POST("", api.SendV2)
}
sendalertmanagerV2 := v2.Group("/sendalertmanager")
{
sendalertmanagerV2.POST("", api.SendAlertManagerV2)
}
sendgraylognotificationV2 := v2.Group("/sendgraylognotification")
{
sendgraylognotificationV2.POST("", api.SendGraylogNotificationV2)
}
}
protocol := "http"