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"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -144,6 +145,85 @@ func (b *Bot) RegisterCommands() error {
|
|||||||
func (b *Bot) onReady(s *discordgo.Session, r *discordgo.Ready) {
|
func (b *Bot) onReady(s *discordgo.Session, r *discordgo.Ready) {
|
||||||
log.Printf("logged in as %s#%s", r.User.Username, r.User.Discriminator)
|
log.Printf("logged in as %s#%s", r.User.Username, r.User.Discriminator)
|
||||||
_ = s.UpdateGameStatus(0, "tracking your KMs 🚴")
|
_ = 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) {
|
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()
|
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 ─────────────────────────────────────────────────────────────────────
|
// ── Admin ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func (d *DB) GetUserLogs(ctx context.Context, guildID, userID string, limit int) ([]*RideLog, error) {
|
func (d *DB) GetUserLogs(ctx context.Context, guildID, userID string, limit int) ([]*RideLog, error) {
|
||||||
|
|||||||
@@ -9,22 +9,25 @@ import (
|
|||||||
|
|
||||||
const miToKM = 1.60934
|
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 (
|
var (
|
||||||
// Matches: 25km, 25.5 km, 25,5km, 25KM, 25 kilometers, 25 kilometres
|
// Matches: 25km, 25.5 km, 25,5km, 25KM, 25 kilometers, 25 kilometres
|
||||||
kmPattern = regexp.MustCompile(
|
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"
|
// 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.)
|
// Only match when followed by a word boundary and a non-unit word (ride, loop, etc.)
|
||||||
// or preceded by cycling verbs.
|
// or preceded by cycling verbs.
|
||||||
kPattern = regexp.MustCompile(
|
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
|
// Matches: 25mi, 25 mi, 25 miles, 25mile
|
||||||
miPattern = regexp.MustCompile(
|
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 {
|
if m == nil {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
// Normalise comma decimal separator
|
numStr := m[1]
|
||||||
numStr := strings.ReplaceAll(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)
|
v, err := strconv.ParseFloat(numStr, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, false
|
return 0, false
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ func TestParseKM(t *testing.T) {
|
|||||||
{"30 miles today", 30 * miToKM, true},
|
{"30 miles today", 30 * miToKM, true},
|
||||||
{"Did a 100k ride", 100, true},
|
{"Did a 100k ride", 100, true},
|
||||||
{"Went for a 80k loop", 80, 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"
|
// Should NOT match — no cycling context for bare "k"
|
||||||
{"Listened to 100k songs", 0, false},
|
{"Listened to 100k songs", 0, false},
|
||||||
// No distance at all
|
// No distance at all
|
||||||
|
|||||||
Reference in New Issue
Block a user