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

@@ -114,9 +114,8 @@ func (p *AchievementsPlugin) handleAchievements(ctx MessageContext) error {
target := ctx.Sender
args := p.GetArgs(ctx.Body, "achievements")
if args != "" {
cleaned := strings.TrimSpace(strings.TrimPrefix(args, "@"))
if cleaned != "" {
target = id.UserID(cleaned)
if resolved, ok := p.ResolveUser(args); ok {
target = resolved
}
}
@@ -164,7 +163,7 @@ func (p *AchievementsPlugin) handleAchievements(ctx MessageContext) error {
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
}
// buildAchievements returns all 32 achievement definitions.
// buildAchievements returns all achievement definitions.
func (p *AchievementsPlugin) buildAchievements() []achievementDef {
return []achievementDef{
// Message milestones
@@ -196,7 +195,7 @@ func (p *AchievementsPlugin) buildAchievements() []achievementDef {
Check: func(d *sql.DB, u id.UserID) bool { return statGTE(d, u, "night_messages", 100) },
},
{
ID: "early_bird", Name: "Early Bird", Description: "Sent 100 messages between 5am and 9am",
ID: "early_bird", Name: "Early Bird", Description: "Sent 100 messages between 6am and noon",
Emoji: "🐦",
Check: func(d *sql.DB, u id.UserID) bool { return statGTE(d, u, "morning_messages", 100) },
},
@@ -411,6 +410,45 @@ func (p *AchievementsPlugin) buildAchievements() []achievementDef {
Emoji: "🏆",
Check: func(d *sql.DB, u id.UserID) bool { return checkStreak(d, u, 365) },
},
// Profile completeness
{
ID: "birthday_set", Name: "Born to Party", Description: "Set your birthday",
Emoji: "🎂",
Check: func(d *sql.DB, u id.UserID) bool {
var month int
err := d.QueryRow(
`SELECT month FROM birthdays WHERE user_id = ? AND month > 0`,
string(u),
).Scan(&month)
return err == nil && month > 0
},
},
{
ID: "timezone_set", Name: "Time Traveler", Description: "Set your timezone",
Emoji: "🌍",
Check: func(d *sql.DB, u id.UserID) bool {
var tz string
err := d.QueryRow(
`SELECT timezone FROM birthdays WHERE user_id = ? AND timezone != ''`,
string(u),
).Scan(&tz)
return err == nil && tz != ""
},
},
{
ID: "profile_complete", Name: "Identity Established", Description: "Set both birthday and timezone",
Emoji: "🪪",
Check: func(d *sql.DB, u id.UserID) bool {
var month int
var tz string
err := d.QueryRow(
`SELECT month, timezone FROM birthdays WHERE user_id = ? AND month > 0 AND timezone != ''`,
string(u),
).Scan(&month, &tz)
return err == nil && month > 0 && tz != ""
},
},
}
}