// Package asks is the contract of asking a person and answering on a channel (novox/hq ADR 0259): the // shapes an asker, the router and a channel exchange on the bus, and the subjects they travel on. No // transport and no channel's service: an asker publishes an Ask under its own name and acts on the Warrant // it hears; the router holds the ask, sends channels a Message, and judges the Choice a channel says; a // channel shows a Message and says what was chosen and by whom, as its service authenticated it. package asks import ( "errors" "fmt" "regexp" "strings" "time" ) // The seats (novox/hq ADR 0259 §3). const ( // Seat is held by the router: an ask and its cancel are its accepts, a warrant its event, each named by // the asker. Seat = "operator-channel" // ChannelSeat is the kinded bench a channel holds to show and say: its accepts carry the kind. ChannelSeat = "channel" // IntakeSeat is the kinded bench a channel holds to say what was chosen: its events and proofs carry // the kind. IntakeSeat = "intake" ) // AskSubject is where an asker publishes an ask, CancelSubject its cancel, and DecidedSubject where it // hears the warrant, or the ask's end without one. func AskSubject(asker string) string { return "mesh.seat." + Seat + ".accept.ask." + asker } func CancelSubject(asker string) string { return "mesh.seat." + Seat + ".accept.cancel." + asker } func DecidedSubject(asker string) string { return "mesh.seat." + Seat + ".event.decided." + asker } // The work a channel takes, on ChannelSubject. const ( Show = "show" // a message offering answers Edit = "edit" // a message shown before, replaced Send = "send" // a message offering nothing ) // ChannelSubject is where the router sends a channel of a kind its work. func ChannelSubject(verb, kind string) string { return "mesh.seat." + ChannelSeat + ".accept." + verb + "." + kind } // What a channel says, on IntakeSubject. const ( Chosen = "choice" // a button tapped Link = "link" // somebody asked to be linked as the operator ) // IntakeSubject is where a channel of a kind says what arrived. func IntakeSubject(what, kind string) string { return "mesh.seat." + IntakeSeat + ".event." + what + "." + kind } // CodeProof is the one proof verb: a code the operator typed, carried by request and reply, never kept. const CodeProof = "code" // ProofSubject is where a channel of a kind asks a proof. func ProofSubject(verb, kind string) string { return "mesh.seat." + IntakeSeat + ".proof." + verb + "." + kind } // A Level is how much proof an option's answer needs (novox/hq ADR 0234 §8, the glossary's assurance level). type Level string const ( // Acknowledge performs only what any granted principal may already do: silencing, details. No proof. Acknowledge Level = "acknowledge" // Approve needs one proof: a verified sender (a linked account, linked an hour or more) or a code. Approve Level = "approve" // Destroy needs two proofs, one of them a code. Destroy Level = "destroy" ) // Rank orders the levels; an unknown level ranks above every known one, so it is never taken as less. func (l Level) Rank() int { switch l { case Acknowledge: return 0 case Approve: return 1 case Destroy: return 2 } return 3 } // Operator is the one role an ask may be answered by today. const Operator = "operator" // Option is one answer an ask offers: a label for the button, what it does in plain words, its level. type Option struct { ID string `json:"id"` Label string `json:"label"` Does string `json:"does"` Level Level `json:"level"` } // Ask is a request for a person's word (novox/hq ADR 0259 §4). type Ask struct { // ID is the asker's own, unique to it. ID string `json:"id"` // Headline names the thing and what is wrong, in a few plain words; Explanation is what happened and // what it means. Both are held to the plain rule and the content rule by the router. Headline string `json:"headline"` Explanation string `json:"explanation"` Options []Option `json:"options"` // Who may answer: Operator. Who string `json:"who"` // Expires is when the ask ends unanswered; OnExpiry is what the asker then does, in words the person // is shown ("the delivery stays held"). An ask that authorises never defaults. Expires time.Time `json:"expires"` OnExpiry string `json:"on-expiry"` // About is what the ask is about (a condition's key): a newer ask about it replaces the older. About string `json:"about,omitempty"` Urgent bool `json:"urgent,omitempty"` } // The bounds of an ask (novox/hq ADR 0234 §8, ADR 0259 §4). const ( MostOptions = 4 MostOpen = 3 ApproveLasts = 24 * time.Hour DestroyLasts = 10 * time.Minute HeadlineLength = 60 LabelLength = 24 ) var usableID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`) // UsableID says whether a name can be an ask's or an option's id: a key and a subject token both. func UsableID(id string) bool { return usableID.MatchString(id) } // Highest is the highest level among the ask's options. func (a Ask) Highest() Level { high := Acknowledge for _, o := range a.Options { if o.Level.Rank() > high.Rank() { high = o.Level } } return high } // Option is the option of this id, or false. func (a Ask) Option(id string) (Option, bool) { for _, o := range a.Options { if o.ID == id { return o, true } } return Option{}, false } // Check is what an ask is held to before anything is shown: every refusal, in words its asker can act on. func (a Ask) Check(now time.Time) error { var problems []string say := func(format string, args ...any) { problems = append(problems, fmt.Sprintf(format, args...)) } if !UsableID(a.ID) { say("its id %q is not letters, digits, - and _, at most 64", a.ID) } if strings.TrimSpace(a.Headline) == "" || len([]rune(a.Headline)) > HeadlineLength { say("its headline is empty or longer than %d characters", HeadlineLength) } if strings.TrimSpace(a.Explanation) == "" { say("it explains nothing") } if a.Who != Operator { say("it is answered by %q, and only the operator answers today", a.Who) } if len(a.Options) == 0 || len(a.Options) > MostOptions { say("it offers %d options, and an ask offers one to %d", len(a.Options), MostOptions) } seen := map[string]bool{} for _, o := range a.Options { switch { case !UsableID(o.ID): say("an option's id %q is not letters, digits, - and _", o.ID) case seen[o.ID]: say("the option %s is offered twice", o.ID) } seen[o.ID] = true if strings.TrimSpace(o.Label) == "" || len([]rune(o.Label)) > LabelLength { say("the option %s's label is empty or longer than %d characters", o.ID, LabelLength) } if strings.TrimSpace(o.Does) == "" { say("the option %s does not say what it does", o.ID) } if o.Level.Rank() > Destroy.Rank() { say("the option %s has the level %q, which is none of acknowledge, approve, destroy", o.ID, o.Level) } } if !a.Expires.After(now) { say("it expires before it is asked") } switch a.Highest() { case Approve: if a.Expires.After(now.Add(ApproveLasts)) { say("an ask that approves lasts at most %s", ApproveLasts) } case Destroy: if a.Expires.After(now.Add(DestroyLasts)) { say("an ask that destroys lasts at most %s", DestroyLasts) } } if a.Highest() != Acknowledge && strings.TrimSpace(a.OnExpiry) == "" { say("it does not say what happens when nobody answers, and an ask that authorises never defaults") } if a.About != "" && strings.ContainsAny(a.About, " \n") { say("what it is about is a key, without spaces") } if len(problems) > 0 { return errors.New("the ask is refused: " + strings.Join(problems, "; ")) } return nil } // Outcome is how an ask ended. type Outcome string const ( OutcomeChosen Outcome = "chosen" // a person chose an option: a warrant OutcomeExpired Outcome = "expired" // nobody answered in time OutcomeCancelled Outcome = "cancelled" // its asker took it back OutcomeReplaced Outcome = "replaced" // a newer ask about the same thing replaced it OutcomeRefused Outcome = "refused" // it was never shown: Words says why ) // Person is who chose, as the router verified them. type Person struct { // Who is the role: Operator. Who string `json:"who"` // Kind is the channel kind they answered on, Identity their account on that service, Display the name // the service shows, and Verified how the router knew it was them. Kind string `json:"kind"` Identity string `json:"identity"` Display string `json:"display,omitempty"` Verified string `json:"verified"` } // Warrant is the router's record that a person chose one option of one ask, or the ask's end without one // (novox/hq ADR 0259 §6). It carries no secret. type Warrant struct { Ask string `json:"ask"` Asker string `json:"asker"` About string `json:"about,omitempty"` Outcome Outcome `json:"outcome"` // Option, Label and Level are the option chosen; By who chose it, Channel the module it came through, // Proofs which proofs were present (P1, P2, P3). Option string `json:"option,omitempty"` Label string `json:"label,omitempty"` Level Level `json:"level,omitempty"` By *Person `json:"by,omitempty"` Channel string `json:"channel,omitempty"` Proofs []string `json:"proofs,omitempty"` At time.Time `json:"at"` // Words are why an ask ended without a choice, or what refused it. Words string `json:"words,omitempty"` } // Says is the warrant in the words an asker records with its act: "the operator, via telegram (user id // verified), chose Release". func (w Warrant) Says() string { if w.Outcome != OutcomeChosen || w.By == nil { return fmt.Sprintf("no person chose: the ask %s %s", w.Ask, w.Outcome) } via := w.By.Kind if w.By.Verified != "" { via += " (" + w.By.Verified + ")" } return fmt.Sprintf("the %s, via %s, chose %s", w.By.Who, via, w.Label) } // For checks a warrant against the ask its asker made: the same asker and ask, a choice, an option the ask // offered, at that option's level. An asker acts on nothing else. func (w Warrant) For(asker string, a Ask) (Option, error) { if w.Asker != asker || w.Ask != a.ID { return Option{}, fmt.Errorf("the warrant is for %s's ask %s, not %s's %s", w.Asker, w.Ask, asker, a.ID) } if w.Outcome != OutcomeChosen || w.By == nil || w.By.Who != a.Who { return Option{}, fmt.Errorf("the ask %s ended %s; no person chose", a.ID, w.Outcome) } o, offered := a.Option(w.Option) if !offered { return Option{}, fmt.Errorf("the ask %s offered no option %s", a.ID, w.Option) } if w.Level != o.Level { return Option{}, fmt.Errorf("the option %s is %s, and the warrant says %s", o.ID, o.Level, w.Level) } return o, nil } // Button is one answer a channel offers: its label and the router's one-time ticket for it. type Button struct { Label string `json:"label"` Ticket string `json:"ticket"` } // Message is the work a channel takes: shown with buttons (Show), shown again in place (Edit), or said // (Send). Handle is the router's name for it, the same across a show and its edits; the channel keeps // which of its own messages that is. The words are the router's, shown as given. type Message struct { Handle string `json:"handle"` Title string `json:"title"` Body string `json:"body"` Buttons []Button `json:"buttons,omitempty"` Urgent bool `json:"urgent,omitempty"` Silent bool `json:"silent,omitempty"` } // Sender is who a channel's service says sent something: the account, the name it shows, and whether the // service authenticated it. The router alone judges whether that is the operator. type Sender struct { Identity string `json:"identity"` Display string `json:"display,omitempty"` Authenticated bool `json:"authenticated"` } // Choice is a button chosen on a channel. type Choice struct { // ID is the channel's own for this arrival, unique, so the router acts on it once. ID string `json:"id"` Ticket string `json:"ticket"` Handle string `json:"handle,omitempty"` Sender Sender `json:"sender"` At time.Time `json:"at"` } // LinkAsked is somebody on a channel asking to be linked as the operator. type LinkAsked struct { ID string `json:"id"` Sender Sender `json:"sender"` At time.Time `json:"at"` } // Code is a code a person typed on a channel, asked as a proof: never in an event, never kept. type Code struct { Sender Sender `json:"sender"` Code string `json:"code"` Purpose string `json:"purpose"` } // ProofAnswer is the router's answer to a proof: whether it was taken, and words to say to the person. type ProofAnswer struct { Accepted bool `json:"accepted"` Words string `json:"words"` }