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:
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ai/aisettings"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/chat"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/dinners"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/forwarded"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/frontend"
|
||||
@@ -40,6 +41,7 @@ type RouteMounter interface {
|
||||
// /media; settings serves /api/v1/settings/household; search serves
|
||||
// /api/v1/settings/recipe-search and its /test; ai serves
|
||||
// /api/v1/settings/ai, its /api-key, /verify, /models and /reset-prompt;
|
||||
// chatSvc serves GET and PUT /api/v1/settings/chat-webhook and its /test;
|
||||
// dinnerSvc serves GET and POST /api/v1/dinners, PATCH and DELETE
|
||||
// /api/v1/dinners/{id}, POST /api/v1/dinners/{id}/favorite, GET
|
||||
// /api/v1/dinners/{id}/recipe-candidates and POST /api/v1/dinners/{id}/ground;
|
||||
@@ -63,7 +65,7 @@ type RouteMounter interface {
|
||||
// GET /media/photos/{id}-{size}.jpg; assets is the web build
|
||||
// served for every other GET. /health, /auth and the web build are public.
|
||||
// Requests to /api/v1 other than GET also need the session's CSRF token in CSRFHeader.
|
||||
func NewRouter(logger *slog.Logger, version string, trusted forwarded.Trusted, auth RouteMounter, sessions SessionLookup, settings *household.SettingsService, search *recipes.SearchSettingsService, ai *aisettings.Service, dinnerSvc *dinners.Service, suggestSvc *suggest.Service, planSvc *plan.Service, shopSvc *shopping.Service, googleSvc *gcal.Service, pushSvc *push.Service, runner *jobs.Runner, photos PhotoFiles, assets fs.FS) (http.Handler, error) {
|
||||
func NewRouter(logger *slog.Logger, version string, trusted forwarded.Trusted, auth RouteMounter, sessions SessionLookup, settings *household.SettingsService, search *recipes.SearchSettingsService, ai *aisettings.Service, chatSvc *chat.Service, dinnerSvc *dinners.Service, suggestSvc *suggest.Service, planSvc *plan.Service, shopSvc *shopping.Service, googleSvc *gcal.Service, pushSvc *push.Service, runner *jobs.Runner, photos PhotoFiles, assets fs.FS) (http.Handler, error) {
|
||||
if settings == nil {
|
||||
return nil, errors.New("server: nil household settings service")
|
||||
}
|
||||
@@ -73,6 +75,9 @@ func NewRouter(logger *slog.Logger, version string, trusted forwarded.Trusted, a
|
||||
if ai == nil {
|
||||
return nil, errors.New("server: nil AI settings service")
|
||||
}
|
||||
if chatSvc == nil {
|
||||
return nil, errors.New("server: nil Google Chat service")
|
||||
}
|
||||
if dinnerSvc == nil {
|
||||
return nil, errors.New("server: nil dinners service")
|
||||
}
|
||||
@@ -134,6 +139,9 @@ func NewRouter(logger *slog.Logger, version string, trusted forwarded.Trusted, a
|
||||
apiV1.Post("/settings/ai/verify", postAIVerify(logger, ai))
|
||||
apiV1.Get("/settings/ai/models", getAIModels(logger, ai))
|
||||
apiV1.Post("/settings/ai/reset-prompt", postAIResetPrompt(logger, ai))
|
||||
apiV1.Get("/settings/chat-webhook", getChatWebhook(logger, chatSvc))
|
||||
apiV1.Put("/settings/chat-webhook", putChatWebhook(logger, chatSvc))
|
||||
apiV1.Post("/settings/chat-webhook/test", postChatWebhookTest(logger, chatSvc))
|
||||
apiV1.Get("/dinners", getDinners(logger, dinnerSvc))
|
||||
apiV1.Post("/dinners", postDinner(logger, dinnerSvc))
|
||||
apiV1.Patch("/dinners/{id}", patchDinner(logger, dinnerSvc))
|
||||
|
||||
Reference in New Issue
Block a user