Add server components
build-winui / winui (push) Has been cancelled

This commit is contained in:
QWQLwToo
2026-06-26 13:28:09 +08:00
parent 7ecc6a8923
commit 079ee4eaeb
168 changed files with 37475 additions and 0 deletions
@@ -0,0 +1,180 @@
package webhook
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"strings"
"time"
"ymhut-box/server/feedback-mailer/internal/config"
"ymhut-box/server/feedback-mailer/internal/db"
)
type Dispatcher struct {
cfg *config.Config
store *db.Store
}
type Event struct {
Event string `json:"event"`
Delivery string `json:"delivery"`
OccurredAt string `json:"occurredAt"`
Data any `json:"data"`
}
func NewDispatcher(cfg *config.Config, store *db.Store) *Dispatcher {
return &Dispatcher{cfg: cfg, store: store}
}
func (d *Dispatcher) Dispatch(event string, data any) {
if d == nil || len(d.cfg.Webhooks) == 0 {
return
}
for _, hook := range d.cfg.Webhooks {
if !hook.Enabled || hook.URL == "" || !matchesEvent(hook.Events, event) {
continue
}
hookCopy := hook
go d.Deliver(hookCopy, event, data)
}
}
func (d *Dispatcher) DispatchSync(event string, data any) {
if d == nil || len(d.cfg.Webhooks) == 0 {
return
}
for _, hook := range d.cfg.Webhooks {
if !hook.Enabled || hook.URL == "" || !matchesEvent(hook.Events, event) {
continue
}
d.Deliver(hook, event, data)
}
}
func (d *Dispatcher) DispatchTest(data any) int {
if d == nil || len(d.cfg.Webhooks) == 0 {
return 0
}
count := 0
for _, hook := range d.cfg.Webhooks {
if !hook.Enabled || hook.URL == "" {
continue
}
d.Deliver(hook, "feedback.test", data)
count++
}
return count
}
func (d *Dispatcher) Deliver(hook config.WebhookConfig, event string, data any) {
if d == nil || hook.URL == "" {
return
}
payload, deliveryKey := buildPayload(event, data)
sum := sha256.Sum256(payload)
id, err := d.store.InsertWebhookDelivery(db.WebhookDelivery{
WebhookName: hook.Name,
Event: event,
Status: "pending",
PayloadSHA256: hex.EncodeToString(sum[:]),
CreatedAt: db.Now(),
})
if err != nil {
return
}
maxRetries := hook.MaxRetries
if maxRetries < 0 {
maxRetries = 0
}
attempts := 0
status := "failed"
responseCode := 0
errorMessage := ""
for attempts <= maxRetries {
attempts++
code, err := postJSON(hook, event, deliveryKey, payload)
responseCode = code
if err == nil && code >= 200 && code < 300 {
status = "sent"
errorMessage = ""
break
}
if err != nil {
errorMessage = err.Error()
} else {
errorMessage = "webhook returned HTTP " + http.StatusText(code)
if errorMessage == "webhook returned HTTP " {
errorMessage = "webhook returned HTTP status"
}
}
if attempts <= maxRetries {
time.Sleep(time.Duration(attempts) * 350 * time.Millisecond)
}
}
_ = d.store.FinishWebhookDelivery(id, status, attempts, responseCode, errorMessage)
}
func buildPayload(event string, data any) ([]byte, string) {
now := db.Now()
rawDelivery := sha256.Sum256([]byte(event + "\n" + now + "\n" + db.ToJSON(data)))
delivery := hex.EncodeToString(rawDelivery[:16])
payload := Event{Event: event, Delivery: delivery, OccurredAt: now, Data: data}
encoded, err := json.Marshal(payload)
if err != nil {
return []byte(`{"event":"` + event + `"}`), delivery
}
return encoded, delivery
}
func postJSON(hook config.WebhookConfig, event, delivery string, body []byte) (int, error) {
timeout := hook.TimeoutSeconds
if timeout <= 0 {
timeout = 5
}
request, err := http.NewRequest(http.MethodPost, hook.URL, bytes.NewReader(body))
if err != nil {
return 0, err
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "YMhut-Feedback-Webhook/1.0")
request.Header.Set("X-YMhut-Event", event)
request.Header.Set("X-YMhut-Delivery", delivery)
request.Header.Set("X-YMhut-Signature", sign(hook.Secret, body))
client := &http.Client{Timeout: time.Duration(timeout) * time.Second}
response, err := client.Do(request)
if err != nil {
return 0, err
}
defer response.Body.Close()
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096))
return response.StatusCode, nil
}
func sign(secret string, body []byte) string {
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write(body)
return "sha256=" + hex.EncodeToString(mac.Sum(nil))
}
func matchesEvent(events []string, event string) bool {
if len(events) == 0 {
return true
}
for _, candidate := range events {
candidate = strings.TrimSpace(candidate)
if candidate == "*" || candidate == event {
return true
}
if strings.HasSuffix(candidate, ".*") && strings.HasPrefix(event, strings.TrimSuffix(candidate, "*")) {
return true
}
}
return false
}