This commit is contained in:
ponzischeme89
2026-08-19 06:57:59 +12:00
parent 8c847c59b8
commit 2b43b9ef12
94 changed files with 6359 additions and 778 deletions
+56 -2
View File
@@ -20,6 +20,7 @@ import (
"time"
"github.com/ponzischeme89/memby/server/internal/adminevents"
"github.com/ponzischeme89/memby/server/internal/notify"
"github.com/ponzischeme89/memby/server/internal/store"
)
@@ -62,6 +63,11 @@ type Dispatcher struct {
log *slog.Logger
client *http.Client
events *adminevents.Bus
// notify is the audit trail every outbound notification lands in. This package is the
// one producer that reports to it rather than being driven by it: the dispatcher has
// its own queue, pacing and transport registry, and routing deliveries through
// notify.Send would make the audit trail the thing deciding what Discord receives.
notify *notify.Service
transports map[string]Transport
queue chan job
@@ -80,9 +86,12 @@ type Dispatcher struct {
// SetPaused installs the server-wide quiet-time gate before Start is called.
func (d *Dispatcher) SetPaused(paused func() bool) { d.paused = paused }
func New(st *store.Store, log *slog.Logger, events *adminevents.Bus) *Dispatcher {
func New(
st *store.Store, log *slog.Logger, events *adminevents.Bus, notifier *notify.Service,
) *Dispatcher {
dispatcher := &Dispatcher{
store: st, log: log.With("component", "integrations"), events: events,
notify: notifier,
client: &http.Client{Timeout: requestTimeout},
transports: map[string]Transport{},
queue: make(chan job, queueDepth),
@@ -211,11 +220,12 @@ func (d *Dispatcher) post(ctx context.Context, integration store.Integration, ev
if err != nil {
message = err.Error()
}
took := time.Since(started)
if d.store != nil {
record := store.IntegrationDelivery{
IntegrationID: integration.ID, EventType: event.Type,
Success: err == nil, StatusCode: status,
DurationMS: time.Since(started).Milliseconds(), Error: message,
DurationMS: took.Milliseconds(), Error: message,
}
if writeErr := d.store.RecordIntegrationDelivery(
context.WithoutCancel(ctx), record,
@@ -223,9 +233,53 @@ func (d *Dispatcher) post(ctx context.Context, integration store.Integration, ev
d.log.Warn("delivery not recorded", "integration", integration.ID, "error", writeErr)
}
}
// The per-integration delivery history above answers "is this destination healthy",
// which is what the integrations page asks. This is the other question — "did Memby
// tell anybody about that event" — and it is answered in one place for every channel,
// which is the whole reason the notification log exists.
d.notify.Log(ctx, notify.Notification{
Channel: notify.ChannelWebhook,
Kind: event.Type,
Source: "integrations",
Title: event.Title,
Body: event.Summary,
// The destination's NAME, never its address: a Discord webhook URL is the
// credential, and this row is rendered in the console.
Target: integration.Name,
SourceKey: integration.ID,
Metadata: map[string]any{
"integrationId": integration.ID,
"kind": integration.Kind,
"statusCode": status,
},
}, deliveryOutcome(status, err), took)
return err
}
// deliveryOutcome turns a transport's answer into an audit status.
//
// A webhook is the one channel that gets Delivered rather than Sent: somebody else's
// service actually acknowledged this, where writing a row into a viewer's list is
// finished the moment it returns with nobody to confirm it. The status code is kept in
// the detail because "failed" on its own sends an operator to the wrong place — a 404 is
// a webhook that has been deleted, a 429 is one that is merely busy.
func deliveryOutcome(status int, err error) notify.Outcome {
if err != nil {
if status > 0 {
return notify.Outcome{
Status: notify.StatusFailed,
Detail: fmt.Sprintf("HTTP %d: %s", status, err.Error()),
Err: err,
}
}
return notify.Failed(err)
}
if status > 0 {
return notify.Delivered(fmt.Sprintf("HTTP %d", status))
}
return notify.Delivered("")
}
// announceFailure puts a failed delivery back into the feed the operator is reading.
//
// It publishes a *different* type from the event that failed, and integration.failed is