187 lines
6.4 KiB
Go
187 lines
6.4 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"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// 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 json.RawMessage `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)
|
|
}
|
|
sentOn, err := readSentOn(out.SentOn)
|
|
if err != nil {
|
|
return "", fmt.Errorf("operator-channel.tell's answer could not be read: %.200s", raw)
|
|
}
|
|
return sentOn, nil
|
|
}
|
|
}
|
|
|
|
// readSentOn reads tell's sent_on: messenger answers the list of channels that said it (null or empty when
|
|
// every channel dropped it), never one string (issue 1926: reading it as a string failed every real answer, so
|
|
// no mail was ever marked told and the same signal went out after every batch). A plain string is still
|
|
// accepted. The channels are joined with a comma; empty means nothing said it.
|
|
func readSentOn(raw json.RawMessage) (string, error) {
|
|
if len(raw) == 0 || string(raw) == "null" {
|
|
return "", nil
|
|
}
|
|
var list []string
|
|
if err := json.Unmarshal(raw, &list); err == nil {
|
|
return strings.Join(list, ","), nil
|
|
}
|
|
var one string
|
|
if err := json.Unmarshal(raw, &one); err != nil {
|
|
return "", err
|
|
}
|
|
return one, nil
|
|
}
|
|
|
|
// signalEntries is what the signal counts out of the entries marked matters and not yet told (issue 1926):
|
|
// a mail whose due date lies before today is left out (AC2: it no longer matters, the mail page still holds
|
|
// it); the same mail seen twice (one message id in two folders, issue 1849) and the same notice in two
|
|
// languages (the same sender address, mail day and due date) count once (AC3). The entries left out stay
|
|
// as they were; the ones that count are returned in the order given.
|
|
func signalEntries(entries []Entry, now time.Time) []Entry {
|
|
today := time.Date(now.UTC().Year(), now.UTC().Month(), now.UTC().Day(), 0, 0, 0, 0, time.UTC)
|
|
seen := map[string]bool{}
|
|
var out []Entry
|
|
for _, e := range entries {
|
|
if !e.DueDate.IsZero() && e.DueDate.UTC().Before(today) {
|
|
continue
|
|
}
|
|
var keys []string
|
|
if e.MessageID != "" {
|
|
keys = append(keys, "id\x00"+e.Account+"\x00"+e.MessageID)
|
|
}
|
|
if !e.DueDate.IsZero() && e.SenderAddress != "" {
|
|
keys = append(keys, "notice\x00"+e.Account+"\x00"+strings.ToLower(e.SenderAddress)+"\x00"+
|
|
e.Date.UTC().Format("2006-01-02")+"\x00"+e.DueDate.UTC().Format("2006-01-02"))
|
|
}
|
|
dup := false
|
|
for _, k := range keys {
|
|
if seen[k] {
|
|
dup = true
|
|
}
|
|
}
|
|
for _, k := range keys {
|
|
seen[k] = true
|
|
}
|
|
if !dup {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 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, now func() time.Time) 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 untold []Entry
|
|
for _, e := range entries {
|
|
if e.Mark == MarkMatters && !e.Told {
|
|
untold = append(untold, e)
|
|
}
|
|
}
|
|
// What counts (signalEntries) is what is said; every untold entry is marked told once it is said, so an
|
|
// unchanged set is never said twice (issue 1926/AC1) and one that only holds past-due or duplicate mails
|
|
// says nothing.
|
|
toTell := signalEntries(untold, now())
|
|
if len(toTell) == 0 {
|
|
return nil
|
|
}
|
|
var mails []SignalMail
|
|
for _, e := range toTell {
|
|
mails = append(mails, SignalMail{SenderName: e.SenderName, Summary: e.Summary, Date: e.Date, DueDate: e.DueDate})
|
|
}
|
|
|
|
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 untold {
|
|
e.Told = true
|
|
if err := store.UpdateEntry(ctx, e); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|