diff --git a/internal/plugin/anime.go b/internal/plugin/anime.go index c164087..43074a6 100644 --- a/internal/plugin/anime.go +++ b/internal/plugin/anime.go @@ -445,7 +445,7 @@ func (p *AnimePlugin) handleUpcoming(ctx MessageContext) error { // PostDailyReleases checks broadcast days and DMs watchers about anime airing today. // Intended to be called by the scheduler daily. func (p *AnimePlugin) PostDailyReleases(roomID id.RoomID) { - todayKey := time.Now().UTC().Format("2006-01-02") + todayKey := fmt.Sprintf("%s:%s", time.Now().UTC().Format("2006-01-02"), roomID) if db.JobCompleted("anime_releases", todayKey) { slog.Info("anime: already sent daily releases", "date", todayKey) return diff --git a/internal/plugin/birthday.go b/internal/plugin/birthday.go index 542fcb1..d0a5b27 100644 --- a/internal/plugin/birthday.go +++ b/internal/plugin/birthday.go @@ -260,13 +260,10 @@ func (p *BirthdayPlugin) CheckAndPost(roomID id.RoomID) error { } for _, m := range matches { - // Check if already fired this year - var fired int - err := d.QueryRow( - `SELECT 1 FROM birthday_fired WHERE user_id = ? AND year = ?`, m.userID, year, - ).Scan(&fired) - if err == nil { - continue // Already fired this year + // Check if already announced in THIS room this year + roomKey := fmt.Sprintf("%s:%d:%s", m.userID, year, roomID) + if db.JobCompleted("birthday", roomKey) { + continue } // Build announcement @@ -282,6 +279,17 @@ func (p *BirthdayPlugin) CheckAndPost(roomID id.RoomID) error { continue } + db.MarkJobCompleted("birthday", roomKey) + + // XP grant and DM only once per user per year (not per room) + var fired int + err := d.QueryRow( + `SELECT 1 FROM birthday_fired WHERE user_id = ? AND year = ?`, m.userID, year, + ).Scan(&fired) + if err == nil { + continue // Already granted XP and sent DM + } + // Send DM to the birthday person dmMsg := fmt.Sprintf("Happy Birthday! The community wishes you a wonderful day!%s You've been granted 100 bonus XP as a birthday gift!", ageStr) if err := p.SendDM(id.UserID(m.userID), dmMsg); err != nil { @@ -295,7 +303,7 @@ func (p *BirthdayPlugin) CheckAndPost(roomID id.RoomID) error { p.grantBirthdayXP(id.UserID(m.userID), 100) } - // Mark as fired + // Mark as fired (for XP/DM dedup) _, err = d.Exec( `INSERT INTO birthday_fired (user_id, year) VALUES (?, ?) ON CONFLICT(user_id, year) DO NOTHING`, diff --git a/internal/plugin/concerts.go b/internal/plugin/concerts.go index 3ce99b3..328467f 100644 --- a/internal/plugin/concerts.go +++ b/internal/plugin/concerts.go @@ -337,7 +337,7 @@ func (p *ConcertsPlugin) PostWeeklyDigest(roomID id.RoomID) { } year, week := time.Now().UTC().ISOWeek() - weekKey := fmt.Sprintf("%d-W%02d", year, week) + weekKey := fmt.Sprintf("%d-W%02d:%s", year, week, roomID) if db.JobCompleted("concert_digest", weekKey) { slog.Info("concerts: already sent digest this week", "week", weekKey) return diff --git a/internal/plugin/gaming.go b/internal/plugin/gaming.go index da450a3..6a1c5d1 100644 --- a/internal/plugin/gaming.go +++ b/internal/plugin/gaming.go @@ -91,7 +91,7 @@ func (p *GamingPlugin) PostReleases(roomID id.RoomID) error { // Check if already posted this week now := time.Now().UTC() year, week := now.ISOWeek() - weekKey := fmt.Sprintf("%d-W%02d", year, week) + weekKey := fmt.Sprintf("%d-W%02d:%s", year, week, roomID) if db.JobCompleted("releases", weekKey) { slog.Info("gaming: already posted releases this week", "week", weekKey) return nil diff --git a/internal/plugin/holidays.go b/internal/plugin/holidays.go index 2619f81..d34e45c 100644 --- a/internal/plugin/holidays.go +++ b/internal/plugin/holidays.go @@ -203,19 +203,25 @@ func (p *HolidaysPlugin) PostHolidays(roomID id.RoomID) error { today := time.Now().UTC().Format("2006-01-02") d := db.Get() + // Per-room dedup + roomKey := fmt.Sprintf("%s:%s", today, roomID) + if db.JobCompleted("holidays", roomKey) { + slog.Info("holidays: already posted today", "date", today, "room", roomID) + return nil + } + var dataStr string - var posted int err := d.QueryRow( - `SELECT data, posted FROM holidays_log WHERE date = ?`, today, - ).Scan(&dataStr, &posted) + `SELECT data FROM holidays_log WHERE date = ?`, today, + ).Scan(&dataStr) if err == sql.ErrNoRows { slog.Warn("holidays: no entry for today, attempting prefetch", "date", today) if err := p.Prefetch(); err != nil { return fmt.Errorf("holidays: prefetch failed: %w", err) } err = d.QueryRow( - `SELECT data, posted FROM holidays_log WHERE date = ?`, today, - ).Scan(&dataStr, &posted) + `SELECT data FROM holidays_log WHERE date = ?`, today, + ).Scan(&dataStr) if err != nil { return fmt.Errorf("holidays: still no entry after prefetch: %w", err) } @@ -223,11 +229,6 @@ func (p *HolidaysPlugin) PostHolidays(roomID id.RoomID) error { return fmt.Errorf("holidays: query: %w", err) } - if posted == 1 { - slog.Info("holidays: already posted today", "date", today) - return nil - } - var data holidaysDayData if err := json.Unmarshal([]byte(dataStr), &data); err != nil { return fmt.Errorf("holidays: unmarshal: %w", err) @@ -243,10 +244,7 @@ func (p *HolidaysPlugin) PostHolidays(roomID id.RoomID) error { return fmt.Errorf("holidays: send: %w", err) } - _, err = d.Exec(`UPDATE holidays_log SET posted = 1 WHERE date = ?`, today) - if err != nil { - slog.Error("holidays: mark posted", "err", err) - } + db.MarkJobCompleted("holidays", roomKey) return nil } diff --git a/internal/plugin/movies.go b/internal/plugin/movies.go index 01c588f..cee8799 100644 --- a/internal/plugin/movies.go +++ b/internal/plugin/movies.go @@ -558,14 +558,15 @@ func (p *MoviesPlugin) PostDailyReleases(roomID id.RoomID) { } today := time.Now().UTC().Format("2006-01-02") - if db.JobCompleted("movie_releases", today) { - slog.Info("movies: already sent daily releases", "date", today) + todayKey := fmt.Sprintf("%s:%s", today, roomID) + if db.JobCompleted("movie_releases", todayKey) { + slog.Info("movies: already sent daily releases", "date", todayKey) return } success := false defer func() { if success { - db.MarkJobCompleted("movie_releases", today) + db.MarkJobCompleted("movie_releases", todayKey) } }() diff --git a/internal/plugin/wotd.go b/internal/plugin/wotd.go index bd8bbc0..2809e4e 100644 --- a/internal/plugin/wotd.go +++ b/internal/plugin/wotd.go @@ -148,19 +148,25 @@ func (p *WOTDPlugin) PostWOTD(roomID id.RoomID) error { today := time.Now().UTC().Format("2006-01-02") d := db.Get() + // Per-room dedup + roomKey := fmt.Sprintf("%s:%s", today, roomID) + if db.JobCompleted("wotd", roomKey) { + slog.Info("wotd: already posted today", "date", today, "room", roomID) + return nil + } + var word, definition, partOfSpeech, example string - var posted int err := d.QueryRow( - `SELECT word, definition, part_of_speech, example, posted FROM wotd_log WHERE date = ?`, today, - ).Scan(&word, &definition, &partOfSpeech, &example, &posted) + `SELECT word, definition, part_of_speech, example FROM wotd_log WHERE date = ?`, today, + ).Scan(&word, &definition, &partOfSpeech, &example) if err == sql.ErrNoRows { slog.Warn("wotd: no entry for today, attempting prefetch", "date", today) if err := p.Prefetch(); err != nil { return fmt.Errorf("wotd: prefetch failed: %w", err) } err = d.QueryRow( - `SELECT word, definition, part_of_speech, example, posted FROM wotd_log WHERE date = ?`, today, - ).Scan(&word, &definition, &partOfSpeech, &example, &posted) + `SELECT word, definition, part_of_speech, example FROM wotd_log WHERE date = ?`, today, + ).Scan(&word, &definition, &partOfSpeech, &example) if err != nil { return fmt.Errorf("wotd: still no entry after prefetch: %w", err) } @@ -168,20 +174,12 @@ func (p *WOTDPlugin) PostWOTD(roomID id.RoomID) error { return fmt.Errorf("wotd: query: %w", err) } - if posted == 1 { - slog.Info("wotd: already posted today", "date", today) - return nil - } - msg := p.formatWOTD(word, definition, partOfSpeech, example) if err := p.SendMessage(roomID, msg); err != nil { return fmt.Errorf("wotd: send message: %w", err) } - _, err = d.Exec(`UPDATE wotd_log SET posted = 1 WHERE date = ?`, today) - if err != nil { - slog.Error("wotd: mark posted", "err", err) - } + db.MarkJobCompleted("wotd", roomKey) return nil }