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_NAME="areweplaying"
|
||||||
ENV APP_DIR="/app"
|
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
|
# Create the working directory
|
||||||
WORKDIR $APP_DIR
|
WORKDIR $APP_DIR
|
||||||
|
|
||||||
|
|
@ -16,10 +20,11 @@ COPY awp-cli /app/awp-cli
|
||||||
RUN chmod +x /app/areweplaying
|
RUN chmod +x /app/areweplaying
|
||||||
RUN chmod +x /app/awp-cli
|
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,
|
# Install the crontab: runs `./awp-cli check` every 2 minutes and
|
||||||
# logging output to /app/logs/cron.log (logs/ is mounted at runtime).
|
# `./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
|
COPY crontab.txt /app/crontab.txt
|
||||||
RUN crontab /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
|
*/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,
|
"inc_days": 7,
|
||||||
"status": "planned",
|
"status": "planned",
|
||||||
"password": "ABC123"
|
"password": "ABC123",
|
||||||
|
"last_notice_sent_date": ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ type Session struct {
|
||||||
IncrementDays int `json:"inc_days"`
|
IncrementDays int `json:"inc_days"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
LastNoticeSentDate string `json:"last_notice_sent_date,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) GMEmail() string {
|
func (s *Session) GMEmail() string {
|
||||||
|
|
@ -64,6 +65,36 @@ func (s *Session) FormatStatusUpdate(playerName, newStatus, source string) (stri
|
||||||
return subject, sb.String()
|
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 {
|
func (s *Session) GetPlayerByEmail(addr string) *Player {
|
||||||
for i, p := range s.Players {
|
for i, p := range s.Players {
|
||||||
if addr == p.Email {
|
if addr == p.Email {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue