mirror of
https://github.com/bbernhard/signal-cli-rest-api.git
synced 2026-05-19 13:34:19 +00:00
Merge 82f71512129b1d32603a6b5c695f5287e652b864 into db63fd15e0bc2e2de4cff0b1969b12c23508a8d7
This commit is contained in:
commit
68ecd0df66
15
Dockerfile
15
Dockerfile
@ -92,10 +92,21 @@ 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/
|
||||
COPY src/plugin_loader.go /tmp/signal-cli-rest-api-src/
|
||||
COPY src/docs/add_v1_receive_schemas.go /tmp/signal-cli-rest-api-src/docs/add_v1_receive_schemas.go
|
||||
|
||||
RUN ls -la /tmp/signal-cli-rest-api-src
|
||||
|
||||
# build the docs
|
||||
RUN cd /tmp/signal-cli-rest-api-src && ${GOPATH}/bin/swag init --requiredByDefault --outputTypes "go,json"
|
||||
|
||||
# manually add the json schemas for the receive V1 endpoint to the docs
|
||||
RUN cd /tmp/signal-cli-rest-api-src/docs \
|
||||
&& wget https://github.com/Gara-Dorta/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}-json-schemas.tar.gz \
|
||||
&& mkdir signal-cli-schemas \
|
||||
&& tar xf signal-cli-${SIGNAL_CLI_VERSION}-json-schemas.tar.gz -C signal-cli-schemas \
|
||||
&& go run add_v1_receive_schemas.go signal-cli-schemas
|
||||
|
||||
# build signal-cli-rest-api
|
||||
RUN ls -la /tmp/signal-cli-rest-api-src
|
||||
RUN cd /tmp/signal-cli-rest-api-src && ${GOPATH}/bin/swag init --requiredByDefault
|
||||
RUN cd /tmp/signal-cli-rest-api-src && go build -o signal-cli-rest-api main.go
|
||||
RUN cd /tmp/signal-cli-rest-api-src && go test ./client -v && go test ./utils -v
|
||||
|
||||
|
||||
@ -4,44 +4,64 @@ These files are generated using the [swaggo/swag](https://github.com/swaggo/swag
|
||||
|
||||
There are two steps, first generating the docs and then running the web server.
|
||||
|
||||
## Generating the docs
|
||||
## With docker compose (recommended)
|
||||
|
||||
Regenerate the files with your local source code changes.
|
||||
1. Build the docs
|
||||
```bash
|
||||
docker compose build
|
||||
```
|
||||
2. Serve the docs
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
3. Go to http://127.0.0.1:8080/swagger/index.html to view the docs
|
||||
* If you get a Network error, replace the IP for the docker internal IP in the error, e.g: http://172.18.0.2:8080/swagger/index.html
|
||||
|
||||
## Locally
|
||||
|
||||
Install [go](https://go.dev/).
|
||||
|
||||
### Generating the docs
|
||||
|
||||
1. Set the current working dir to `src`
|
||||
```bash
|
||||
cd src
|
||||
```
|
||||
1. Run swag to generate the docs
|
||||
* Option 1, via docker
|
||||
* Option 1, via go
|
||||
```bash
|
||||
docker run --rm -v $(pwd):/code ghcr.io/swaggo/swag:latest init --requiredByDefault
|
||||
go run github.com/swaggo/swag/cmd/swag@v1.16.6 init --requiredByDefault --outputTypes "go,json"
|
||||
```
|
||||
* Option 2, install swag and run the command line tool
|
||||
* Option 2, directly with swag
|
||||
```bash
|
||||
swag init --requiredByDefault
|
||||
swag init --requiredByDefault --outputTypes "go,json"
|
||||
```
|
||||
* Option 3, swag via docker
|
||||
```bash
|
||||
docker run --rm -v $(pwd):/code ghcr.io/swaggo/swag:latest init --requiredByDefault --outputTypes "go,json"
|
||||
```
|
||||
1. Set the current working dir to `src/docs`
|
||||
```bash
|
||||
cd docs
|
||||
```
|
||||
1. Add the signal-cli receive V1 schemas
|
||||
* Download the `signal-cli-x.y.z-json-schemas.tar.gz` schema files from https://github.com/Gara-Dorta/signal-cli/releases
|
||||
* Extract the files
|
||||
* Run the script to add the schemas
|
||||
```bash
|
||||
go run add_v1_receive_schemas.go ./path-to-signal-cli-json-schema-folder
|
||||
```
|
||||
|
||||
## Run the web server
|
||||
### Run the web server
|
||||
|
||||
Run the web server to visualize the generated docs.
|
||||
|
||||
1. Run the main script
|
||||
* Option 1, via docker, run the command at the root of the repository
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
* Option 2, install go and run the command line tool
|
||||
1. Navigate to the `src` folder
|
||||
```bash
|
||||
cd src
|
||||
```
|
||||
1. Run the main script
|
||||
```bash
|
||||
go run main.go
|
||||
```
|
||||
|
||||
|
||||
## Navigate to the docs
|
||||
|
||||
The docs are served at: http://127.0.0.1:8080/swagger/index.html
|
||||
|
||||
When serving with docker, if you get a Network error, replace the IP for the docker internal IP in the error, e.g: http://172.18.0.2:8080/swagger/index.html
|
||||
1. Go to http://127.0.0.1:8080/swagger/index.html
|
||||
|
||||
409
src/docs/add_v1_receive_schemas.go
Normal file
409
src/docs/add_v1_receive_schemas.go
Normal file
@ -0,0 +1,409 @@
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
_ "github.com/bbernhard/signal-cli-rest-api/docs"
|
||||
)
|
||||
|
||||
const (
|
||||
goDocsPath = "docs.go"
|
||||
jsonDocsPath = "swagger.json"
|
||||
openMarker = "const docTemplate = `"
|
||||
closeMarker = "`\n\n// SwaggerInfo"
|
||||
definitionsKey = `"definitions": {`
|
||||
receivePrefix = "receive."
|
||||
receivePathKey = `"/v1/receive/{number}":`
|
||||
receiveWrapper = "data.Message"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintf(os.Stderr, "usage: go run update_receive_docs.go <receiveDir>\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
receiveDir := os.Args[1]
|
||||
|
||||
if err := run(receiveDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(receiveDir string) error {
|
||||
definitions := make(map[string]interface{})
|
||||
|
||||
titleByFile, err := addReceiveSchemas(definitions, receiveDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateReceiveSchemaRefs(definitions, titleByFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addEnvelopeWrapperDefinition(definitions)
|
||||
|
||||
managedDefinitions, err := renderManagedDefinitions(definitions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateDocsGo(managedDefinitions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateSwaggerJSON(managedDefinitions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("updated %s\n", goDocsPath)
|
||||
fmt.Printf("updated %s\n", jsonDocsPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateDocsGo(managedDefinitions string) error {
|
||||
content, err := os.ReadFile(goDocsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", goDocsPath, err)
|
||||
}
|
||||
|
||||
template, err := extractDocTemplate(string(content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updatedTemplate, err := applyReceiveSchemaUpdates(template, managedDefinitions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updated := strings.Replace(string(content), template, updatedTemplate, 1)
|
||||
if err := os.WriteFile(goDocsPath, []byte(updated), 0644); err != nil {
|
||||
return fmt.Errorf("write %s: %w", goDocsPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateSwaggerJSON(managedDefinitions string) error {
|
||||
content, err := os.ReadFile(jsonDocsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", jsonDocsPath, err)
|
||||
}
|
||||
|
||||
updated, err := applyReceiveSchemaUpdates(string(content), managedDefinitions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(jsonDocsPath, []byte(updated), 0644); err != nil {
|
||||
return fmt.Errorf("write %s: %w", jsonDocsPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyReceiveSchemaUpdates(content string, managedDefinitions string) (string, error) {
|
||||
updated, err := appendDefinitionsEntries(content, managedDefinitions)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
updated, err = replaceReceiveResponseSchema(updated)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func extractDocTemplate(content string) (string, error) {
|
||||
start := strings.Index(content, openMarker)
|
||||
if start == -1 {
|
||||
return "", fmt.Errorf("could not find docTemplate start in %s", goDocsPath)
|
||||
}
|
||||
|
||||
start += len(openMarker)
|
||||
endOffset := strings.Index(content[start:], closeMarker)
|
||||
if endOffset == -1 {
|
||||
return "", fmt.Errorf("could not find docTemplate end in %s", goDocsPath)
|
||||
}
|
||||
|
||||
return content[start : start+endOffset], nil
|
||||
}
|
||||
|
||||
func definitionsBounds(template string) (int, int, error) {
|
||||
definitionsIndex := strings.Index(template, definitionsKey)
|
||||
if definitionsIndex == -1 {
|
||||
return -1, -1, fmt.Errorf("could not find definitions block in docTemplate")
|
||||
}
|
||||
|
||||
braceIndex := definitionsIndex + strings.Index(definitionsKey, "{")
|
||||
closingBraceIndex, err := findMatchingBrace(template, braceIndex)
|
||||
if err != nil {
|
||||
return -1, -1, err
|
||||
}
|
||||
|
||||
return braceIndex, closingBraceIndex, nil
|
||||
}
|
||||
|
||||
func appendDefinitionsEntries(template string, entries string) (string, error) {
|
||||
braceIndex, closingBraceIndex, err := definitionsBounds(template)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
definitionsBlock := template[braceIndex : closingBraceIndex+1]
|
||||
if strings.Contains(definitionsBlock, `"`+receiveWrapper+`"`) || strings.Contains(definitionsBlock, `"`+receivePrefix) {
|
||||
return "", fmt.Errorf("definitions already contain receive entries; run swag init first")
|
||||
}
|
||||
|
||||
inner := strings.TrimSpace(template[braceIndex+1 : closingBraceIndex])
|
||||
|
||||
if inner == "" {
|
||||
return template[:braceIndex+1] + "\n" + entries + "\n" + template[closingBraceIndex:], nil
|
||||
}
|
||||
|
||||
closingLine := "\n }"
|
||||
insertIndex := strings.LastIndex(template[:closingBraceIndex+1], closingLine)
|
||||
if insertIndex == -1 {
|
||||
return "", fmt.Errorf("could not determine definitions closing line")
|
||||
}
|
||||
|
||||
return template[:insertIndex] + ",\n" + entries + template[insertIndex:], nil
|
||||
}
|
||||
|
||||
func addReceiveSchemas(definitions map[string]interface{}, receiveDir string) (map[string]string, error) {
|
||||
entries, err := os.ReadDir(receiveDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", receiveDir, err)
|
||||
}
|
||||
|
||||
files := make([]string, 0)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".schema.json") {
|
||||
continue
|
||||
}
|
||||
files = append(files, entry.Name())
|
||||
}
|
||||
sort.Strings(files)
|
||||
titleByFile := make(map[string]string, len(files))
|
||||
|
||||
for _, name := range files {
|
||||
fullPath := filepath.Join(receiveDir, name)
|
||||
data, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read schema file %s: %w", fullPath, err)
|
||||
}
|
||||
|
||||
var schemaObj map[string]interface{}
|
||||
if err := json.Unmarshal(data, &schemaObj); err != nil {
|
||||
return nil, fmt.Errorf("parse schema file %s: %w", fullPath, err)
|
||||
}
|
||||
|
||||
title, ok := schemaObj["title"].(string)
|
||||
if !ok || strings.TrimSpace(title) == "" {
|
||||
return nil, fmt.Errorf("schema file %s is missing title", fullPath)
|
||||
}
|
||||
|
||||
titleByFile[name] = title
|
||||
definitions[receivePrefix+title] = removeSchemaKeysRecursive(schemaObj, "")
|
||||
}
|
||||
|
||||
return titleByFile, nil
|
||||
}
|
||||
|
||||
func removeSchemaKeysRecursive(value interface{}, parentKey string) interface{} {
|
||||
switch typed := value.(type) {
|
||||
case map[string]interface{}:
|
||||
updated := make(map[string]interface{}, len(typed))
|
||||
for key, item := range typed {
|
||||
if key == "$schema" || key == "$id" {
|
||||
continue
|
||||
}
|
||||
if key == "title" && parentKey != "properties" {
|
||||
continue
|
||||
}
|
||||
updated[key] = removeSchemaKeysRecursive(item, key)
|
||||
}
|
||||
return updated
|
||||
case []interface{}:
|
||||
updated := make([]interface{}, len(typed))
|
||||
for idx, item := range typed {
|
||||
updated[idx] = removeSchemaKeysRecursive(item, parentKey)
|
||||
}
|
||||
return updated
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func updateReceiveSchemaRefs(definitions map[string]interface{}, titleByFile map[string]string) error {
|
||||
for key, value := range definitions {
|
||||
if !strings.HasPrefix(key, receivePrefix) {
|
||||
continue
|
||||
}
|
||||
definitions[key] = rewriteSchemaRefs(value, titleByFile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rewriteSchemaRefs(value interface{}, titleByFile map[string]string) interface{} {
|
||||
switch typed := value.(type) {
|
||||
case map[string]interface{}:
|
||||
updated := make(map[string]interface{}, len(typed))
|
||||
for k, v := range typed {
|
||||
if k == "$ref" {
|
||||
if refValue, ok := v.(string); ok {
|
||||
if strings.HasSuffix(refValue, ".schema.json") {
|
||||
base := filepath.Base(refValue)
|
||||
if title, exists := titleByFile[base]; exists {
|
||||
updated[k] = "#/definitions/" + receivePrefix + title
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updated[k] = rewriteSchemaRefs(v, titleByFile)
|
||||
}
|
||||
return updated
|
||||
case []interface{}:
|
||||
updated := make([]interface{}, len(typed))
|
||||
for idx, item := range typed {
|
||||
updated[idx] = rewriteSchemaRefs(item, titleByFile)
|
||||
}
|
||||
return updated
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func addEnvelopeWrapperDefinition(definitions map[string]interface{}) {
|
||||
definitions[receiveWrapper] = map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"account": map[string]interface{}{"type": "string"},
|
||||
"envelope": map[string]interface{}{"$ref": "#/definitions/receive.MessageEnvelope"},
|
||||
},
|
||||
"required": []interface{}{"account", "envelope"},
|
||||
}
|
||||
}
|
||||
|
||||
func renderManagedDefinitions(definitions map[string]interface{}) (string, error) {
|
||||
keys := make([]string, 0, len(definitions))
|
||||
for key := range definitions {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
raw, err := json.MarshalIndent(definitions[key], "", " ")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal definition %s: %w", key, err)
|
||||
}
|
||||
|
||||
lines := strings.Split(string(raw), "\n")
|
||||
for idx := range lines {
|
||||
lines[idx] = " " + lines[idx]
|
||||
}
|
||||
|
||||
entry := " " + strconv.Quote(key) + ": " + strings.TrimPrefix(lines[0], " ")
|
||||
if len(lines) > 1 {
|
||||
entry += "\n" + strings.Join(lines[1:], "\n")
|
||||
}
|
||||
|
||||
parts = append(parts, entry)
|
||||
}
|
||||
|
||||
return strings.Join(parts, ",\n"), nil
|
||||
}
|
||||
|
||||
func replaceReceiveResponseSchema(template string) (string, error) {
|
||||
pathIndex := strings.Index(template, receivePathKey)
|
||||
if pathIndex == -1 {
|
||||
return "", fmt.Errorf("could not find receive path block; run swag init first")
|
||||
}
|
||||
braceOffset := strings.Index(template[pathIndex+len(receivePathKey):], "{")
|
||||
if braceOffset == -1 {
|
||||
return "", fmt.Errorf("could not find opening brace for receive path block")
|
||||
}
|
||||
pathOpenBrace := pathIndex + len(receivePathKey) + braceOffset
|
||||
pathCloseBrace, err := findMatchingBrace(template, pathOpenBrace)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pathBlock := template[pathOpenBrace : pathCloseBrace+1]
|
||||
|
||||
oldSchema := `"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}`
|
||||
|
||||
newSchema := `"schema": {
|
||||
"$ref": "#/definitions/data.Message"
|
||||
}`
|
||||
|
||||
updatedPathBlock := strings.Replace(pathBlock, oldSchema, newSchema, 1)
|
||||
if updatedPathBlock == pathBlock {
|
||||
return "", fmt.Errorf("could not replace /v1/receive schema; ensure generated docs are freshly generated by swag")
|
||||
}
|
||||
|
||||
return template[:pathOpenBrace] + updatedPathBlock + template[pathCloseBrace+1:], nil
|
||||
}
|
||||
|
||||
func findMatchingBrace(input string, openBraceIndex int) (int, error) {
|
||||
depth := 0
|
||||
inString := false
|
||||
escaped := false
|
||||
|
||||
for index := openBraceIndex; index < len(input); index++ {
|
||||
char := input[index]
|
||||
|
||||
if inString {
|
||||
if escaped {
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
if char == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
if char == '"' {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if char == '"' {
|
||||
inString = true
|
||||
continue
|
||||
}
|
||||
|
||||
switch char {
|
||||
case '{':
|
||||
depth++
|
||||
case '}':
|
||||
depth--
|
||||
if depth == 0 {
|
||||
return index, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1, fmt.Errorf("could not find matching brace")
|
||||
}
|
||||
1345
src/docs/docs.go
1345
src/docs/docs.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user