Files
mesh-catalog/modules/messenger/cmd/messenger/asks.go
T
jschoubben 8ee75ec1bd messenger: tell the operator, by name and number, when another account answers a question meant for them
The lab's proof showed a stranger's refused tap said as "That answer was not taken" on the operator's
own chat (the router sends to the linked account), where it reads as the operator's own answer refused.
It now says which account tried, that nothing was done, and what it may mean.
2026-10-09 10:40:52 +02:00

827 lines
27 KiB
Go

package main
// Asks and warrants (novox/hq ADR 0259): the router holds every ask, judges each answer and issues a
// warrant; the asker performs the option with its own grants. The router performs nothing.
//
// - **An ask arrives on the seat's work queue under its asker's name** (`accept.ask.<asker>`), which the
// bus grants that asker alone; the name is read from the subject, never from the body. It is recorded
// in the bucket `asks` under `<asker>.<id>` before the work is acknowledged, so a restart loses none.
// - **It is refused at once, in words, on its asker's `decided` subject** when its words break a rule,
// when no ready channel can carry any of its answers, or when its asker already has three open. An
// option no channel can carry is named in the words, with where it can still be given.
// - **Each option gets a one-time ticket per channel kind that can carry it**: a button carries only the
// ticket. Bound to the ask, the option and the kind; dead when the ask closes or expires.
// - **An answer is judged in order, refusing at the first failure and saying so on the channel**: the ticket
// is known, unused and for this kind; the ask is open and unexpired; the sender is the operator's
// linked account where the kind verifies its sender; the kind's capabilities, from the controller's
// record of its claim, meet the option's level; for approve, the account was linked an hour ago or more.
// Then the ask is closed by compare-and-set — a second answer loses — and the warrant published.
// - **A warrant not yet heard by its asker is published again** every minute until the bus takes it.
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// ErrChanged is a compare-and-set that lost: another writer was first.
var ErrChanged = errors.New("written by another writer first")
// LinkedApproveAfter is how long a newly linked account waits before it can approve (decided by the
// operator on 2026-10-08, until an authenticator-app code exists).
const LinkedApproveAfter = time.Hour
// StateOpen is an ask not yet ended; every other state is the asks.Outcome it ended with.
const StateOpen = "open"
// Ticket is one option's one-time ticket on one channel kind.
type Ticket struct {
Option string `json:"option"`
Kind string `json:"kind"`
Used bool `json:"used,omitempty"`
}
// AskRecord is one ask as the router keeps it, under `<asker>.<id>` in the bucket `asks`. Its asker reads
// it there under its own name.
type AskRecord struct {
Asker string `json:"asker"`
Ask asks.Ask `json:"ask"`
State string `json:"state"`
Opened time.Time `json:"opened"`
Closed time.Time `json:"closed,omitempty"`
Tickets map[string]Ticket `json:"tickets,omitempty"`
Warrant *asks.Warrant `json:"warrant,omitempty"`
// Told: the warrant, or the end, was published to the asker.
Told bool `json:"told,omitempty"`
// Record is the message the ask is shown on: a condition's key, or its own.
Record string `json:"record,omitempty"`
// Only is the one account that may answer it, on its kind: an ask of the router's own, put to the account
// an unlink or a relink concerns (novox/hq ADR 0259 §7).
Only *Identity `json:"only,omitempty"`
// Act is what the router does on its own ask's warrant: unlink, or link (selfact.go).
Act *SelfAct `json:"act,omitempty"`
}
// Identity is one account linked as the operator on a channel kind.
type Identity struct {
Kind string `json:"kind"`
Identity string `json:"identity"`
Display string `json:"display,omitempty"`
Linked time.Time `json:"linked"`
ApproveFrom time.Time `json:"approve-from"`
}
// AskStore is the router's own state for asks and the operator's identities (ADR 0201).
type AskStore interface {
GetAsk(key string) (*AskRecord, uint64, error)
CreateAsk(key string, r AskRecord) (uint64, error)
UpdateAsk(key string, r AskRecord, revision uint64) (uint64, error)
AskKeys() ([]string, error)
GetIdentity(key string) (*Identity, error)
PutIdentity(key string, id Identity) error
DeleteIdentity(key string) error
IdentityKeys() ([]string, error)
GetLink(kind string) (*PendingLink, error)
PutLink(kind string, l PendingLink) error
DeleteLink(kind string) error
GetUnlinked(kind string) (*Unlinked, error)
PutUnlinked(kind string, u Unlinked) error
}
type askEntry struct {
rec AskRecord
rev uint64
}
func askKey(asker, id string) string { return asker + "." + id }
// loadAsks reads back every ask, and indexes the tickets of the open ones.
func (h *Holder) loadAsks() error {
if h.Asks == nil {
return nil
}
keys, err := h.Asks.AskKeys()
if err != nil {
return err
}
for _, k := range keys {
rec, rev, err := h.Asks.GetAsk(k)
if err != nil {
return err
}
if rec == nil {
continue
}
h.keepAsk(k, *rec, rev)
}
return nil
}
func (h *Holder) keepAsk(key string, rec AskRecord, rev uint64) {
h.mu.Lock()
defer h.mu.Unlock()
if h.asks == nil {
h.asks = map[string]*askEntry{}
h.tickets = map[string]string{}
}
h.asks[key] = &askEntry{rec: rec, rev: rev}
// Kept after the ask ends, so a late answer is told why it was not taken.
for t := range rec.Tickets {
h.tickets[t] = key
}
}
func (h *Holder) askOf(key string) *askEntry {
h.mu.Lock()
defer h.mu.Unlock()
if h.asks == nil {
return nil
}
return h.asks[key]
}
// openAsk is the ask a record shows, when it is still open.
func (h *Holder) openAsk(r *Record) *AskRecord {
if r == nil || r.Ask == "" {
return nil
}
e := h.askOf(r.Ask)
if e == nil || e.rec.State != StateOpen {
return nil
}
rec := e.rec
return &rec
}
// carriers are the kinds that can carry an answer of a level now: ready, offering a choice, and for
// approve verifying their sender with an account of the operator's linked.
func (h *Holder) carriers(level asks.Level) []string {
if h.Bench == nil {
return nil
}
var out []string
for _, k := range h.Bench.Kinds() {
// Every level needs a choice made in one act, shown exactly as the router rendered it (ADR 0234 §8).
if h.Bench.Ready(k.Kind) != nil || !h.Bench.Promises(k.Kind, "choice") || !h.Bench.Promises(k.Kind, "exact-render") {
continue
}
switch level {
case asks.Acknowledge:
out = append(out, k.Kind)
case asks.Approve:
if h.Bench.Promises(k.Kind, "verified-sender") && h.linked(k.Kind) && h.rootFreeFor(k) == "" {
out = append(out, k.Kind)
}
}
// Destroy needs a code, which no channel carries yet.
}
return out
}
// rootFreeFor is why a kind's proof is not believed now, or "": an agent can become root without a person
// where the router or the kind's holder runs, which the controller's self-check says as an open condition
// (novox/hq ADR 0259 §8, rule 2).
func (h *Holder) rootFreeFor(k KindInfo) string {
h.mu.Lock()
defer h.mu.Unlock()
for _, r := range h.open {
if r.Kind != KindAgentRoot || !r.Cleared.IsZero() {
continue
}
machine := strings.TrimPrefix(strings.TrimSuffix(r.Key, "."+KindAgentRoot), "machine.")
if machine == k.Node || (h.Node != "" && machine == h.Node) {
return "a program working for you can become root on " + machine + " without asking you, so an answer " +
"proven there proves nothing until that changes"
}
}
return ""
}
// ConditionOwner is the module whose asks may be shown on a condition's message: the controller, which
// raises the conditions.
const ConditionOwner = "mesh-controller"
// KindAgentRoot is the controller's condition while an agent can become root without a person on a machine.
const KindAgentRoot = "agent-root"
// TakeAsk takes one ask or cancel from the seat's queue. asker is the subject's last token, which the bus
// grants that module alone. An error is returned only when the ask could not be recorded, so the queue
// offers it again; a refusal is said to the asker and the work is taken.
func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
h.work.Lock()
defer h.work.Unlock()
h.mu.Lock()
h.init()
h.mu.Unlock()
if !asks.UsableID(strings.ReplaceAll(asker, "-", "_")) {
h.Logf("[messenger] an ask from %q was taken and dropped: not a module's name", asker)
return nil
}
if verb == "cancel" {
var c struct {
ID string `json:"id"`
}
if json.Unmarshal(body, &c) != nil || !asks.UsableID(c.ID) {
h.Logf("[messenger] a cancel from %s names no ask", asker)
return nil
}
return h.closeAsk(askKey(asker, c.ID), asks.OutcomeCancelled, "its asker took it back", nil, "")
}
var a asks.Ask
if err := json.Unmarshal(body, &a); err != nil {
h.refuseAsk(asker, asks.Ask{ID: "unreadable"}, "it could not be read: "+err.Error())
return nil
}
key := askKey(asker, a.ID)
if h.askOf(key) != nil {
return nil // a redelivery: already recorded
}
now := h.Now()
if err := a.Check(now); err != nil {
h.refuseAsk(asker, a, err.Error())
return nil
}
words := []string{a.Headline, a.Explanation, a.OnExpiry}
for _, o := range a.Options {
words = append(words, o.Label, o.Does)
}
if refusal, ok := Check(strings.Join(words, "\n")); !ok {
h.refuseAsk(asker, a, "its words carry "+refusal.String()+", which may not leave the mesh: move it into a reference or say it in words")
return nil
}
open, replaced := 0, ""
h.mu.Lock()
for k, e := range h.asks {
if e.rec.Asker != asker || e.rec.State != StateOpen {
continue
}
if a.About != "" && e.rec.Ask.About == a.About {
replaced = k
continue
}
open++
}
h.mu.Unlock()
if open >= asks.MostOpen {
h.refuseAsk(asker, a, fmt.Sprintf("its asker already has %d open asks", open))
return nil
}
rec := AskRecord{Asker: asker, Ask: a, State: StateOpen, Opened: now, Tickets: map[string]Ticket{}}
for _, o := range a.Options {
for _, kind := range h.carriers(o.Level) {
rec.Tickets[ticket()] = Ticket{Option: o.ID, Kind: kind}
}
}
if len(rec.Tickets) == 0 {
h.refuseAsk(asker, a, "no channel can carry any of its answers now: "+h.whyNot())
return nil
}
if replaced != "" {
if err := h.closeAsk(replaced, asks.OutcomeReplaced, "a newer ask about the same thing replaced it", nil, ""); err != nil && !errors.Is(err, ErrChanged) {
return err
}
}
// Shown on the condition it is about, when that is open here; otherwise as a message of its own.
h.mu.Lock()
target := h.open["ask."+key]
// Shown on a condition's message only when asked by the condition's owner: another asker naming a
// condition's key does not take its message over.
if a.About != "" && asker == ConditionOwner {
if r := h.open[a.About]; r != nil && r.Cleared.IsZero() && r.Origin == "condition" {
target = r
}
}
h.mu.Unlock()
if target != nil {
rec.Record = target.Key
} else {
rec.Record = "ask." + key
}
if h.Asks != nil {
rev, err := h.Asks.CreateAsk(key, rec)
if errors.Is(err, ErrChanged) {
got, rev, err := h.Asks.GetAsk(key)
if err != nil || got == nil {
return fmt.Errorf("the ask %s is on record and cannot be read: %v", key, err)
}
h.keepAsk(key, *got, rev)
return nil
}
if err != nil {
return fmt.Errorf("the ask %s could not be recorded: %w", key, err)
}
h.keepAsk(key, rec, rev)
} else {
h.keepAsk(key, rec, 0)
}
h.Logf("[messenger] %s asks %q (%s), shown on %s", asker, a.Headline, a.ID, rec.Record)
if target != nil {
h.mu.Lock()
target.Ask = key
h.mu.Unlock()
if !target.silenced(now) {
h.offerAsk(target)
}
h.persist(target)
return nil
}
severity := Warning
if a.Urgent {
severity = Urgent
}
r := Record{Key: "ask." + key, Kind: "ask", Subject: asker, Severity: severity, Summary: a.Headline,
Headline: a.Headline, Explanation: a.Explanation, Origin: "ask", More: "operator-channel.asks",
Raised: now, Sent: map[string]string{}, Seen: now, Count: 1, Ask: key}
h.mu.Lock()
h.open[r.Key] = &r
h.mu.Unlock()
h.deliver(&r, "raised")
h.persist(&r)
return nil
}
// askKinds are the kinds an open ask about a record is offered on.
func (h *Holder) askKinds(r *Record) map[string]bool {
rec := h.openAsk(r)
if rec == nil {
return nil
}
out := map[string]bool{}
for _, t := range rec.Tickets {
out[t.Kind] = true
}
return out
}
// offerAsk shows a record's new ask: edited in place where the record was shown, a message of its own on a
// channel that carries the ask and has not shown the record.
func (h *Holder) offerAsk(r *Record) {
kinds := h.askKinds(r)
h.mu.Lock()
sent := map[string]string{}
for k, v := range r.Sent {
sent[k] = v
}
h.mu.Unlock()
now := h.Now()
for _, ch := range h.channels() {
id, shown := sent[ch.Name()]
switch {
case shown && id != "" && ch.CanEdit():
err := ch.Edit(id, h.sayOn(r, "asked", ch.Name()))
h.noteChannel(ch.Name(), err)
if err == nil {
h.record(Sent{At: now, Channel: ch.Name(), Key: r.Key, What: "asked", Outcome: "edited"})
continue
}
h.enqueue(ch.Name(), item{Key: r.Key, What: "asked", Severity: r.Severity, At: now})
case kinds[ch.Name()] && ch.Ready() == nil:
h.enqueue(ch.Name(), item{Key: r.Key, What: "asked", Severity: r.Severity, At: now})
}
}
h.flush(now, false)
}
// refuseAsk says to its asker that an ask was not taken, and why.
func (h *Holder) refuseAsk(asker string, a asks.Ask, why string) {
h.Logf("[messenger] refused an ask from %s (%s): %s", asker, a.ID, why)
h.tell(asks.Warrant{Ask: a.ID, Asker: asker, About: a.About, Outcome: asks.OutcomeRefused, Words: why, At: h.Now()})
}
// tell publishes a warrant, or an ask's end, to its asker alone, deduplicated by the ask.
func (h *Holder) tell(w asks.Warrant) error {
if h.Publish == nil {
return errors.New("this router cannot reach the bus")
}
err := h.Publish(asks.DecidedSubject(w.Asker), w, askKey(w.Asker, w.Ask)+"."+string(w.Outcome))
if err != nil {
h.Logf("[messenger] could not tell %s how its ask %s ended (%v); told again in a minute", w.Asker, w.Ask, err)
}
return err
}
// closeAsk ends an open ask by compare-and-set, tells its asker, and edits every copy. A second close
// loses: ErrChanged when another outcome was first.
func (h *Holder) closeAsk(key string, outcome asks.Outcome, words string, w *asks.Warrant, used string) error {
e := h.askOf(key)
if e == nil {
return nil
}
rec, rev := e.rec, e.rev
if h.Asks != nil {
got, r, err := h.Asks.GetAsk(key)
if err != nil {
return err
}
if got != nil {
rec, rev = *got, r
}
}
if rec.State != StateOpen {
h.keepAsk(key, rec, rev)
return ErrChanged
}
now := h.Now()
rec.State = string(outcome)
rec.Closed = now
if t, ok := rec.Tickets[used]; ok {
t.Used = true
rec.Tickets[used] = t
}
if w == nil {
w = &asks.Warrant{Ask: rec.Ask.ID, Asker: rec.Asker, About: rec.Ask.About, Outcome: outcome, Words: words, At: now}
}
rec.Warrant = w
if h.Asks != nil {
newRev, err := h.Asks.UpdateAsk(key, rec, rev)
if err != nil {
if errors.Is(err, ErrChanged) {
if got, r, gerr := h.Asks.GetAsk(key); gerr == nil && got != nil {
h.keepAsk(key, *got, r)
}
}
return err
}
rev = newRev
}
h.keepAsk(key, rec, rev)
if rec.Asker == SelfAsker {
// The router's own ask: it does what was decided, and tells nobody else (selfact.go).
h.markTold(key)
h.showClosed(rec)
h.selfAct(rec, *w)
return nil
}
if h.tell(*w) == nil {
h.markTold(key)
}
h.showClosed(rec)
return nil
}
// markTold records that the asker was told.
func (h *Holder) markTold(key string) {
e := h.askOf(key)
if e == nil {
return
}
rec, rev := e.rec, e.rev
rec.Told = true
if h.Asks != nil {
newRev, err := h.Asks.UpdateAsk(key, rec, rev)
if err != nil {
return // read again and told again by the sweep
}
rev = newRev
}
h.keepAsk(key, rec, rev)
}
// showClosed edits every copy of an ask's message to say how it ended, its buttons gone.
func (h *Holder) showClosed(rec AskRecord) {
h.mu.Lock()
r := h.open[rec.Record]
h.mu.Unlock()
if r == nil {
return
}
h.mu.Lock()
switch {
case rec.Warrant != nil && rec.Warrant.Outcome == asks.OutcomeChosen:
r.Answered = rec.Warrant.Label + ": chosen at " + rec.Closed.In(h.zone()).Format("15:04") + " on " + Display(rec.Warrant.By.Kind)
case rec.State == string(asks.OutcomeExpired):
r.Answered = "No longer answerable: nobody answered in time. " + capital(rec.Ask.OnExpiry)
case rec.State == string(asks.OutcomeReplaced):
r.Answered = ""
default:
r.Answered = "No longer needed."
}
ownMessage := r.Origin == "ask"
h.mu.Unlock()
if r.told() {
h.edit(r, "answered", false)
}
if ownMessage && rec.State != string(asks.OutcomeReplaced) {
h.mu.Lock()
r.Cleared = h.Now()
h.mu.Unlock()
}
h.persist(r)
}
// sweepAsks ends asks past their expiry, and tells again an asker not yet told. Called every minute.
func (h *Holder) sweepAsks(now time.Time) {
h.mu.Lock()
var expired, untold []string
for k, e := range h.asks {
switch {
case e.rec.State == StateOpen && !now.Before(e.rec.Ask.Expires):
expired = append(expired, k)
case e.rec.State != StateOpen && !e.rec.Told && e.rec.Warrant != nil:
untold = append(untold, k)
}
}
h.mu.Unlock()
sort.Strings(expired)
for _, k := range expired {
e := h.askOf(k)
if err := h.closeAsk(k, asks.OutcomeExpired, "nobody answered in time: "+e.rec.Ask.OnExpiry, nil, ""); err != nil && !errors.Is(err, ErrChanged) {
h.Logf("[messenger] could not end the expired ask %s: %v", k, err)
}
}
for _, k := range untold {
e := h.askOf(k)
if e != nil && h.tell(*e.rec.Warrant) == nil {
h.markTold(k)
}
}
}
// offers are an open ask's buttons on one kind: each option offered there, by its ticket.
func (h *Holder) offers(rec *AskRecord, kind string) []Choice {
var out []Choice
for _, o := range rec.Ask.Options {
for t, tk := range rec.Tickets {
if tk.Kind == kind && tk.Option == o.ID && !tk.Used {
out = append(out, Choice{Token: t, Label: o.Label})
}
}
}
return out
}
// elsewhere is the line an ask's message carries on a kind for the options it cannot carry there: where
// else each can be given — "Release or Stop: answer on Telegram, or from the mesh MCP server."
func (h *Holder) elsewhere(rec *AskRecord, kind string) string {
var labels []string
where := map[string]bool{}
for _, o := range rec.Ask.Options {
here := false
for _, tk := range rec.Tickets {
if tk.Option == o.ID && tk.Kind == kind {
here = true
}
}
if here {
continue
}
labels = append(labels, o.Label)
for _, tk := range rec.Tickets {
if tk.Option == o.ID && tk.Kind != kind {
where[tk.Kind] = true
}
}
}
if len(labels) == 0 {
return ""
}
var kinds []string
for k := range where {
kinds = append(kinds, Display(k))
}
sort.Strings(kinds)
answer := "from the mesh MCP server"
if len(kinds) > 0 {
answer = "answer on " + strings.Join(kinds, " or ") + ", or from the mesh MCP server"
}
return joinOr(labels) + ": " + answer + "."
}
func joinOr(labels []string) string {
if len(labels) <= 1 {
return strings.Join(labels, "")
}
return strings.Join(labels[:len(labels)-1], ", ") + " or " + labels[len(labels)-1]
}
// judge is an answer to an ask's option, chosen on a kind: refused at the first failure, in words said on
// the kind; otherwise the ask closed and the warrant published (ADR 0259 §5).
func (h *Holder) judge(kind string, c asks.Choice, key string) (string, error) {
e := h.askOf(key)
if e == nil {
return h.refuseChoice(kind, c, "it belongs to no ask this router holds")
}
rec := e.rec
if h.Asks != nil {
if got, _, err := h.Asks.GetAsk(key); err == nil && got != nil {
rec = *got
}
}
now := h.Now()
t, issued := rec.Tickets[c.Ticket]
switch {
case !issued:
return h.refuseChoice(kind, c, "it belongs to no ask this router holds")
case t.Kind != kind:
return h.refuseChoice(kind, c, "it was not offered here")
case rec.State != StateOpen:
return h.refuseChoice(kind, c, "the question was already answered or has ended")
case t.Used:
return h.refuseChoice(kind, c, "it was already chosen once")
case !now.Before(rec.Ask.Expires):
return h.refuseChoice(kind, c, "the question expired at "+rec.Ask.Expires.In(h.zone()).Format("15:04"))
}
o, offered := rec.Ask.Option(t.Option)
if !offered {
return h.refuseChoice(kind, c, "it names an answer the question does not offer")
}
info, held := h.Bench.Kind(kind)
if !held || !h.Bench.Promises(kind, "choice") || !h.Bench.Promises(kind, "exact-render") {
return h.refuseChoice(kind, c, "this channel is not one the mesh holds for answers")
}
person := asks.Person{Who: asks.Operator, Kind: kind, Identity: c.Sender.Identity, Display: c.Sender.Display}
verifies := h.Bench.Promises(kind, "verified-sender")
var id *Identity
if verifies {
if !c.Sender.Authenticated {
return h.refuseChoice(kind, c, "the service did not authenticate who sent it")
}
id = h.identity(kind, c.Sender.Identity)
if rec.Only != nil {
// An ask put to one account is answered by that account alone, linked or not any more.
if rec.Only.Kind != kind || rec.Only.Identity != c.Sender.Identity {
return h.refuseChoice(kind, c, "this question was put to another account")
}
only := *rec.Only
id = &only
}
if id == nil {
return h.refuseStranger(kind, c)
}
person.Verified = "user id verified"
} else {
person.Verified = "a desk click: whoever was at the operator's session on " + c.Sender.Identity
}
proofs := []string{}
switch o.Level {
case asks.Acknowledge:
case asks.Approve:
if !verifies {
return h.refuseChoice(kind, c, Display(kind)+" cannot prove who answered, so it can only acknowledge")
}
if now.Before(id.ApproveFrom) {
return h.refuseChoice(kind, c, "this account was linked at "+id.Linked.In(h.zone()).Format("15:04")+
" and can approve from "+id.ApproveFrom.In(h.zone()).Format("15:04"))
}
if why := h.rootFreeFor(info); why != "" {
return h.refuseChoice(kind, c, why)
}
proofs = append(proofs, "P1")
default:
return h.refuseChoice(kind, c, "it needs a code from the authenticator app, which the mesh does not take yet")
}
w := &asks.Warrant{Ask: rec.Ask.ID, Asker: rec.Asker, About: rec.Ask.About, Outcome: asks.OutcomeChosen,
Option: o.ID, Label: o.Label, Level: o.Level, By: &person, Channel: info.Module, Proofs: proofs, At: now,
// The warrant answers this ask as it was taken and shown, with what each option binds (ADR 0259 §6).
AskDigest: rec.Ask.Digest()}
if err := h.closeAsk(key, asks.OutcomeChosen, "", w, c.Ticket); err != nil {
if errors.Is(err, ErrChanged) {
return h.refuseChoice(kind, c, "the question was already answered")
}
return h.refuseChoice(kind, c, "it could not be recorded: "+err.Error())
}
h.record(Sent{At: now, Channel: kind, Key: rec.Record, What: "answer " + o.Label, Outcome: "warrant to " + rec.Asker})
h.Logf("[messenger] %s: a warrant for %s's ask %s", w.Says(), rec.Asker, rec.Ask.ID)
return "chosen", nil
}
// refuseStranger refuses an answer from an account that is not the operator's, and tells the operator, by name
// and number, on the account that is linked: a question's buttons reached somebody else (a forwarded message,
// or a phone in other hands). Nothing is said to the other account.
func (h *Holder) refuseStranger(kind string, c asks.Choice) (string, error) {
why := "this account is not linked as the operator"
h.Logf("[messenger] an answer on %s from %s was refused: %s", kind, c.Sender.Identity, why)
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "answer", Outcome: "refused: " + why})
who := c.Sender.Display
if who == "" {
who = "an account"
}
_ = h.showOn(kind, Message{Title: "Another account tried to answer for you",
Body: fmt.Sprintf("The %s account %s (number %s) chose an answer on a question meant for you. It was refused "+
"and nothing was done. If you did not pass the question on, somebody else can read your messages.",
Display(kind), who, c.Sender.Identity), Urgent: true})
return "refused", errorf("%s", why)
}
// refuseChoice says on the kind that an answer was not taken, and why.
func (h *Holder) refuseChoice(kind string, c asks.Choice, why string) (string, error) {
h.Logf("[messenger] an answer on %s from %s was refused: %s", kind, c.Sender.Identity, why)
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "answer", Outcome: "refused: " + why})
h.showOn(kind, Message{Title: "That answer was not taken", Body: capital(why) + ". Nothing was done.",
Quiet: true, Reply: c.ID})
return "refused", errorf("%s", why)
}
// showOnHandle is showOn, answering the handle the message is known by.
func (h *Holder) showOnHandle(kind string, m Message) (string, error) {
for _, ch := range h.channels() {
if ch.Name() == kind {
id, err := ch.Send(m)
h.noteChannel(kind, err)
return id, err
}
}
if h.Publish == nil {
return "", errors.New("this router cannot reach the bus")
}
out := asks.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply, To: m.To}
if out.To == "" && h.Bench != nil && h.Bench.To != nil {
out.To = h.Bench.To(kind)
}
for _, a := range m.Actions {
out.Buttons = append(out.Buttons, asks.Button{Label: a.Label, Ticket: a.Token})
}
err := h.Publish(asks.ChannelSubject(asks.Show, kind), out, out.Handle)
h.noteChannel(kind, err)
return out.Handle, err
}
// showOn says a message of the router's own on one kind, at once: an answer's refusal, details, a link's code.
func (h *Holder) showOn(kind string, m Message) error {
for _, ch := range h.channels() {
if ch.Name() == kind {
_, err := ch.Send(m)
h.noteChannel(kind, err)
return err
}
}
if h.Publish == nil {
return errors.New("this router cannot reach the bus")
}
out := asks.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply}
if h.Bench != nil && h.Bench.To != nil {
out.To = h.Bench.To(kind)
}
if m.To != "" {
out.To = m.To
}
out.Secret = m.Secret
for _, a := range m.Actions {
out.Buttons = append(out.Buttons, asks.Button{Label: a.Label, Ticket: a.Token})
}
err := h.Publish(asks.ChannelSubject(asks.Send, kind), out, out.Handle)
h.noteChannel(kind, err)
return err
}
func ticket() string {
var b [16]byte
_, _ = rand.Read(b[:])
return hex.EncodeToString(b[:])
}
// AskView is one ask as the tool `asks` answers it.
type AskView struct {
Key string `json:"key"`
Asker string `json:"asker"`
Headline string `json:"headline"`
State string `json:"state"`
Opened string `json:"opened"`
Expires string `json:"expires"`
Offered []string `json:"offered_on,omitempty"`
Answer string `json:"answer,omitempty"`
Told bool `json:"asker_told"`
}
// AsksNow is every ask, open first, newest first.
func (h *Holder) AsksNow() []AskView {
h.mu.Lock()
defer h.mu.Unlock()
var out []AskView
for k, e := range h.asks {
v := AskView{Key: k, Asker: e.rec.Asker, Headline: e.rec.Ask.Headline, State: e.rec.State,
Opened: e.rec.Opened.UTC().Format(time.RFC3339), Expires: e.rec.Ask.Expires.UTC().Format(time.RFC3339),
Told: e.rec.Told}
kinds := map[string]bool{}
for _, t := range e.rec.Tickets {
kinds[t.Kind] = true
}
for k := range kinds {
v.Offered = append(v.Offered, k)
}
sort.Strings(v.Offered)
if w := e.rec.Warrant; w != nil {
if w.Outcome == asks.OutcomeChosen {
v.Answer = w.Says()
} else {
v.Answer = string(w.Outcome) + ": " + w.Words
}
}
out = append(out, v)
}
sort.Slice(out, func(i, j int) bool {
if (out[i].State == StateOpen) != (out[j].State == StateOpen) {
return out[i].State == StateOpen
}
return out[i].Opened > out[j].Opened
})
return out
}