Add Google Chat webhook sender and settings API (Feature 19)
internal/chat keeps the incoming webhook in chat_settings, sealed with
secretbox, and is the notify sender of the chat channel.
- PUT /api/v1/settings/chat-webhook {url, space_label} accepts only
https://chat.googleapis.com/v1/spaces/{space}/messages?key=…&token=…;
GET answers the space label, a masked URL tail and the last test outcome.
- POST /api/v1/settings/chat-webhook/test posts "MealQuest · test message",
answers Google Chat's HTTP status and records last_test_at/status.
- Notifications post {text}, plus cardsV2 with an "Open" button under
MEALQUEST_PUBLIC_URL for plan_ready and list_ready, threaded by
threadKey=mealquest-week-YYYY-Www with
REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD. 429 and 5xx are retried; any other
non-2xx fails the row at once through the new notify.ErrUndeliverable.
- One place builds every event's title, body and Chat text.
The webhook URL never appears in a response, log line or error; tests
against an httptest Chat endpoint scan both.
Maestro task 919e9422-3a41-4f1f-8967-77da17f5f9f3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,8 +48,14 @@ type Message struct {
|
||||
Attempt int64
|
||||
}
|
||||
|
||||
// ErrUndeliverable is wrapped by a Sender's error when retrying cannot
|
||||
// help, such as a channel refusing the message outright: the worker marks
|
||||
// the row failed at once instead of retrying it.
|
||||
var ErrUndeliverable = errors.New("notify: undeliverable")
|
||||
|
||||
// Sender delivers notifications on one channel. Send returns nil once the
|
||||
// channel has accepted m, and an error otherwise; the worker retries it.
|
||||
// channel has accepted m, and an error otherwise; the worker retries it,
|
||||
// unless the error wraps ErrUndeliverable.
|
||||
// The error's text is stored in notification_outbox.last_error and logged,
|
||||
// so it must never carry a secret: no webhook URL, key or token (a
|
||||
// *url.Error quotes its URL).
|
||||
@@ -235,7 +241,7 @@ func (s *Service) deliver(ctx context.Context, row db.NotificationOutbox) {
|
||||
return
|
||||
}
|
||||
|
||||
if !now.Before(deadline) {
|
||||
if errors.Is(err, ErrUndeliverable) || !now.Before(deadline) {
|
||||
logger.ErrorContext(ctx, "notification failed; giving up", "attempt", attempt, "error", err)
|
||||
s.giveUp(ctx, logger, row, 1, err.Error())
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user