136 lines
5.8 KiB
Go
136 lines
5.8 KiB
Go
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
|
|
}
|