diff --git a/.gitignore b/.gitignore index 336e0a7..4d82527 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ signal-cli-config src/main +src/signal-cli-rest-api diff --git a/Dockerfile b/Dockerfile index 8503cf1..df24c9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM golang:1.13-buster ARG SIGNAL_CLI_VERSION=0.6.8 +ARG SWAG_VERSION=1.6.7 ENV GIN_MODE=release @@ -12,34 +13,36 @@ RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ dpkg-reconfigure --frontend=noninteractive locales && \ update-locale LANG=en_US.UTF-8 -ENV LANG en_US.UTF-8 - -#RUN cd /tmp/ \ -# && wget -P /tmp/ https://github.com/AsamK/signal-cli/archive/v${SIGNAL_CLI_VERSION}.tar.gz \ -# && tar -xvf /tmp/v${SIGNAL_CLI_VERSION}.tar.gz \ -# && cd signal-cli-${SIGNAL_CLI_VERSION} \ -# && ./gradlew build \ -# && ./gradlew installDist \ -# && ln -s /tmp/signal-cli-${SIGNAL_CLI_VERSION}/build/install/signal-cli/bin/signal-cli /usr/bin/signal-cli \ -# && rm -rf /tmp/v${SIGNAL_CLI_VERSION}.tar.gz - -# https://github.com/AsamK/signal-cli/issues/259 is not yet in a release, so we need to check out the repository +ENV LANG en_US.UTF-8 +RUN cd /tmp/ \ + && git clone https://github.com/swaggo/swag.git swag-${SWAG_VERSION} \ + && cd swag-${SWAG_VERSION} \ + && git checkout v${SWAG_VERSION} \ + && make \ + && cp /tmp/swag-${SWAG_VERSION}/swag /usr/bin/swag \ + && rm -r /tmp/swag-${SWAG_VERSION} RUN cd /tmp/ \ && git clone https://github.com/AsamK/signal-cli.git signal-cli-${SIGNAL_CLI_VERSION} \ && cd signal-cli-${SIGNAL_CLI_VERSION} \ + && git checkout v${SIGNAL_CLI_VERSION} \ && ./gradlew build \ && ./gradlew installDist \ && ln -s /tmp/signal-cli-${SIGNAL_CLI_VERSION}/build/install/signal-cli/bin/signal-cli /usr/bin/signal-cli RUN mkdir -p /signal-cli-config/ RUN mkdir -p /home/.local/share/signal-cli -COPY src/ /tmp/signal-cli-rest-api-src -RUN cd /tmp/signal-cli-rest-api-src && go get -d ./... && go build main.go + +COPY src/api /tmp/signal-cli-rest-api-src/api +COPY src/main.go /tmp/signal-cli-rest-api-src/ +COPY src/go.mod /tmp/signal-cli-rest-api-src/ +COPY src/go.sum /tmp/signal-cli-rest-api-src/ + +RUN cd /tmp/signal-cli-rest-api-src && swag init && go build ENV PATH /tmp/signal-cli-rest-api-src/:/usr/bin/signal-cli-${SIGNAL_CLI_VERSION}/bin/:$PATH EXPOSE 8080 -ENTRYPOINT ["main"] +ENTRYPOINT ["signal-cli-rest-api"] diff --git a/index.html b/index.html new file mode 100644 index 0000000..a36f060 --- /dev/null +++ b/index.html @@ -0,0 +1,29 @@ + + + + + + My New API + + +
+ + + diff --git a/src/api/api.go b/src/api/api.go new file mode 100644 index 0000000..277dce9 --- /dev/null +++ b/src/api/api.go @@ -0,0 +1,616 @@ +package api + +import ( + "bufio" + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "github.com/gin-gonic/gin" + uuid "github.com/gofrs/uuid" + "github.com/h2non/filetype" + log "github.com/sirupsen/logrus" + qrcode "github.com/skip2/go-qrcode" + "os" + "os/exec" + "strings" + "time" +) + +const groupPrefix = "group." + +type GroupEntry struct { + Name string `json:"name"` + Id string `json:"id"` + InternalId string `json:"internal_id"` + Members []string `json:"members"` + Active bool `json:"active"` + Blocked bool `json:"blocked"` +} + +type RegisterNumberRequest struct { + UseVoice bool `json:"use_voice"` +} + +type SendMessageV1 struct { + Number string `json:"number"` + Recipients []string `json:"recipients"` + Message string `json:"message"` + Base64Attachment string `json:"base64_attachment"` + IsGroup bool `json:"is_group"` +} + +type SendMessageV2 struct { + Number string `json:"number"` + Recipients []string `json:"recipients"` + Message string `json:"message"` + Base64Attachments []string `json:"base64_attachments"` +} + +type Error struct { + Msg string `json:"error"` +} + +type About struct { + SupportedApiVersions []string `json:"versions"` + BuildNr int `json:"build"` +} + +type CreateGroup struct { + Id string `json:"id"` +} + +func convertInternalGroupIdToGroupId(internalId string) string { + return groupPrefix + base64.StdEncoding.EncodeToString([]byte(internalId)) +} + +func getStringInBetween(str string, start string, end string) (result string) { + i := strings.Index(str, start) + if i == -1 { + return + } + i += len(start) + j := strings.Index(str[i:], end) + if j == -1 { + return + } + return str[i : i+j] +} + +func cleanupTmpFiles(paths []string) { + for _, path := range paths { + os.Remove(path) + } +} + +func send(c *gin.Context, attachmentTmpDir string, signalCliConfig string, number string, message string, + recipients []string, base64Attachments []string, isGroup bool) { + cmd := []string{"--config", signalCliConfig, "-u", number, "send", "-m", message} + + if len(recipients) == 0 { + c.JSON(400, gin.H{"error": "Please specify at least one recipient"}) + return + } + + if !isGroup { + cmd = append(cmd, recipients...) + } else { + if len(recipients) > 1 { + c.JSON(400, gin.H{"error": "More than one recipient is currently not allowed"}) + return + } + + groupId, err := base64.StdEncoding.DecodeString(recipients[0]) + if err != nil { + c.JSON(400, gin.H{"error": "Invalid group id"}) + return + } + + cmd = append(cmd, []string{"-g", string(groupId)}...) + } + + attachmentTmpPaths := []string{} + for _, base64Attachment := range base64Attachments { + u, err := uuid.NewV4() + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + dec, err := base64.StdEncoding.DecodeString(base64Attachment) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + fType, err := filetype.Get(dec) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + attachmentTmpPath := attachmentTmpDir + u.String() + "." + fType.Extension + attachmentTmpPaths = append(attachmentTmpPaths, attachmentTmpPath) + + f, err := os.Create(attachmentTmpPath) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + defer f.Close() + + if _, err := f.Write(dec); err != nil { + cleanupTmpFiles(attachmentTmpPaths) + c.JSON(400, gin.H{"error": err.Error()}) + return + } + if err := f.Sync(); err != nil { + cleanupTmpFiles(attachmentTmpPaths) + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + f.Close() + } + + if len(attachmentTmpPaths) > 0 { + cmd = append(cmd, "-a") + cmd = append(cmd, attachmentTmpPaths...) + } + + _, err := runSignalCli(true, cmd) + if err != nil { + cleanupTmpFiles(attachmentTmpPaths) + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(201, nil) +} + +func getGroups(number string, signalCliConfig string) ([]GroupEntry, error) { + groupEntries := []GroupEntry{} + + out, err := runSignalCli(true, []string{"--config", signalCliConfig, "-u", number, "listGroups", "-d"}) + if err != nil { + return groupEntries, err + } + + lines := strings.Split(out, "\n") + for _, line := range lines { + var groupEntry GroupEntry + if line == "" { + continue + } + + idIdx := strings.Index(line, " Name: ") + idPair := line[:idIdx] + groupEntry.InternalId = strings.TrimPrefix(idPair, "Id: ") + groupEntry.Id = convertInternalGroupIdToGroupId(groupEntry.InternalId) + lineWithoutId := strings.TrimLeft(line[idIdx:], " ") + + nameIdx := strings.Index(lineWithoutId, " Active: ") + namePair := lineWithoutId[:nameIdx] + groupEntry.Name = strings.TrimRight(strings.TrimPrefix(namePair, "Name: "), " ") + lineWithoutName := strings.TrimLeft(lineWithoutId[nameIdx:], " ") + + activeIdx := strings.Index(lineWithoutName, " Blocked: ") + activePair := lineWithoutName[:activeIdx] + active := strings.TrimPrefix(activePair, "Active: ") + if active == "true" { + groupEntry.Active = true + } else { + groupEntry.Active = false + } + lineWithoutActive := strings.TrimLeft(lineWithoutName[activeIdx:], " ") + + blockedIdx := strings.Index(lineWithoutActive, " Members: ") + blockedPair := lineWithoutActive[:blockedIdx] + blocked := strings.TrimPrefix(blockedPair, "Blocked: ") + if blocked == "true" { + groupEntry.Blocked = true + } else { + groupEntry.Blocked = false + } + lineWithoutBlocked := strings.TrimLeft(lineWithoutActive[blockedIdx:], " ") + + membersPair := lineWithoutBlocked + members := strings.TrimPrefix(membersPair, "Members: ") + trimmedMembers := strings.TrimRight(strings.TrimLeft(members, "["), "]") + trimmedMembersList := strings.Split(trimmedMembers, ",") + for _, member := range trimmedMembersList { + groupEntry.Members = append(groupEntry.Members, strings.Trim(member, " ")) + } + + groupEntries = append(groupEntries, groupEntry) + } + + return groupEntries, nil +} + +func runSignalCli(wait bool, args []string) (string, error) { + cmd := exec.Command("signal-cli", args...) + if wait { + var errBuffer bytes.Buffer + var outBuffer bytes.Buffer + cmd.Stderr = &errBuffer + cmd.Stdout = &outBuffer + + err := cmd.Start() + if err != nil { + return "", err + } + + done := make(chan error, 1) + go func() { + done <- cmd.Wait() + }() + select { + case <-time.After(60 * time.Second): + err := cmd.Process.Kill() + if err != nil { + return "", err + } + return "", errors.New("process killed as timeout reached") + case err := <-done: + if err != nil { + return "", errors.New(errBuffer.String()) + } + } + return outBuffer.String(), nil + } else { + stdout, err := cmd.StdoutPipe() + if err != nil { + return "", err + } + cmd.Start() + buf := bufio.NewReader(stdout) // Notice that this is not in a loop + line, _, _ := buf.ReadLine() + return string(line), nil + } +} + +type Api struct { + signalCliConfig string + attachmentTmpDir string +} + +func NewApi(signalCliConfig string, attachmentTmpDir string) *Api { + return &Api{ + signalCliConfig: signalCliConfig, + attachmentTmpDir: attachmentTmpDir, + } +} + +// @Summary Lists general information about the API +// @Tags General +// @Description Returns the supported API versions and the internal build nr +// @Produce json +// @Success 200 {object} About +// @Router /v1/about [get] +func (a *Api) About(c *gin.Context) { + + about := About{SupportedApiVersions: []string{"v1", "v2"}, BuildNr: 2} + c.JSON(200, about) +} + +// @Summary Register a phone number. +// @Tags Devices +// @Description Register a phone number with the signal network. +// @Accept json +// @Produce json +// @Success 201 +// @Failure 400 {object} Error +// @Param number path string true "Registered Phone Number" +// @Router /v1/register/{number} [post] +func (a *Api) RegisterNumber(c *gin.Context) { + number := c.Param("number") + + var req RegisterNumberRequest + + buf := new(bytes.Buffer) + buf.ReadFrom(c.Request.Body) + if buf.String() != "" { + err := json.Unmarshal(buf.Bytes(), &req) + if err != nil { + log.Error("Couldn't register number: ", err.Error()) + c.JSON(400, Error{Msg: "Couldn't process request - invalid request."}) + return + } + } else { + req.UseVoice = false + } + + if number == "" { + c.JSON(400, gin.H{"error": "Please provide a number"}) + return + } + + command := []string{"--config", a.signalCliConfig, "-u", number, "register"} + + if req.UseVoice == true { + command = append(command, "--voice") + } + + _, err := runSignalCli(true, command) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(201, nil) +} + +// @Summary Verify a registered phone number. +// @Tags Devices +// @Description Verify a registered phone number with the signal network. +// @Accept json +// @Produce json +// @Success 201 {string} string "OK" +// @Failure 400 {object} Error +// @Param number path string true "Registered Phone Number" +// @Param token path string true "Verification Code" +// @Router /v1/register/{number}/verify/{token} [post] +func (a *Api) VerifyRegisteredNumber(c *gin.Context) { + number := c.Param("number") + token := c.Param("token") + + if number == "" { + c.JSON(400, gin.H{"error": "Please provide a number"}) + return + } + + if token == "" { + c.JSON(400, gin.H{"error": "Please provide a verification code"}) + return + } + + _, err := runSignalCli(true, []string{"--config", a.signalCliConfig, "-u", number, "verify", token}) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(201, nil) +} + +// @Summary Send a signal message. +// @Tags Messages +// @Description Send a signal message +// @Accept json +// @Produce json +// @Success 201 {string} string "OK" +// @Failure 400 {object} Error +// @Param number path string true "Registered Phone Number" +// @Param data body SendMessageV1 true "Input Data" +// @Router /v1/send/{number} [post] +// @Deprecated +func (a *Api) Send(c *gin.Context) { + + var req SendMessageV1 + err := c.BindJSON(&req) + if err != nil { + c.JSON(400, gin.H{"error": "Couldn't process request - invalid request"}) + return + } + + base64Attachments := []string{} + if req.Base64Attachment != "" { + base64Attachments = append(base64Attachments, req.Base64Attachment) + } + + send(c, a.signalCliConfig, a.signalCliConfig, req.Number, req.Message, req.Recipients, base64Attachments, req.IsGroup) +} + +// @Summary Send a signal message. +// @Tags Messages +// @Description Send a signal message +// @Accept json +// @Produce json +// @Success 201 {string} string "OK" +// @Failure 400 {object} Error +// @Param number path string true "Registered Phone Number" +// @Param data body SendMessageV2 true "Input Data" +// @Router /v2/send/{number} [post] +func (a *Api) SendV2(c *gin.Context) { + var req SendMessageV2 + err := c.BindJSON(&req) + if err != nil { + c.JSON(400, gin.H{"error": "Couldn't process request - invalid request"}) + log.Error(err.Error()) + return + } + + if len(req.Recipients) == 0 { + c.JSON(400, gin.H{"error": "Couldn't process request - please provide at least one recipient"}) + return + } + + groups := []string{} + recipients := []string{} + + for _, recipient := range req.Recipients { + if strings.HasPrefix(recipient, groupPrefix) { + groups = append(groups, strings.TrimPrefix(recipient, groupPrefix)) + } else { + recipients = append(recipients, recipient) + } + } + + if len(recipients) > 0 && len(groups) > 0 { + c.JSON(400, gin.H{"error": "Signal Messenger Groups and phone numbers cannot be specified together in one request! Please split them up into multiple REST API calls."}) + return + } + + if len(groups) > 1 { + c.JSON(400, gin.H{"error": "A signal message cannot be sent to more than one group at once! Please use multiple REST API calls for that."}) + return + } + + for _, group := range groups { + send(c, a.attachmentTmpDir, a.signalCliConfig, req.Number, req.Message, []string{group}, req.Base64Attachments, true) + } + + if len(recipients) > 0 { + send(c, a.attachmentTmpDir, a.signalCliConfig, req.Number, req.Message, recipients, req.Base64Attachments, false) + } +} + +// @Summary Receive Signal Messages. +// @Tags Messages +// @Description Receives Signal Messages from the Signal Network. +// @Accept json +// @Produce json +// @Success 200 {object} []string +// @Failure 400 {object} Error +// @Param number path string true "Registered Phone Number" +// @Router /v1/receive/{number} [get] +func (a *Api) Receive(c *gin.Context) { + number := c.Param("number") + + command := []string{"--config", a.signalCliConfig, "-u", number, "receive", "-t", "1", "--json"} + out, err := runSignalCli(true, command) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + out = strings.Trim(out, "\n") + lines := strings.Split(out, "\n") + + jsonStr := "[" + for i, line := range lines { + jsonStr += line + if i != (len(lines) - 1) { + jsonStr += "," + } + } + jsonStr += "]" + + c.String(200, jsonStr) +} + +// @Summary Create a new Signal Group. +// @Tags Groups +// @Description Create a new Signal Group with the specified members. +// @Accept json +// @Produce json +// @Success 201 {object} CreateGroup +// @Failure 400 {object} Error +// @Param number path string true "Registered Phone Number" +// @Router /v1/groups/{number} [post] +func (a *Api) CreateGroup(c *gin.Context) { + number := c.Param("number") + + type Request struct { + Name string `json:"name"` + Members []string `json:"members"` + } + + var req Request + err := c.BindJSON(&req) + if err != nil { + c.JSON(400, gin.H{"error": "Couldn't process request - invalid request"}) + log.Error(err.Error()) + return + } + + cmd := []string{"--config", a.signalCliConfig, "-u", number, "updateGroup", "-n", req.Name, "-m"} + cmd = append(cmd, req.Members...) + + out, err := runSignalCli(true, cmd) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + internalGroupId := getStringInBetween(out, `"`, `"`) + c.JSON(201, CreateGroup{Id: convertInternalGroupIdToGroupId(internalGroupId)}) +} + +// @Summary List all Signal Groups. +// @Tags Groups +// @Description List all Signal Groups. +// @Accept json +// @Produce json +// @Success 200 {object} []GroupEntry +// @Failure 400 {object} Error +// @Param number path string true "Registered Phone Number" +// @Router /v1/groups/{number} [get] +func (a *Api) GetGroups(c *gin.Context) { + number := c.Param("number") + + groups, err := getGroups(number, a.signalCliConfig) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + c.JSON(200, groups) +} + +// @Summary Delete a Signal Group. +// @Tags Groups +// @Description Delete a Signal Group. +// @Accept json +// @Produce json +// @Success 200 {string} string "OK" +// @Failure 400 {object} Error +// @Param number path string true "Registered Phone Number" +// @Param groupid path string true "Group Id" +// @Router /v1/groups/{number}/{groupid} [delete] +func (a *Api) DeleteGroup(c *gin.Context) { + base64EncodedGroupId := c.Param("groupid") + number := c.Param("number") + + if base64EncodedGroupId == "" { + c.JSON(400, gin.H{"error": "Please specify a group id"}) + return + } + + groupId, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(base64EncodedGroupId, groupPrefix)) + if err != nil { + c.JSON(400, gin.H{"error": "Invalid group id"}) + return + } + + _, err = runSignalCli(true, []string{"--config", a.signalCliConfig, "-u", number, "quitGroup", "-g", string(groupId)}) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } +} + +// @Summary Link device and generate QR code. +// @Tags Devices +// @Description test +// @Produce json +// @Success 200 {string} string "Image" +// @Router /v1/qrcodelink [get] +func (a *Api) GetQrCodeLink(c *gin.Context) { + deviceName := c.Query("device_name") + + if deviceName == "" { + c.JSON(400, gin.H{"error": "Please provide a name for the device"}) + return + } + + command := []string{"--config", a.signalCliConfig, "link", "-n", deviceName} + + tsdeviceLink, err := runSignalCli(false, command) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + q, err := qrcode.New(string(tsdeviceLink), qrcode.Medium) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + } + + q.DisableBorder = true + var png []byte + png, err = q.PNG(256) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + } + + c.Data(200, "image/png", png) +} diff --git a/src/docs/docs.go b/src/docs/docs.go new file mode 100644 index 0000000..3e91921 --- /dev/null +++ b/src/docs/docs.go @@ -0,0 +1,574 @@ +// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by swaggo/swag + +package docs + +import ( + "bytes" + "encoding/json" + "strings" + + "github.com/alecthomas/template" + "github.com/swaggo/swag" +) + +var doc = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{.Description}}", + "title": "{{.Title}}", + "contact": {}, + "license": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/v1/about": { + "get": { + "description": "Returns the supported API versions and the internal build nr", + "produces": [ + "application/json" + ], + "tags": [ + "General" + ], + "summary": "Lists general information about the API", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.About" + } + } + } + } + }, + "/v1/groups/{number}": { + "get": { + "description": "List all Signal Groups.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Groups" + ], + "summary": "List all Signal Groups.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/api.GroupEntry" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + }, + "post": { + "description": "Create a new Signal Group with the specified members.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Groups" + ], + "summary": "Create a new Signal Group.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/api.CreateGroup" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/groups/{number}/{groupid}": { + "delete": { + "description": "Delete a Signal Group.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Groups" + ], + "summary": "Delete a Signal Group.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Group Id", + "name": "groupid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/qrcodelink": { + "get": { + "description": "test", + "produces": [ + "application/json" + ], + "tags": [ + "Devices" + ], + "summary": "Link device and generate QR code.", + "responses": { + "200": { + "description": "Image", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/receive/{number}": { + "get": { + "description": "Receives Signal Messages from the Signal Network.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Receive Signal Messages.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/register/{number}": { + "post": { + "description": "Register a phone number with the signal network.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Devices" + ], + "summary": "Register a phone number.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + } + ], + "responses": { + "201": {}, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/register/{number}/verify/{token}": { + "post": { + "description": "Verify a registered phone number with the signal network.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Devices" + ], + "summary": "Verify a registered phone number.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Verification Code", + "name": "token", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/send/{number}": { + "post": { + "description": "Send a signal message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Send a signal message.", + "deprecated": true, + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Input Data", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.SendMessageV1" + } + } + ], + "responses": { + "201": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v2/send/{number}": { + "post": { + "description": "Send a signal message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Send a signal message.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Input Data", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.SendMessageV2" + } + } + ], + "responses": { + "201": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + } + }, + "definitions": { + "api.About": { + "type": "object", + "properties": { + "build": { + "type": "integer" + }, + "versions": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "api.CreateGroup": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + "api.Error": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "api.GroupEntry": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "blocked": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "internal_id": { + "type": "string" + }, + "members": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + } + } + }, + "api.SendMessageV1": { + "type": "object", + "properties": { + "base64_attachment": { + "type": "string" + }, + "is_group": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "number": { + "type": "string" + }, + "recipients": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "api.SendMessageV2": { + "type": "object", + "properties": { + "base64_attachments": { + "type": "array", + "items": { + "type": "string" + } + }, + "message": { + "type": "string" + }, + "number": { + "type": "string" + }, + "recipients": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "tags": [ + { + "description": "List general information.", + "name": "General" + }, + { + "description": "Register and link Devices.", + "name": "Devices" + }, + { + "description": "Create, List and Delete Signal Groups.", + "name": "Groups" + }, + { + "description": "Send and Receive Signal Messages.", + "name": "Messages" + } + ] +}` + +type swaggerInfo struct { + Version string + Host string + BasePath string + Schemes []string + Title string + Description string +} + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = swaggerInfo{ + Version: "1.0", + Host: "127.0.0.1:8080", + BasePath: "/", + Schemes: []string{}, + Title: "Signal Cli REST API", + Description: "This is the Signal Cli REST API documentation.", +} + +type s struct{} + +func (s *s) ReadDoc() string { + sInfo := SwaggerInfo + sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) + + t, err := template.New("swagger_info").Funcs(template.FuncMap{ + "marshal": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, + }).Parse(doc) + if err != nil { + return doc + } + + var tpl bytes.Buffer + if err := t.Execute(&tpl, sInfo); err != nil { + return doc + } + + return tpl.String() +} + +func init() { + swag.Register(swag.Name, &s{}) +} diff --git a/src/docs/swagger.json b/src/docs/swagger.json new file mode 100644 index 0000000..05b270c --- /dev/null +++ b/src/docs/swagger.json @@ -0,0 +1,512 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is the Signal Cli REST API documentation.", + "title": "Signal Cli REST API", + "contact": {}, + "license": {}, + "version": "1.0" + }, + "host": "127.0.0.1:8080", + "basePath": "/", + "paths": { + "/v1/about": { + "get": { + "description": "Returns the supported API versions and the internal build nr", + "produces": [ + "application/json" + ], + "tags": [ + "General" + ], + "summary": "Lists general information about the API", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.About" + } + } + } + } + }, + "/v1/groups/{number}": { + "get": { + "description": "List all Signal Groups.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Groups" + ], + "summary": "List all Signal Groups.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/api.GroupEntry" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + }, + "post": { + "description": "Create a new Signal Group with the specified members.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Groups" + ], + "summary": "Create a new Signal Group.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/api.CreateGroup" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/groups/{number}/{groupid}": { + "delete": { + "description": "Delete a Signal Group.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Groups" + ], + "summary": "Delete a Signal Group.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Group Id", + "name": "groupid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/qrcodelink": { + "get": { + "description": "test", + "produces": [ + "application/json" + ], + "tags": [ + "Devices" + ], + "summary": "Link device and generate QR code.", + "responses": { + "200": { + "description": "Image", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/receive/{number}": { + "get": { + "description": "Receives Signal Messages from the Signal Network.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Receive Signal Messages.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/register/{number}": { + "post": { + "description": "Register a phone number with the signal network.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Devices" + ], + "summary": "Register a phone number.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + } + ], + "responses": { + "201": {}, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/register/{number}/verify/{token}": { + "post": { + "description": "Verify a registered phone number with the signal network.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Devices" + ], + "summary": "Verify a registered phone number.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Verification Code", + "name": "token", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v1/send/{number}": { + "post": { + "description": "Send a signal message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Send a signal message.", + "deprecated": true, + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Input Data", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.SendMessageV1" + } + } + ], + "responses": { + "201": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + }, + "/v2/send/{number}": { + "post": { + "description": "Send a signal message", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Messages" + ], + "summary": "Send a signal message.", + "parameters": [ + { + "type": "string", + "description": "Registered Phone Number", + "name": "number", + "in": "path", + "required": true + }, + { + "description": "Input Data", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.SendMessageV2" + } + } + ], + "responses": { + "201": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.Error" + } + } + } + } + } + }, + "definitions": { + "api.About": { + "type": "object", + "properties": { + "build": { + "type": "integer" + }, + "versions": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "api.CreateGroup": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + "api.Error": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "api.GroupEntry": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "blocked": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "internal_id": { + "type": "string" + }, + "members": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + } + } + }, + "api.SendMessageV1": { + "type": "object", + "properties": { + "base64_attachment": { + "type": "string" + }, + "is_group": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "number": { + "type": "string" + }, + "recipients": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "api.SendMessageV2": { + "type": "object", + "properties": { + "base64_attachments": { + "type": "array", + "items": { + "type": "string" + } + }, + "message": { + "type": "string" + }, + "number": { + "type": "string" + }, + "recipients": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "tags": [ + { + "description": "List general information.", + "name": "General" + }, + { + "description": "Register and link Devices.", + "name": "Devices" + }, + { + "description": "Create, List and Delete Signal Groups.", + "name": "Groups" + }, + { + "description": "Send and Receive Signal Messages.", + "name": "Messages" + } + ] +} \ No newline at end of file diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml new file mode 100644 index 0000000..65e4f19 --- /dev/null +++ b/src/docs/swagger.yaml @@ -0,0 +1,335 @@ +basePath: / +definitions: + api.About: + properties: + build: + type: integer + versions: + items: + type: string + type: array + type: object + api.CreateGroup: + properties: + id: + type: string + type: object + api.Error: + properties: + error: + type: string + type: object + api.GroupEntry: + properties: + active: + type: boolean + blocked: + type: boolean + id: + type: string + internal_id: + type: string + members: + items: + type: string + type: array + name: + type: string + type: object + api.SendMessageV1: + properties: + base64_attachment: + type: string + is_group: + type: boolean + message: + type: string + number: + type: string + recipients: + items: + type: string + type: array + type: object + api.SendMessageV2: + properties: + base64_attachments: + items: + type: string + type: array + message: + type: string + number: + type: string + recipients: + items: + type: string + type: array + type: object +host: 127.0.0.1:8080 +info: + contact: {} + description: This is the Signal Cli REST API documentation. + license: {} + title: Signal Cli REST API + version: "1.0" +paths: + /v1/about: + get: + description: Returns the supported API versions and the internal build nr + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.About' + summary: Lists general information about the API + tags: + - General + /v1/groups/{number}: + get: + consumes: + - application/json + description: List all Signal Groups. + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/api.GroupEntry' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: List all Signal Groups. + tags: + - Groups + post: + consumes: + - application/json + description: Create a new Signal Group with the specified members. + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/api.CreateGroup' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Create a new Signal Group. + tags: + - Groups + /v1/groups/{number}/{groupid}: + delete: + consumes: + - application/json + description: Delete a Signal Group. + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + - description: Group Id + in: path + name: groupid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + type: string + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Delete a Signal Group. + tags: + - Groups + /v1/qrcodelink: + get: + description: test + produces: + - application/json + responses: + "200": + description: Image + schema: + type: string + summary: Link device and generate QR code. + tags: + - Devices + /v1/receive/{number}: + get: + consumes: + - application/json + description: Receives Signal Messages from the Signal Network. + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + type: string + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Receive Signal Messages. + tags: + - Messages + /v1/register/{number}: + post: + consumes: + - application/json + description: Register a phone number with the signal network. + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + produces: + - application/json + responses: + "201": {} + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Register a phone number. + tags: + - Devices + /v1/register/{number}/verify/{token}: + post: + consumes: + - application/json + description: Verify a registered phone number with the signal network. + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + - description: Verification Code + in: path + name: token + required: true + type: string + produces: + - application/json + responses: + "201": + description: OK + schema: + type: string + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Verify a registered phone number. + tags: + - Devices + /v1/send/{number}: + post: + consumes: + - application/json + deprecated: true + description: Send a signal message + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + - description: Input Data + in: body + name: data + required: true + schema: + $ref: '#/definitions/api.SendMessageV1' + produces: + - application/json + responses: + "201": + description: OK + schema: + type: string + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Send a signal message. + tags: + - Messages + /v2/send/{number}: + post: + consumes: + - application/json + description: Send a signal message + parameters: + - description: Registered Phone Number + in: path + name: number + required: true + type: string + - description: Input Data + in: body + name: data + required: true + schema: + $ref: '#/definitions/api.SendMessageV2' + produces: + - application/json + responses: + "201": + description: OK + schema: + type: string + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.Error' + summary: Send a signal message. + tags: + - Messages +swagger: "2.0" +tags: +- description: List general information. + name: General +- description: Register and link Devices. + name: Devices +- description: Create, List and Delete Signal Groups. + name: Groups +- description: Send and Receive Signal Messages. + name: Messages diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..6337774 --- /dev/null +++ b/src/go.mod @@ -0,0 +1,24 @@ +module github.com/bbernhard/signal-cli-rest-api + +go 1.14 + +require ( + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 + github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + github.com/gin-gonic/gin v1.6.3 + github.com/go-openapi/spec v0.19.8 // indirect + github.com/go-openapi/swag v0.19.9 // indirect + github.com/gofrs/uuid v3.3.0+incompatible + github.com/h2non/filetype v1.1.0 + github.com/mailru/easyjson v0.7.1 // indirect + github.com/sirupsen/logrus v1.6.0 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 + github.com/swaggo/gin-swagger v1.2.0 + github.com/swaggo/swag v1.6.7 + github.com/urfave/cli/v2 v2.2.0 // indirect + golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect + golang.org/x/text v0.3.3 // indirect + golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect +) diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..d900900 --- /dev/null +++ b/src/go.sum @@ -0,0 +1,186 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo= +github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.8 h1:qAdZLh1r6QF/hI/gTq+TJTvsQUodZsM7KLqkAJdiJNg= +github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE= +github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gofrs/uuid v1.2.0 h1:coDhrjgyJaglxSjxuJdqQSSdUpG3w6p1OwN2od6frBU= +github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= +github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/h2non/filetype v1.1.0 h1:Or/gjocJrJRNK/Cri/TDEKFjAR+cfG6eK65NGYB6gBA= +github.com/h2non/filetype v1.1.0/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8= +github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= +github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= +github.com/swaggo/gin-swagger v1.2.0 h1:YskZXEiv51fjOMTsXrOetAjrMDfFaXD79PEoQBOe2W0= +github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI= +github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y= +github.com/swaggo/swag v1.6.7 h1:e8GC2xDllJZr3omJkm9YfmK0Y56+rMO3cg0JBKNz09s= +github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= +github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59 h1:QjA/9ArTfVTLfEhClDCG7SGrZkZixxWpwNCDiwJfh88= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f h1:JcoF/bowzCDI+MXu1yLqQGNO3ibqWsWq+Sk7pOT218w= +golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/src/main.go b/src/main.go index 9ddd4b9..6309f8f 100644 --- a/src/main.go +++ b/src/main.go @@ -1,244 +1,37 @@ package main import ( - "bufio" - "bytes" - "encoding/base64" - "encoding/json" - "errors" "flag" - "os" - "os/exec" - "strings" - "time" "github.com/gin-gonic/gin" - "github.com/h2non/filetype" - uuid "github.com/satori/go.uuid" log "github.com/sirupsen/logrus" - qrcode "github.com/skip2/go-qrcode" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + "github.com/bbernhard/signal-cli-rest-api/api" + _ "github.com/bbernhard/signal-cli-rest-api/docs" + ) -const groupPrefix = "group." -type GroupEntry struct { - Name string `json:"name"` - Id string `json:"id"` - InternalId string `json:"internal_id"` - Members []string `json:"members"` - Active bool `json:"active"` - Blocked bool `json:"blocked"` -} -func convertInternalGroupIdToGroupId(internalId string) string { - return groupPrefix + base64.StdEncoding.EncodeToString([]byte(internalId)) -} +// @title Signal Cli REST API +// @version 1.0 +// @description This is the Signal Cli REST API documentation. -func getStringInBetween(str string, start string, end string) (result string) { - i := strings.Index(str, start) - if i == -1 { - return - } - i += len(start) - j := strings.Index(str[i:], end) - if j == -1 { - return - } - return str[i : i+j] -} +// @tag.name General +// @tag.description List general information. -func cleanupTmpFiles(paths []string) { - for _, path := range paths { - os.Remove(path) - } -} +// @tag.name Devices +// @tag.description Register and link Devices. -func send(c *gin.Context, attachmentTmpDir string, signalCliConfig string, number string, message string, - recipients []string, base64Attachments []string, isGroup bool) { - cmd := []string{"--config", signalCliConfig, "-u", number, "send", "-m", message} +// @tag.name Groups +// @tag.description Create, List and Delete Signal Groups. - if len(recipients) == 0 { - c.JSON(400, gin.H{"error": "Please specify at least one recipient"}) - return - } - - if !isGroup { - cmd = append(cmd, recipients...) - } else { - if len(recipients) > 1 { - c.JSON(400, gin.H{"error": "More than one recipient is currently not allowed"}) - return - } - - groupId, err := base64.StdEncoding.DecodeString(recipients[0]) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid group id"}) - return - } - - cmd = append(cmd, []string{"-g", string(groupId)}...) - } - - attachmentTmpPaths := []string{} - for _, base64Attachment := range base64Attachments { - u, err := uuid.NewV4() - if err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - return - } - - dec, err := base64.StdEncoding.DecodeString(base64Attachment) - if err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - return - } - - fType, err := filetype.Get(dec) - if err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - return - } - - attachmentTmpPath := attachmentTmpDir + u.String() + "." + fType.Extension - attachmentTmpPaths = append(attachmentTmpPaths, attachmentTmpPath) - - f, err := os.Create(attachmentTmpPath) - if err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - return - } - defer f.Close() - - if _, err := f.Write(dec); err != nil { - cleanupTmpFiles(attachmentTmpPaths) - c.JSON(400, gin.H{"error": err.Error()}) - return - } - if err := f.Sync(); err != nil { - cleanupTmpFiles(attachmentTmpPaths) - c.JSON(400, gin.H{"error": err.Error()}) - return - } - - f.Close() - } - - if len(attachmentTmpPaths) > 0 { - cmd = append(cmd, "-a") - cmd = append(cmd, attachmentTmpPaths...) - } - - _, err := runSignalCli(true, cmd) - if err != nil { - cleanupTmpFiles(attachmentTmpPaths) - c.JSON(400, gin.H{"error": err.Error()}) - return - } - c.JSON(201, nil) -} - -func getGroups(number string, signalCliConfig string) ([]GroupEntry, error) { - groupEntries := []GroupEntry{} - - out, err := runSignalCli(true, []string{"--config", signalCliConfig, "-u", number, "listGroups", "-d"}) - if err != nil { - return groupEntries, err - } - - lines := strings.Split(out, "\n") - for _, line := range lines { - var groupEntry GroupEntry - if line == "" { - continue - } - - idIdx := strings.Index(line, " Name: ") - idPair := line[:idIdx] - groupEntry.InternalId = strings.TrimPrefix(idPair, "Id: ") - groupEntry.Id = convertInternalGroupIdToGroupId(groupEntry.InternalId) - lineWithoutId := strings.TrimLeft(line[idIdx:], " ") - - nameIdx := strings.Index(lineWithoutId, " Active: ") - namePair := lineWithoutId[:nameIdx] - groupEntry.Name = strings.TrimRight(strings.TrimPrefix(namePair, "Name: "), " ") - lineWithoutName := strings.TrimLeft(lineWithoutId[nameIdx:], " ") - - activeIdx := strings.Index(lineWithoutName, " Blocked: ") - activePair := lineWithoutName[:activeIdx] - active := strings.TrimPrefix(activePair, "Active: ") - if active == "true" { - groupEntry.Active = true - } else { - groupEntry.Active = false - } - lineWithoutActive := strings.TrimLeft(lineWithoutName[activeIdx:], " ") - - blockedIdx := strings.Index(lineWithoutActive, " Members: ") - blockedPair := lineWithoutActive[:blockedIdx] - blocked := strings.TrimPrefix(blockedPair, "Blocked: ") - if blocked == "true" { - groupEntry.Blocked = true - } else { - groupEntry.Blocked = false - } - lineWithoutBlocked := strings.TrimLeft(lineWithoutActive[blockedIdx:], " ") - - membersPair := lineWithoutBlocked - members := strings.TrimPrefix(membersPair, "Members: ") - trimmedMembers := strings.TrimRight(strings.TrimLeft(members, "["), "]") - trimmedMembersList := strings.Split(trimmedMembers, ",") - for _, member := range trimmedMembersList { - groupEntry.Members = append(groupEntry.Members, strings.Trim(member, " ")) - } - - groupEntries = append(groupEntries, groupEntry) - } - - return groupEntries, nil -} - -func runSignalCli(wait bool, args []string) (string, error) { - cmd := exec.Command("signal-cli", args...) - if wait { - var errBuffer bytes.Buffer - var outBuffer bytes.Buffer - cmd.Stderr = &errBuffer - cmd.Stdout = &outBuffer - - err := cmd.Start() - if err != nil { - return "", err - } - - done := make(chan error, 1) - go func() { - done <- cmd.Wait() - }() - select { - case <-time.After(60 * time.Second): - err := cmd.Process.Kill() - if err != nil { - return "", err - } - return "", errors.New("process killed as timeout reached") - case err := <-done: - if err != nil { - return "", errors.New(errBuffer.String()) - } - } - return outBuffer.String(), nil - } else { - stdout, err := cmd.StdoutPipe() - if err != nil { - return "", err - } - cmd.Start() - buf := bufio.NewReader(stdout) // Notice that this is not in a loop - line, _, _ := buf.ReadLine() - return string(line), nil - } -} +// @tag.name Messages +// @tag.description Send and Receive Signal Messages. +// @host 127.0.0.1:8080 +// @BasePath / func main() { signalCliConfig := flag.String("signal-cli-config", "/home/.local/share/signal-cli/", "Config directory where signal-cli config is stored") attachmentTmpDir := flag.String("attachment-tmp-dir", "/tmp/", "Attachment tmp directory") @@ -247,270 +40,53 @@ func main() { router := gin.Default() log.Info("Started Signal Messenger REST API") - router.GET("/v1/about", func(c *gin.Context) { - type About struct { - SupportedApiVersions []string `json:"versions"` - BuildNr int `json:"build"` + api := api.NewApi(*signalCliConfig, *attachmentTmpDir) + v1 := router.Group("/v1") + { + about := v1.Group("/about") + { + about.GET("", api.About) } - about := About{SupportedApiVersions: []string{"v1", "v2"}, BuildNr: 2} - c.JSON(200, about) - }) - - router.POST("/v1/register/:number", func(c *gin.Context) { - number := c.Param("number") - - type Request struct { - UseVoice bool `json:"use_voice"` + register := v1.Group("/register") + { + register.POST(":number", api.RegisterNumber) + register.POST(":number/verify/:token", api.VerifyRegisteredNumber) } - var req Request - - buf := new(bytes.Buffer) - buf.ReadFrom(c.Request.Body) - if buf.String() != "" { - err := json.Unmarshal(buf.Bytes(), &req) - if err != nil { - log.Error("Couldn't register number: ", err.Error()) - c.JSON(400, gin.H{"error": "Couldn't process request - invalid request."}) - return - } - } else { - req.UseVoice = false + sendV1 := v1.Group("/send") + { + sendV1.POST("", api.Send) } - if number == "" { - c.JSON(400, gin.H{"error": "Please provide a number"}) - return + receive := v1.Group("/receive") + { + receive.GET(":number", api.Receive) } - command := []string{"--config", *signalCliConfig, "-u", number, "register"} - - if req.UseVoice == true { - command = append(command, "--voice") + groups := v1.Group("/groups") + { + groups.POST(":number", api.CreateGroup) + groups.GET(":number", api.GetGroups) + groups.DELETE(":number/:groupid", api.DeleteGroup) } - _, err := runSignalCli(true, command) - if err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - return + link := v1.Group("qrcodelink") + { + link.GET("", api.GetQrCodeLink) } - c.JSON(201, nil) - }) + } - router.POST("/v1/register/:number/verify/:token", func(c *gin.Context) { - number := c.Param("number") - token := c.Param("token") - - if number == "" { - c.JSON(400, gin.H{"error": "Please provide a number"}) - return + v2 := router.Group("/v2") + { + sendV2 := v2.Group("/send") + { + sendV2.POST("", api.SendV2) } + } - if token == "" { - c.JSON(400, gin.H{"error": "Please provide a verification code"}) - return - } - - _, err := runSignalCli(true, []string{"--config", *signalCliConfig, "-u", number, "verify", token}) - if err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - return - } - c.JSON(201, nil) - }) - - router.POST("/v1/send", func(c *gin.Context) { - type Request struct { - Number string `json:"number"` - Recipients []string `json:"recipients"` - Message string `json:"message"` - Base64Attachment string `json:"base64_attachment"` - IsGroup bool `json:"is_group"` - } - var req Request - err := c.BindJSON(&req) - if err != nil { - c.JSON(400, gin.H{"error": "Couldn't process request - invalid request"}) - return - } - - base64Attachments := []string{} - if req.Base64Attachment != "" { - base64Attachments = append(base64Attachments, req.Base64Attachment) - } - - send(c, *signalCliConfig, *signalCliConfig, req.Number, req.Message, req.Recipients, base64Attachments, req.IsGroup) - }) - - router.POST("/v2/send", func(c *gin.Context) { - type Request struct { - Number string `json:"number"` - Recipients []string `json:"recipients"` - Message string `json:"message"` - Base64Attachments []string `json:"base64_attachments"` - } - var req Request - err := c.BindJSON(&req) - if err != nil { - c.JSON(400, gin.H{"error": "Couldn't process request - invalid request"}) - log.Error(err.Error()) - return - } - - if len(req.Recipients) == 0 { - c.JSON(400, gin.H{"error": "Couldn't process request - please provide at least one recipient"}) - return - } - - groups := []string{} - recipients := []string{} - - for _, recipient := range req.Recipients { - if strings.HasPrefix(recipient, groupPrefix) { - groups = append(groups, strings.TrimPrefix(recipient, groupPrefix)) - } else { - recipients = append(recipients, recipient) - } - } - - if len(recipients) > 0 && len(groups) > 0 { - c.JSON(400, gin.H{"error": "Signal Messenger Groups and phone numbers cannot be specified together in one request! Please split them up into multiple REST API calls."}) - return - } - - if len(groups) > 1 { - c.JSON(400, gin.H{"error": "A signal message cannot be sent to more than one group at once! Please use multiple REST API calls for that."}) - return - } - - for _, group := range groups { - send(c, *attachmentTmpDir, *signalCliConfig, req.Number, req.Message, []string{group}, req.Base64Attachments, true) - } - - if len(recipients) > 0 { - send(c, *attachmentTmpDir, *signalCliConfig, req.Number, req.Message, recipients, req.Base64Attachments, false) - } - }) - - router.GET("/v1/receive/:number", func(c *gin.Context) { - number := c.Param("number") - - command := []string{"--config", *signalCliConfig, "-u", number, "receive", "-t", "1", "--json"} - out, err := runSignalCli(true, command) - if err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - return - } - - out = strings.Trim(out, "\n") - lines := strings.Split(out, "\n") - - jsonStr := "[" - for i, line := range lines { - jsonStr += line - if i != (len(lines) - 1) { - jsonStr += "," - } - } - jsonStr += "]" - - c.String(200, jsonStr) - }) - - router.POST("/v1/groups/:number", func(c *gin.Context) { - number := c.Param("number") - - type Request struct { - Name string `json:"name"` - Members []string `json:"members"` - } - - var req Request - err := c.BindJSON(&req) - if err != nil { - c.JSON(400, gin.H{"error": "Couldn't process request - invalid request"}) - log.Error(err.Error()) - return - } - - cmd := []string{"--config", *signalCliConfig, "-u", number, "updateGroup", "-n", req.Name, "-m"} - cmd = append(cmd, req.Members...) - - out, err := runSignalCli(true, cmd) - if err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - return - } - - internalGroupId := getStringInBetween(out, `"`, `"`) - c.JSON(201, gin.H{"id": convertInternalGroupIdToGroupId(internalGroupId)}) - }) - - router.GET("/v1/groups/:number", func(c *gin.Context) { - number := c.Param("number") - - groups, err := getGroups(number, *signalCliConfig) - if err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - return - } - - c.JSON(200, groups) - }) - - router.DELETE("/v1/groups/:number/:groupid", func(c *gin.Context) { - base64EncodedGroupId := c.Param("groupid") - number := c.Param("number") - - if base64EncodedGroupId == "" { - c.JSON(400, gin.H{"error": "Please specify a group id"}) - return - } - - groupId, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(base64EncodedGroupId, groupPrefix)) - if err != nil { - c.JSON(400, gin.H{"error": "Invalid group id"}) - return - } - - _, err = runSignalCli(true, []string{"--config", *signalCliConfig, "-u", number, "quitGroup", "-g", string(groupId)}) - if err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - return - } - }) - - router.GET("/v1/qrcodelink", func(c *gin.Context) { - deviceName := c.Query("device_name") - - if deviceName == "" { - c.JSON(400, gin.H{"error": "Please provide a name for the device"}) - return - } - - command := []string{"--config", *signalCliConfig, "link", "-n", deviceName} - - tsdeviceLink, err := runSignalCli(false, command) - if err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - return - } - - q, err := qrcode.New(string(tsdeviceLink), qrcode.Medium) - if err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - } - - q.DisableBorder = true - var png []byte - png, err = q.PNG(256) - if err != nil { - c.JSON(400, gin.H{"error": err.Error()}) - } - - c.Data(200, "image/png", png) - }) + swaggerUrl := ginSwagger.URL("http://127.0.0.1:8080/swagger/doc.json") + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, swaggerUrl)) router.Run() }