215 lines
6.5 KiB
Go
215 lines
6.5 KiB
Go
package chat
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/household"
|
|||
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/notify"
|
|||
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// This file is the one place the notification texts are written. The
|
|||
|
|
// builders make the notify.Event of each event type, whose title and body
|
|||
|
|
// the push channel shows as they are; Text turns its payload into the Google
|
|||
|
|
// Chat message, such as "This week's plan is ready: Tacos, Bolognese,
|
|||
|
|
// Tikka masala + 4 more. Open the plan →".
|
|||
|
|
|
|||
|
|
// TestText is the message POST /settings/chat-webhook/test sends.
|
|||
|
|
const TestText = "MealQuest · test message"
|
|||
|
|
|
|||
|
|
// listedNames is how many names a message lists before "+ N more".
|
|||
|
|
const listedNames = 3
|
|||
|
|
|
|||
|
|
// actions is the closing call to action of each event's Chat message.
|
|||
|
|
var actions = map[notify.EventType]string{
|
|||
|
|
notify.PlanReady: "Open the plan",
|
|||
|
|
notify.ListReady: "Open the Supply run",
|
|||
|
|
notify.DayOfReminder: "Open the recipe",
|
|||
|
|
notify.NewSuggestions: "Open Discover",
|
|||
|
|
notify.RotationReview: "Open MealQuest",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// carded are the events whose Chat message also has a card with an "Open"
|
|||
|
|
// button.
|
|||
|
|
var carded = map[notify.EventType]bool{notify.PlanReady: true, notify.ListReady: true}
|
|||
|
|
|
|||
|
|
// PlanReady is the plan_ready event of the week starting on week, drafted
|
|||
|
|
// in the week starting on thisWeek, naming its dinners in plan order. Its
|
|||
|
|
// title names the week as seen from thisWeek: "Next week's plan is ready",
|
|||
|
|
// "This week's plan is ready", or "The plan for the week of Oct 26 is
|
|||
|
|
// ready" for any other week.
|
|||
|
|
func PlanReady(week, thisWeek store.LocalDate, dinners []string) notify.Event {
|
|||
|
|
return notify.Event{
|
|||
|
|
Type: notify.PlanReady,
|
|||
|
|
Key: string(notify.PlanReady) + ":" + week.String(),
|
|||
|
|
Title: planTitle(week, thisWeek),
|
|||
|
|
Body: nameList(dinners),
|
|||
|
|
URL: "/plan",
|
|||
|
|
Week: week,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ListReady is the list_ready event of the Supply run of the week starting
|
|||
|
|
// on week: needed items to buy for its dinners.
|
|||
|
|
func ListReady(week store.LocalDate, needed, dinners int) notify.Event {
|
|||
|
|
return notify.Event{
|
|||
|
|
Type: notify.ListReady,
|
|||
|
|
Key: string(notify.ListReady) + ":" + week.String(),
|
|||
|
|
Title: "This week's Supply run is ready",
|
|||
|
|
Body: fmt.Sprintf("%s to buy for %s", count(needed, "item", "items"), count(dinners, "dinner", "dinners")),
|
|||
|
|
URL: "/plan/shopping",
|
|||
|
|
Week: week,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DayOfReminder is the day_of_reminder event of the dinner dinnerID, named
|
|||
|
|
// name, planned on date.
|
|||
|
|
func DayOfReminder(date store.LocalDate, dinnerID, name string) notify.Event {
|
|||
|
|
return notify.Event{
|
|||
|
|
Type: notify.DayOfReminder,
|
|||
|
|
Key: string(notify.DayOfReminder) + ":" + date.String(),
|
|||
|
|
Title: "Tonight's dinner",
|
|||
|
|
Body: name,
|
|||
|
|
URL: "/dinner/" + dinnerID,
|
|||
|
|
Week: household.WeekStart(date),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewSuggestions is the new_suggestions event of suggestion batch batchID,
|
|||
|
|
// made in the week starting on week, naming its Side quests.
|
|||
|
|
func NewSuggestions(batchID string, week store.LocalDate, names []string) notify.Event {
|
|||
|
|
return notify.Event{
|
|||
|
|
Type: notify.NewSuggestions,
|
|||
|
|
Key: string(notify.NewSuggestions) + ":" + batchID,
|
|||
|
|
Title: "New Side quests are in",
|
|||
|
|
Body: nameList(names),
|
|||
|
|
URL: "/discover",
|
|||
|
|
Week: week,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RotationReview is the rotation_review event asking whether the dinner
|
|||
|
|
// dinnerID, named name and tried in the week starting on week, becomes a
|
|||
|
|
// Regular.
|
|||
|
|
func RotationReview(dinnerID, name string, week store.LocalDate) notify.Event {
|
|||
|
|
return notify.Event{
|
|||
|
|
Type: notify.RotationReview,
|
|||
|
|
Key: string(notify.RotationReview) + ":" + dinnerID,
|
|||
|
|
Title: "Keep it in rotation?",
|
|||
|
|
Body: "You tried " + name + ". Make it a Regular?",
|
|||
|
|
URL: "/",
|
|||
|
|
Week: week,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SupplyRunItem is a Supply run item still needed, as sent to Google Chat;
|
|||
|
|
// Qty is empty when the recipe gives none.
|
|||
|
|
type SupplyRunItem struct {
|
|||
|
|
Name string
|
|||
|
|
Qty string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SupplyRunGroup is one dinner of a Supply run sent to Google Chat, with
|
|||
|
|
// its items still needed.
|
|||
|
|
type SupplyRunGroup struct {
|
|||
|
|
Dinner string
|
|||
|
|
Items []SupplyRunItem
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SupplyRunText is the Google Chat message of the Supply run named name
|
|||
|
|
// (its title, or its week label until it has one): each dinner with items
|
|||
|
|
// still needed in bold, its items one per line, and the call to action.
|
|||
|
|
//
|
|||
|
|
// Supply run · Week 42 · Oct 12 – 18
|
|||
|
|
//
|
|||
|
|
// *Tacos*
|
|||
|
|
// • ground beef — 1 lb
|
|||
|
|
// • tortillas
|
|||
|
|
//
|
|||
|
|
// Open the Supply run →
|
|||
|
|
//
|
|||
|
|
// A Supply run with nothing still needed says so.
|
|||
|
|
func SupplyRunText(name string, groups []SupplyRunGroup) string {
|
|||
|
|
var b strings.Builder
|
|||
|
|
b.WriteString("Supply run · " + name)
|
|||
|
|
listed := false
|
|||
|
|
for _, g := range groups {
|
|||
|
|
if len(g.Items) == 0 {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
listed = true
|
|||
|
|
b.WriteString("\n\n*" + g.Dinner + "*")
|
|||
|
|
for _, it := range g.Items {
|
|||
|
|
b.WriteString("\n• " + it.Name)
|
|||
|
|
if it.Qty != "" {
|
|||
|
|
b.WriteString(" — " + it.Qty)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if !listed {
|
|||
|
|
b.WriteString("\n\nNothing left to buy.")
|
|||
|
|
}
|
|||
|
|
b.WriteString("\n\n" + actions[notify.ListReady] + " →")
|
|||
|
|
return b.String()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// planTitle is the plan_ready title of the week starting on week, seen
|
|||
|
|
// from the week starting on thisWeek.
|
|||
|
|
func planTitle(week, thisWeek store.LocalDate) string {
|
|||
|
|
y, m, d := thisWeek.Date()
|
|||
|
|
next := store.LocalDateOf(time.Date(y, m, d+7, 0, 0, 0, 0, time.UTC))
|
|||
|
|
switch week {
|
|||
|
|
case thisWeek:
|
|||
|
|
return "This week's plan is ready"
|
|||
|
|
case next:
|
|||
|
|
return "Next week's plan is ready"
|
|||
|
|
}
|
|||
|
|
_, m, d = week.Date()
|
|||
|
|
return fmt.Sprintf("The plan for the week of %s %d is ready", m.String()[:3], d)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Text is the Google Chat message of p: its title, its body after a colon
|
|||
|
|
// and the event's call to action with an arrow.
|
|||
|
|
func Text(p notify.Payload) string {
|
|||
|
|
text := p.Title
|
|||
|
|
if p.Body != "" {
|
|||
|
|
if endsSentence(text) {
|
|||
|
|
text += " "
|
|||
|
|
} else {
|
|||
|
|
text += ": "
|
|||
|
|
}
|
|||
|
|
text += p.Body
|
|||
|
|
}
|
|||
|
|
if !endsSentence(text) {
|
|||
|
|
text += "."
|
|||
|
|
}
|
|||
|
|
if action, ok := actions[p.Event]; ok {
|
|||
|
|
text += " " + action + " →"
|
|||
|
|
}
|
|||
|
|
return text
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// endsSentence reports whether s ends with sentence punctuation.
|
|||
|
|
func endsSentence(s string) bool {
|
|||
|
|
return strings.HasSuffix(s, ".") || strings.HasSuffix(s, "?") || strings.HasSuffix(s, "!")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// nameList lists the first listedNames of names, then how many more there
|
|||
|
|
// are: "Tacos, Bolognese, Tikka masala + 4 more".
|
|||
|
|
func nameList(names []string) string {
|
|||
|
|
if len(names) <= listedNames {
|
|||
|
|
return strings.Join(names, ", ")
|
|||
|
|
}
|
|||
|
|
return fmt.Sprintf("%s + %d more", strings.Join(names[:listedNames], ", "), len(names)-listedNames)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// count is n with the singular or plural noun.
|
|||
|
|
func count(n int, one, many string) string {
|
|||
|
|
if n == 1 {
|
|||
|
|
return "1 " + one
|
|||
|
|
}
|
|||
|
|
return fmt.Sprintf("%d %s", n, many)
|
|||
|
|
}
|