Files
mesh-catalog/modules/mesh-mail-reader/cmd/mesh-mail-reader/tell.go
T

119 lines
3.9 KiB
Go

package main
// Sending the mail signal (novox/hq ADR 0461 §1, §2, §4 to §6; issue 1522): the words (signal.go, issue 1429)
// checked with messenger.messenger_check, then one seat:operator-channel.tell with subject "mail" — or the
// safe form when the words are refused. mesh-mail-reader's manifest invokes both (1522/AC4).
import (
"context"
"encoding/json"
"fmt"
)
// MessengerCheck is one seat:messenger.messenger_check ask (ADR 0461 §4): whether text is safe to say on an
// operator channel.
type MessengerCheck func(text string) (allowed bool, err error)
// NewMessengerCheck is MessengerCheck over the module's own Ask (batch.go).
func NewMessengerCheck(ask Ask) MessengerCheck {
return func(text string) (bool, error) {
raw, err := ask("messenger.messenger_check", map[string]any{"text": text})
if err != nil {
return false, err
}
var out struct {
Allowed bool `json:"allowed"`
}
if err := json.Unmarshal(raw, &out); err != nil {
return false, fmt.Errorf("messenger_check's answer could not be read: %.200s", raw)
}
return out.Allowed, nil
}
}
// Tell is one seat:operator-channel.tell ask (ADR 0461 §1): the headline, at most two lines, and the
// subject "mail" (1522/AC1). SentOn answers non-empty once a channel actually said it (1522/AC2); empty
// means every channel dropped it (the cap of 10 an hour, messenger's outbox.go) or refused it outright.
type Tell func(headline string, lines []string, subject string) (sentOn string, err error)
// NewTell is Tell over the module's own Ask (batch.go).
func NewTell(ask Ask) Tell {
return func(headline string, lines []string, subject string) (string, error) {
raw, err := ask("seat:operator-channel.tell", map[string]any{"headline": headline, "lines": lines, "subject": subject})
if err != nil {
return "", err
}
var out struct {
SentOn string `json:"sent_on"`
}
if err := json.Unmarshal(raw, &out); err != nil {
return "", fmt.Errorf("operator-channel.tell's answer could not be read: %.200s", raw)
}
return out.SentOn, nil
}
}
// SendSignal is 1522: every entry marked matters and not yet told — including one carried over from an
// earlier batch that went untold (1429/AC4) — is gathered; its words are built (signal.go) and checked word
// by word with check; any one refused falls back to the safe form (1522/AC1). Nothing is sent, and nothing
// asked at all, when there is nothing to say (a batch whose only mark is scam warning, 1522/AC3) or while
// the reader is unhealthy (1522/AC3, ADR 0461 §6) — read fresh from the store, not assumed from the caller's
// own last outcome. The tell sent, every one of those entries is marked told only once it answers sent_on
// (1522/AC2); a tell that answers sent_on empty leaves them exactly as they were, for the next signal to
// count (1522/AC2, AC3).
func SendSignal(ctx context.Context, store Store, check MessengerCheck, tell Tell) error {
health, err := store.ReaderHealth(ctx)
if err != nil {
return err
}
if !health.Healthy {
return nil
}
entries, err := store.Entries(ctx)
if err != nil {
return err
}
var toTell []Entry
var mails []SignalMail
for _, e := range entries {
if e.Mark == MarkMatters && !e.Told {
toTell = append(toTell, e)
mails = append(mails, SignalMail{SenderName: e.SenderName, Summary: e.Summary, Date: e.Date, DueDate: e.DueDate})
}
}
if len(toTell) == 0 {
return nil
}
words := BuildSignal(mails)
allowed := true
for _, line := range append([]string{words.Headline}, words.Lines...) {
ok, err := check(line)
if err != nil {
return err
}
if !ok {
allowed = false
break
}
}
if !allowed {
words = SafeSignal(len(toTell))
}
sentOn, err := tell(words.Headline, words.Lines, "mail")
if err != nil {
return err
}
if sentOn == "" {
return nil
}
for _, e := range toTell {
e.Told = true
if err := store.UpdateEntry(ctx, e); err != nil {
return err
}
}
return nil
}