feat(swagger): add Swagger UI for API documentation
Some checks failed
CI / Test (1.23) (push) Failing after -20m40s
CI / Test (1.22) (push) Failing after -20m58s
CI / Build (push) Failing after -21m33s
CI / Lint (push) Failing after -20m51s

* Create index.html for Swagger UI integration
* Link to Swagger UI CSS and JS from CDN
* Configure Swagger UI to load API specification from api.json
This commit is contained in:
Hein
2026-02-20 17:41:20 +02:00
parent 5b3aaba198
commit a3ff2dc497
7 changed files with 3516 additions and 319 deletions

View File

@@ -131,65 +131,6 @@
color: #666;
}
.endpoints {
background: #f8f9fa;
border-radius: 10px;
padding: 30px;
text-align: left;
animation: fadeInUp 0.8s ease 1s both;
}
.endpoints h2 {
color: #1a237e;
margin-bottom: 20px;
text-align: center;
}
.endpoint-group {
margin-bottom: 20px;
}
.endpoint-group h3 {
color: #1e88e5;
font-size: 1em;
margin-bottom: 10px;
}
.endpoint {
background: white;
padding: 12px 15px;
margin-bottom: 8px;
border-radius: 6px;
font-family: 'Monaco', 'Courier New', monospace;
font-size: 0.85em;
display: flex;
align-items: center;
gap: 10px;
}
.endpoint-method {
background: #1e88e5;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-weight: 600;
font-size: 0.85em;
min-width: 60px;
text-align: center;
}
.endpoint-method.post {
background: #4caf50;
}
.endpoint-method.delete {
background: #f44336;
}
.endpoint-path {
color: #666;
}
.footer {
margin-top: 40px;
padding-top: 30px;
@@ -265,11 +206,6 @@
width: 150px;
height: 150px;
}
.endpoint {
flex-direction: column;
align-items: flex-start;
}
}
</style>
</head>
@@ -311,258 +247,6 @@
</div>
</div>
<div class="endpoints">
<h2>Available API Endpoints</h2>
<div class="endpoint-group">
<h3>📊 Status & Health</h3>
<div class="endpoint">
<span class="endpoint-method">GET</span>
<span class="endpoint-path">/health</span>
</div>
</div>
<div class="endpoint-group">
<h3>🔌 Webhooks</h3>
<div class="endpoint">
<span class="endpoint-method">GET</span>
<span class="endpoint-path">/api/hooks</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/hooks/add</span>
</div>
<div class="endpoint">
<span class="endpoint-method delete">DELETE</span>
<span class="endpoint-path">/api/hooks/remove</span>
</div>
</div>
<div class="endpoint-group">
<h3>👤 Accounts</h3>
<div class="endpoint">
<span class="endpoint-method">GET</span>
<span class="endpoint-path">/api/accounts</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/accounts/add</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/accounts/update</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/accounts/disable</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/accounts/enable</span>
</div>
<div class="endpoint">
<span class="endpoint-method delete">DELETE</span>
<span class="endpoint-path">/api/accounts/remove</span>
</div>
</div>
<div class="endpoint-group">
<h3>💬 Send Messages</h3>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/image</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/video</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/document</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/audio</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/sticker</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/location</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/contacts</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/interactive</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/template</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/flow</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/reaction</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/messages/read</span>
</div>
</div>
<div class="endpoint-group">
<h3>📄 Templates</h3>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/templates</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/templates/upload</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/templates/delete</span>
</div>
</div>
<div class="endpoint-group">
<h3>🔄 Flows</h3>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/flows</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/flows/create</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/flows/get</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/flows/upload</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/flows/publish</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/flows/deprecate</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/flows/delete</span>
</div>
</div>
<div class="endpoint-group">
<h3>📞 Phone Numbers</h3>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/phone-numbers</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/phone-numbers/request-code</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/phone-numbers/verify-code</span>
</div>
</div>
<div class="endpoint-group">
<h3>🏪 Catalog / Commerce</h3>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/catalogs</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/catalogs/products</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/catalog</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/product</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/send/product-list</span>
</div>
</div>
<div class="endpoint-group">
<h3>🏢 Business Profile</h3>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/business-profile</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/business-profile/update</span>
</div>
</div>
<div class="endpoint-group">
<h3>🗑️ Media Management</h3>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/media/upload</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/media-delete</span>
</div>
</div>
<div class="endpoint-group">
<h3>💾 Message Cache</h3>
<div class="endpoint">
<span class="endpoint-method">GET</span>
<span class="endpoint-path">/api/cache</span>
</div>
<div class="endpoint">
<span class="endpoint-method">GET</span>
<span class="endpoint-path">/api/cache/stats</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/cache/replay</span>
</div>
</div>
<div class="endpoint-group">
<h3>🔔 WhatsApp Business API</h3>
<div class="endpoint">
<span class="endpoint-method">GET</span>
<span class="endpoint-path">/webhooks/whatsapp/{account_id}</span>
</div>
<div class="endpoint">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/webhooks/whatsapp/{account_id}</span>
</div>
</div>
</div>
<div class="footer">
<p>Need help? Check out the <a href="https://git.warky.dev/wdevs/whatshooked" target="_blank">documentation</a> &middot; <a href="/privacy-policy">Privacy Policy</a> &middot; <a href="/terms-of-service">Terms of Service</a></p>
<p style="margin-top: 10px;">Made by Warky Devs</p>

