diff --git a/commands/admin.go b/commands/admin.go index e135650..8c25eac 100644 --- a/commands/admin.go +++ b/commands/admin.go @@ -1,11 +1,20 @@ package commands import ( + "bytes" "fmt" + "image/color" + "image/png" "nignoggobot/metrics" "sort" "strings" + "gonum.org/v1/plot" + "gonum.org/v1/plot/plotter" + "gonum.org/v1/plot/vg" + "gonum.org/v1/plot/vg/draw" + "gonum.org/v1/plot/vg/vgimg" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) @@ -35,6 +44,7 @@ func (StatsCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) { Name string AllTime int Last30Sum int + Last30 [30]int } var statsList []cmdStat for name, stat := range stats.Commands { @@ -42,9 +52,94 @@ func (StatsCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) { for _, v := range stat.Last30 { sum += v } - statsList = append(statsList, cmdStat{Name: name, AllTime: stat.AllTime, Last30Sum: sum}) + if sum > 0 { + statsList = append(statsList, cmdStat{Name: name, AllTime: stat.AllTime, Last30Sum: sum, Last30: stat.Last30}) + } } + // Sort by last 30 days usage for the graph + sort.Slice(statsList, func(i, j int) bool { + return statsList[i].Last30Sum > statsList[j].Last30Sum + }) + + // Prepare data for stacked bar chart + nCmds := len(statsList) + nDays := 30 + if nCmds > 0 { + colors := []color.Color{ + color.RGBA{0x1f, 0x77, 0xb4, 0xff}, // blue + color.RGBA{0xff, 0x7f, 0x0e, 0xff}, // orange + color.RGBA{0x2c, 0xa0, 0x2c, 0xff}, // green + color.RGBA{0xd6, 0x27, 0x28, 0xff}, // red + color.RGBA{0x94, 0x67, 0xbd, 0xff}, // purple + color.RGBA{0x8c, 0x56, 0x4b, 0xff}, // brown + color.RGBA{0xe3, 0x77, 0xc2, 0xff}, // pink + color.RGBA{0x7f, 0x7f, 0x7f, 0xff}, // gray + color.RGBA{0xbc, 0xbd, 0x22, 0xff}, // yellow-green + color.RGBA{0x17, 0xbe, 0xcf, 0xff}, // cyan + } + for len(colors) < nCmds { + colors = append(colors, color.RGBA{uint8(50 * len(colors)), uint8(100 * len(colors)), uint8(150 * len(colors)), 0xff}) + } + + p := plot.New() + p.Title.Text = "Command Usage (Last 30 Days)" + p.Y.Label.Text = "Count" + p.X.Label.Text = "Days Ago" + p.Legend.Top = true + p.Legend.Left = false + p.Legend.XOffs = 0 + p.Legend.YOffs = 0 + + // Prepare the stacked values + stacks := make([][]float64, nCmds) + for i := range stacks { + stacks[i] = make([]float64, nDays) + } + for cmdIdx, stat := range statsList { + for day := 0; day < nDays; day++ { + stacks[cmdIdx][day] = float64(stat.Last30[day]) // 0=today, 29=oldest + } + } + + barCharts := make([]*plotter.BarChart, nCmds) + barWidth := vg.Points(10) + for cmdIdx := 0; cmdIdx < nCmds; cmdIdx++ { + values := make(plotter.Values, nDays) + for day := 0; day < nDays; day++ { + values[day] = stacks[cmdIdx][day] + } + bar, err := plotter.NewBarChart(values, barWidth) + if err != nil { + continue + } + bar.Color = colors[cmdIdx] + bar.Offset = 0 + if cmdIdx > 0 { + bar.StackOn(barCharts[cmdIdx-1]) + } + p.Add(bar) + p.Legend.Add(statsList[cmdIdx].Name, bar) + barCharts[cmdIdx] = bar + } + labels := make([]string, nDays) + for i := 0; i < nDays; i++ { + labels[i] = fmt.Sprintf("%d", i) // 0=Today, 29=Oldest + } + p.NominalX(labels...) + + img := vgimg.New(800, 400) + dc := draw.New(img) + p.Draw(dc) + buf := new(bytes.Buffer) + png.Encode(buf, img.Image()) + photo := tgbotapi.FileBytes{Name: "stats.png", Bytes: buf.Bytes()} + photoMsg := tgbotapi.NewPhoto(update.Message.Chat.ID, photo) + photoMsg.Caption = "Command usage for the last 30 days (0 = today)" + bot.Send(photoMsg) + } + + // Text stats as before sort.Slice(statsList, func(i, j int) bool { return statsList[i].AllTime > statsList[j].AllTime }) diff --git a/go.mod b/go.mod index 7c0b51a..6f6f1ce 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,18 @@ module nignoggobot go 1.24.3 require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 + +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.10.0 // 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/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/image v0.25.0 // indirect + golang.org/x/text v0.23.0 // indirect + gonum.org/v1/plot v0.16.0 // indirect + rsc.io/pdf v0.1.1 // indirect +) diff --git a/go.sum b/go.sum index db8e45c..e24e284 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,53 @@ +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.10.0 h1:u+w669foDDx5Ds43mpiiayp40Ov6sZalgcPMDBcZRd4= +codeberg.org/go-pdf/fpdf v0.10.0/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/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= +github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +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/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/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +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/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/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-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +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= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=