add lookback on start, added parsing for ','s
This commit is contained in:
80
bot/bot.go
80
bot/bot.go
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -144,6 +145,85 @@ func (b *Bot) RegisterCommands() error {
|
||||
func (b *Bot) onReady(s *discordgo.Session, r *discordgo.Ready) {
|
||||
log.Printf("logged in as %s#%s", r.User.Username, r.User.Discriminator)
|
||||
_ = s.UpdateGameStatus(0, "tracking your KMs 🚴")
|
||||
go b.recoverMissingLogs(context.Background())
|
||||
}
|
||||
|
||||
func (b *Bot) recoverMissingLogs(ctx context.Context) {
|
||||
channels, err := b.db.GetAllChannelSettings(ctx)
|
||||
if err != nil {
|
||||
log.Printf("recoverMissingLogs: get channels: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cutoff := time.Now().Add(-7 * 24 * time.Hour)
|
||||
|
||||
for guildID, channelID := range channels {
|
||||
recovered := 0
|
||||
beforeID := ""
|
||||
|
||||
for {
|
||||
msgs, err := b.session.ChannelMessages(channelID, 100, beforeID, "", "")
|
||||
if err != nil {
|
||||
log.Printf("recoverMissingLogs: fetch messages %s: %v", channelID, err)
|
||||
break
|
||||
}
|
||||
if len(msgs) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
done := false
|
||||
for _, msg := range msgs {
|
||||
if msg.Author == nil || msg.Author.Bot {
|
||||
continue
|
||||
}
|
||||
if snowflakeTime(msg.ID).Before(cutoff) {
|
||||
done = true
|
||||
continue
|
||||
}
|
||||
if hasCheckmark(msg) {
|
||||
continue
|
||||
}
|
||||
km, ok := parser.ParseKM(msg.Content)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
added, err := b.db.AddLog(ctx, guildID, msg.Author.ID, displayName(msg.Member, msg.Author), msg.ID, channelID, km)
|
||||
if err != nil {
|
||||
log.Printf("recoverMissingLogs: AddLog: %v", err)
|
||||
continue
|
||||
}
|
||||
if added {
|
||||
_ = b.session.MessageReactionAdd(channelID, msg.ID, reactionOK)
|
||||
recovered++
|
||||
log.Printf("recoverMissingLogs: recovered %.2f km from %s", km, msg.Author.Username)
|
||||
}
|
||||
}
|
||||
|
||||
if done {
|
||||
break
|
||||
}
|
||||
beforeID = msgs[len(msgs)-1].ID
|
||||
}
|
||||
|
||||
log.Printf("recoverMissingLogs: guild %s done, recovered %d rides", guildID, recovered)
|
||||
}
|
||||
}
|
||||
|
||||
func snowflakeTime(id string) time.Time {
|
||||
sf, err := strconv.ParseInt(id, 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Unix(0, ((sf>>22)+1420070400000)*int64(time.Millisecond))
|
||||
}
|
||||
|
||||
func hasCheckmark(msg *discordgo.Message) bool {
|
||||
for _, r := range msg.Reactions {
|
||||
if r.Emoji.Name == reactionOK && r.Me {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *Bot) onMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
|
||||
20
db/db.go
20
db/db.go
@@ -514,6 +514,26 @@ func (d *DB) GetChallengeArchive(ctx context.Context, guildID string) ([]*Challe
|
||||
return results, rows.Err()
|
||||
}
|
||||
|
||||
// GetAllChannelSettings returns a map of guildID -> channelID for all configured fitness channels.
|
||||
func (d *DB) GetAllChannelSettings(ctx context.Context) (map[string]string, error) {
|
||||
rows, err := d.conn.QueryContext(ctx,
|
||||
`SELECT guild_id, value FROM settings WHERE key = 'fitness_channel_id'`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := make(map[string]string)
|
||||
for rows.Next() {
|
||||
var guildID, channelID string
|
||||
if err := rows.Scan(&guildID, &channelID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[guildID] = channelID
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// ── Admin ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (d *DB) GetUserLogs(ctx context.Context, guildID, userID string, limit int) ([]*RideLog, error) {
|
||||
|
||||
@@ -9,22 +9,25 @@ import (
|
||||
|
||||
const miToKM = 1.60934
|
||||
|
||||
// thousandsRe detects numbers where comma is a thousands separator (e.g. 1,000 or 5,981.9)
|
||||
var thousandsRe = regexp.MustCompile(`^\d{1,3}(,\d{3})+`)
|
||||
|
||||
var (
|
||||
// Matches: 25km, 25.5 km, 25,5km, 25KM, 25 kilometers, 25 kilometres
|
||||
kmPattern = regexp.MustCompile(
|
||||
`(?i)\b(\d+(?:[.,]\d+)?)\s*(?:km|kms|kilometer|kilometers|kilometre|kilometres)\b`,
|
||||
`(?i)\b(\d{1,3}(?:,\d{3})*(?:\.\d+)?|\d+(?:[.,]\d+)?)\s*(?:km|kms|kilometer|kilometers|kilometre|kilometres)\b`,
|
||||
)
|
||||
|
||||
// Matches standalone "k" used in cycling context: "did a 100k", "50k ride"
|
||||
// Only match when followed by a word boundary and a non-unit word (ride, loop, etc.)
|
||||
// or preceded by cycling verbs.
|
||||
kPattern = regexp.MustCompile(
|
||||
`(?i)\b(\d+(?:[.,]\d+)?)\s*k\b`,
|
||||
`(?i)\b(\d{1,3}(?:,\d{3})*(?:\.\d+)?|\d+(?:[.,]\d+)?)\s*k\b`,
|
||||
)
|
||||
|
||||
// Matches: 25mi, 25 mi, 25 miles, 25mile
|
||||
miPattern = regexp.MustCompile(
|
||||
`(?i)\b(\d+(?:[.,]\d+)?)\s*(?:mi|mile|miles)\b`,
|
||||
`(?i)\b(\d{1,3}(?:,\d{3})*(?:\.\d+)?|\d+(?:[.,]\d+)?)\s*(?:mi|mile|miles)\b`,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -51,8 +54,16 @@ func firstMatch(re *regexp.Regexp, text string, multiplier float64) (float64, bo
|
||||
if m == nil {
|
||||
return 0, false
|
||||
}
|
||||
// Normalise comma decimal separator
|
||||
numStr := strings.ReplaceAll(m[1], ",", ".")
|
||||
numStr := m[1]
|
||||
if strings.Contains(numStr, ",") {
|
||||
if thousandsRe.MatchString(numStr) {
|
||||
// e.g. "1,000" or "5,981.9" — comma is thousands separator
|
||||
numStr = strings.ReplaceAll(numStr, ",", "")
|
||||
} else {
|
||||
// e.g. "25,5" — comma is decimal separator
|
||||
numStr = strings.ReplaceAll(numStr, ",", ".")
|
||||
}
|
||||
}
|
||||
v, err := strconv.ParseFloat(numStr, 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
|
||||
@@ -19,6 +19,8 @@ func TestParseKM(t *testing.T) {
|
||||
{"30 miles today", 30 * miToKM, true},
|
||||
{"Did a 100k ride", 100, true},
|
||||
{"Went for a 80k loop", 80, true},
|
||||
{"5,981.9 miles", 5981.9 * miToKM, true},
|
||||
{"1,000km ride", 1000, true},
|
||||
// Should NOT match — no cycling context for bare "k"
|
||||
{"Listened to 100k songs", 0, false},
|
||||
// No distance at all
|
||||
|
||||
Reference in New Issue
Block a user