Documentation
¶
Overview ¶
Package actions (piece ⑥) decides what every reminder says and which buttons it carries, and runs the multi-step flows behind those buttons. It knows nothing about Telegram or Slack: channel adapters only render Message values and pass button data back (ADR 0003).
Index ¶
- Constants
- Variables
- func AlreadyLinkedText(locale string) string
- func ButtonVerb(data string) (verb, chargeID string)
- func ChangedByText(locale, who string) string
- func CodeExpiredText(locale string) string
- func CommandDescription(locale, description string) string
- func CommandRoute(cmd string) (r string, ok bool)
- func ErrorText(locale string, err error) string
- func FailedText(locale string) string
- func FileTooLargeText(locale string) string
- func HelpText(locale string) string
- func InviteExpiredText(locale string) string
- func Language(code string) string
- func LinkedText(locale string) string
- func OpenFailedText(locale string) string
- func PrivateBotText(locale string) string
- func Say(locale string, t domain.Text) string
- func SlackAlreadyLinkedText(locale string) string
- func SlackBadCodeText(locale string) string
- func SlackHelpText(locale string) string
- func SlackLinkFirstText(locale string) string
- func SlackLinkedText(locale string) string
- func SlackNotLinkedText(locale string) string
- func SlackOpenInDMText(locale string) string
- func SlackTypeLinkText(locale string) string
- func SnoozeData(chargeID, kind string, days int) string
- func WelcomeText(locale, household, name string) string
- type Assistant
- type Button
- type Channel
- type ChatRef
- type Engine
- func (e *Engine) AppSent(ctx context.Context, actor app.Actor, chat ChatRef, messageID, r string) ([]string, error)
- func (e *Engine) AppSurface(ctx context.Context, actor app.Actor, chat ChatRef, messageID, r string) error
- func (e *Engine) Ask(ctx context.Context, actor app.Actor, chat ChatRef, text string) (msgs []Message, ok bool)
- func (e *Engine) AskLive(ctx context.Context, actor app.Actor, chat ChatRef, text string, ch Channel, ...) (ok bool)
- func (e *Engine) AskStream(ctx context.Context, actor app.Actor, chat ChatRef, text string, ...) (msgs []Message, ok bool)
- func (e *Engine) CanAsk(ctx context.Context) bool
- func (e *Engine) Command(ctx context.Context, actor app.Actor, chat ChatRef, cmd string) (Reply, bool)
- func (e *Engine) DefaultLocale(ctx context.Context) string
- func (e *Engine) Deliver(ctx context.Context, item app.OutboxItem) error
- func (e *Engine) Handle(ctx context.Context, actor app.Actor, chat ChatRef, raw string) (Reply, error)
- func (e *Engine) HandlePush(ctx context.Context, memberID, device, verb, chargeID string) (PushResult, error)
- func (e *Engine) HandleText(ctx context.Context, actor app.Actor, chat ChatRef, text string) (Reply, bool, error)
- func (e *Engine) ImportFile(ctx context.Context, actor app.Actor, chat ChatRef, data []byte) Message
- func (e *Engine) Locale(ctx context.Context, actor app.Actor) string
- func (e *Engine) OpenApp(ctx context.Context, actor app.Actor, r string) (Message, error)
- func (e *Engine) Render(ctx context.Context, actor app.Actor, kind, chargeID string) (Message, error)
- func (e *Engine) SetAssistant(a Assistant)
- func (e *Engine) SetPublicURL(u string)
- func (e *Engine) Sync(ctx context.Context) error
- type Event
- type FileReader
- type Message
- type PushResult
- type Reply
- type Section
Constants ¶
const ( EventReminder = "reminder" // a charge is coming EventDecision = "decision" // the last day to cancel is near: keep or cancel? EventConfirm = "confirm" // the due date passed: was it charged? EventFailed = "failed" // the charge failed EventPaid = "paid" EventSkipped = "skipped" EventTest = "test" // a test message sent from Settings // Reminders that end in an answer. EventDuplicate = "duplicate" // the same service paid more than once EventCardExpiring = "card_expiring" // a card that pays subscriptions is about to expire )
Event kinds: what a message is telling the member.
const (
// RouteHome is the route of the home screen.
RouteHome = appPrefix + "|" + scrHome
)
The app inside a message (ADR 0019): one message per chat that navigates between screens by editing itself and stays live as data changes.
Routes double as callback data: "a|<screen>|<arg>…". View routes are stored on the message so any change can redraw it; confirmation and mid-flow screens store "" and are left alone until the user moves on.
const SurfacePrefix = "@"
SurfacePrefix starts the message id of an app surface that is not a chat message, such as Slack's Home tab ("@home"). A surface is an app message of its chat (same flows, same navigation, kept live through Channel.Edit) that AppSent never retires and that retires nothing itself.
Variables ¶
var Commands = []struct{ Command, Description string }{
{"home", "Open Exhale: spend, next charges, everything"},
{"subs", "Your subscriptions"},
{"upcoming", "Charges in the next 30 days"},
{"summary", "Spend per month, category and payment method"},
{"add", "Add a subscription, step by step"},
{"checkup", "What deserves a look: trials, deadlines, price rises"},
{"methods", "Cards and accounts that pay"},
{"settings", "Reminder time, payday, currency, language"},
}
Commands lists the app commands for a channel's command menu.
var ErrGone = errors.New("destination gone")
ErrGone is returned by Channel.Send when the destination no longer exists (a browser that dropped its push subscription). The channel has already removed it; Deliver skips it instead of retrying.
var PushVerbs = []string{vPaid, vSnooze}
PushVerbs are the button verbs a push notification may offer, in order. Browsers show at most two.
"Remind me tomorrow" is the snooze button with its arguments (one day, the message's kind); its label comes from the message's button.
Functions ¶
func AlreadyLinkedText ¶
AlreadyLinkedText says the chat already belongs to a member.
func ButtonVerb ¶
ButtonVerb splits a button's data into its verb and charge id. Arguments after the charge id stay with the verb, joined by ':' ("snz:1:r"), so a token that covers one verb also covers its arguments.
func ChangedByText ¶
ChangedByText is the footer of a message someone else changed.
func CodeExpiredText ¶
CodeExpiredText explains that a link code no longer works.
func CommandDescription ¶
CommandDescription returns the description of a command in a language.
func CommandRoute ¶
CommandRoute maps a chat command (e.g. "/subs") to the app screen it opens. ok is false for commands that are not app screens.
func ErrorText ¶
ErrorText is what a failure says to a person, in their language: the sentence of a domain.Error, or the error's own text without the sentinel in front.
func FailedText ¶
FailedText is the generic "it did not work" answer.
func FileTooLargeText ¶
FileTooLargeText answers a file over the import limit.
func InviteExpiredText ¶
InviteExpiredText says an invite cannot be used.
func Language ¶
Language picks the language of a message for someone Exhale does not know yet (an unlinked chat) from the code the chat platform reports, e.g. "es-ES": a registered language, else English.
func OpenFailedText ¶
OpenFailedText says the app could not be opened.
func PrivateBotText ¶
PrivateBotText is what an unlinked chat hears.
func SlackAlreadyLinkedText ¶
SlackAlreadyLinkedText says the account already belongs to a member.
func SlackBadCodeText ¶
SlackBadCodeText explains that a link code no longer works.
func SlackHelpText ¶
SlackHelpText lists what can be typed in Slack.
func SlackLinkFirstText ¶
SlackLinkFirstText answers a button pressed by someone not linked yet.
func SlackLinkedText ¶
SlackLinkedText confirms that a Slack account was linked.
func SlackNotLinkedText ¶
SlackNotLinkedText is what an unlinked Slack account hears.
func SlackOpenInDMText ¶
SlackOpenInDMText says where the app was opened.
func SlackTypeLinkText ¶
SlackTypeLinkText is the hint under the Home tab of an unlinked account.
func SnoozeData ¶
SnoozeData is the button data that snoozes the message of kind (an outbox kind) about chargeID for days.
func WelcomeText ¶
WelcomeText greets someone who joined from an invite.
Types ¶
type Assistant ¶
type Assistant interface {
Enabled(ctx context.Context) bool
AskStream(ctx context.Context, actor app.Actor, conv, text string, progress assistant.Progress) (assistant.Turn, error)
Decide(ctx context.Context, actor app.Actor, id string, accept bool) (assistant.Outcome, error)
Undo(ctx context.Context, actor app.Actor, id string) (assistant.Outcome, error)
}
Assistant is the model behind free text. *assistant.Assistant implements it; an Engine without one answers free text with the channel's help.
type Button ¶
Button is an action attached to a message. Data is opaque to channels and at most 64 bytes, Telegram's limit for callback data. A button with a URL opens that page instead of running an action.
func PushButtons ¶
PushButtons returns the buttons of a message that a push notification offers: the first button of each verb in PushVerbs, about a charge and with no extra arguments (a token covers one charge and one verb).
type Channel ¶
type Channel interface {
Name() string
Send(ctx context.Context, chatID string, m Message) (messageID string, err error)
Edit(ctx context.Context, chatID, messageID string, m Message) error
}
Channel delivers messages to one chat platform. Channels that only notify and cannot edit what they sent (email, ntfy, webhooks) return an empty message id from Send; their messages are not tracked or re-edited.
type ChatRef ¶
ChatRef identifies a conversation on a channel and, for button presses, the message the button belongs to.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine renders reminders, runs button flows and keeps sent messages in sync with the data.
func (*Engine) AppSent ¶
func (e *Engine) AppSent(ctx context.Context, actor app.Actor, chat ChatRef, messageID, r string) ([]string, error)
AppSent makes messageID the live app message of the chat. Earlier app messages are retired (their buttons removed) and their ids returned, so the adapter can unpin them.
func (*Engine) AppSurface ¶
func (e *Engine) AppSurface(ctx context.Context, actor app.Actor, chat ChatRef, messageID, r string) error
AppSurface records that the surface messageID of the chat shows route r. The adapter calls it each time it draws the surface from scratch.
func (*Engine) Ask ¶
func (e *Engine) Ask(ctx context.Context, actor app.Actor, chat ChatRef, text string) (msgs []Message, ok bool)
Ask answers free text typed in a chat. It returns the messages to send: the answer, then one message per prepared change with its buttons. ok is false when the assistant is off; the channel then shows its help.
func (*Engine) AskLive ¶
func (e *Engine) AskLive(ctx context.Context, actor app.Actor, chat ChatRef, text string, ch Channel, every time.Duration) (ok bool)
AskLive answers free text in a chat whose messages can be edited: the answer appears in one message as it is written, edited at most once per every (chat platforms limit edits: Telegram allows about one message a second per chat and twenty a minute in a group), and that message then becomes the finished answer. The cards follow as their own messages. ok is false when the assistant is off.
func (*Engine) AskStream ¶
func (e *Engine) AskStream(ctx context.Context, actor app.Actor, chat ChatRef, text string, progress func(Message)) (msgs []Message, ok bool)
AskStream is Ask, telling progress the answer as it is written (see AskLive). The cards come only in the messages returned at the end.
func (*Engine) CanAsk ¶
CanAsk reports whether free text will be answered by the assistant: it is wired, configured and switched on.
func (*Engine) Command ¶
func (e *Engine) Command(ctx context.Context, actor app.Actor, chat ChatRef, cmd string) (Reply, bool)
Command starts a chat flow for a command that is not a screen (currently /add). ok is false for other commands.
func (*Engine) DefaultLocale ¶
DefaultLocale returns the language of this instance's household, for people Exhale does not know yet (an unlinked chat).
func (*Engine) Deliver ¶
Deliver sends an outbox item to every chat the member has linked. It implements the scheduler's delivery step.
func (*Engine) Handle ¶
func (e *Engine) Handle(ctx context.Context, actor app.Actor, chat ChatRef, raw string) (Reply, error)
Handle runs the action behind a button: app navigation (ADR 0019) or a charge action. Charge actions pressed inside an app message stay inside the app.
func (*Engine) HandlePush ¶
func (e *Engine) HandlePush(ctx context.Context, memberID, device, verb, chargeID string) (PushResult, error)
HandlePush runs a notification button for a member, through the same use case as the chat button with that verb. device is the channel link the notification went to.
func (*Engine) HandleText ¶
func (e *Engine) HandleText(ctx context.Context, actor app.Actor, chat ChatRef, text string) (Reply, bool, error)
HandleText handles a typed message that answers a pending question (for example a new amount). ok is false if no question was pending.
func (*Engine) ImportFile ¶
func (e *Engine) ImportFile(ctx context.Context, actor app.Actor, chat ChatRef, data []byte) Message
ImportFile reads a file sent to a chat and returns the review to show.
func (*Engine) Locale ¶
Locale returns the language to speak to the actor: their own, else the household's.
func (*Engine) OpenApp ¶
OpenApp renders the screen at route for a new app message. After sending it, the channel adapter calls AppSent.
func (*Engine) Render ¶
func (e *Engine) Render(ctx context.Context, actor app.Actor, kind, chargeID string) (Message, error)
Render returns the message for a charge.
func (*Engine) SetAssistant ¶
SetAssistant wires the assistant. Without it free text gets the help.
func (*Engine) SetPublicURL ¶
SetPublicURL sets the address of the web, for links from chat to it.
type Event ¶
type Event struct {
Kind string
SubscriptionID string
Subscription string
ChargeID string
Amount string // decimal, e.g. "12.99"
Currency string // ISO 4217
DueDate string // YYYY-MM-DD
DaysUntil int // negative when overdue
PaymentMethod string
Category string
// Path is the web page that shows it, e.g. /subscriptions/sub_…
Path string
}
Event is the data behind a message, for channels that cannot show a screen with buttons: a webhook sends it as JSON, an email builds its subject from it, and all of them link to Path in the web. Messages that are not about a charge leave the fields they do not have empty.
type FileReader ¶
type FileReader interface {
ReadImport(ctx context.Context, actor app.Actor, data []byte) (importer.Result, error)
SaveReview(ctx context.Context, actor app.Actor, r assistant.Review) error
LoadReview(ctx context.Context, actor app.Actor) (assistant.Review, error)
DropReview(ctx context.Context, actor app.Actor) error
}
FileReader reads an uploaded file into a review list and keeps the review while the person decides. *assistant.Assistant implements it.
type Message ¶
type Message struct {
Title string
Lines []string
Sections []Section
Rows [][]Button
Footer string
// Event is the data behind the message, for channels that only notify
// (email, ntfy, webhooks). Nil for screens that are not about a charge.
Event *Event
}
Message is a channel-agnostic screen: a title, detail lines, sections and rows of buttons. Each channel renders it at the best fidelity it offers (ADR 0019).
func SlackChannelHello ¶
SlackChannelHello is the note posted in the team channel when it is set.
type PushResult ¶
type PushResult struct {
Title string `json:"title,omitempty" doc:"the charge as it is now, e.g. \"✅ Netflix paid\""`
Message string `json:"message" doc:"what happened, e.g. \"Marked as paid ✅\""`
}
PushResult is what a notification shows after its button ran.
type Reply ¶
type Reply struct {
// Edit replaces the message the button belongs to.
Edit *Message
// Toast is a short notification (Telegram callback answer).
Toast string
// Send is a new message (e.g. a question that expects a typed answer).
Send *Message
}
Reply is what the user sees after pressing a button or typing.
type Section ¶
type Section struct {
Heading string
Lines []string
// Table rows are cells; channels without tables show TableText in a
// monospace block. Cells should not contain emoji (they break alignment).
Table [][]string
}
Section is a titled block of a screen: a heading followed by lines and an optional table.