metrics #1
@ -1,5 +1,14 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"nignoggobot/metrics"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
var AdminIDs = []int64{126131628}
|
||||
|
||||
func IsAdmin(userID int64) bool {
|
||||
@ -10,3 +19,94 @@ func IsAdmin(userID int64) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type StatsCommand struct{}
|
||||
|
||||
func (StatsCommand) Name() string { return "stats" }
|
||||
func (StatsCommand) Help() string { return "Show bot usage stats (admin only)." }
|
||||
func (StatsCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
if !IsAdmin(update.Message.From.ID) {
|
||||
return
|
||||
}
|
||||
stats := metrics.GetStats()
|
||||
var sb strings.Builder
|
||||
|
||||
type cmdStat struct {
|
||||
Name string
|
||||
AllTime int
|
||||
Last30Sum int
|
||||
}
|
||||
var statsList []cmdStat
|
||||
for name, stat := range stats.Commands {
|
||||
sum := 0
|
||||
for _, v := range stat.Last30 {
|
||||
sum += v
|
||||
}
|
||||
statsList = append(statsList, cmdStat{Name: name, AllTime: stat.AllTime, Last30Sum: sum})
|
||||
}
|
||||
|
||||
sort.Slice(statsList, func(i, j int) bool {
|
||||
return statsList[i].AllTime > statsList[j].AllTime
|
||||
})
|
||||
sb.WriteString("Command usage (all time):\n")
|
||||
for _, s := range statsList {
|
||||
sb.WriteString(fmt.Sprintf("/%s: %d\n", s.Name, s.AllTime))
|
||||
}
|
||||
|
||||
sort.Slice(statsList, func(i, j int) bool {
|
||||
return statsList[i].Last30Sum > statsList[j].Last30Sum
|
||||
})
|
||||
sb.WriteString("\nCommand usage (last 30 days):\n")
|
||||
for _, s := range statsList {
|
||||
sb.WriteString(fmt.Sprintf("/%s: %d\n", s.Name, s.Last30Sum))
|
||||
}
|
||||
|
||||
groups, privs, blocked := 0, 0, 0
|
||||
for _, chat := range stats.Chats {
|
||||
if chat.IsGroup {
|
||||
groups++
|
||||
} else {
|
||||
privs++
|
||||
}
|
||||
if chat.Blocked {
|
||||
blocked++
|
||||
}
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("\nGroups: %d\nPrivate chats: %d\nBlocked/muted: %d\n", groups, privs, blocked))
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, sb.String())
|
||||
bot.Send(msg)
|
||||
}
|
||||
|
||||
type BroadcastCommand struct{}
|
||||
|
||||
func (BroadcastCommand) Name() string { return "broadcast" }
|
||||
func (BroadcastCommand) Help() string {
|
||||
return "Broadcast a <message> to all chats (admin only)."
|
||||
}
|
||||
func (BroadcastCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
if !IsAdmin(update.Message.From.ID) {
|
||||
return
|
||||
}
|
||||
args := strings.TrimSpace(update.Message.CommandArguments())
|
||||
if args == "" {
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Usage: /broadcast <message>")
|
||||
bot.Send(msg)
|
||||
return
|
||||
}
|
||||
count := 0
|
||||
metrics.BroadcastMessage(func(chatID int64) error {
|
||||
msg := tgbotapi.NewMessage(chatID, args)
|
||||
_, err := bot.Send(msg)
|
||||
if err == nil {
|
||||
count++
|
||||
}
|
||||
return err
|
||||
})
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, fmt.Sprintf("Broadcast sent to %d chats.", count))
|
||||
bot.Send(msg)
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(StatsCommand{})
|
||||
Register(BroadcastCommand{})
|
||||
}
|
||||
|
9
main.go
9
main.go
@ -8,6 +8,7 @@ import (
|
||||
"syscall"
|
||||
|
||||
"nignoggobot/commands"
|
||||
"nignoggobot/metrics"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
|
||||
@ -66,10 +67,18 @@ func main() {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Received command: %s from chat %d (user %d)", update.Message.Command(), update.Message.Chat.ID, update.Message.From.ID)
|
||||
|
||||
cmdName := update.Message.Command()
|
||||
isGroup := update.Message.Chat.IsGroup() || update.Message.Chat.IsSuperGroup()
|
||||
metrics.UpdateChat(update.Message.Chat.ID, isGroup)
|
||||
if cmd, ok := commands.All()[cmdName]; ok {
|
||||
log.Printf("Executing command: %s", cmdName)
|
||||
// Bottleneck here locking mutexes and writing to json every time but oh well
|
||||
metrics.IncrementCommandUsage(cmdName)
|
||||
cmd.Execute(update, bot)
|
||||
} else {
|
||||
log.Printf("Unknown command: %s", cmdName)
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Unknown command.")
|
||||
bot.Send(msg)
|
||||
}
|
||||
|
148
metrics/metrics.go
Normal file
148
metrics/metrics.go
Normal file
@ -0,0 +1,148 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CommandStats struct {
|
||||
AllTime int `json:"all_time"`
|
||||
Last30 [30]int `json:"last_30"`
|
||||
Last30Day int64 `json:"last_30_day"` // Unix day of the first slot
|
||||
}
|
||||
|
||||
type ChatInfo struct {
|
||||
IsGroup bool `json:"is_group"`
|
||||
LastActive int64 `json:"last_active"`
|
||||
Blocked bool `json:"blocked"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
type Metrics struct {
|
||||
Commands map[string]*CommandStats `json:"commands"`
|
||||
Chats map[int64]*ChatInfo `json:"chats"`
|
||||
}
|
||||
|
||||
var (
|
||||
metricsFile = "metrics/metrics.json"
|
||||
metrics *Metrics
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
func loadMetrics() {
|
||||
if metrics != nil {
|
||||
return
|
||||
}
|
||||
metrics = &Metrics{
|
||||
Commands: make(map[string]*CommandStats),
|
||||
Chats: make(map[int64]*ChatInfo),
|
||||
}
|
||||
f, err := os.Open(metricsFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
json.NewDecoder(f).Decode(metrics)
|
||||
}
|
||||
|
||||
func saveMetrics() {
|
||||
// Do not lock here; caller must hold the lock else it will shitty deadlock
|
||||
loadMetrics()
|
||||
f, err := os.Create(metricsFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
enc.Encode(metrics)
|
||||
}
|
||||
|
||||
func IncrementCommandUsage(cmd string) {
|
||||
fmt.Printf("[metrics] IncrementCommandUsage called with cmd=%s\n", cmd)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
loadMetrics()
|
||||
stat, ok := metrics.Commands[cmd]
|
||||
if !ok {
|
||||
stat = &CommandStats{Last30Day: time.Now().Unix() / 86400}
|
||||
metrics.Commands[cmd] = stat
|
||||
}
|
||||
today := time.Now().Unix() / 86400
|
||||
shift := int(today - stat.Last30Day)
|
||||
if shift > 0 && shift < 30 {
|
||||
copy(stat.Last30[shift:], stat.Last30[:30-shift])
|
||||
for i := 0; i < shift; i++ {
|
||||
stat.Last30[i] = 0
|
||||
}
|
||||
stat.Last30Day = today
|
||||
} else if shift >= 30 {
|
||||
for i := 0; i < 30; i++ {
|
||||
stat.Last30[i] = 0
|
||||
}
|
||||
stat.Last30Day = today
|
||||
}
|
||||
stat.AllTime++
|
||||
stat.Last30[0]++
|
||||
saveMetrics()
|
||||
}
|
||||
|
||||
func UpdateChat(chatID int64, isGroup bool) {
|
||||
fmt.Printf("[metrics] UpdateChat called with chatID=%d isGroup=%v\n", chatID, isGroup)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
loadMetrics()
|
||||
info, ok := metrics.Chats[chatID]
|
||||
if !ok {
|
||||
info = &ChatInfo{IsGroup: isGroup}
|
||||
metrics.Chats[chatID] = info
|
||||
}
|
||||
info.LastActive = time.Now().Unix()
|
||||
info.Blocked = false
|
||||
info.LastError = ""
|
||||
saveMetrics()
|
||||
}
|
||||
|
||||
func MarkChatBlocked(chatID int64, errMsg string) {
|
||||
fmt.Printf("[metrics] MarkChatBlocked called with chatID=%d errMsg=%s\n", chatID, errMsg)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
loadMetrics()
|
||||
info, ok := metrics.Chats[chatID]
|
||||
if !ok {
|
||||
info = &ChatInfo{}
|
||||
metrics.Chats[chatID] = info
|
||||
}
|
||||
info.Blocked = true
|
||||
info.LastError = errMsg
|
||||
saveMetrics()
|
||||
}
|
||||
|
||||
func GetStats() *Metrics {
|
||||
fmt.Printf("[metrics] GetStats called\n")
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
loadMetrics()
|
||||
copy := *metrics
|
||||
return ©
|
||||
}
|
||||
|
||||
func BroadcastMessage(sendFunc func(chatID int64) error) {
|
||||
fmt.Printf("[metrics] BroadcastMessage called\n")
|
||||
mu.Lock()
|
||||
loadMetrics()
|
||||
ids := make([]int64, 0, len(metrics.Chats))
|
||||
for id := range metrics.Chats {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
mu.Unlock()
|
||||
for _, id := range ids {
|
||||
err := sendFunc(id)
|
||||
if err != nil {
|
||||
MarkChatBlocked(id, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
196
metrics/metrics.json
Normal file
196
metrics/metrics.json
Normal file
@ -0,0 +1,196 @@
|
||||
{
|
||||
"commands": {
|
||||
"bandname": {
|
||||
"all_time": 3,
|
||||
"last_30": [
|
||||
3,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"last_30_day": 20236
|
||||
},
|
||||
"debug": {
|
||||
"all_time": 1,
|
||||
"last_30": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"last_30_day": 20236
|
||||
},
|
||||
"gayname": {
|
||||
"all_time": 1,
|
||||
"last_30": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"last_30_day": 20236
|
||||
},
|
||||
"help": {
|
||||
"all_time": 1,
|
||||
"last_30": [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"last_30_day": 20236
|
||||
},
|
||||
"stats": {
|
||||
"all_time": 4,
|
||||
"last_30": [
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"last_30_day": 20236
|
||||
}
|
||||
},
|
||||
"chats": {
|
||||
"-1001327903329": {
|
||||
"is_group": true,
|
||||
"last_active": 1748473223,
|
||||
"blocked": false
|
||||
},
|
||||
"126131628": {
|
||||
"is_group": false,
|
||||
"last_active": 1748472356,
|
||||
"blocked": false
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user