This commit is contained in:
parent
d7274d723d
commit
f35433af77
24
bot/bot.go
24
bot/bot.go
@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"log"
|
||||
"matrix-bot/game"
|
||||
database "matrix-bot/persistent"
|
||||
"matrix-bot/wordleimg"
|
||||
"matrix-bot/words"
|
||||
"os"
|
||||
@ -46,6 +47,11 @@ func HandleCommand(body string, sender string, roomID string,
|
||||
pair := roomUserPair{roomID, sender}
|
||||
part := strings.Fields(strings.ToLower(body))
|
||||
|
||||
db, err := database.InitDB("wordle.db")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
_, e := userSettings[sender]
|
||||
if !e {
|
||||
userSettings[sender] = make(map[string]string)
|
||||
@ -133,8 +139,16 @@ func HandleCommand(body string, sender string, roomID string,
|
||||
delete(wordleGames, pair)
|
||||
switch sig {
|
||||
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())
|
||||
case 1:
|
||||
err = db.IncrementWins(sender)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return jia(txt("Wordle:\nYou guessed right!", replyTo), wordleState())
|
||||
}
|
||||
} else if equals(part[1], "status", "stat", "s") {
|
||||
@ -146,8 +160,18 @@ func HandleCommand(body string, sender string, roomID string,
|
||||
if !wgEx {
|
||||
return jia(txt("Wordle:\nNone to end", replyTo))
|
||||
}
|
||||
|
||||
err = db.IncrementGamesPlayed(sender)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
delete(wordleGames, pair)
|
||||
return jia(txt("Wordle:\nEnded", replyTo))
|
||||
} else if equals(part[1], "leaderboard", "l", "lead") {
|
||||
stats := db.FormatLeaderboard()
|
||||
|
||||
return jia(txt(stats, nil))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
12
go.mod
12
go.mod
@ -4,16 +4,21 @@ go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/fogleman/gg v1.3.0
|
||||
github.com/mattn/go-sqlite3 v1.14.34
|
||||
maunium.net/go/mautrix v0.26.2
|
||||
modernc.org/sqlite v1.50.1
|
||||
)
|
||||
|
||||
require (
|
||||
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/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rs/zerolog v1.34.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // 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/image v0.36.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
|
||||
modernc.org/libc v1.72.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
50
go.sum
50
go.sum
@ -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/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/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/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
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/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.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
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-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/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/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
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/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/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
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/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||
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/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.6.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.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
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/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
maunium.net/go/mautrix v0.26.2 h1:rLiZLQoSKCJDZ+mF1gBQS4p74h3jZXs83g8D4W6Te8g=
|
||||
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
230
main.go
@ -2,29 +2,31 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"matrix-bot/bot"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto"
|
||||
"maunium.net/go/mautrix/crypto/cryptohelper"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var homeserver string
|
||||
var username string
|
||||
var password string
|
||||
var roomID string
|
||||
var userId string
|
||||
var accessToken string
|
||||
var deviceId string
|
||||
var pickleKeyString string
|
||||
var recoveryKey string
|
||||
var cryptoDBPath string
|
||||
@ -37,8 +39,114 @@ type storedCredentials struct {
|
||||
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) {
|
||||
// remember to use a secure key for the pickle key in production
|
||||
pickleKey := []byte(pickleKeyString)
|
||||
|
||||
if cryptoDBPath != "" {
|
||||
@ -138,14 +246,46 @@ func saveStoredCredentials(path string, creds *storedCredentials) error {
|
||||
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() {
|
||||
homeserver = envOrFatal("MATRIX_HOMESERVER")
|
||||
username = envOrDefault("MATRIX_USERNAME", "")
|
||||
password = envOrDefault("MATRIX_PASSWORD", "")
|
||||
username = envOrFatal("MATRIX_USERNAME")
|
||||
password = envOrFatal("MATRIX_PASSWORD")
|
||||
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")
|
||||
recoveryKey = envOrFatal("MATRIX_RECOVERY_KEY")
|
||||
cryptoDBPath = envOrDefault("MATRIX_CRYPTO_DB", "crypto.db")
|
||||
@ -181,35 +321,48 @@ func main() {
|
||||
loadConfig()
|
||||
bot.Load()
|
||||
|
||||
cryptoDeviceID, err := readDeviceIDFromCryptoDB(cryptoDBPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
forceLogin := false
|
||||
|
||||
stored, err := loadStoredCredentials(credentialsPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var cachedUserID string
|
||||
var cachedAccessToken string
|
||||
var cachedDeviceID string
|
||||
if stored != nil {
|
||||
if accessToken == "" {
|
||||
accessToken = stored.AccessToken
|
||||
}
|
||||
if deviceId == "" {
|
||||
deviceId = stored.DeviceID
|
||||
}
|
||||
if userId == "" {
|
||||
userId = stored.UserID
|
||||
}
|
||||
if accessToken != "" || deviceId != "" || userId != "" {
|
||||
cachedUserID = stored.UserID
|
||||
cachedAccessToken = stored.AccessToken
|
||||
cachedDeviceID = stored.DeviceID
|
||||
if cachedUserID != "" || cachedAccessToken != "" || cachedDeviceID != "" {
|
||||
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 {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if accessToken == "" || deviceId == "" {
|
||||
if username == "" || password == "" {
|
||||
log.Fatal("missing MATRIX_USERNAME or MATRIX_PASSWORD for credential bootstrap")
|
||||
// Device ID source of truth:
|
||||
// 1. crypto DB (if present)
|
||||
// 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{
|
||||
Type: mautrix.AuthTypePassword,
|
||||
Identifier: mautrix.UserIdentifier{
|
||||
@ -217,30 +370,28 @@ func main() {
|
||||
User: username,
|
||||
},
|
||||
Password: password,
|
||||
DeviceID: client.DeviceID,
|
||||
StoreCredentials: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
accessToken = client.AccessToken
|
||||
userId = client.UserID.String()
|
||||
deviceId = client.DeviceID.String()
|
||||
if err := saveStoredCredentials(credentialsPath, &storedCredentials{
|
||||
UserID: userId,
|
||||
AccessToken: accessToken,
|
||||
DeviceID: deviceId,
|
||||
UserID: client.UserID.String(),
|
||||
AccessToken: client.AccessToken,
|
||||
DeviceID: client.DeviceID.String(),
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Saved credentials to", credentialsPath)
|
||||
}
|
||||
|
||||
if userId == "" {
|
||||
log.Fatal("missing MATRIX_USER_ID and no stored credentials")
|
||||
auth := &authManager{
|
||||
client: client,
|
||||
credentialsPath: credentialsPath,
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
|
||||
client.DeviceID = id.DeviceID(deviceId)
|
||||
|
||||
syncer := mautrix.NewDefaultSyncer()
|
||||
client.Syncer = syncer
|
||||
|
||||
@ -259,10 +410,13 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
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) {
|
||||
// Ignore our own messages
|
||||
if evt.Sender.String() == userId {
|
||||
if client.UserID != "" && evt.Sender == client.UserID {
|
||||
return
|
||||
}
|
||||
|
||||
@ -301,9 +455,7 @@ func main() {
|
||||
})
|
||||
|
||||
go func() {
|
||||
if err := client.Sync(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
runSyncWithAutoRelogin(context.Background(), client, auth)
|
||||
}()
|
||||
|
||||
log.Println("Waiting for initial sync...")
|
||||
|
||||
230
persistent/database.go
Normal file
230
persistent/database.go
Normal 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,
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user