View File

@@ -390,7 +390,11 @@ func (m *Manager) sendToHook(ctx context.Context, hook config.Hook, payload inte
}
parsedURL.RawQuery = query.Encode()
req, err := http.NewRequestWithContext(hookCtx, hook.Method, parsedURL.String(), bytes.NewReader(data))
method := hook.Method
if method == "" {
method = http.MethodPost
}
req, err := http.NewRequestWithContext(hookCtx, method, parsedURL.String(), bytes.NewReader(data))
if err != nil {
logging.Error("Failed to create request", "hook_id", hook.ID, "error", err)
m.eventBus.Publish(events.HookFailedEvent(eventCtx, hook.ID, hook.Name, err))
@@ -413,8 +417,14 @@ func (m *Manager) sendToHook(ctx context.Context, hook config.Hook, payload inte
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
logging.Warn("Hook returned non-success status", "hook_id", hook.ID, "status", resp.StatusCode)
m.eventBus.Publish(events.HookFailedEvent(eventCtx, hook.ID, hook.Name, fmt.Errorf("status code %d", resp.StatusCode)))
errBody, _ := io.ReadAll(resp.Body)
logging.Warn("Hook returned non-success status",
"hook_id", hook.ID,
"status", resp.StatusCode,
"status_text", resp.Status,
"body", string(errBody),
)
m.eventBus.Publish(events.HookFailedEvent(eventCtx, hook.ID, hook.Name, fmt.Errorf("%s: %s", resp.Status, string(errBody))))
return nil
}

1704
pkg/serverembed/dist/api.json vendored Normal file

File diff suppressed because it is too large Load Diff

29
pkg/serverembed/dist/swagger/index.html vendored Normal file
View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WhatsHooked API</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
<style>
body { margin: 0; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
SwaggerUIBundle({
url: "../api.json",
dom_id: "#swagger-ui",
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset,
],
layout: "BaseLayout",
deepLinking: true,
tryItOutEnabled: true,
});
</script>
</body>
</html>

View File

@@ -18,6 +18,7 @@ import (
"git.warky.dev/wdevs/whatshooked/pkg/utils"
"git.warky.dev/wdevs/whatshooked/pkg/whatsapp"
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
"github.com/google/uuid"
"go.mau.fi/whatsmeow/types"
)
@@ -463,6 +464,15 @@ func (wh *WhatsHooked) StartAPIServer(ctx context.Context) error {
// Mark database as ready for account/hook loading
wh.dbReady = true
// Persist hook events to the event_log table
eventLogRepo := storage.NewEventLogRepository(db)
wh.eventBus.Subscribe(events.EventHookFailed, func(event events.Event) {
wh.persistEventLog(eventLogRepo, event, false)
})
wh.eventBus.Subscribe(events.EventHookSuccess, func(event events.Event) {
wh.persistEventLog(eventLogRepo, event, true)
})
// Sync config.json hooks and accounts into the database so they are never silently ignored
logging.Info("Syncing config to database")
if err := wh.syncConfigToDatabase(ctx); err != nil {
@@ -507,6 +517,33 @@ func (wh *WhatsHooked) StopAPIServer(ctx context.Context) error {
return nil
}
// persistEventLog writes a hook event to the public.event_log table.
func (wh *WhatsHooked) persistEventLog(repo *storage.EventLogRepository, event events.Event, success bool) {
hookID, _ := event.Data["hook_id"].(string)
errStr := ""
if !success {
errStr, _ = event.Data["error"].(string)
}
data, _ := json.Marshal(event.Data)
entry := models.ModelPublicEventLog{
ID: resolvespec_common.NewSqlString(uuid.New().String()),
EventType: resolvespec_common.NewSqlString(string(event.Type)),
EntityType: resolvespec_common.NewSqlString("hook"),
EntityID: resolvespec_common.NewSqlString(hookID),
Data: resolvespec_common.NewSqlString(string(data)),
Error: resolvespec_common.NewSqlString(errStr),
Success: success,
CreatedAt: resolvespec_common.NewSqlTimeStamp(event.Timestamp),
}
if err := repo.Create(context.Background(), &entry); err != nil {
logging.Error("Failed to persist event log entry", "event_type", event.Type, "hook_id", hookID, "error", err)
}
}
// handleHookResponse processes hook success events for two-way communication
func (wh *WhatsHooked) handleHookResponse(event events.Event) {
// Use event context for sending message

1704
web/public/api.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WhatsHooked API</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
<style>
body { margin: 0; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
SwaggerUIBundle({
url: "../api.json",
dom_id: "#swagger-ui",
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset,
],
layout: "BaseLayout",
deepLinking: true,
tryItOutEnabled: true,
});
</script>
</body>
</html>