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 }