Rewrite the service in Go and ship it as a static container
The PHP app, Apache, and .htaccess are replaced by a single Go binary (standard library only). Routing, the JSON response, the landing page, the shared email design, and all four parsers are ported with the same output; JSON object keys are kept in sender order via an ordered decoder. - mail.go: SMTP delivery with quoted-printable body and RFC 2047 subject - main.go: routing, client IP from the proxy, /healthz, -check flag - render.go: email shell, escaping, table helpers, generic renderer - parser_*.go: github, grafana, json, wxinteract - main_test.go: handler, response shape, IP forwarding, every parser - Dockerfile: multi-stage build to distroless static, non-root, HEALTHCHECK - deploy/webhooks.dixon.cx.service: systemd unit for the container - README and PARSER_PROMPT updated for Go; IMPLEMENTATION_SUMMARY removed
This commit is contained in:
+128
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// jsonParse renders any JSON body as tables, splitting nested objects into
|
||||
// their own tables under path breadcrumbs like order->items->[0]->tags.
|
||||
func jsonParse(raw []byte, headers http.Header, meta Meta) (string, string, bool) {
|
||||
v, err := decodeJSON(raw)
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
body := section("📋 Request Information", table(
|
||||
row("IP Address", h(meta.IP)),
|
||||
row("Method", h(meta.Method)),
|
||||
row("Content-Type", h(meta.ContentType)),
|
||||
)) + section("📦 JSON Data", renderJSON(v, ""))
|
||||
|
||||
html := emailShell("📊 JSON Webhook Data", h(meta.Date), body, "JSON data automatically parsed and formatted.")
|
||||
return html, "📊 JSON Webhook - " + meta.Date, true
|
||||
}
|
||||
|
||||
func pathTitle(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
return `<div class="path-title">` + h(path) + `</div>`
|
||||
}
|
||||
|
||||
func renderJSON(data any, path string) string {
|
||||
var b strings.Builder
|
||||
|
||||
switch d := data.(type) {
|
||||
case []any:
|
||||
if len(d) == 0 {
|
||||
return ""
|
||||
}
|
||||
allScalar := true
|
||||
for _, it := range d {
|
||||
if isContainer(it) {
|
||||
allScalar = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allScalar {
|
||||
// Array of plain values: one table under a single breadcrumb
|
||||
rows := make([]string, 0, len(d))
|
||||
for i, it := range d {
|
||||
rows = append(rows, row("Item "+fmt.Sprint(i+1), esc(str(it))))
|
||||
}
|
||||
return pathTitle(path) + table(rows...)
|
||||
}
|
||||
for i, item := range d {
|
||||
itemPath := fmt.Sprintf("[%d]", i)
|
||||
if path != "" {
|
||||
itemPath = path + "->" + itemPath
|
||||
}
|
||||
if m := asObject(item); m != nil {
|
||||
var simple []string
|
||||
var sub []string
|
||||
for _, k := range m.Keys {
|
||||
if isContainer(m.M[k]) {
|
||||
sub = append(sub, k)
|
||||
} else {
|
||||
simple = append(simple, row(h(k), esc(str(m.M[k]))))
|
||||
}
|
||||
}
|
||||
if len(simple) > 0 {
|
||||
b.WriteString(pathTitle(path2(path, itemPath)))
|
||||
b.WriteString(`<div class="array-item"><div class="array-item-title">Item ` + fmt.Sprint(i+1) + `</div>` + table(simple...) + `</div>`)
|
||||
}
|
||||
for _, k := range sub {
|
||||
b.WriteString(renderJSON(m.M[k], itemPath+"->"+k))
|
||||
}
|
||||
} else if isContainer(item) {
|
||||
b.WriteString(renderJSON(item, itemPath))
|
||||
} else {
|
||||
b.WriteString(table(row("Item "+fmt.Sprint(i+1), esc(str(item)))))
|
||||
}
|
||||
}
|
||||
|
||||
case *OMap:
|
||||
var simple []string
|
||||
var sub []string
|
||||
for _, k := range d.Keys {
|
||||
if isContainer(d.M[k]) {
|
||||
sub = append(sub, k)
|
||||
} else {
|
||||
simple = append(simple, row(h(k), esc(str(d.M[k]))))
|
||||
}
|
||||
}
|
||||
if len(simple) > 0 {
|
||||
b.WriteString(pathTitle(path) + table(simple...))
|
||||
}
|
||||
for _, k := range sub {
|
||||
subPath := k
|
||||
if path != "" {
|
||||
subPath = path + "->" + k
|
||||
}
|
||||
b.WriteString(renderJSON(d.M[k], subPath))
|
||||
}
|
||||
|
||||
default:
|
||||
// Scalar (or null): one-row table so it lines up with everything else
|
||||
label := path
|
||||
if label == "" {
|
||||
label = "value"
|
||||
}
|
||||
val := str(data)
|
||||
if data == nil {
|
||||
val = "null"
|
||||
}
|
||||
return table(row(h(label), esc(val)))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// path2 mirrors the PHP behaviour: an item breadcrumb is only shown when nested under a path.
|
||||
func path2(parent, itemPath string) string {
|
||||
if parent == "" {
|
||||
return ""
|
||||
}
|
||||
return itemPath
|
||||
}
|
||||
Reference in New Issue
Block a user