Add horoscope, !time @user, profile achievements; fix mention detection and multiple bugs

- Add !horoscope command and daily horoscope digest using Free Horoscope API
- Enhance !time to support @user timezone lookups
- Add profile completeness achievements (birthday, timezone, both set)
- Fix Matrix mention detection in reputation and LLM passive plugins
- Fix !whois reputation query (reason string mismatch and calculation)
- Fix !time IANA case-sensitivity and username/city collision
- Fix timezone default to empty string for achievement detection
- Scope all leaderboards to room members (XP, rep, potty, insult, first, rankings, emoji)
- Fix XP rank calculation to handle tied scores correctly
- Replace skull emoji with bomb for bot insult reactions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-03-09 23:55:02 -07:00
parent 2c191ec464
commit 03def6463f
24 changed files with 5057 additions and 116 deletions

View File

@@ -115,7 +115,7 @@ func (p *FunPlugin) Commands() []CommandDef {
{Name: "roll", Description: "Roll dice", Usage: "!roll [NdM+X]", Category: "Fun & Games"},
{Name: "8ball", Description: "Magic 8-ball", Usage: "!8ball <question>", Category: "Fun & Games"},
{Name: "coin", Description: "Flip a coin", Usage: "!coin", Category: "Fun & Games"},
{Name: "time", Description: "World clock", Usage: "!time [city]", Category: "Lookup & Reference"},
{Name: "time", Description: "World clock or user's local time", Usage: "!time [city|@user]", Category: "Lookup & Reference"},
{Name: "hltb", Description: "HowLongToBeat lookup", Usage: "!hltb <game>", Category: "Lookup & Reference"},
{Name: "twinbee", Description: "Random Twinbee lore/trivia", Usage: "!twinbee", Category: "Fun & Games"},
{Name: "poll", Description: "Create a reaction poll", Usage: "!poll <question> | <option1> | <option2> ...", Category: "Fun & Games"},
@@ -229,7 +229,7 @@ func (p *FunPlugin) handleCoin(ctx MessageContext) error {
}
func (p *FunPlugin) handleTime(ctx MessageContext) error {
args := strings.TrimSpace(strings.ToLower(p.GetArgs(ctx.Body, "time")))
args := strings.TrimSpace(p.GetArgs(ctx.Body, "time"))
if args == "" {
// Show a few major cities
cities := []string{"new york", "london", "tokyo", "sydney"}
@@ -240,23 +240,55 @@ func (p *FunPlugin) handleTime(ctx MessageContext) error {
t := time.Now().In(tz)
sb.WriteString(fmt.Sprintf(" • %s: %s\n", titleCase(city), t.Format("Mon Jan 2 15:04 MST")))
}
sb.WriteString("\nUse !time <city> for a specific location.")
sb.WriteString("\nUse !time <city> or !time <@user> for a specific location or person.")
return p.SendMessage(ctx.RoomID, sb.String())
}
tzName, ok := cityTimezones[args]
if !ok {
// Try loading it as a raw IANA zone
tzName = args
// If it looks like a Matrix user ID, go straight to user lookup
if strings.HasPrefix(args, "@") && strings.Contains(args, ":") {
return p.showUserTime(ctx, args)
}
loc, err := time.LoadLocation(tzName)
// Try city map first (cheap, deterministic lookup — avoids username collisions)
argsLower := strings.ToLower(args)
if tzName, ok := cityTimezones[argsLower]; ok {
loc, _ := time.LoadLocation(tzName)
t := time.Now().In(loc)
return p.SendMessage(ctx.RoomID, fmt.Sprintf("🕐 %s: **%s**", titleCase(argsLower), t.Format("Monday, January 2, 2006 3:04 PM MST")))
}
// Try raw IANA timezone (preserve original case — IANA names are case-sensitive)
if loc, err := time.LoadLocation(args); err == nil {
t := time.Now().In(loc)
return p.SendMessage(ctx.RoomID, fmt.Sprintf("🕐 %s: **%s**", args, t.Format("Monday, January 2, 2006 3:04 PM MST")))
}
// Fall back to user lookup
return p.showUserTime(ctx, args)
}
func (p *FunPlugin) showUserTime(ctx MessageContext, input string) error {
resolved, ok := p.ResolveUser(input)
if !ok {
return p.SendMessage(ctx.RoomID, fmt.Sprintf("Unknown city, timezone, or user: %s", input))
}
d := db.Get()
var tz string
err := d.QueryRow(`SELECT timezone FROM birthdays WHERE user_id = ?`, string(resolved)).Scan(&tz)
if err != nil || tz == "" {
return p.SendMessage(ctx.RoomID,
fmt.Sprintf("%s hasn't set their timezone yet. They can use !settz <timezone> to set it.", string(resolved)))
}
loc, err := time.LoadLocation(tz)
if err != nil {
return p.SendMessage(ctx.RoomID, fmt.Sprintf("Unknown city or timezone: %s", args))
return p.SendMessage(ctx.RoomID, fmt.Sprintf("Invalid timezone stored for %s: %s", string(resolved), tz))
}
t := time.Now().In(loc)
return p.SendMessage(ctx.RoomID, fmt.Sprintf("🕐 %s: **%s**", args, t.Format("Monday, January 2, 2006 3:04 PM MST")))
return p.SendMessage(ctx.RoomID,
fmt.Sprintf("🕐 %s: **%s** (%s)", string(resolved), t.Format("Monday, January 2, 2006 3:04 PM"), tz))
}
func (p *FunPlugin) handleHLTB(ctx MessageContext) error {