From e00f05d7c636f9a1b7a10930402cfeb9331f6631 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Thu, 21 Aug 2025 15:41:40 +0200 Subject: [PATCH 1/4] feat(command): fetch stats --- commands/credits.go | 2 +- commands/stats.go | 62 +++++++++++++++++++++++++++++++++++++++++++++ create_db.sh | 4 +-- main.go | 4 +++ updates.json | 4 ++- 5 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 commands/stats.go diff --git a/commands/credits.go b/commands/credits.go index 9682d0e..5c3c4bd 100644 --- a/commands/credits.go +++ b/commands/credits.go @@ -10,7 +10,7 @@ import ( func Credits(_ *discordgo.Session, i *discordgo.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) { msg := "**Les Copaings**, le bot gérant les serveurs privés de [anhgelus]().\n" msg += "Code source : \n\n" - msg += "Host du bot : " + gokord.BaseCfg.GetAuthor() + "\n\n" + msg += "Host du bot : " + gokord.BaseCfg.GetAuthor() + ".\n\n" msg += "Utilise :\n- [anhgelus/gokord]()" err := resp.SetMessage(msg).Send() if err != nil { diff --git a/commands/stats.go b/commands/stats.go new file mode 100644 index 0000000..80a9b1b --- /dev/null +++ b/commands/stats.go @@ -0,0 +1,62 @@ +package commands + +import ( + "time" + + "git.anhgelus.world/anhgelus/les-copaings-bot/config" + "git.anhgelus.world/anhgelus/les-copaings-bot/exp" + "git.anhgelus.world/anhgelus/les-copaings-bot/user" + "github.com/anhgelus/gokord" + "github.com/anhgelus/gokord/cmd" + "github.com/anhgelus/gokord/logger" + "github.com/bwmarrin/discordgo" +) + +type data struct { + CreatedAt time.Time + XP int + CopaingID int + copaing *user.Copaing +} + +func Stats(_ *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionMap, resp *cmd.ResponseBuilder) { + cfg := config.GetGuildConfig(i.GuildID) + days := cfg.DaysXPRemains + if v, ok := opt["days"]; ok { + in := v.IntValue() + if in < 0 || uint(in) > days { + if err := resp.SetMessage("Nombre de jours invalide").IsEphemeral().Send(); err != nil { + logger.Alert("commands/stats.go - Sending invalid days", err.Error()) + } + return + } + days = uint(in) + } + var stats []*data + res := gokord.DB.Raw( + `SELECT "created_at"::date::text, sum(xp) as xp, copaing_id FROM copaing_xps GROUP BY "created_at"::date `+ + ` WHERE guild_id = ? and created_at < ?`, + i.GuildID, exp.TimeStampNDaysBefore(days), + ) + if res.Error != nil { + logger.Alert("commands/stats.go - Fetching XP data", res.Error.Error(), "guild_id", i.GuildID) + return + } + if err := res.Scan(&stats).Error; err != nil { + logger.Alert("commands/stats.go - Scanning result", err.Error(), "res") + return + } + copaings := map[int]*user.Copaing{} + for _, s := range stats { + c, ok := copaings[s.CopaingID] + if ok { + s.copaing = c + } else { + if err := gokord.DB.First(s.copaing, s.CopaingID).Error; err != nil { + logger.Alert("commands/stats.go - Finding copaing", err.Error(), "id", s.CopaingID) + return + } + copaings[s.CopaingID] = s.copaing + } + } +} diff --git a/create_db.sh b/create_db.sh index b098405..07deca1 100644 --- a/create_db.sh +++ b/create_db.sh @@ -1,5 +1,5 @@ #!/usr/bin/bash podman network create db -podman run -p 5432:5432 --rm --network db --name postgres --env-file .env -v ./data:/var/lib/postgres/data -d postgres:alpine -podman run -p 8080:8080 --rm --network db --name adminer -d adminer \ No newline at end of file +podman run -p 5432:5432 --rm --network db --name postgres --env-file .env -v ./data:/var/lib/postgresql/data -d postgres:alpine +podman run -p 8080:8080 --rm --network db --name adminer -d adminer diff --git a/main.go b/main.go index 9c0a3b7..e2856eb 100644 --- a/main.go +++ b/main.go @@ -84,6 +84,9 @@ func main() { creditsCmd := cmd.New("credits", "Crédits"). SetHandler(commands.Credits) + statsCmd := cmd.New("stats", "Affiche des stats :D"). + SetHandler(commands.Stats) + innovations, err := gokord.LoadInnovationFromJson(updatesData) if err != nil { panic(err) @@ -116,6 +119,7 @@ func main() { resetCmd, resetUserCmd, creditsCmd, + statsCmd, }, AfterInit: func(dg *discordgo.Session) { d := 24 * time.Hour diff --git a/updates.json b/updates.json index debf5a5..452a306 100644 --- a/updates.json +++ b/updates.json @@ -32,7 +32,9 @@ { "version": "3.2.0", "commands": { - "added": [], + "added": [ + "stats" + ], "removed": [], "updated": [ "config" From a795e62723a2eb04dab421f171881597be139374 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Thu, 21 Aug 2025 16:50:04 +0200 Subject: [PATCH 2/4] feat(command): generate plot and send --- commands/stats.go | 62 ++++++++++++++++++++++++++++++++++++++++++++--- go.mod | 10 ++++++++ go.sum | 43 ++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 4 deletions(-) diff --git a/commands/stats.go b/commands/stats.go index 80a9b1b..6900051 100644 --- a/commands/stats.go +++ b/commands/stats.go @@ -1,6 +1,7 @@ package commands import ( + "bytes" "time" "git.anhgelus.world/anhgelus/les-copaings-bot/config" @@ -10,6 +11,10 @@ import ( "github.com/anhgelus/gokord/cmd" "github.com/anhgelus/gokord/logger" "github.com/bwmarrin/discordgo" + "gonum.org/v1/plot" + "gonum.org/v1/plot/plotter" + "gonum.org/v1/plot/plotutil" + "gonum.org/v1/plot/vg" ) type data struct { @@ -19,7 +24,7 @@ type data struct { copaing *user.Copaing } -func Stats(_ *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionMap, resp *cmd.ResponseBuilder) { +func Stats(s *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionMap, resp *cmd.ResponseBuilder) { cfg := config.GetGuildConfig(i.GuildID) days := cfg.DaysXPRemains if v, ok := opt["days"]; ok { @@ -32,7 +37,7 @@ func Stats(_ *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionM } days = uint(in) } - var stats []*data + var rawData []*data res := gokord.DB.Raw( `SELECT "created_at"::date::text, sum(xp) as xp, copaing_id FROM copaing_xps GROUP BY "created_at"::date `+ ` WHERE guild_id = ? and created_at < ?`, @@ -42,12 +47,15 @@ func Stats(_ *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionM logger.Alert("commands/stats.go - Fetching XP data", res.Error.Error(), "guild_id", i.GuildID) return } - if err := res.Scan(&stats).Error; err != nil { + if err := res.Scan(&rawData).Error; err != nil { logger.Alert("commands/stats.go - Scanning result", err.Error(), "res") return } + copaings := map[int]*user.Copaing{} - for _, s := range stats { + stats := map[int]*[]*plotter.XY{} + + for _, s := range rawData { c, ok := copaings[s.CopaingID] if ok { s.copaing = c @@ -58,5 +66,51 @@ func Stats(_ *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionM } copaings[s.CopaingID] = s.copaing } + pts, ok := stats[s.CopaingID] + if !ok { + pts = &[]*plotter.XY{} + stats[s.CopaingID] = pts + } + t := float64(s.CreatedAt.Unix()-time.Now().Unix()) / (24 * 60 * 60) + *pts = append(*pts, &plotter.XY{ + X: t, + Y: float64(s.XP), + }) + } + + p := plot.New() + p.Title.Text = "XP" + p.X.Label.Text = "Jours" + p.Y.Label.Text = "XP" + + for in, c := range copaings { + m, err := s.GuildMember(i.GuildID, c.DiscordID) + if err != nil { + logger.Alert("commands/stats.go - Fetching guild member", err.Error()) + return + } + err = plotutil.AddLinePoints(p, m.DisplayName(), stats[in]) + if err != nil { + logger.Alert("commands/stats.go - Adding line points", err.Error()) + return + } + } + w, err := p.WriterTo(4*vg.Inch, 4*vg.Inch, "png") + if err != nil { + logger.Alert("commands/stats.go - Generating png", err.Error()) + return + } + b := new(bytes.Buffer) + _, err = w.WriteTo(b) + if err != nil { + logger.Alert("commands/stats.go - Writing png", err.Error()) + } + err = resp.AddFile(&discordgo.File{ + Name: "plot.png", + ContentType: "image/png", + Reader: b, + }).Send() + if err != nil { + logger.Alert("commands/stats.go - Sending response", err.Error()) } } diff --git a/go.mod b/go.mod index f6b1b2d..8d3de69 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,15 @@ require ( ) require ( + codeberg.org/go-fonts/liberation v0.5.0 // indirect + codeberg.org/go-latex/latex v0.1.0 // indirect + codeberg.org/go-pdf/fpdf v0.11.1 // indirect + git.sr.ht/~sbinet/gg v0.6.0 // indirect + github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect + github.com/campoy/embedmd v1.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -21,9 +28,12 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/redis/go-redis/v9 v9.12.1 // indirect golang.org/x/crypto v0.41.0 // indirect + golang.org/x/image v0.30.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect + gonum.org/v1/plot v0.16.0 // indirect ) diff --git a/go.sum b/go.sum index c011b86..86aabdb 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,16 @@ +codeberg.org/go-fonts/liberation v0.5.0 h1:SsKoMO1v1OZmzkG2DY+7ZkCL9U+rrWI09niOLfQ5Bo0= +codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= +codeberg.org/go-latex/latex v0.1.0 h1:hoGO86rIbWVyjtlDLzCqZPjNykpWQ9YuTZqAzPcfL3c= +codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw= +codeberg.org/go-pdf/fpdf v0.11.1 h1:U8+coOTDVLxHIXZgGvkfQEi/q0hYHYvEHFuGNX2GzGs= +codeberg.org/go-pdf/fpdf v0.11.1/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= +git.sr.ht/~sbinet/gg v0.6.0 h1:RIzgkizAk+9r7uPzf/VfbJHBMKUr0F5hRFxTUGMnt38= +git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/anhgelus/gokord v0.11.1-0.20250806000243-ddfebe2ca6f1 h1:irHDC/xUm65yLFx5HnVeCbM0qQRpm0i1vQHsyLXeEbo= github.com/anhgelus/gokord v0.11.1-0.20250806000243-ddfebe2ca6f1/go.mod h1:4xpwLzIG34/XG9QZiPsnYScQhckiCpQMAI0CjP0Nc2k= github.com/anhgelus/gokord v0.11.1-0.20250806003339-90cf89cde031 h1:56vqHQzCHCcMeBBhAWUyC466BETAhaIl1Qiq93WdrYI= @@ -22,6 +35,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno= github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= +github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -29,6 +44,8 @@ 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +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/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -46,6 +63,7 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -59,26 +77,50 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4= +golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/plot v0.16.0 h1:dK28Qx/Ky4VmPUN/2zeW0ELyM6ucDnBAj5yun7M9n1g= +gonum.org/v1/plot v0.16.0/go.mod h1:Xz6U1yDMi6Ni6aaXILqmVIb6Vro8E+K7Q/GeeH+Pn0c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -89,3 +131,4 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= From cf4e7a6efd08e50885cf85d3e198e8ca59105589 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Thu, 21 Aug 2025 17:35:26 +0200 Subject: [PATCH 3/4] fix(command): wrong sql and weird gorm behaviors --- commands/stats.go | 46 ++++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/commands/stats.go b/commands/stats.go index 6900051..116e89f 100644 --- a/commands/stats.go +++ b/commands/stats.go @@ -11,6 +11,7 @@ import ( "github.com/anhgelus/gokord/cmd" "github.com/anhgelus/gokord/logger" "github.com/bwmarrin/discordgo" + "github.com/jackc/pgx/v5/pgtype" "gonum.org/v1/plot" "gonum.org/v1/plot/plotter" "gonum.org/v1/plot/plotutil" @@ -18,10 +19,9 @@ import ( ) type data struct { - CreatedAt time.Time + CreatedAt *pgtype.Date XP int CopaingID int - copaing *user.Copaing } func Stats(s *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionMap, resp *cmd.ResponseBuilder) { @@ -39,47 +39,41 @@ func Stats(s *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionM } var rawData []*data res := gokord.DB.Raw( - `SELECT "created_at"::date::text, sum(xp) as xp, copaing_id FROM copaing_xps GROUP BY "created_at"::date `+ - ` WHERE guild_id = ? and created_at < ?`, + `SELECT "created_at"::date::text, sum("xp") as xp, "copaing_id" FROM copaing_xps WHERE "guild_id" = ? and "created_at" > ? GROUP BY "created_at"::date, "copaing_id"`, i.GuildID, exp.TimeStampNDaysBefore(days), ) - if res.Error != nil { - logger.Alert("commands/stats.go - Fetching XP data", res.Error.Error(), "guild_id", i.GuildID) - return - } if err := res.Scan(&rawData).Error; err != nil { - logger.Alert("commands/stats.go - Scanning result", err.Error(), "res") + logger.Alert("commands/stats.go - Fetching result", err.Error()) return } copaings := map[int]*user.Copaing{} - stats := map[int]*[]*plotter.XY{} + stats := map[int]*[]plotter.XY{} - for _, s := range rawData { - c, ok := copaings[s.CopaingID] - if ok { - s.copaing = c - } else { - if err := gokord.DB.First(s.copaing, s.CopaingID).Error; err != nil { - logger.Alert("commands/stats.go - Finding copaing", err.Error(), "id", s.CopaingID) + for _, raw := range rawData { + _, ok := copaings[raw.CopaingID] + if !ok { + var cp user.Copaing + if err := gokord.DB.First(&cp, raw.CopaingID).Error; err != nil { + logger.Alert("commands/stats.go - Finding copaing", err.Error(), "id", raw.CopaingID) return } - copaings[s.CopaingID] = s.copaing + copaings[raw.CopaingID] = &cp } - pts, ok := stats[s.CopaingID] + pts, ok := stats[raw.CopaingID] if !ok { - pts = &[]*plotter.XY{} - stats[s.CopaingID] = pts + pts = &[]plotter.XY{} + stats[raw.CopaingID] = pts } - t := float64(s.CreatedAt.Unix()-time.Now().Unix()) / (24 * 60 * 60) - *pts = append(*pts, &plotter.XY{ + t := float64(raw.CreatedAt.Time.Unix()-time.Now().Unix()) / (24 * 60 * 60) + *pts = append(*pts, plotter.XY{ X: t, - Y: float64(s.XP), + Y: float64(raw.XP), }) } p := plot.New() - p.Title.Text = "XP" + p.Title.Text = "Évolution de l'XP" p.X.Label.Text = "Jours" p.Y.Label.Text = "XP" @@ -89,7 +83,7 @@ func Stats(s *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionM logger.Alert("commands/stats.go - Fetching guild member", err.Error()) return } - err = plotutil.AddLinePoints(p, m.DisplayName(), stats[in]) + err = plotutil.AddLinePoints(p, m.DisplayName(), plotter.XYs(*stats[in])) if err != nil { logger.Alert("commands/stats.go - Adding line points", err.Error()) return From adf1e6008e5166232c24b1de09640f9c9fb6ffba Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Thu, 21 Aug 2025 18:41:13 +0200 Subject: [PATCH 4/4] feat(command): better plot for stats --- commands/stats.go | 90 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/commands/stats.go b/commands/stats.go index 116e89f..e464890 100644 --- a/commands/stats.go +++ b/commands/stats.go @@ -2,6 +2,10 @@ package commands import ( "bytes" + "image/color" + "math" + "math/rand/v2" + "slices" "time" "git.anhgelus.world/anhgelus/les-copaings-bot/config" @@ -14,11 +18,16 @@ import ( "github.com/jackc/pgx/v5/pgtype" "gonum.org/v1/plot" "gonum.org/v1/plot/plotter" - "gonum.org/v1/plot/plotutil" "gonum.org/v1/plot/vg" ) type data struct { + CreatedAt time.Time + XP int + CopaingID int +} + +type dbData struct { CreatedAt *pgtype.Date XP int CopaingID int @@ -38,13 +47,40 @@ func Stats(s *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionM days = uint(in) } var rawData []*data - res := gokord.DB.Raw( - `SELECT "created_at"::date::text, sum("xp") as xp, "copaing_id" FROM copaing_xps WHERE "guild_id" = ? and "created_at" > ? GROUP BY "created_at"::date, "copaing_id"`, - i.GuildID, exp.TimeStampNDaysBefore(days), - ) - if err := res.Scan(&rawData).Error; err != nil { - logger.Alert("commands/stats.go - Fetching result", err.Error()) - return + if gokord.Debug { + var rawCopaingData []*user.CopaingXP + err := gokord.DB. + Where("guild_id = ? and created_at > ?", i.GuildID, exp.TimeStampNDaysBefore(days)). + Find(&rawCopaingData). + Error + if err != nil { + logger.Alert("commands/stats.go - Fetching result", err.Error()) + return + } + rawData = make([]*data, len(rawCopaingData)) + for in, d := range rawCopaingData { + rawData[in] = &data{ + CreatedAt: d.CreatedAt, + XP: int(d.XP), + CopaingID: int(d.CopaingID), + } + } + } else { + sql := `SELECT "created_at"::date::text, sum("xp") as xp, "copaing_id" FROM copaing_xps WHERE "guild_id" = ? and "created_at" > ? GROUP BY "created_at"::date, "copaing_id"` + var rawDbData []dbData + res := gokord.DB.Raw(sql, i.GuildID, exp.TimeStampNDaysBefore(days)) + if err := res.Scan(&rawDbData).Error; err != nil { + logger.Alert("commands/stats.go - Fetching result", err.Error()) + return + } + rawData = make([]*data, len(rawDbData)) + for in, d := range rawDbData { + rawData[in] = &data{ + CreatedAt: d.CreatedAt.Time, + XP: d.XP, + CopaingID: d.CopaingID, + } + } } copaings := map[int]*user.Copaing{} @@ -65,7 +101,10 @@ func Stats(s *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionM pts = &[]plotter.XY{} stats[raw.CopaingID] = pts } - t := float64(raw.CreatedAt.Time.Unix()-time.Now().Unix()) / (24 * 60 * 60) + t := float64(raw.CreatedAt.Unix() - time.Now().Unix()) + if !gokord.Debug { + t = math.Ceil(t / (24 * 60 * 60)) + } *pts = append(*pts, plotter.XY{ X: t, Y: float64(raw.XP), @@ -77,19 +116,48 @@ func Stats(s *discordgo.Session, i *discordgo.InteractionCreate, opt cmd.OptionM p.X.Label.Text = "Jours" p.Y.Label.Text = "XP" + p.Add(plotter.NewGrid()) + + r := rand.New(rand.NewPCG(uint64(time.Now().Unix()), uint64(time.Now().Unix()))) for in, c := range copaings { m, err := s.GuildMember(i.GuildID, c.DiscordID) if err != nil { logger.Alert("commands/stats.go - Fetching guild member", err.Error()) return } - err = plotutil.AddLinePoints(p, m.DisplayName(), plotter.XYs(*stats[in])) + slices.SortFunc(*stats[in], func(a, b plotter.XY) int { + if a.X < b.X { + return -1 + } + if a.X > b.X { + return 1 + } + return 0 + }) + first := (*stats[in])[0] + if first.X > float64(-days) { + *stats[in] = append([]plotter.XY{{ + X: first.X - 1, Y: 0, + }}, *stats[in]...) + } + last := (*stats[in])[len(*stats[in])-1] + if last.X <= -1 { + *stats[in] = append(*stats[in], plotter.XY{ + X: last.X + 1, Y: 0, + }) + } + l, err := plotter.NewLine(plotter.XYs(*stats[in])) if err != nil { logger.Alert("commands/stats.go - Adding line points", err.Error()) return } + l.LineStyle.Width = vg.Points(1) + l.LineStyle.Dashes = []vg.Length{vg.Points(5), vg.Points(5)} + l.LineStyle.Color = color.RGBA{R: uint8(r.UintN(255)), G: uint8(r.UintN(255)), B: uint8(r.UintN(255)), A: 255} + p.Add(l) + p.Legend.Add(m.DisplayName(), l) } - w, err := p.WriterTo(4*vg.Inch, 4*vg.Inch, "png") + w, err := p.WriterTo(8*vg.Inch, 6*vg.Inch, "png") if err != nil { logger.Alert("commands/stats.go - Generating png", err.Error()) return