added swagger documentation

This commit is contained in:
Bernhard B
2020-07-03 17:32:52 +02:00
parent 65385d300e
commit e1c2853494
9 changed files with 2258 additions and 490 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
signal-cli-config
src/main
src/signal-cli-rest-api

View File

@@ -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 build
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"]

619
src/api/api.go Normal file
View File

@@ -0,0 +1,619 @@
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)
}

574
src/docs/docs.go Normal file
View File

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

512
src/docs/swagger.json Normal file
View File

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

335
src/docs/swagger.yaml Normal file
View File

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

View File

@@ -3,9 +3,22 @@ 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
)

View File

@@ -1,9 +1,50 @@
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=
@@ -14,38 +55,132 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
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=

View File

@@ -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/gofrs/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()
}