Add noon-of-gameday GM status email via awp-cli gameday-notice
Adds a cron-driven CLI command that emails the GM a player attendance summary at noon on the session date, skipping automatically if the game was canceled or today isn't game day, and tracking last-sent date so it won't double-send. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
01a4d51543
commit
7370d1291e
5 changed files with 130 additions and 9 deletions
11
Dockerfile
11
Dockerfile
|
|
@ -5,6 +5,10 @@ FROM alpine:latest
|
|||
ENV APP_NAME="areweplaying"
|
||||
ENV APP_DIR="/app"
|
||||
|
||||
# Cron times (e.g. the noon gameday-notice job) are evaluated in this
|
||||
# timezone. Without tzdata + TZ, Alpine's cron defaults to UTC.
|
||||
ENV TZ="America/New_York"
|
||||
|
||||
# Create the working directory
|
||||
WORKDIR $APP_DIR
|
||||
|
||||
|
|
@ -16,10 +20,11 @@ COPY awp-cli /app/awp-cli
|
|||
RUN chmod +x /app/areweplaying
|
||||
RUN chmod +x /app/awp-cli
|
||||
|
||||
RUN apk add --no-cache cronie
|
||||
RUN apk add --no-cache cronie tzdata
|
||||
|
||||
# Install the crontab: runs `./awp-cli check` every 2 minutes,
|
||||
# logging output to /app/logs/cron.log (logs/ is mounted at runtime).
|
||||
# Install the crontab: runs `./awp-cli check` every 2 minutes and
|
||||
# `./awp-cli gameday-notice` daily at noon, logging output to
|
||||
# /app/logs/cron.log (logs/ is mounted at runtime).
|
||||
COPY crontab.txt /app/crontab.txt
|
||||
RUN crontab /app/crontab.txt
|
||||
|
||||
|
|
|
|||
83
cmd/cli/cmd/gameday.go
Normal file
83
cmd/cli/cmd/gameday.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/hculpan/areweplaying/pkg/data"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var gameDayForce bool
|
||||
|
||||
// gameDayNoticeCmd represents the gameday-notice command
|
||||
var gameDayNoticeCmd = &cobra.Command{
|
||||
Use: "gameday-notice",
|
||||
Short: "Emails the GM a player status summary if today is game day",
|
||||
Long: `Checks whether today is the date of the next session and, if the
|
||||
session is still "planned", emails the GM a summary of current player
|
||||
attendance. Intended to be run once a day, e.g. by cron at noon. Does
|
||||
nothing if the session was canceled, if today isn't the session date, or
|
||||
if the notice was already sent today (unless --force is given).`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
session, err := data.ReadSessionData()
|
||||
if err != nil {
|
||||
log.Default().Printf("ERROR reading session data: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
sessionDate, err := time.ParseInLocation(data.DATE_FORMAT, session.Date, time.Local)
|
||||
if err != nil {
|
||||
log.Default().Printf("ERROR parsing session date %q: %s", session.Date, err)
|
||||
return err
|
||||
}
|
||||
|
||||
today := time.Now().Local()
|
||||
isGameDay := sessionDate.Year() == today.Year() && sessionDate.YearDay() == today.YearDay()
|
||||
todayStr := today.Format("2006-01-02")
|
||||
|
||||
if !isGameDay {
|
||||
log.Default().Printf("Skipping game day notice: today is not the session date (%s)", session.Date)
|
||||
return nil
|
||||
}
|
||||
|
||||
if session.Status != "planned" {
|
||||
log.Default().Printf("Skipping game day notice: session status is %q", session.Status)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !gameDayForce && session.LastNoticeSentDate == todayStr {
|
||||
log.Default().Printf("Skipping game day notice: already sent today (%s)", todayStr)
|
||||
return nil
|
||||
}
|
||||
|
||||
gmEmail := session.GMEmail()
|
||||
if gmEmail == "" {
|
||||
log.Default().Printf("Skipping game day notice: no GM email configured")
|
||||
return nil
|
||||
}
|
||||
|
||||
subject, body := session.FormatGameDayNotice()
|
||||
if err := sendNotice(gmEmail, subject, body); err != nil {
|
||||
log.Default().Printf("ERROR sending game day notice: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
session.LastNoticeSentDate = todayStr
|
||||
if err := data.PersistSession(session); err != nil {
|
||||
log.Default().Printf("ERROR persisting session: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Default().Printf("Sent game day notice to %s", gmEmail)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(gameDayNoticeCmd)
|
||||
gameDayNoticeCmd.Flags().BoolVar(&gameDayForce, "force", false, "Send the notice even if it was already sent today")
|
||||
}
|
||||
|
|
@ -1 +1,2 @@
|
|||
*/2 * * * * cd /app && ./awp-cli check >> /app/logs/cron.log 2>&1
|
||||
0 12 * * * cd /app && ./awp-cli gameday-notice >> /app/logs/cron.log 2>&1
|
||||
|
|
|
|||
|
|
@ -16,5 +16,6 @@
|
|||
],
|
||||
"inc_days": 7,
|
||||
"status": "planned",
|
||||
"password": "ABC123"
|
||||
"password": "ABC123",
|
||||
"last_notice_sent_date": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ type Session struct {
|
|||
IncrementDays int `json:"inc_days"`
|
||||
Status string `json:"status"`
|
||||
Password string `json:"password"`
|
||||
LastNoticeSentDate string `json:"last_notice_sent_date,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Session) GMEmail() string {
|
||||
|
|
@ -64,6 +65,36 @@ func (s *Session) FormatStatusUpdate(playerName, newStatus, source string) (stri
|
|||
return subject, sb.String()
|
||||
}
|
||||
|
||||
func (s *Session) FormatGameDayNotice() (string, string) {
|
||||
subject := fmt.Sprintf("Game Day Status Check: %s", s.Date)
|
||||
|
||||
var yes, no, unknown int
|
||||
for _, p := range s.Players {
|
||||
switch p.Attending {
|
||||
case "yes":
|
||||
yes++
|
||||
case "no":
|
||||
no++
|
||||
default:
|
||||
unknown++
|
||||
}
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "Today is game day (%s). Here is the current player status.\n\n", s.Date)
|
||||
fmt.Fprintf(&sb, "Attending: %d | Not attending: %d | No response: %d\n\n", yes, no, unknown)
|
||||
sb.WriteString("Current player status:\n")
|
||||
for _, p := range s.Players {
|
||||
role := ""
|
||||
if p.Gm {
|
||||
role = " (GM)"
|
||||
}
|
||||
fmt.Fprintf(&sb, " %s%s: %s\n", p.Name, role, p.Attending)
|
||||
}
|
||||
|
||||
return subject, sb.String()
|
||||
}
|
||||
|
||||
func (s *Session) GetPlayerByEmail(addr string) *Player {
|
||||
for i, p := range s.Players {
|
||||
if addr == p.Email {
|
||||
|
|
|
|||
Loading…
Reference in a new issue