leaderboard added
All checks were successful
Deploy Matrix Bot / deploy (push) Successful in 10s

This commit is contained in:
shinya 2026-05-16 09:58:43 +02:00
parent d7274d723d
commit f35433af77
5 changed files with 503 additions and 43 deletions

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"log" "log"
"matrix-bot/game" "matrix-bot/game"
database "matrix-bot/persistent"
"matrix-bot/wordleimg" "matrix-bot/wordleimg"
"matrix-bot/words" "matrix-bot/words"
"os" "os"
@ -46,6 +47,11 @@ func HandleCommand(body string, sender string, roomID string,
pair := roomUserPair{roomID, sender} pair := roomUserPair{roomID, sender}
part := strings.Fields(strings.ToLower(body)) part := strings.Fields(strings.ToLower(body))
db, err := database.InitDB("wordle.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, e := userSettings[sender] _, e := userSettings[sender]
if !e { if !e {
userSettings[sender] = make(map[string]string) userSettings[sender] = make(map[string]string)
@ -133,8 +139,16 @@ func HandleCommand(body string, sender string, roomID string,
delete(wordleGames, pair) delete(wordleGames, pair)
switch sig { switch sig {
case -1: case -1:
err = db.IncrementLosses(sender)
if err != nil {
log.Fatal(err)
}
return jia(txt("Wordle:\nEnded in lost, word was "+wg.GetSecret(), replyTo), wordleState()) return jia(txt("Wordle:\nEnded in lost, word was "+wg.GetSecret(), replyTo), wordleState())
case 1: case 1:
err = db.IncrementWins(sender)
if err != nil {
log.Fatal(err)
}
return jia(txt("Wordle:\nYou guessed right!", replyTo), wordleState()) return jia(txt("Wordle:\nYou guessed right!", replyTo), wordleState())
} }
} else if equals(part[1], "status", "stat", "s") { } else if equals(part[1], "status", "stat", "s") {
@ -146,8 +160,18 @@ func HandleCommand(body string, sender string, roomID string,
if !wgEx { if !wgEx {
return jia(txt("Wordle:\nNone to end", replyTo)) return jia(txt("Wordle:\nNone to end", replyTo))
} }
err = db.IncrementGamesPlayed(sender)
if err != nil {
log.Fatal(err)
}
delete(wordleGames, pair) delete(wordleGames, pair)
return jia(txt("Wordle:\nEnded", replyTo)) return jia(txt("Wordle:\nEnded", replyTo))
} else if equals(part[1], "leaderboard", "l", "lead") {
stats := db.FormatLeaderboard()
return jia(txt(stats, nil))
} }
} }
return nil return nil

12
go.mod
View File

@ -4,16 +4,21 @@ go 1.25.5
require ( require (
github.com/fogleman/gg v1.3.0 github.com/fogleman/gg v1.3.0
github.com/mattn/go-sqlite3 v1.14.34
maunium.net/go/mautrix v0.26.2 maunium.net/go/mautrix v0.26.2
modernc.org/sqlite v1.50.1
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.34 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rs/zerolog v1.34.0 // indirect github.com/rs/zerolog v1.34.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
@ -24,6 +29,9 @@ require (
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/image v0.36.0 // indirect golang.org/x/image v0.36.0 // indirect
golang.org/x/net v0.49.0 // indirect golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.34.0 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
) )

50
go.sum
View File

@ -5,11 +5,19 @@ github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@ -19,11 +27,15 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14= github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
@ -47,16 +59,50 @@ golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mautrix v0.26.2 h1:rLiZLQoSKCJDZ+mF1gBQS4p74h3jZXs83g8D4W6Te8g= maunium.net/go/mautrix v0.26.2 h1:rLiZLQoSKCJDZ+mF1gBQS4p74h3jZXs83g8D4W6Te8g=
maunium.net/go/mautrix v0.26.2/go.mod h1:CUxSZcjPtQNxsZLRQqETAxg2hiz7bjWT+L1HCYoMMKo= maunium.net/go/mautrix v0.26.2/go.mod h1:CUxSZcjPtQNxsZLRQqETAxg2hiz7bjWT+L1HCYoMMKo=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

230
main.go
View File

@ -2,29 +2,31 @@ package main
import ( import (
"context" "context"
"database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"log" "log"
"matrix-bot/bot" "matrix-bot/bot"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"time"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto" "maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/crypto/cryptohelper" "maunium.net/go/mautrix/crypto/cryptohelper"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
_ "github.com/mattn/go-sqlite3"
) )
var homeserver string var homeserver string
var username string var username string
var password string var password string
var roomID string var roomID string
var userId string
var accessToken string
var deviceId string
var pickleKeyString string var pickleKeyString string
var recoveryKey string var recoveryKey string
var cryptoDBPath string var cryptoDBPath string
@ -37,8 +39,114 @@ type storedCredentials struct {
DeviceID string `json:"device_id"` DeviceID string `json:"device_id"`
} }
type authManager struct {
mu sync.Mutex
client *mautrix.Client
credentialsPath string
username string
password string
afterLogin func(ctx context.Context) error
}
func isInvalidToken(err error) bool {
if err == nil {
return false
}
if errors.Is(err, mautrix.MUnknownToken) || errors.Is(err, mautrix.MMissingToken) {
return true
}
var httpErr mautrix.HTTPError
if errors.As(err, &httpErr) {
// Some servers return 401 without a Matrix errcode.
return httpErr.IsStatus(401)
}
return false
}
func (a *authManager) relogin(ctx context.Context) error {
a.mu.Lock()
defer a.mu.Unlock()
if a.username == "" || a.password == "" {
return errors.New("missing MATRIX_USERNAME or MATRIX_PASSWORD for re-login")
}
preferredDeviceID := a.client.DeviceID
log.Printf("Re-logging in to Matrix (preferred device_id=%q)", preferredDeviceID)
_, err := a.client.Login(ctx, &mautrix.ReqLogin{
Type: mautrix.AuthTypePassword,
Identifier: mautrix.UserIdentifier{
Type: mautrix.IdentifierTypeUser,
User: a.username,
},
Password: a.password,
DeviceID: preferredDeviceID,
StoreCredentials: true,
})
if err != nil {
return err
}
if err := saveStoredCredentials(a.credentialsPath, &storedCredentials{
UserID: a.client.UserID.String(),
AccessToken: a.client.AccessToken,
DeviceID: a.client.DeviceID.String(),
}); err != nil {
return err
}
log.Println("Updated credentials in", a.credentialsPath)
if a.afterLogin != nil {
if err := a.afterLogin(ctx); err != nil {
if isOlmAccountMismatch(err) {
log.Println("Detected olm account mismatch with server keys")
if cryptoResetOnMismatch {
if resetErr := resetCryptoState(); resetErr != nil {
log.Fatal(resetErr)
}
log.Fatal("Reset crypto state due to mismatch. Restart the container to re-login.")
}
log.Fatal("Crypto mismatch. Remove crypto DB and credentials, then restart.")
}
log.Println("Post-login recovery key verification failed:", err)
}
}
return nil
}
func runSyncWithAutoRelogin(ctx context.Context, client *mautrix.Client, auth *authManager) {
backoff := 2 * time.Second
maxBackoff := 30 * time.Second
for {
err := client.SyncWithContext(ctx)
if err == nil {
return
}
if ctx.Err() != nil {
return
}
if isInvalidToken(err) {
log.Println("Matrix token invalid/expired; attempting re-login:", err)
if err2 := auth.relogin(ctx); err2 != nil {
log.Println("Re-login failed:", err2)
time.Sleep(backoff)
backoff *= 2
if backoff > maxBackoff {
backoff = maxBackoff
}
continue
}
backoff = 2 * time.Second
continue
}
log.Fatal(err)
}
}
func setupCryptoHelper(cli *mautrix.Client) (*cryptohelper.CryptoHelper, error) { func setupCryptoHelper(cli *mautrix.Client) (*cryptohelper.CryptoHelper, error) {
// remember to use a secure key for the pickle key in production
pickleKey := []byte(pickleKeyString) pickleKey := []byte(pickleKeyString)
if cryptoDBPath != "" { if cryptoDBPath != "" {
@ -138,14 +246,46 @@ func saveStoredCredentials(path string, creds *storedCredentials) error {
return os.WriteFile(path, data, 0o600) return os.WriteFile(path, data, 0o600)
} }
func readDeviceIDFromCryptoDB(path string) (id.DeviceID, error) {
if path == "" {
return "", nil
}
if _, err := os.Stat(path); err != nil {
if errors.Is(err, os.ErrNotExist) {
return "", nil
}
return "", err
}
// Match mautrix's default DSN style.
dsn := fmt.Sprintf("file:%s?_txlock=immediate", path)
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return "", err
}
defer db.Close()
var deviceID id.DeviceID
// CryptoHelper defaults DBAccountID to "", so account_id is "" unless explicitly set.
err = db.QueryRow("SELECT device_id FROM crypto_account WHERE account_id = '' LIMIT 1").Scan(&deviceID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", nil
}
// Fresh/invalid DB.
if strings.Contains(err.Error(), "no such table") {
return "", nil
}
return "", err
}
return deviceID, nil
}
func loadConfig() { func loadConfig() {
homeserver = envOrFatal("MATRIX_HOMESERVER") homeserver = envOrFatal("MATRIX_HOMESERVER")
username = envOrDefault("MATRIX_USERNAME", "") username = envOrFatal("MATRIX_USERNAME")
password = envOrDefault("MATRIX_PASSWORD", "") password = envOrFatal("MATRIX_PASSWORD")
roomID = envOrDefault("MATRIX_ROOM_ID", "") roomID = envOrDefault("MATRIX_ROOM_ID", "")
userId = envOrDefault("MATRIX_USER_ID", "")
accessToken = envOrDefault("MATRIX_ACCESS_TOKEN", "")
deviceId = envOrDefault("MATRIX_DEVICE_ID", "")
pickleKeyString = envOrFatal("MATRIX_PICKLE_KEY") pickleKeyString = envOrFatal("MATRIX_PICKLE_KEY")
recoveryKey = envOrFatal("MATRIX_RECOVERY_KEY") recoveryKey = envOrFatal("MATRIX_RECOVERY_KEY")
cryptoDBPath = envOrDefault("MATRIX_CRYPTO_DB", "crypto.db") cryptoDBPath = envOrDefault("MATRIX_CRYPTO_DB", "crypto.db")
@ -181,35 +321,48 @@ func main() {
loadConfig() loadConfig()
bot.Load() bot.Load()
cryptoDeviceID, err := readDeviceIDFromCryptoDB(cryptoDBPath)
if err != nil {
log.Fatal(err)
}
forceLogin := false
stored, err := loadStoredCredentials(credentialsPath) stored, err := loadStoredCredentials(credentialsPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
var cachedUserID string
var cachedAccessToken string
var cachedDeviceID string
if stored != nil { if stored != nil {
if accessToken == "" { cachedUserID = stored.UserID
accessToken = stored.AccessToken cachedAccessToken = stored.AccessToken
} cachedDeviceID = stored.DeviceID
if deviceId == "" { if cachedUserID != "" || cachedAccessToken != "" || cachedDeviceID != "" {
deviceId = stored.DeviceID
}
if userId == "" {
userId = stored.UserID
}
if accessToken != "" || deviceId != "" || userId != "" {
log.Println("Loaded credentials from", credentialsPath) log.Println("Loaded credentials from", credentialsPath)
} }
} }
client, err := mautrix.NewClient(homeserver, id.UserID(userId), accessToken) client, err := mautrix.NewClient(homeserver, id.UserID(cachedUserID), cachedAccessToken)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if accessToken == "" || deviceId == "" { // Device ID source of truth:
if username == "" || password == "" { // 1. crypto DB (if present)
log.Fatal("missing MATRIX_USERNAME or MATRIX_PASSWORD for credential bootstrap") // 2. cached credentials
if cryptoDeviceID != "" {
client.DeviceID = cryptoDeviceID
if cachedDeviceID != "" && cachedDeviceID != cryptoDeviceID.String() {
log.Printf("Device ID mismatch between credentials and crypto DB (%q != %q). Will re-login using crypto DB device ID.", cachedDeviceID, cryptoDeviceID)
forceLogin = true
} }
log.Println("Logging in to Matrix to bootstrap credentials") } else if cachedDeviceID != "" {
client.DeviceID = id.DeviceID(cachedDeviceID)
}
if client.AccessToken == "" || client.UserID == "" || client.DeviceID == "" || forceLogin {
log.Println("Logging in to Matrix")
_, err = client.Login(context.Background(), &mautrix.ReqLogin{ _, err = client.Login(context.Background(), &mautrix.ReqLogin{
Type: mautrix.AuthTypePassword, Type: mautrix.AuthTypePassword,
Identifier: mautrix.UserIdentifier{ Identifier: mautrix.UserIdentifier{
@ -217,30 +370,28 @@ func main() {
User: username, User: username,
}, },
Password: password, Password: password,
DeviceID: client.DeviceID,
StoreCredentials: true, StoreCredentials: true,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
accessToken = client.AccessToken
userId = client.UserID.String()
deviceId = client.DeviceID.String()
if err := saveStoredCredentials(credentialsPath, &storedCredentials{ if err := saveStoredCredentials(credentialsPath, &storedCredentials{
UserID: userId, UserID: client.UserID.String(),
AccessToken: accessToken, AccessToken: client.AccessToken,
DeviceID: deviceId, DeviceID: client.DeviceID.String(),
}); err != nil { }); err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Println("Saved credentials to", credentialsPath) log.Println("Saved credentials to", credentialsPath)
} }
auth := &authManager{
if userId == "" { client: client,
log.Fatal("missing MATRIX_USER_ID and no stored credentials") credentialsPath: credentialsPath,
username: username,
password: password,
} }
client.DeviceID = id.DeviceID(deviceId)
syncer := mautrix.NewDefaultSyncer() syncer := mautrix.NewDefaultSyncer()
client.Syncer = syncer client.Syncer = syncer
@ -259,10 +410,13 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
client.Crypto = cryptoHelper client.Crypto = cryptoHelper
auth.afterLogin = func(ctx context.Context) error {
return verifyWithRecoveryKey(cryptoHelper.Machine())
}
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) { syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
// Ignore our own messages // Ignore our own messages
if evt.Sender.String() == userId { if client.UserID != "" && evt.Sender == client.UserID {
return return
} }
@ -301,9 +455,7 @@ func main() {
}) })
go func() { go func() {
if err := client.Sync(); err != nil { runSyncWithAutoRelogin(context.Background(), client, auth)
log.Fatal(err)
}
}() }()
log.Println("Waiting for initial sync...") log.Println("Waiting for initial sync...")

230
persistent/database.go Normal file
View File

@ -0,0 +1,230 @@
package database
import (
"database/sql"
"fmt"
"strings"
_ "modernc.org/sqlite"
)
type DB struct {
conn *sql.DB
}
type UserStats struct {
UserID string
Wins int
Losses int
GamesPlayed int
}
// InitDB opens the database and creates tables if needed
func InitDB(path string) (*DB, error) {
conn, err := sql.Open("sqlite", path)
if err != nil {
return nil, err
}
createTable := `
CREATE TABLE IF NOT EXISTS user_scores (
user_id TEXT PRIMARY KEY,
wins INTEGER NOT NULL DEFAULT 0,
losses INTEGER NOT NULL DEFAULT 0,
games_played INTEGER NOT NULL DEFAULT 0
);
`
_, err = conn.Exec(createTable)
if err != nil {
return nil, err
}
return &DB{
conn: conn,
}, nil
}
// ensureUser creates the user row if it doesn't exist
func (db *DB) ensureUser(userID string) error {
query := `
INSERT INTO user_scores (user_id)
VALUES (?)
ON CONFLICT(user_id) DO NOTHING;
`
_, err := db.conn.Exec(query, userID)
return err
}
// IncrementWins increments wins and games played
func (db *DB) IncrementWins(userID string) error {
if err := db.ensureUser(userID); err != nil {
return err
}
query := `
UPDATE user_scores
SET wins = wins + 1,
games_played = games_played + 1
WHERE user_id = ?;
`
_, err := db.conn.Exec(query, userID)
return err
}
// IncrementLosses increments losses and games played
func (db *DB) IncrementLosses(userID string) error {
if err := db.ensureUser(userID); err != nil {
return err
}
query := `
UPDATE user_scores
SET losses = losses + 1,
games_played = games_played + 1
WHERE user_id = ?;
`
_, err := db.conn.Exec(query, userID)
return err
}
// IncrementGamesPlayed increments only games played
func (db *DB) IncrementGamesPlayed(userID string) error {
if err := db.ensureUser(userID); err != nil {
return err
}
query := `
UPDATE user_scores
SET games_played = games_played + 1
WHERE user_id = ?;
`
_, err := db.conn.Exec(query, userID)
return err
}
// GetUserStats returns stats for a user
func (db *DB) GetUserStats(userID string) (*UserStats, error) {
if err := db.ensureUser(userID); err != nil {
return nil, err
}
query := `
SELECT user_id, wins, losses, games_played
FROM user_scores
WHERE user_id = ?;
`
row := db.conn.QueryRow(query, userID)
stats := &UserStats{}
err := row.Scan(
&stats.UserID,
&stats.Wins,
&stats.Losses,
&stats.GamesPlayed,
)
if err != nil {
return nil, err
}
return stats, nil
}
func (db *DB) FormatLeaderboard() string {
var b strings.Builder
users, err := db.GetUsersStats()
if err != nil {
return "failed to load leaderboard"
}
b.WriteString("🏆 WORDLE LEADERBOARD 🏆\n\n")
for i, u := range users {
rank := fmt.Sprintf("#%d", i+1)
switch i {
case 0:
rank = "🥇"
case 1:
rank = "🥈"
case 2:
rank = "🥉"
}
username := u.UserID
row := fmt.Sprintf(
"%s %s\nWins: %d | Losses: %d | Games: %d\n\n",
rank,
username,
u.Wins,
u.Losses,
u.GamesPlayed,
)
b.WriteString(row)
}
return b.String()
}
func (db *DB) GetUsersStats() ([]UserStats, error) {
query := `
SELECT user_id, wins, losses, games_played
FROM user_scores
ORDER BY wins DESC;
`
rows, err := db.conn.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var users []UserStats
for rows.Next() {
var user UserStats
err := rows.Scan(
&user.UserID,
&user.Wins,
&user.Losses,
&user.GamesPlayed,
)
if err != nil {
return nil, err
}
users = append(users, user)
}
if err := rows.Err(); err != nil {
return nil, err
}
return users, nil
}
// Close closes the database connection
func (db *DB) Close() error {
return db.conn.Close()
}
// Optional helper to print stats
func (s UserStats) String() string {
return fmt.Sprintf(
"User: %s | Wins: %d | Losses: %d | Games: %d",
s.UserID,
s.Wins,
s.Losses,
s.GamesPlayed,
)
}