package main import ( "context" "encoding/json" "errors" "net/http" "strings" "time" "git.cin.su/m/Assistant/adapters/notificationexecutor" commitcontract "git.cin.su/m/Assistant/contracts/commit" "git.cin.su/m/Assistant/contracts/continuity" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/work" commitdomain "git.cin.su/m/Assistant/modules/commit/domain" "git.cin.su/m/Assistant/modules/commit/ports" "git.cin.su/m/Assistant/platform/ids" ) const prospectiveNotificationActor identity.ActorID = "domovoy-creator-actor" type internalNotificationContinuity interface { InternalItem(context.Context, continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) } type internalNotificationHandler struct { core creatorSessionSource continuity internalNotificationContinuity authority creatorAuthoritySource repository creatorCommitRepository executor ports.Executor newID func() (ids.ID, error) now func() time.Time } type internalProspectiveNotificationRequest struct { ProspectiveItemID continuity.ProspectiveItemID `json:"prospective_item_id"` ExpectedRevision continuity.ProspectiveRevision `json:"expected_revision"` } func registerInternalNotificationRoutes(mux *http.ServeMux, handler internalNotificationHandler, token string) { mux.HandleFunc("POST /v1/internal/continuity/notify-due", requireCommitToken(token, handler.notifyDue)) } func (h internalNotificationHandler) notifyDue(w http.ResponseWriter, r *http.Request) { var request internalProspectiveNotificationRequest decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 8<<10)) decoder.DisallowUnknownFields() if err := decoder.Decode(&request); err != nil || ensureCreatorEOF(decoder) != nil || request.ProspectiveItemID == "" || request.ExpectedRevision == 0 { http.Error(w, "invalid prospective notification request", http.StatusBadRequest) return } item, err := h.continuity.InternalItem(r.Context(), request.ProspectiveItemID) if errors.Is(err, errCreatorSubjectMissing) { http.Error(w, "prospective item not found", http.StatusNotFound) return } if err != nil { http.Error(w, "Continuity item unavailable", http.StatusServiceUnavailable) return } if item.State != continuity.ProspectiveDelivered || item.Revision != request.ExpectedRevision || item.OwnerPrincipalID == "" || item.ScopeID == "" { http.Error(w, "prospective item not deliverable", http.StatusConflict) return } grant, scope, err := h.authority.LoadGrant(r.Context(), creatorGrantID) if err != nil || scope.ID != item.ScopeID { http.Error(w, "Creator authority unavailable", http.StatusConflict) return } identityBinding, err := h.core.Binding(r.Context(), prospectiveNotificationActor, item.OwnerPrincipalID) if err != nil { http.Error(w, "Creator identity binding unavailable", http.StatusConflict) return } capabilityBinding, err := h.core.ActiveCapabilityBinding(r.Context(), commitcontract.CapabilityProspectiveNotification, item.OwnerPrincipalID) if err != nil { http.Error(w, "notification capability unavailable", http.StatusConflict) return } intentID, authorizedID, attemptID, err := internalActionIDs(h.newID) if err != nil { http.Error(w, "action identity unavailable", http.StatusServiceUnavailable) return } title := "Домовой: напоминание" message := "Пора: " + strings.TrimSpace(item.Summary) payload := notificationexecutor.Payload{ActionIntentID: intentID, TemplateCode: commitcontract.ProspectiveDeliveredTemplateV1, ProspectiveItemID: string(item.ID), ExpectedRevision: int64(item.Revision), Title: title, Message: message} digest, err := notificationexecutor.Digest(payload) if err != nil { http.Error(w, "notification evidence unavailable", http.StatusInternalServerError) return } proposal := commitcontract.ActionProposal{ IntentID: intentID, ActorID: prospectiveNotificationActor, CapabilityID: commitcontract.CapabilityProspectiveNotification, Target: work.TaskTarget{ScopeID: item.ScopeID, Revision: 1}, Operation: commitcontract.OperationDeliverProspectiveNotification, NotificationSubject: commitcontract.ProspectiveNotificationSubject{ProspectiveItemID: item.ID, ExpectedRevision: item.Revision, TemplateCode: commitcontract.ProspectiveDeliveredTemplateV1, BindingID: capabilityBinding.ID, BindingRevision: capabilityBinding.Revision, RenderedTitle: title, RenderedMessage: message, RequestDigest: digest}, } action, err := commitdomain.Authorize(proposal, commitdomain.AuthorizationContext{Binding: identityBinding, Scope: scope, Grant: grant}, authorityValidator{}, authorizedID, h.now().UnixMilli()) if err != nil { http.Error(w, "Creator authorization denied", http.StatusConflict) return } if err = h.repository.SaveAuthorized(r.Context(), action); err != nil { http.Error(w, "commit authorized action", http.StatusInternalServerError) return } attempt, operationErr := executeAuthorized(r.Context(), h.repository, h.executor, action, attemptID) if operationErr != nil && !errors.Is(operationErr, commitdomain.ErrUnknownOutcome) { http.Error(w, "execute committed notification", http.StatusInternalServerError) return } status := http.StatusCreated if errors.Is(operationErr, commitdomain.ErrUnknownOutcome) { status = http.StatusAccepted } writeCreatorActionResponse(w, status, action, attempt) } func internalActionIDs(newID func() (ids.ID, error)) (commitcontract.ActionIntentID, commitcontract.AuthorizedActionID, commitcontract.ActionAttemptID, error) { intent, err := newID() if err != nil { return "", "", "", err } authorized, err := newID() if err != nil { return "", "", "", err } attempt, err := newID() if err != nil { return "", "", "", err } return commitcontract.ActionIntentID(intent), commitcontract.AuthorizedActionID(authorized), commitcontract.ActionAttemptID(attempt), nil }