Compare commits
15 Commits
2c4ac42019
...
metrics
Author | SHA1 | Date | |
---|---|---|---|
a78c8df5a0 | |||
aa58aa6f2a | |||
fc29af19b3 | |||
71566d9a3a | |||
49a142b076 | |||
af1080fb6a | |||
3ab37d88f4 | |||
5df5dde506 | |||
bd15d70d3f | |||
cbb4a27334 | |||
f83333d9d3 | |||
522a9f60b4 | |||
d05a04ff4b | |||
e2a090b702 | |||
16877c67f4 |
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,5 +1,10 @@
|
||||
old_json/
|
||||
json/
|
||||
old_commands/
|
||||
metrics/metrics.json
|
||||
|
||||
# ---> Go
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# If you prefer the white list template instead of the nigger list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
|
207
commands/admin.go
Normal file
207
commands/admin.go
Normal file
@ -0,0 +1,207 @@
|
||||
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"
|
||||
)
|
||||
|
||||
var AdminIDs = []int64{126131628}
|
||||
|
||||
func IsAdmin(userID int64) bool {
|
||||
for _, id := range AdminIDs {
|
||||
if id == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
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
|
||||
Last30 [30]int
|
||||
}
|
||||
var statsList []cmdStat
|
||||
for name, stat := range stats.Commands {
|
||||
sum := 0
|
||||
for _, v := range stat.Last30 {
|
||||
sum += v
|
||||
}
|
||||
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
|
||||
})
|
||||
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{})
|
||||
}
|
87
commands/bandname.go
Normal file
87
commands/bandname.go
Normal file
@ -0,0 +1,87 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type BandnameCommand struct{}
|
||||
|
||||
var (
|
||||
bandnameOnce sync.Once
|
||||
randomArticle = []string{"the", ""}
|
||||
randomAdj = []string{
|
||||
"massive", "gargantuan", "scorched", "spiteful", "hateful", "deadly", "unwise", "ignorant",
|
||||
"disgusting", "fearless", "honest", "fleeting", "bloody", "foolish", "unmoving", "black", "sick", "diseased",
|
||||
"undead", "terrible", "unkind", "burning", "disgraced", "conflicted", "malicious", "unclean", "dirty", "unwashed",
|
||||
"muddy", "sickly", "fearless", "moon-lit", "bony", "skeletal", "burning", "flaming", "savage", "wild", "gold",
|
||||
"silver", "overgrown", "wild", "untamed", "ethereal", "ghostly", "haunted", "buried", "nuclear", "radioactive",
|
||||
"mutated", "accidental", "selfish", "self-serving", "hypocritical", "wicked", "dastardly", "dreadful", "obscene",
|
||||
"filthy", "vulgar", "vile", "worthless", "despicable", "cruel", "inhuman", "bloodthirsty", "vicious", "sadistic",
|
||||
"diabolical", "abominable", "bitter", "", "", "", "", "", "", "", "", "", "",
|
||||
}
|
||||
randomNounPlural = []string{
|
||||
"excuses", "clocks", "machines", "needles", "cultists", "slayers", "tyrants", "sound",
|
||||
"vision", "sight", "taste", "smell", "trumpets", "wanderers", "sirens", "kings", "queens", "knights", "priests",
|
||||
"liars", "ogres", "devils", "angels", "demons", "screams", "cries", "pigs", "fiends", "locusts", "worms", "ravens",
|
||||
"vultures", "theives", "warnings", "bodies", "bones", "fingers", "hands", "mouths", "ears", "tongues", "eyes",
|
||||
"magic", "men", "women", "promises", "confessions", "kingdoms", "kisses", "fists", "suns", "moons", "stars", "weight",
|
||||
"altars", "tombstones", "monuments", "rain", "artifacts", "wizards", "warlocks", "barbarians", "druids", "warriors",
|
||||
"monks", "diamonds", "hoard", "embers", "ashes", "swamps", "mountains", "plains", "rivers", "islands", "forests",
|
||||
"mines", "dungeons", "tombs", "hills", "sons", "daughters", "spires", "pyramids", "crypts", "catacombs", "shrines",
|
||||
"pits", "whores", "bandits", "stains", "delusion", "mistakes", "weapons", "chains", "pillars", "beasts", "creatures",
|
||||
}
|
||||
randomNounSingular = []string{
|
||||
"reason", "famine", "disease", "death", "war", "hatred", "sadness", "fear", "rage",
|
||||
"terror", "awe", "disgust", "pain", "suffering", "misery", "avarice", "malice", "lust", "filth", "dirt", "skin",
|
||||
"control", "pride", "decay", "flesh", "bone", "stone", "metal", "iron", "steel", "burden", "memory", "sorrow", "fire",
|
||||
"hell", "silence", "loathing", "contempt", "revulsion", "horror", "abomination", "devastation", "dismay", "dread",
|
||||
"panic", "frenzy", "hysteria", "grief", "remorse", "woe", "anguish", "misery", "desolation", "misfortune", "mourning",
|
||||
"despair", "lamentation", "faith", "eternity", "gloom", "melancholy", "regret", "distress",
|
||||
}
|
||||
)
|
||||
|
||||
func (b BandnameCommand) Name() string {
|
||||
return "bandname"
|
||||
}
|
||||
|
||||
func (b BandnameCommand) Help() string {
|
||||
return "Generate a random metal band name"
|
||||
}
|
||||
|
||||
func (b BandnameCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
// Generate band name: [article] [adj] [plural noun] of [singular noun]
|
||||
parts := []string{
|
||||
randChoice(randomArticle),
|
||||
randChoice(randomAdj),
|
||||
randChoice(randomNounPlural),
|
||||
"of",
|
||||
randChoice(randomNounSingular),
|
||||
}
|
||||
// Remove empty strings and extra spaces
|
||||
name := strings.Join(filterNonEmpty(parts), " ")
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, name)
|
||||
msg.ParseMode = "Markdown"
|
||||
bot.Send(msg)
|
||||
}
|
||||
|
||||
func randChoice(arr []string) string {
|
||||
return arr[rand.Intn(len(arr))]
|
||||
}
|
||||
|
||||
func filterNonEmpty(arr []string) []string {
|
||||
var out []string
|
||||
for _, s := range arr {
|
||||
if strings.TrimSpace(s) != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(BandnameCommand{})
|
||||
}
|
37
commands/debug.go
Normal file
37
commands/debug.go
Normal file
@ -0,0 +1,37 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type DebugCommand struct{}
|
||||
|
||||
func (d DebugCommand) Name() string {
|
||||
return "debug"
|
||||
}
|
||||
|
||||
func (d DebugCommand) Help() string {
|
||||
return "Show debug info (admin only)"
|
||||
}
|
||||
|
||||
func (d DebugCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
user := update.Message.From
|
||||
if user == nil || !IsAdmin(user.ID) {
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "You are not authorized to use this command.")
|
||||
bot.Send(msg)
|
||||
return
|
||||
}
|
||||
info := fmt.Sprintf("UserID: %d\nChatID: %d\nText: %s", user.ID, update.Message.Chat.ID, update.Message.Text)
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, info)
|
||||
bot.Send(msg)
|
||||
|
||||
if update.Message.CommandArguments() == "panic" {
|
||||
panic("test panic")
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(DebugCommand{})
|
||||
}
|
58
commands/gayname.go
Normal file
58
commands/gayname.go
Normal file
@ -0,0 +1,58 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type GaynameCommand struct{}
|
||||
|
||||
var (
|
||||
gaynameData struct {
|
||||
First []string `json:"first"`
|
||||
Last []string `json:"last"`
|
||||
}
|
||||
gaynameOnce sync.Once
|
||||
)
|
||||
|
||||
func loadGaynameData() {
|
||||
file, err := os.Open("json/gayname.json")
|
||||
if err != nil {
|
||||
gaynameData.First = []string{"Failed"}
|
||||
gaynameData.Last = []string{"to load"}
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
json.NewDecoder(file).Decode(&gaynameData)
|
||||
}
|
||||
|
||||
func (g GaynameCommand) Name() string {
|
||||
return "gayname"
|
||||
}
|
||||
|
||||
func (g GaynameCommand) Help() string {
|
||||
return "Your gay name"
|
||||
}
|
||||
|
||||
func (g GaynameCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
gaynameOnce.Do(loadGaynameData)
|
||||
user := update.Message.From
|
||||
var result string
|
||||
if user != nil && user.LanguageCode == "pl-PL" {
|
||||
result = user.FirstName + " aka FILTHY PIECE OF RETARD POLAK MIDGETSHIT"
|
||||
} else {
|
||||
first := gaynameData.First[rand.Intn(len(gaynameData.First))]
|
||||
last := gaynameData.Last[rand.Intn(len(gaynameData.Last))]
|
||||
result = user.FirstName + " aka " + first + " " + last
|
||||
}
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, result)
|
||||
bot.Send(msg)
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(GaynameCommand{})
|
||||
}
|
51
commands/kek.go
Normal file
51
commands/kek.go
Normal file
@ -0,0 +1,51 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type KekCommand struct{}
|
||||
|
||||
var (
|
||||
kekJokes []string
|
||||
kekJokesOnce sync.Once
|
||||
)
|
||||
|
||||
func loadKekJokes() {
|
||||
file, err := os.Open("json/jokes.json")
|
||||
if err != nil {
|
||||
kekJokes = []string{"Failed to load jokes."}
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
json.NewDecoder(file).Decode(&kekJokes)
|
||||
}
|
||||
|
||||
func (k KekCommand) Name() string {
|
||||
return "kek"
|
||||
}
|
||||
|
||||
func (k KekCommand) Help() string {
|
||||
return "Get niggerkeks"
|
||||
}
|
||||
|
||||
func (k KekCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
kekJokesOnce.Do(loadKekJokes)
|
||||
if len(kekJokes) == 0 {
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "No jokes available.")
|
||||
bot.Send(msg)
|
||||
return
|
||||
}
|
||||
joke := kekJokes[rand.Intn(len(kekJokes))]
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, joke)
|
||||
bot.Send(msg)
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(KekCommand{})
|
||||
}
|
62
commands/mol.go
Normal file
62
commands/mol.go
Normal file
@ -0,0 +1,62 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type MolCommand struct{}
|
||||
|
||||
func (m MolCommand) Name() string {
|
||||
return "mol"
|
||||
}
|
||||
|
||||
func (m MolCommand) Help() string {
|
||||
return "2D molecular structure. Example usage: /mol tetraethylgermane"
|
||||
}
|
||||
|
||||
func (m MolCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
args := strings.TrimSpace(update.Message.CommandArguments())
|
||||
if args == "" {
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Input a molecule name dumbass")
|
||||
bot.Send(msg)
|
||||
return
|
||||
}
|
||||
cid, err := fetchPubchemCID(args)
|
||||
if err != nil {
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Structure not found")
|
||||
bot.Send(msg)
|
||||
return
|
||||
}
|
||||
imgURL := fmt.Sprintf("https://pubchem.ncbi.nlm.nih.gov/image/imgsrv.fcgi?t=l&cid=%s", cid)
|
||||
photo := tgbotapi.NewPhoto(update.Message.Chat.ID, tgbotapi.FileURL(imgURL))
|
||||
photo.Caption = args
|
||||
bot.Send(photo)
|
||||
}
|
||||
|
||||
func fetchPubchemCID(compound string) (string, error) {
|
||||
apiURL := "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/" + url.QueryEscape(compound) + "/cids/TXT"
|
||||
resp, err := http.Get(apiURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cid := strings.TrimSpace(string(body))
|
||||
if cid == "" || strings.Contains(cid, "Status:") {
|
||||
return "", fmt.Errorf("CID not found")
|
||||
}
|
||||
return cid, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(MolCommand{})
|
||||
}
|
15
go.mod
15
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
|
||||
)
|
||||
|
51
go.sum
51
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=
|
||||
|
46
main.go
46
main.go
@ -1,17 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"nignoggobot/commands"
|
||||
"nignoggobot/metrics"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
|
||||
_ "nignoggobot/commands" // ensure init() is called
|
||||
)
|
||||
|
||||
var (
|
||||
devChannelID int64 = -1001327903329
|
||||
AdminIDs = []int64{126131628}
|
||||
)
|
||||
|
||||
func notifyShutdown(reason string) {
|
||||
token := os.Getenv("TELEGRAM_TOKEN")
|
||||
bot, err := tgbotapi.NewBotAPI(token)
|
||||
if err != nil {
|
||||
log.Println("Failed to create bot for shutdown notification:", err)
|
||||
return
|
||||
}
|
||||
msg := tgbotapi.NewMessage(devChannelID, "Bot shutting down/crashed: "+reason)
|
||||
bot.Send(msg)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Signal handling for SIGINT/SIGTERM
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
sig := <-sigs
|
||||
notifyShutdown(fmt.Sprintf("Received signal: %v", sig))
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
notifyShutdown("panic: " + fmt.Sprint(r))
|
||||
panic(r) // re-throw after notifying
|
||||
} else {
|
||||
notifyShutdown("normal shutdown")
|
||||
}
|
||||
}()
|
||||
|
||||
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_TOKEN"))
|
||||
log.Println(os.Getenv("TELEGRAM_TOKEN"))
|
||||
if err != nil {
|
||||
@ -29,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())
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user