Compare commits
17 Commits
522a9f60b4
...
master
Author | SHA1 | Date | |
---|---|---|---|
e7a8cbc2a1 | |||
1eaef9f22f | |||
335912ea8a | |||
a0f0918504 | |||
279a94c754 | |||
38a318f694 | |||
a78c8df5a0 | |||
aa58aa6f2a | |||
fc29af19b3 | |||
71566d9a3a | |||
49a142b076 | |||
af1080fb6a | |||
3ab37d88f4 | |||
5df5dde506 | |||
bd15d70d3f | |||
cbb4a27334 | |||
f83333d9d3 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,9 +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{})
|
||||
}
|
56
commands/debug.go
Normal file
56
commands/debug.go
Normal file
@ -0,0 +1,56 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
type DebugCommand struct{}
|
||||
|
||||
func (d DebugCommand) Name() string {
|
||||
return "debug"
|
||||
}
|
||||
|
||||
func (d DebugCommand) Help() string {
|
||||
return "Show debug info"
|
||||
}
|
||||
|
||||
func (d DebugCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
user := update.Message.From
|
||||
isAdmin := user != nil && IsAdmin(user.ID)
|
||||
var info string
|
||||
if isAdmin {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
uptime := time.Since(startTime)
|
||||
info = fmt.Sprintf(
|
||||
"[ADMIN DEBUG]\nUserID: %d\nChatID: %d\nText: %s\nGo version: %s\nUptime: %s\nGoroutines: %d\nMemory: %.2f MB\nOS/Arch: %s/%s\n",
|
||||
user.ID,
|
||||
update.Message.Chat.ID,
|
||||
update.Message.Text,
|
||||
runtime.Version(),
|
||||
uptime.Truncate(time.Second),
|
||||
runtime.NumGoroutine(),
|
||||
float64(m.Alloc)/1024/1024,
|
||||
runtime.GOOS,
|
||||
runtime.GOARCH,
|
||||
)
|
||||
} else {
|
||||
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 isAdmin && update.Message.CommandArguments() == "panic" {
|
||||
panic("test panic")
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(DebugCommand{})
|
||||
}
|
76
commands/fortune.go
Normal file
76
commands/fortune.go
Normal file
@ -0,0 +1,76 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type FortuneCommand struct{}
|
||||
|
||||
func (f FortuneCommand) Name() string {
|
||||
return "fortune"
|
||||
}
|
||||
|
||||
func (f FortuneCommand) Help() string {
|
||||
return "Get your fortune. Usage: /fortune"
|
||||
}
|
||||
|
||||
func (f FortuneCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
fortune, err := getRandomFortune()
|
||||
if err != nil {
|
||||
log.Printf("Error getting fortune: %v", err)
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "❌ Error loading fortune database. Please try again later.")
|
||||
bot.Send(msg)
|
||||
return
|
||||
}
|
||||
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, fortune)
|
||||
bot.Send(msg)
|
||||
}
|
||||
|
||||
func getRandomFortune() (string, error) {
|
||||
// Try current json directory first, then fallback to old_json
|
||||
file, err := os.Open("json/fortune.json")
|
||||
if err != nil {
|
||||
// Try old_json directory
|
||||
file, err = os.Open("old_json/fortune.json")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open fortune.json: %v", err)
|
||||
}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read the file content
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read fortune.json: %v", err)
|
||||
}
|
||||
|
||||
// Parse JSON array
|
||||
var fortunes []string
|
||||
if err := json.Unmarshal(data, &fortunes); err != nil {
|
||||
return "", fmt.Errorf("failed to parse fortune.json: %v", err)
|
||||
}
|
||||
|
||||
if len(fortunes) == 0 {
|
||||
return "", fmt.Errorf("fortune database is empty")
|
||||
}
|
||||
|
||||
// Seed random number generator
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// Pick a random fortune
|
||||
randomIndex := rand.Intn(len(fortunes))
|
||||
return fortunes[randomIndex], nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(FortuneCommand{})
|
||||
}
|
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{})
|
||||
}
|
@ -2,6 +2,7 @@ package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
|
@ -2,6 +2,7 @@ package commands
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
@ -16,7 +17,7 @@ func (i InfoCommand) Help() string {
|
||||
}
|
||||
|
||||
func (i InfoCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "This bot does XYZ.")
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "This bot does dumb stuff and chemistry. Now version 2.0, rewritten in Go.\n\nSauce: https://gitea.boner.be/nignogbot/nignoggobot\n\nComplaints and support: @fatboners")
|
||||
_, err := bot.Send(msg)
|
||||
if err != nil {
|
||||
log.Println("Failed to send info message:", err)
|
||||
|
66
commands/kekget.go
Normal file
66
commands/kekget.go
Normal file
@ -0,0 +1,66 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type KekgetCommand struct{}
|
||||
|
||||
func (k KekgetCommand) Name() string {
|
||||
return "kekget"
|
||||
}
|
||||
|
||||
func (k KekgetCommand) Help() string {
|
||||
return "Try to get a KEK or KKK, or even a multiKEK. Usage: /kekget"
|
||||
}
|
||||
|
||||
func (k KekgetCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
result := generateKekget()
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, result)
|
||||
bot.Send(msg)
|
||||
}
|
||||
|
||||
func generateKekget() string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// Generate random length between 1 and 20
|
||||
length := rand.Intn(20) + 1
|
||||
|
||||
var text strings.Builder
|
||||
for i := 0; i < length; i++ {
|
||||
if rand.Float64() > 0.5 {
|
||||
text.WriteString("K")
|
||||
} else {
|
||||
text.WriteString("E")
|
||||
}
|
||||
}
|
||||
|
||||
result := text.String()
|
||||
|
||||
// Check for special patterns
|
||||
switch result {
|
||||
case "KEK":
|
||||
return fmt.Sprintf("%s\nYOU WIN TOPKEK!!!", result)
|
||||
case "KKK":
|
||||
return fmt.Sprintf("%s\nYOU WIN TOPKKK HEIL HITLER!!!", result)
|
||||
case "KEKKEK":
|
||||
return fmt.Sprintf("%s\nYOU WIN DOUBLE TOPKEKKEK!!!", result)
|
||||
case "KEKKEKKEK":
|
||||
return fmt.Sprintf("%s\nYOU WIN ULTIMATE TRIPLE TOPKEKKEKKEK!!!", result)
|
||||
case "KEKKEKKEKKEK":
|
||||
return fmt.Sprintf("%s\nQUADDRUPPLE TOPKEKKEKKEKKEK!!! YOU ARE GAY!!!", result)
|
||||
case "KEKKEKKEKKEKKEK":
|
||||
return fmt.Sprintf("%s\nQUINTUPLE TOPKEKKEKKEKKEKKEK!!! UNBELIEVABLE M8!!!", result)
|
||||
default:
|
||||
return fmt.Sprintf("%s\nLength: %d", result, len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(KekgetCommand{})
|
||||
}
|
24
commands/lenny.go
Normal file
24
commands/lenny.go
Normal file
@ -0,0 +1,24 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type LennyCommand struct{}
|
||||
|
||||
func (l LennyCommand) Name() string {
|
||||
return "lenny"
|
||||
}
|
||||
|
||||
func (l LennyCommand) Help() string {
|
||||
return "( ͡° ͜ʖ ͡°) Usage: /lenny"
|
||||
}
|
||||
|
||||
func (l LennyCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "( ͡° ͜ʖ ͡°)")
|
||||
bot.Send(msg)
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(LennyCommand{})
|
||||
}
|
740
commands/medchem.go
Normal file
740
commands/medchem.go
Normal file
@ -0,0 +1,740 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type MedchemCommand struct{}
|
||||
|
||||
func (m MedchemCommand) Name() string {
|
||||
return "medchem"
|
||||
}
|
||||
|
||||
func (m MedchemCommand) Help() string {
|
||||
return "Get comprehensive medicinal chemistry properties for compounds. Usage: /medchem <compound name>"
|
||||
}
|
||||
|
||||
// PubChem JSON structures
|
||||
type PubChemCompound struct {
|
||||
ID struct {
|
||||
ID struct {
|
||||
CID int `json:"cid"`
|
||||
} `json:"id"`
|
||||
} `json:"id"`
|
||||
Props []struct {
|
||||
URN struct {
|
||||
Label string `json:"label"`
|
||||
Name string `json:"name,omitempty"`
|
||||
DataType int `json:"datatype"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Software string `json:"software,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Release string `json:"release,omitempty"`
|
||||
} `json:"urn"`
|
||||
Value struct {
|
||||
IVal int `json:"ival,omitempty"`
|
||||
FVal float64 `json:"fval,omitempty"`
|
||||
SVal string `json:"sval,omitempty"`
|
||||
Binary string `json:"binary,omitempty"`
|
||||
} `json:"value"`
|
||||
} `json:"props"`
|
||||
Atoms struct {
|
||||
AID []int `json:"aid"`
|
||||
Element []int `json:"element"`
|
||||
} `json:"atoms,omitempty"`
|
||||
Bonds struct {
|
||||
AID1 []int `json:"aid1"`
|
||||
AID2 []int `json:"aid2"`
|
||||
Order []int `json:"order"`
|
||||
} `json:"bonds,omitempty"`
|
||||
Count struct {
|
||||
HeavyAtom int `json:"heavy_atom"`
|
||||
} `json:"count,omitempty"`
|
||||
}
|
||||
|
||||
type PubChemResponse struct {
|
||||
PCCompounds []PubChemCompound `json:"PC_Compounds"`
|
||||
}
|
||||
|
||||
type PubChemSearchResponse struct {
|
||||
IdentifierList struct {
|
||||
CID []int `json:"CID"`
|
||||
} `json:"IdentifierList"`
|
||||
}
|
||||
|
||||
type PubChemSynonymsResponse struct {
|
||||
InformationList struct {
|
||||
Information []struct {
|
||||
CID int `json:"CID"`
|
||||
Synonym []string `json:"Synonym"`
|
||||
} `json:"Information"`
|
||||
} `json:"InformationList"`
|
||||
}
|
||||
|
||||
type CompoundData struct {
|
||||
CID int
|
||||
Name string
|
||||
CommonNames []string // Top 3 most common names
|
||||
IUPACName string
|
||||
MolecularFormula string
|
||||
MolecularWeight float64
|
||||
ExactMass float64
|
||||
XLogP float64
|
||||
TPSA float64
|
||||
Complexity float64
|
||||
HBondDonors int
|
||||
HBondAcceptors int
|
||||
RotatableBonds int
|
||||
InChI string
|
||||
InChIKey string
|
||||
CanonicalSMILES string
|
||||
HeavyAtomCount int
|
||||
TotalAtomCount int
|
||||
BondCount int
|
||||
}
|
||||
|
||||
type PropertyCategory int
|
||||
|
||||
const (
|
||||
CategoryBasic PropertyCategory = iota
|
||||
CategoryADME
|
||||
CategoryStructure
|
||||
CategoryIdentifiers
|
||||
)
|
||||
|
||||
func (m MedchemCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
query := strings.TrimSpace(update.Message.CommandArguments())
|
||||
if query == "" {
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "🧪 *Medchem Command*\n\nUsage: `/medchem <compound name>`\n\nExample: `/medchem aspirin`\n\nThis command provides comprehensive medicinal chemistry properties including ADME parameters, structural information, and molecular identifiers.")
|
||||
msg.ParseMode = "Markdown"
|
||||
bot.Send(msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Send "typing" action
|
||||
typingAction := tgbotapi.NewChatAction(update.Message.Chat.ID, tgbotapi.ChatTyping)
|
||||
bot.Send(typingAction)
|
||||
|
||||
compound, err := fetchCompoundData(query)
|
||||
if err != nil {
|
||||
handleError(bot, update.Message.Chat.ID, err, query)
|
||||
return
|
||||
}
|
||||
|
||||
sendCompoundInfo(bot, update.Message.Chat.ID, compound, CategoryBasic)
|
||||
}
|
||||
|
||||
func fetchCompoundData(query string) (*CompoundData, error) {
|
||||
// First, search for compound to get CID
|
||||
cid, err := searchCompoundCID(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("compound not found: %v", err)
|
||||
}
|
||||
|
||||
// Get full compound record
|
||||
url := fmt.Sprintf("https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/%d/record/JSON", cid)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("network error: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("PubChem API error: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %v", err)
|
||||
}
|
||||
|
||||
var pubchemResp PubChemResponse
|
||||
if err := json.Unmarshal(body, &pubchemResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %v", err)
|
||||
}
|
||||
|
||||
if len(pubchemResp.PCCompounds) == 0 {
|
||||
return nil, fmt.Errorf("no compound data found")
|
||||
}
|
||||
|
||||
compound := parsePubChemData(&pubchemResp.PCCompounds[0], query)
|
||||
|
||||
// Fetch common names/synonyms
|
||||
commonNames, err := fetchCommonNames(compound.CID)
|
||||
if err == nil && len(commonNames) > 0 {
|
||||
compound.CommonNames = commonNames
|
||||
// Use most common name for display
|
||||
compound.Name = fmt.Sprintf("%s (CID %d)", commonNames[0], compound.CID)
|
||||
} else {
|
||||
// Fallback to original query
|
||||
compound.Name = fmt.Sprintf("%s (CID %d)", query, compound.CID)
|
||||
}
|
||||
|
||||
return compound, nil
|
||||
}
|
||||
|
||||
func searchCompoundCID(query string) (int, error) {
|
||||
url := fmt.Sprintf("https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/%s/cids/JSON", strings.ReplaceAll(query, " ", "%20"))
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return 0, fmt.Errorf("compound '%s' not found", query)
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return 0, fmt.Errorf("search failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var searchResp PubChemSearchResponse
|
||||
if err := json.Unmarshal(body, &searchResp); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(searchResp.IdentifierList.CID) == 0 {
|
||||
return 0, fmt.Errorf("no CID found for compound")
|
||||
}
|
||||
|
||||
return searchResp.IdentifierList.CID[0], nil
|
||||
}
|
||||
|
||||
func fetchCommonNames(cid int) ([]string, error) {
|
||||
url := fmt.Sprintf("https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/%d/synonyms/JSON", cid)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("synonyms not found")
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var synonymsResp PubChemSynonymsResponse
|
||||
if err := json.Unmarshal(body, &synonymsResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(synonymsResp.InformationList.Information) == 0 {
|
||||
return nil, fmt.Errorf("no synonyms found")
|
||||
}
|
||||
|
||||
synonyms := synonymsResp.InformationList.Information[0].Synonym
|
||||
return filterCommonNames(synonyms), nil
|
||||
}
|
||||
|
||||
func filterCommonNames(synonyms []string) []string {
|
||||
var commonNames []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
// Priority filters to find the most "common" names
|
||||
for _, synonym := range synonyms {
|
||||
// Skip if already seen or too long/complex
|
||||
if seen[synonym] || len(synonym) > 40 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert to lowercase for filtering
|
||||
lower := strings.ToLower(synonym)
|
||||
|
||||
// Skip very technical names
|
||||
if strings.Contains(lower, "iupac") ||
|
||||
strings.Contains(lower, "cas") ||
|
||||
strings.Contains(lower, "einecs") ||
|
||||
strings.Contains(lower, "unii") ||
|
||||
strings.Contains(lower, "dtxsid") ||
|
||||
strings.Contains(lower, "pubchem") ||
|
||||
strings.Contains(lower, "chembl") ||
|
||||
strings.Contains(lower, "zinc") ||
|
||||
strings.Contains(lower, "inchi") ||
|
||||
strings.Contains(lower, "smiles") ||
|
||||
strings.Contains(lower, "registry") ||
|
||||
len(synonym) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Prefer shorter, simpler names
|
||||
if len(synonym) <= 30 && !strings.Contains(synonym, "[") && !strings.Contains(synonym, "(") {
|
||||
commonNames = append([]string{synonym}, commonNames...)
|
||||
} else {
|
||||
commonNames = append(commonNames, synonym)
|
||||
}
|
||||
|
||||
seen[synonym] = true
|
||||
|
||||
// Limit to top 3
|
||||
if len(commonNames) >= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return commonNames
|
||||
}
|
||||
|
||||
func parsePubChemData(compound *PubChemCompound, originalName string) *CompoundData {
|
||||
data := &CompoundData{
|
||||
CID: compound.ID.ID.CID,
|
||||
Name: originalName,
|
||||
}
|
||||
|
||||
// Parse properties from the props array
|
||||
for _, prop := range compound.Props {
|
||||
label := prop.URN.Label
|
||||
name := prop.URN.Name
|
||||
|
||||
switch {
|
||||
case label == "IUPAC Name" && name == "Preferred":
|
||||
data.IUPACName = prop.Value.SVal
|
||||
case label == "Molecular Formula":
|
||||
data.MolecularFormula = prop.Value.SVal
|
||||
case label == "Molecular Weight":
|
||||
if weight, err := strconv.ParseFloat(prop.Value.SVal, 64); err == nil {
|
||||
data.MolecularWeight = weight
|
||||
}
|
||||
case label == "Mass" && name == "Exact":
|
||||
if mass, err := strconv.ParseFloat(prop.Value.SVal, 64); err == nil {
|
||||
data.ExactMass = mass
|
||||
}
|
||||
case label == "Log P" && name == "XLogP3":
|
||||
data.XLogP = prop.Value.FVal
|
||||
case label == "Topological" && name == "Polar Surface Area":
|
||||
data.TPSA = prop.Value.FVal
|
||||
case label == "Compound Complexity":
|
||||
data.Complexity = prop.Value.FVal
|
||||
case label == "Count" && name == "Hydrogen Bond Donor":
|
||||
data.HBondDonors = prop.Value.IVal
|
||||
case label == "Count" && name == "Hydrogen Bond Acceptor":
|
||||
data.HBondAcceptors = prop.Value.IVal
|
||||
case label == "Count" && name == "Rotatable Bond":
|
||||
data.RotatableBonds = prop.Value.IVal
|
||||
case label == "InChI" && name == "Standard":
|
||||
data.InChI = prop.Value.SVal
|
||||
case label == "InChIKey" && name == "Standard":
|
||||
data.InChIKey = prop.Value.SVal
|
||||
case label == "SMILES" && name == "Canonical":
|
||||
data.CanonicalSMILES = prop.Value.SVal
|
||||
}
|
||||
}
|
||||
|
||||
// Get atom and bond counts
|
||||
if compound.Count.HeavyAtom > 0 {
|
||||
data.HeavyAtomCount = compound.Count.HeavyAtom
|
||||
}
|
||||
if len(compound.Atoms.AID) > 0 {
|
||||
data.TotalAtomCount = len(compound.Atoms.AID)
|
||||
}
|
||||
if len(compound.Bonds.AID1) > 0 {
|
||||
data.BondCount = len(compound.Bonds.AID1)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func sendCompoundInfo(bot *tgbotapi.BotAPI, chatID int64, compound *CompoundData, category PropertyCategory) {
|
||||
imageURL := fmt.Sprintf("https://pubchem.ncbi.nlm.nih.gov/image/imgsrv.fcgi?t=l&cid=%d", compound.CID)
|
||||
caption := formatCompoundCaption(compound, category)
|
||||
keyboard := createNavigationKeyboard(compound.CID, category)
|
||||
|
||||
photo := tgbotapi.NewPhoto(chatID, tgbotapi.FileURL(imageURL))
|
||||
photo.Caption = caption
|
||||
photo.ParseMode = "Markdown"
|
||||
photo.ReplyMarkup = keyboard
|
||||
bot.Send(photo)
|
||||
}
|
||||
|
||||
func formatCompoundCaption(compound *CompoundData, category PropertyCategory) string {
|
||||
switch category {
|
||||
case CategoryBasic:
|
||||
return formatBasicInfo(compound)
|
||||
case CategoryADME:
|
||||
return formatADMEInfo(compound)
|
||||
case CategoryStructure:
|
||||
return formatStructureInfo(compound)
|
||||
case CategoryIdentifiers:
|
||||
return formatIdentifiersInfo(compound)
|
||||
default:
|
||||
return formatBasicInfo(compound)
|
||||
}
|
||||
}
|
||||
|
||||
func formatBasicInfo(c *CompoundData) string {
|
||||
b := &strings.Builder{}
|
||||
fmt.Fprintf(b, "🧪 *%s*\n", c.Name)
|
||||
fmt.Fprintf(b, "📋 *Basic Properties*\n\n")
|
||||
|
||||
// Show common names first
|
||||
if len(c.CommonNames) > 0 {
|
||||
fmt.Fprintf(b, "*Common Names:*\n")
|
||||
for i, name := range c.CommonNames {
|
||||
if i >= 3 { // Limit to 3
|
||||
break
|
||||
}
|
||||
fmt.Fprintf(b, "• %s\n", name)
|
||||
}
|
||||
fmt.Fprintf(b, "\n")
|
||||
}
|
||||
|
||||
if c.IUPACName != "" {
|
||||
fmt.Fprintf(b, "*IUPAC Name:* %s\n\n", c.IUPACName)
|
||||
}
|
||||
|
||||
if c.MolecularFormula != "" {
|
||||
fmt.Fprintf(b, "*Formula:* `%s`\n", c.MolecularFormula)
|
||||
}
|
||||
if c.MolecularWeight > 0 {
|
||||
fmt.Fprintf(b, "*Molecular Weight:* %.2f g/mol\n", c.MolecularWeight)
|
||||
}
|
||||
if c.ExactMass > 0 {
|
||||
fmt.Fprintf(b, "*Exact Mass:* %.6f\n", c.ExactMass)
|
||||
}
|
||||
|
||||
fmt.Fprintf(b, "\n🔗 [PubChem](%s)",
|
||||
fmt.Sprintf("https://pubchem.ncbi.nlm.nih.gov/compound/%d", c.CID))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatADMEInfo(c *CompoundData) string {
|
||||
b := &strings.Builder{}
|
||||
fmt.Fprintf(b, "🧪 *%s*\n", c.Name)
|
||||
fmt.Fprintf(b, "💊 *ADME Properties*\n\n")
|
||||
|
||||
if c.XLogP != 0 {
|
||||
fmt.Fprintf(b, "*XLogP:* %.2f\n", c.XLogP)
|
||||
fmt.Fprintf(b, "├ Lipophilicity indicator\n")
|
||||
}
|
||||
if c.TPSA > 0 {
|
||||
fmt.Fprintf(b, "*TPSA:* %.1f Ų\n", c.TPSA)
|
||||
fmt.Fprintf(b, "├ Topological Polar Surface Area\n")
|
||||
}
|
||||
if c.HBondDonors >= 0 {
|
||||
fmt.Fprintf(b, "*H-bond Donors:* %d\n", c.HBondDonors)
|
||||
}
|
||||
if c.HBondAcceptors >= 0 {
|
||||
fmt.Fprintf(b, "*H-bond Acceptors:* %d\n", c.HBondAcceptors)
|
||||
}
|
||||
if c.RotatableBonds >= 0 {
|
||||
fmt.Fprintf(b, "*Rotatable Bonds:* %d\n", c.RotatableBonds)
|
||||
fmt.Fprintf(b, "├ Flexibility indicator\n")
|
||||
}
|
||||
|
||||
// Lipinski's Rule of Five analysis
|
||||
fmt.Fprintf(b, "\n📊 *Lipinski's Rule of Five:*\n")
|
||||
violations := 0
|
||||
if c.MolecularWeight > 500 {
|
||||
violations++
|
||||
fmt.Fprintf(b, "❌ MW > 500\n")
|
||||
} else {
|
||||
fmt.Fprintf(b, "✅ MW ≤ 500\n")
|
||||
}
|
||||
if c.XLogP > 5 {
|
||||
violations++
|
||||
fmt.Fprintf(b, "❌ XLogP > 5\n")
|
||||
} else {
|
||||
fmt.Fprintf(b, "✅ XLogP ≤ 5\n")
|
||||
}
|
||||
if c.HBondDonors > 5 {
|
||||
violations++
|
||||
fmt.Fprintf(b, "❌ HBD > 5\n")
|
||||
} else {
|
||||
fmt.Fprintf(b, "✅ HBD ≤ 5\n")
|
||||
}
|
||||
if c.HBondAcceptors > 10 {
|
||||
violations++
|
||||
fmt.Fprintf(b, "❌ HBA > 10\n")
|
||||
} else {
|
||||
fmt.Fprintf(b, "✅ HBA ≤ 10\n")
|
||||
}
|
||||
|
||||
if violations == 0 {
|
||||
fmt.Fprintf(b, "\n🎯 *Drug-like* (0 violations)")
|
||||
} else {
|
||||
fmt.Fprintf(b, "\n⚠️ *%d violation(s)*", violations)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatStructureInfo(c *CompoundData) string {
|
||||
b := &strings.Builder{}
|
||||
fmt.Fprintf(b, "🧪 *%s*\n", c.Name)
|
||||
fmt.Fprintf(b, "🏗️ *Structural Properties*\n\n")
|
||||
|
||||
if c.Complexity > 0 {
|
||||
fmt.Fprintf(b, "*Complexity:* %.0f\n", c.Complexity)
|
||||
fmt.Fprintf(b, "├ Structural complexity score\n")
|
||||
}
|
||||
if c.HeavyAtomCount > 0 {
|
||||
fmt.Fprintf(b, "*Heavy Atoms:* %d\n", c.HeavyAtomCount)
|
||||
}
|
||||
if c.TotalAtomCount > 0 {
|
||||
fmt.Fprintf(b, "*Total Atoms:* %d\n", c.TotalAtomCount)
|
||||
}
|
||||
if c.BondCount > 0 {
|
||||
fmt.Fprintf(b, "*Bonds:* %d\n", c.BondCount)
|
||||
}
|
||||
if c.RotatableBonds >= 0 {
|
||||
fmt.Fprintf(b, "*Rotatable Bonds:* %d\n", c.RotatableBonds)
|
||||
}
|
||||
|
||||
// Structural complexity assessment
|
||||
if c.Complexity > 0 {
|
||||
fmt.Fprintf(b, "\n📈 *Complexity Assessment:*\n")
|
||||
if c.Complexity < 100 {
|
||||
fmt.Fprintf(b, "🟢 Simple structure")
|
||||
} else if c.Complexity < 300 {
|
||||
fmt.Fprintf(b, "🟡 Moderate complexity")
|
||||
} else if c.Complexity < 500 {
|
||||
fmt.Fprintf(b, "🟠 Complex structure")
|
||||
} else {
|
||||
fmt.Fprintf(b, "🔴 Highly complex")
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatIdentifiersInfo(c *CompoundData) string {
|
||||
b := &strings.Builder{}
|
||||
fmt.Fprintf(b, "🧪 *%s*\n", c.Name)
|
||||
fmt.Fprintf(b, "🏷️ *Chemical Identifiers*\n\n")
|
||||
|
||||
fmt.Fprintf(b, "*CID:* `%d`\n", c.CID)
|
||||
|
||||
if c.InChIKey != "" {
|
||||
fmt.Fprintf(b, "*InChIKey:*\n`%s`\n\n", c.InChIKey)
|
||||
}
|
||||
|
||||
if c.CanonicalSMILES != "" {
|
||||
fmt.Fprintf(b, "*SMILES:*\n`%s`\n\n", c.CanonicalSMILES)
|
||||
}
|
||||
|
||||
if c.InChI != "" {
|
||||
// Truncate InChI if too long
|
||||
inchi := c.InChI
|
||||
if len(inchi) > 200 {
|
||||
inchi = inchi[:197] + "..."
|
||||
}
|
||||
fmt.Fprintf(b, "*InChI:*\n`%s`", inchi)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func createNavigationKeyboard(cid int, currentCategory PropertyCategory) tgbotapi.InlineKeyboardMarkup {
|
||||
var buttons [][]tgbotapi.InlineKeyboardButton
|
||||
|
||||
// Category buttons
|
||||
categoryRow := []tgbotapi.InlineKeyboardButton{
|
||||
tgbotapi.NewInlineKeyboardButtonData(getButtonText("📋", currentCategory == CategoryBasic), fmt.Sprintf("medchem:%d:basic", cid)),
|
||||
tgbotapi.NewInlineKeyboardButtonData(getButtonText("💊", currentCategory == CategoryADME), fmt.Sprintf("medchem:%d:adme", cid)),
|
||||
}
|
||||
buttons = append(buttons, categoryRow)
|
||||
|
||||
categoryRow2 := []tgbotapi.InlineKeyboardButton{
|
||||
tgbotapi.NewInlineKeyboardButtonData(getButtonText("🏗️", currentCategory == CategoryStructure), fmt.Sprintf("medchem:%d:structure", cid)),
|
||||
tgbotapi.NewInlineKeyboardButtonData(getButtonText("🏷️", currentCategory == CategoryIdentifiers), fmt.Sprintf("medchem:%d:identifiers", cid)),
|
||||
}
|
||||
buttons = append(buttons, categoryRow2)
|
||||
|
||||
return tgbotapi.NewInlineKeyboardMarkup(buttons...)
|
||||
}
|
||||
|
||||
func getButtonText(emoji string, isActive bool) string {
|
||||
if isActive {
|
||||
return emoji + " ●"
|
||||
}
|
||||
return emoji
|
||||
}
|
||||
|
||||
func handleError(bot *tgbotapi.BotAPI, chatID int64, err error, query string) {
|
||||
var message string
|
||||
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
// Try to get suggestions
|
||||
suggestions := getSuggestions(query)
|
||||
if len(suggestions) > 0 {
|
||||
message = fmt.Sprintf("❌ Compound '%s' not found.\n\n💡 *Did you mean:*\n%s",
|
||||
query, strings.Join(suggestions, "\n"))
|
||||
} else {
|
||||
message = fmt.Sprintf("❌ Compound '%s' not found.\n\n💡 *Try:*\n• Check spelling\n• Use common name or IUPAC name\n• Try synonyms (e.g., 'aspirin' instead of 'acetylsalicylic acid')", query)
|
||||
}
|
||||
} else if strings.Contains(err.Error(), "network") {
|
||||
message = "🌐 Network error. Please try again later."
|
||||
} else {
|
||||
message = "⚠️ An error occurred while fetching compound data. Please try again."
|
||||
}
|
||||
|
||||
msg := tgbotapi.NewMessage(chatID, message)
|
||||
msg.ParseMode = "Markdown"
|
||||
bot.Send(msg)
|
||||
}
|
||||
|
||||
func getSuggestions(query string) []string {
|
||||
url := fmt.Sprintf("https://pubchem.ncbi.nlm.nih.gov/rest/autocomplete/compound/%s/json?limit=3",
|
||||
strings.ReplaceAll(query, " ", "%20"))
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Dictionary struct {
|
||||
Terms []string `json:"terms"`
|
||||
} `json:"dictionary"`
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var suggestions []string
|
||||
for _, term := range data.Dictionary.Terms {
|
||||
suggestions = append(suggestions, "• "+term)
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
// HandleCallback handles inline keyboard button presses for medchem
|
||||
func (m MedchemCommand) HandleCallback(update tgbotapi.Update, bot *tgbotapi.BotAPI, params []string) {
|
||||
if len(params) < 2 {
|
||||
log.Printf("Invalid medchem callback params: %v", params)
|
||||
return
|
||||
}
|
||||
|
||||
cidStr := params[0]
|
||||
categoryStr := params[1]
|
||||
|
||||
cid, err := strconv.Atoi(cidStr)
|
||||
if err != nil {
|
||||
log.Printf("Invalid CID in callback: %s", cidStr)
|
||||
return
|
||||
}
|
||||
|
||||
var category PropertyCategory
|
||||
switch categoryStr {
|
||||
case "basic":
|
||||
category = CategoryBasic
|
||||
case "adme":
|
||||
category = CategoryADME
|
||||
case "structure":
|
||||
category = CategoryStructure
|
||||
case "identifiers":
|
||||
category = CategoryIdentifiers
|
||||
default:
|
||||
log.Printf("Invalid category in callback: %s", categoryStr)
|
||||
return
|
||||
}
|
||||
|
||||
// Get compound data by CID
|
||||
compound, err := fetchCompoundDataByCID(cid)
|
||||
if err != nil {
|
||||
log.Printf("Error fetching compound data for CID %d: %v", cid, err)
|
||||
// Send error message
|
||||
callback := tgbotapi.NewCallback(update.CallbackQuery.ID, "Error loading compound data")
|
||||
bot.Request(callback)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit the message caption and keyboard
|
||||
caption := formatCompoundCaption(compound, category)
|
||||
keyboard := createNavigationKeyboard(compound.CID, category)
|
||||
|
||||
editCaption := tgbotapi.NewEditMessageCaption(
|
||||
update.CallbackQuery.Message.Chat.ID,
|
||||
update.CallbackQuery.Message.MessageID,
|
||||
caption,
|
||||
)
|
||||
editCaption.ParseMode = "Markdown"
|
||||
editCaption.ReplyMarkup = &keyboard
|
||||
|
||||
if _, err := bot.Send(editCaption); err != nil {
|
||||
log.Printf("Error editing message caption: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// fetchCompoundDataByCID fetches compound data directly by CID
|
||||
func fetchCompoundDataByCID(cid int) (*CompoundData, error) {
|
||||
url := fmt.Sprintf("https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/%d/record/JSON", cid)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("network error: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("PubChem API error: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %v", err)
|
||||
}
|
||||
|
||||
var pubchemResp PubChemResponse
|
||||
if err := json.Unmarshal(body, &pubchemResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %v", err)
|
||||
}
|
||||
|
||||
if len(pubchemResp.PCCompounds) == 0 {
|
||||
return nil, fmt.Errorf("no compound data found")
|
||||
}
|
||||
|
||||
compound := parsePubChemData(&pubchemResp.PCCompounds[0], "")
|
||||
|
||||
// Fetch common names/synonyms
|
||||
commonNames, err := fetchCommonNames(cid)
|
||||
if err == nil && len(commonNames) > 0 {
|
||||
compound.CommonNames = commonNames
|
||||
// Use most common name for display
|
||||
compound.Name = fmt.Sprintf("%s (CID %d)", commonNames[0], cid)
|
||||
} else if compound.IUPACName != "" {
|
||||
// Fallback to IUPAC name, truncate if too long
|
||||
name := compound.IUPACName
|
||||
if len(name) > 50 {
|
||||
name = name[:47] + "..."
|
||||
}
|
||||
compound.Name = fmt.Sprintf("%s (CID %d)", name, cid)
|
||||
} else {
|
||||
compound.Name = fmt.Sprintf("CID %d", cid)
|
||||
}
|
||||
|
||||
return compound, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(MedchemCommand{})
|
||||
}
|
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, maybe try the superior /medchem command")
|
||||
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 + "\nIf you want more info, try the /medchem command instead"
|
||||
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{})
|
||||
}
|
549
commands/osrs.go
Normal file
549
commands/osrs.go
Normal file
@ -0,0 +1,549 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type OSRSCommand struct{}
|
||||
|
||||
func (o OSRSCommand) Name() string {
|
||||
return "osrs"
|
||||
}
|
||||
|
||||
func (o OSRSCommand) Help() string {
|
||||
return "Get Old School RuneScape player stats with interactive navigation. Usage: /osrs <username>"
|
||||
}
|
||||
|
||||
// OSRS JSON API structures
|
||||
type OSRSResponse struct {
|
||||
Skills []OSRSSkill `json:"skills"`
|
||||
Activities []OSRSActivity `json:"activities"`
|
||||
}
|
||||
|
||||
type OSRSSkill struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Rank int `json:"rank"`
|
||||
Level int `json:"level"`
|
||||
XP int64 `json:"xp"`
|
||||
}
|
||||
|
||||
type OSRSActivity struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Rank int `json:"rank"`
|
||||
Score int64 `json:"score"`
|
||||
}
|
||||
|
||||
type OSRSStats struct {
|
||||
Username string
|
||||
AccountType string
|
||||
CombatLevel int
|
||||
TotalLevel int
|
||||
TotalXP int64
|
||||
Skills map[string]OSRSSkill
|
||||
Activities map[string]OSRSActivity
|
||||
}
|
||||
|
||||
type StatsCategory int
|
||||
|
||||
const (
|
||||
CategoryOverview StatsCategory = iota
|
||||
CategoryCombat
|
||||
CategorySkilling
|
||||
CategoryActivities
|
||||
)
|
||||
|
||||
func (o OSRSCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
args := strings.TrimSpace(update.Message.CommandArguments())
|
||||
if args == "" {
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "⚔️ *OSRS Stats Command*\n\nUsage: `/osrs <username>`\n\nExample: `/osrs Zezima`\n\nThis command shows Old School RuneScape player statistics with interactive navigation through different skill categories.")
|
||||
msg.ParseMode = "Markdown"
|
||||
bot.Send(msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Send "typing" action
|
||||
typingAction := tgbotapi.NewChatAction(update.Message.Chat.ID, tgbotapi.ChatTyping)
|
||||
bot.Send(typingAction)
|
||||
|
||||
stats, err := fetchOSRSStats(args, "normal")
|
||||
if err != nil {
|
||||
handleOSRSError(bot, update.Message.Chat.ID, err, args)
|
||||
return
|
||||
}
|
||||
|
||||
sendOSRSStats(bot, update.Message.Chat.ID, stats, CategoryOverview)
|
||||
}
|
||||
|
||||
func fetchOSRSStats(username, accountType string) (*OSRSStats, error) {
|
||||
var url string
|
||||
switch accountType {
|
||||
case "ironman":
|
||||
url = fmt.Sprintf("https://secure.runescape.com/m=hiscore_oldschool_ironman/index_lite.json?player=%s", username)
|
||||
case "hardcore":
|
||||
url = fmt.Sprintf("https://secure.runescape.com/m=hiscore_oldschool_hardcore_ironman/index_lite.json?player=%s", username)
|
||||
case "ultimate":
|
||||
url = fmt.Sprintf("https://secure.runescape.com/m=hiscore_oldschool_ultimate/index_lite.json?player=%s", username)
|
||||
default: // normal
|
||||
url = fmt.Sprintf("https://secure.runescape.com/m=hiscore_oldschool/index_lite.json?player=%s", username)
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("network error: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, fmt.Errorf("player '%s' not found", username)
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("OSRS API error: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %v", err)
|
||||
}
|
||||
|
||||
var osrsResp OSRSResponse
|
||||
if err := json.Unmarshal(body, &osrsResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JSON response: %v", err)
|
||||
}
|
||||
|
||||
return parseOSRSStats(&osrsResp, username, accountType)
|
||||
}
|
||||
|
||||
func parseOSRSStats(resp *OSRSResponse, username, accountType string) (*OSRSStats, error) {
|
||||
stats := &OSRSStats{
|
||||
Username: username,
|
||||
AccountType: accountType,
|
||||
Skills: make(map[string]OSRSSkill),
|
||||
Activities: make(map[string]OSRSActivity),
|
||||
}
|
||||
|
||||
// Parse skills
|
||||
for _, skill := range resp.Skills {
|
||||
stats.Skills[skill.Name] = skill
|
||||
|
||||
if skill.Name == "Overall" {
|
||||
stats.TotalLevel = skill.Level
|
||||
stats.TotalXP = skill.XP
|
||||
}
|
||||
}
|
||||
|
||||
// Parse activities
|
||||
for _, activity := range resp.Activities {
|
||||
stats.Activities[activity.Name] = activity
|
||||
}
|
||||
|
||||
// Calculate combat level
|
||||
stats.CombatLevel = calculateCombatLevel(stats.Skills)
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func calculateCombatLevel(skills map[string]OSRSSkill) int {
|
||||
att := float64(getSkillLevel(skills, "Attack"))
|
||||
def := float64(getSkillLevel(skills, "Defence"))
|
||||
str := float64(getSkillLevel(skills, "Strength"))
|
||||
hp := float64(getSkillLevel(skills, "Hitpoints"))
|
||||
pray := float64(getSkillLevel(skills, "Prayer"))
|
||||
rang := float64(getSkillLevel(skills, "Ranged"))
|
||||
mage := float64(getSkillLevel(skills, "Magic"))
|
||||
|
||||
base := 0.25 * (def + hp + (pray / 2))
|
||||
melee := 0.325 * (att + str)
|
||||
ranged := 0.325 * (rang + (rang / 2))
|
||||
magic := 0.325 * (mage + (mage / 2))
|
||||
|
||||
var max float64
|
||||
if melee >= ranged && melee >= magic {
|
||||
max = melee
|
||||
} else if ranged >= magic {
|
||||
max = ranged
|
||||
} else {
|
||||
max = magic
|
||||
}
|
||||
|
||||
return int(base + max)
|
||||
}
|
||||
|
||||
func getSkillLevel(skills map[string]OSRSSkill, skillName string) int {
|
||||
if skill, exists := skills[skillName]; exists {
|
||||
return skill.Level
|
||||
}
|
||||
return 1 // Default level
|
||||
}
|
||||
|
||||
func sendOSRSStats(bot *tgbotapi.BotAPI, chatID int64, stats *OSRSStats, category StatsCategory) {
|
||||
caption := formatOSRSCaption(stats, category)
|
||||
keyboard := createOSRSKeyboard(stats.Username, stats.AccountType, category)
|
||||
|
||||
// Use OSRS logo
|
||||
imageURL := "https://oldschool.runescape.wiki/images/thumb/b/b2/Old_School_RuneScape_logo.png/300px-Old_School_RuneScape_logo.png"
|
||||
|
||||
photo := tgbotapi.NewPhoto(chatID, tgbotapi.FileURL(imageURL))
|
||||
photo.Caption = caption
|
||||
photo.ParseMode = "Markdown"
|
||||
photo.ReplyMarkup = keyboard
|
||||
bot.Send(photo)
|
||||
}
|
||||
|
||||
func formatOSRSCaption(stats *OSRSStats, category StatsCategory) string {
|
||||
switch category {
|
||||
case CategoryOverview:
|
||||
return formatOverview(stats)
|
||||
case CategoryCombat:
|
||||
return formatCombatStats(stats)
|
||||
case CategorySkilling:
|
||||
return formatSkillingStats(stats)
|
||||
case CategoryActivities:
|
||||
return formatActivities(stats)
|
||||
default:
|
||||
return formatOverview(stats)
|
||||
}
|
||||
}
|
||||
|
||||
func formatOverview(stats *OSRSStats) string {
|
||||
b := &strings.Builder{}
|
||||
fmt.Fprintf(b, "⚔️ *%s*", stats.Username)
|
||||
|
||||
if stats.AccountType != "normal" {
|
||||
accountTypeEmoji := map[string]string{
|
||||
"ironman": "⚫",
|
||||
"hardcore": "🔴",
|
||||
"ultimate": "⚪",
|
||||
}
|
||||
emoji := accountTypeEmoji[stats.AccountType]
|
||||
fmt.Fprintf(b, " %s %s", emoji, strings.Title(stats.AccountType))
|
||||
}
|
||||
fmt.Fprintf(b, "\n📊 *Overview*\n\n")
|
||||
|
||||
fmt.Fprintf(b, "*Combat Level:* %d\n", stats.CombatLevel)
|
||||
fmt.Fprintf(b, "*Total Level:* %s\n", formatNumber(stats.TotalLevel))
|
||||
fmt.Fprintf(b, "*Total XP:* %s\n", formatNumber(int(stats.TotalXP)))
|
||||
|
||||
// Show overall rank if available
|
||||
if overallSkill, exists := stats.Skills["Overall"]; exists && overallSkill.Rank > 0 {
|
||||
fmt.Fprintf(b, "*Overall Rank:* %d\n", overallSkill.Rank)
|
||||
}
|
||||
fmt.Fprintf(b, "\n")
|
||||
|
||||
// Show top 5 skills by XP
|
||||
var skillList []OSRSSkill
|
||||
for name, skill := range stats.Skills {
|
||||
if name != "Overall" && skill.Level > 1 {
|
||||
skillList = append(skillList, skill)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by XP (bubble sort for simplicity)
|
||||
for i := 0; i < len(skillList)-1; i++ {
|
||||
for j := i + 1; j < len(skillList); j++ {
|
||||
if skillList[j].XP > skillList[i].XP {
|
||||
skillList[i], skillList[j] = skillList[j], skillList[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(b, "*Top Skills:*\n")
|
||||
maxSkills := 5
|
||||
if len(skillList) < 5 {
|
||||
maxSkills = len(skillList)
|
||||
}
|
||||
for i := 0; i < maxSkills; i++ {
|
||||
skill := skillList[i]
|
||||
fmt.Fprintf(b, "%d. *%s* - Level %d (%s XP)",
|
||||
i+1, skill.Name, skill.Level, formatNumber(int(skill.XP)))
|
||||
if skill.Rank > 0 {
|
||||
fmt.Fprintf(b, " - Rank: %d", skill.Rank)
|
||||
}
|
||||
fmt.Fprintf(b, "\n")
|
||||
}
|
||||
|
||||
fmt.Fprintf(b, "\n🔗 [OSRS Hiscores](https://secure.runescape.com/m=hiscore_oldschool/hiscorepersonal?user1=%s)", stats.Username)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatCombatStats(stats *OSRSStats) string {
|
||||
b := &strings.Builder{}
|
||||
fmt.Fprintf(b, "⚔️ *%s*", stats.Username)
|
||||
if stats.AccountType != "normal" {
|
||||
accountTypeEmoji := map[string]string{
|
||||
"ironman": "⚫",
|
||||
"hardcore": "🔴",
|
||||
"ultimate": "⚪",
|
||||
}
|
||||
emoji := accountTypeEmoji[stats.AccountType]
|
||||
fmt.Fprintf(b, " %s %s", emoji, strings.Title(stats.AccountType))
|
||||
}
|
||||
fmt.Fprintf(b, "\n⚔️ *Combat Stats*\n\n")
|
||||
|
||||
fmt.Fprintf(b, "*Combat Level:* %d\n\n", stats.CombatLevel)
|
||||
|
||||
combatSkillNames := []string{"Attack", "Strength", "Defence", "Hitpoints", "Ranged", "Prayer", "Magic"}
|
||||
|
||||
// Collect combat skills that exist
|
||||
var combatSkills []OSRSSkill
|
||||
for _, skillName := range combatSkillNames {
|
||||
if skill, exists := stats.Skills[skillName]; exists {
|
||||
combatSkills = append(combatSkills, skill)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by XP descending
|
||||
for i := 0; i < len(combatSkills)-1; i++ {
|
||||
for j := i + 1; j < len(combatSkills); j++ {
|
||||
if combatSkills[j].XP > combatSkills[i].XP {
|
||||
combatSkills[i], combatSkills[j] = combatSkills[j], combatSkills[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display combat stats sorted by XP
|
||||
for _, skill := range combatSkills {
|
||||
fmt.Fprintf(b, "*%s:* Level %d\n", skill.Name, skill.Level)
|
||||
fmt.Fprintf(b, "└ XP: %s", formatNumber(int(skill.XP)))
|
||||
if skill.Rank > 0 {
|
||||
fmt.Fprintf(b, " (Rank: %d)", skill.Rank)
|
||||
}
|
||||
fmt.Fprintf(b, "\n")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatSkillingStats(stats *OSRSStats) string {
|
||||
b := &strings.Builder{}
|
||||
fmt.Fprintf(b, "⚔️ *%s*", stats.Username)
|
||||
if stats.AccountType != "normal" {
|
||||
accountTypeEmoji := map[string]string{
|
||||
"ironman": "⚫",
|
||||
"hardcore": "🔴",
|
||||
"ultimate": "⚪",
|
||||
}
|
||||
emoji := accountTypeEmoji[stats.AccountType]
|
||||
fmt.Fprintf(b, " %s %s", emoji, strings.Title(stats.AccountType))
|
||||
}
|
||||
fmt.Fprintf(b, "\n🔨 *Skilling Stats*\n\n")
|
||||
|
||||
skillingSkillNames := []string{
|
||||
"Woodcutting", "Fishing", "Mining", "Cooking", "Firemaking",
|
||||
"Crafting", "Smithing", "Fletching", "Herblore", "Agility",
|
||||
"Thieving", "Slayer", "Farming", "Runecraft", "Hunter", "Construction",
|
||||
}
|
||||
|
||||
// Collect skilling skills that exist and have level > 1
|
||||
var skillingSkills []OSRSSkill
|
||||
for _, skillName := range skillingSkillNames {
|
||||
if skill, exists := stats.Skills[skillName]; exists && skill.Level > 1 {
|
||||
skillingSkills = append(skillingSkills, skill)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by XP descending
|
||||
for i := 0; i < len(skillingSkills)-1; i++ {
|
||||
for j := i + 1; j < len(skillingSkills); j++ {
|
||||
if skillingSkills[j].XP > skillingSkills[i].XP {
|
||||
skillingSkills[i], skillingSkills[j] = skillingSkills[j], skillingSkills[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display in the same format as combat stats
|
||||
for _, skill := range skillingSkills {
|
||||
fmt.Fprintf(b, "*%s:* Level %d\n", skill.Name, skill.Level)
|
||||
fmt.Fprintf(b, "└ XP: %s", formatNumber(int(skill.XP)))
|
||||
if skill.Rank > 0 {
|
||||
fmt.Fprintf(b, " (Rank: %d)", skill.Rank)
|
||||
}
|
||||
fmt.Fprintf(b, "\n")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatActivities(stats *OSRSStats) string {
|
||||
b := &strings.Builder{}
|
||||
fmt.Fprintf(b, "⚔️ *%s*", stats.Username)
|
||||
if stats.AccountType != "normal" {
|
||||
accountTypeEmoji := map[string]string{
|
||||
"ironman": "⚫",
|
||||
"hardcore": "🔴",
|
||||
"ultimate": "⚪",
|
||||
}
|
||||
emoji := accountTypeEmoji[stats.AccountType]
|
||||
fmt.Fprintf(b, " %s %s", emoji, strings.Title(stats.AccountType))
|
||||
}
|
||||
fmt.Fprintf(b, "\n🏆 *Activities & Bosses*\n\n")
|
||||
|
||||
// Collect all activities with scores > 0
|
||||
var activeActivities []OSRSActivity
|
||||
for _, activity := range stats.Activities {
|
||||
if activity.Score > 0 {
|
||||
activeActivities = append(activeActivities, activity)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score descending (highest to lowest)
|
||||
for i := 0; i < len(activeActivities)-1; i++ {
|
||||
for j := i + 1; j < len(activeActivities); j++ {
|
||||
if activeActivities[j].Score > activeActivities[i].Score {
|
||||
activeActivities[i], activeActivities[j] = activeActivities[j], activeActivities[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display all activities with non-zero scores
|
||||
if len(activeActivities) > 0 {
|
||||
for _, activity := range activeActivities {
|
||||
fmt.Fprintf(b, "*%s:* %s", activity.Name, formatNumber(int(activity.Score)))
|
||||
if activity.Rank > 0 {
|
||||
fmt.Fprintf(b, " (Rank: %d)", activity.Rank)
|
||||
}
|
||||
fmt.Fprintf(b, "\n")
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(b, "No activities found.\nStart bossing to see stats here! 💪")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatNumber(n int) string {
|
||||
if n >= 1000000 {
|
||||
return fmt.Sprintf("%.2fM", float64(n)/1000000)
|
||||
} else if n >= 1000 {
|
||||
return fmt.Sprintf("%.1fK", float64(n)/1000)
|
||||
}
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
|
||||
func createOSRSKeyboard(username, accountType string, currentCategory StatsCategory) tgbotapi.InlineKeyboardMarkup {
|
||||
var buttons [][]tgbotapi.InlineKeyboardButton
|
||||
|
||||
// Category buttons
|
||||
categoryRow1 := []tgbotapi.InlineKeyboardButton{
|
||||
tgbotapi.NewInlineKeyboardButtonData(getOSRSButtonText("📊", currentCategory == CategoryOverview), fmt.Sprintf("osrs:%s:%s:overview", username, accountType)),
|
||||
tgbotapi.NewInlineKeyboardButtonData(getOSRSButtonText("⚔️", currentCategory == CategoryCombat), fmt.Sprintf("osrs:%s:%s:combat", username, accountType)),
|
||||
}
|
||||
buttons = append(buttons, categoryRow1)
|
||||
|
||||
categoryRow2 := []tgbotapi.InlineKeyboardButton{
|
||||
tgbotapi.NewInlineKeyboardButtonData(getOSRSButtonText("🔨", currentCategory == CategorySkilling), fmt.Sprintf("osrs:%s:%s:skilling", username, accountType)),
|
||||
tgbotapi.NewInlineKeyboardButtonData(getOSRSButtonText("🏆", currentCategory == CategoryActivities), fmt.Sprintf("osrs:%s:%s:activities", username, accountType)),
|
||||
}
|
||||
buttons = append(buttons, categoryRow2)
|
||||
|
||||
// Account type buttons
|
||||
if accountType == "normal" {
|
||||
accountRow := []tgbotapi.InlineKeyboardButton{
|
||||
tgbotapi.NewInlineKeyboardButtonData("⚫ Ironman", fmt.Sprintf("osrs:%s:ironman:overview", username)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("🔴 Hardcore", fmt.Sprintf("osrs:%s:hardcore:overview", username)),
|
||||
}
|
||||
buttons = append(buttons, accountRow)
|
||||
|
||||
ultimateRow := []tgbotapi.InlineKeyboardButton{
|
||||
tgbotapi.NewInlineKeyboardButtonData("⚪ Ultimate", fmt.Sprintf("osrs:%s:ultimate:overview", username)),
|
||||
}
|
||||
buttons = append(buttons, ultimateRow)
|
||||
} else {
|
||||
// Return to normal account button
|
||||
normalRow := []tgbotapi.InlineKeyboardButton{
|
||||
tgbotapi.NewInlineKeyboardButtonData("🔙 Normal Account", fmt.Sprintf("osrs:%s:normal:overview", username)),
|
||||
}
|
||||
buttons = append(buttons, normalRow)
|
||||
}
|
||||
|
||||
return tgbotapi.NewInlineKeyboardMarkup(buttons...)
|
||||
}
|
||||
|
||||
func getOSRSButtonText(emoji string, isActive bool) string {
|
||||
if isActive {
|
||||
return emoji + " ●"
|
||||
}
|
||||
return emoji
|
||||
}
|
||||
|
||||
func handleOSRSError(bot *tgbotapi.BotAPI, chatID int64, err error, username string) {
|
||||
var message string
|
||||
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
message = fmt.Sprintf("❌ Player '%s' not found.\n\n💡 *Tips:*\n• Check spelling\n• Username is case-sensitive\n• Player must have logged in recently\n• Try different account type (ironman, etc.)", username)
|
||||
} else if strings.Contains(err.Error(), "network") {
|
||||
message = "🌐 Network error. OSRS servers might be down. Try again later."
|
||||
} else {
|
||||
message = "⚠️ An error occurred while fetching player data. Please try again."
|
||||
}
|
||||
|
||||
msg := tgbotapi.NewMessage(chatID, message)
|
||||
msg.ParseMode = "Markdown"
|
||||
bot.Send(msg)
|
||||
}
|
||||
|
||||
// HandleCallback handles inline keyboard button presses for OSRS
|
||||
func (o OSRSCommand) HandleCallback(update tgbotapi.Update, bot *tgbotapi.BotAPI, params []string) {
|
||||
if len(params) < 3 {
|
||||
log.Printf("Invalid OSRS callback params: %v", params)
|
||||
return
|
||||
}
|
||||
|
||||
username := params[0]
|
||||
accountType := params[1]
|
||||
categoryStr := params[2]
|
||||
|
||||
var category StatsCategory
|
||||
switch categoryStr {
|
||||
case "overview":
|
||||
category = CategoryOverview
|
||||
case "combat":
|
||||
category = CategoryCombat
|
||||
case "skilling":
|
||||
category = CategorySkilling
|
||||
case "activities":
|
||||
category = CategoryActivities
|
||||
default:
|
||||
log.Printf("Invalid OSRS category in callback: %s", categoryStr)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch stats
|
||||
stats, err := fetchOSRSStats(username, accountType)
|
||||
if err != nil {
|
||||
log.Printf("Error fetching OSRS stats for %s: %v", username, err)
|
||||
callback := tgbotapi.NewCallback(update.CallbackQuery.ID, "Error loading player data")
|
||||
bot.Request(callback)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit the message caption and keyboard
|
||||
caption := formatOSRSCaption(stats, category)
|
||||
keyboard := createOSRSKeyboard(stats.Username, stats.AccountType, category)
|
||||
|
||||
editCaption := tgbotapi.NewEditMessageCaption(
|
||||
update.CallbackQuery.Message.Chat.ID,
|
||||
update.CallbackQuery.Message.MessageID,
|
||||
caption,
|
||||
)
|
||||
editCaption.ParseMode = "Markdown"
|
||||
editCaption.ReplyMarkup = &keyboard
|
||||
|
||||
if _, err := bot.Send(editCaption); err != nil {
|
||||
log.Printf("Error editing OSRS message caption: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(OSRSCommand{})
|
||||
}
|
81
commands/troll.go
Normal file
81
commands/troll.go
Normal file
@ -0,0 +1,81 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type TrollCommand struct{}
|
||||
|
||||
func (t TrollCommand) Name() string {
|
||||
return "troll"
|
||||
}
|
||||
|
||||
func (t TrollCommand) Help() string {
|
||||
return "Get a random troll message from the database. Usage: /troll"
|
||||
}
|
||||
|
||||
func (t TrollCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
// Send "typing" action
|
||||
typingAction := tgbotapi.NewChatAction(update.Message.Chat.ID, tgbotapi.ChatTyping)
|
||||
bot.Send(typingAction)
|
||||
|
||||
trollMessage, err := getRandomTrollMessage()
|
||||
if err != nil {
|
||||
log.Printf("Error getting troll message: %v", err)
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "❌ Error loading troll database. Please try again later.")
|
||||
bot.Send(msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Limit message length for Telegram (max 4096 characters)
|
||||
if len(trollMessage) > 4000 {
|
||||
trollMessage = trollMessage[:3997] + "..."
|
||||
}
|
||||
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, trollMessage)
|
||||
bot.Send(msg)
|
||||
}
|
||||
|
||||
func getRandomTrollMessage() (string, error) {
|
||||
// Open the troll database file
|
||||
file, err := os.Open("json/trolldb.json")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open trolldb.json: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read the file content
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read trolldb.json: %v", err)
|
||||
}
|
||||
|
||||
// Parse JSON array
|
||||
var trollMessages []string
|
||||
if err := json.Unmarshal(data, &trollMessages); err != nil {
|
||||
return "", fmt.Errorf("failed to parse trolldb.json: %v", err)
|
||||
}
|
||||
|
||||
if len(trollMessages) == 0 {
|
||||
return "", fmt.Errorf("troll database is empty")
|
||||
}
|
||||
|
||||
// Seed random number generator
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// Pick a random message
|
||||
randomIndex := rand.Intn(len(trollMessages))
|
||||
return trollMessages[randomIndex], nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(TrollCommand{})
|
||||
}
|
133
commands/xkcd.go
Normal file
133
commands/xkcd.go
Normal file
@ -0,0 +1,133 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type XkcdCommand struct{}
|
||||
|
||||
type XkcdComic struct {
|
||||
Num int `json:"num"`
|
||||
Title string `json:"title"`
|
||||
SafeTitle string `json:"safe_title"`
|
||||
Img string `json:"img"`
|
||||
Alt string `json:"alt"`
|
||||
Transcript string `json:"transcript"`
|
||||
Link string `json:"link"`
|
||||
News string `json:"news"`
|
||||
Year string `json:"year"`
|
||||
Month string `json:"month"`
|
||||
Day string `json:"day"`
|
||||
}
|
||||
|
||||
func (x XkcdCommand) Name() string {
|
||||
return "xkcd"
|
||||
}
|
||||
|
||||
func (x XkcdCommand) Help() string {
|
||||
return "Get a random XKCD comic. Usage: /xkcd"
|
||||
}
|
||||
|
||||
func (x XkcdCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
// Send "typing" action
|
||||
typingAction := tgbotapi.NewChatAction(update.Message.Chat.ID, tgbotapi.ChatTyping)
|
||||
bot.Send(typingAction)
|
||||
|
||||
comic, err := getRandomXkcdComic()
|
||||
if err != nil {
|
||||
log.Printf("Error getting XKCD comic: %v", err)
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "❌ Error fetching XKCD comic. Please try again later.")
|
||||
bot.Send(msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Create caption
|
||||
caption := createXkcdCaption(comic)
|
||||
|
||||
// Send photo with caption
|
||||
photo := tgbotapi.NewPhoto(update.Message.Chat.ID, tgbotapi.FileURL(comic.Img))
|
||||
photo.Caption = caption
|
||||
bot.Send(photo)
|
||||
}
|
||||
|
||||
func getRandomXkcdComic() (*XkcdComic, error) {
|
||||
// First get the latest comic to know the range
|
||||
resp, err := http.Get("https://xkcd.com/info.0.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get latest comic info: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("XKCD API returned status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %v", err)
|
||||
}
|
||||
|
||||
var latest XkcdComic
|
||||
if err := json.Unmarshal(body, &latest); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse latest comic: %v", err)
|
||||
}
|
||||
|
||||
// Generate random comic number (avoiding 404 which doesn't exist)
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
var randomNum int
|
||||
for {
|
||||
randomNum = rand.Intn(latest.Num) + 1
|
||||
if randomNum != 404 { // Comic 404 doesn't exist
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Get the random comic
|
||||
url := fmt.Sprintf("https://xkcd.com/%d/info.0.json", randomNum)
|
||||
resp, err = http.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get comic %d: %v", randomNum, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("comic %d returned status: %d", randomNum, resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read comic response: %v", err)
|
||||
}
|
||||
|
||||
var comic XkcdComic
|
||||
if err := json.Unmarshal(body, &comic); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse comic: %v", err)
|
||||
}
|
||||
|
||||
return &comic, nil
|
||||
}
|
||||
|
||||
func createXkcdCaption(comic *XkcdComic) string {
|
||||
url := fmt.Sprintf("https://xkcd.com/%d/", comic.Num)
|
||||
|
||||
// Try to include alt text if caption isn't too long
|
||||
fullCaption := fmt.Sprintf("%s\n\n%s\n\n%s", comic.Title, comic.Alt, url)
|
||||
if len(fullCaption) <= 1000 { // Keep it reasonable for Telegram
|
||||
return fullCaption
|
||||
}
|
||||
|
||||
// Fallback to just title and URL if too long
|
||||
return fmt.Sprintf("%s\n\n%s", comic.Title, url)
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(XkcdCommand{})
|
||||
}
|
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=
|
||||
|
98
main.go
98
main.go
@ -1,17 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"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 handleCallbackQuery(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||
query := update.CallbackQuery
|
||||
|
||||
// Answer the callback query to remove the loading state
|
||||
callback := tgbotapi.NewCallback(query.ID, "")
|
||||
bot.Request(callback)
|
||||
|
||||
// Parse callback data format: "command:param1:param2:..."
|
||||
parts := strings.Split(query.Data, ":")
|
||||
if len(parts) < 2 {
|
||||
log.Printf("Invalid callback data format: %s", query.Data)
|
||||
return
|
||||
}
|
||||
|
||||
commandName := parts[0]
|
||||
|
||||
switch commandName {
|
||||
case "medchem":
|
||||
// Get the medchem command and handle callback
|
||||
if cmd, ok := commands.All()["medchem"]; ok {
|
||||
if medchemCmd, ok := cmd.(interface {
|
||||
HandleCallback(tgbotapi.Update, *tgbotapi.BotAPI, []string)
|
||||
}); ok {
|
||||
medchemCmd.HandleCallback(update, bot, parts[1:])
|
||||
}
|
||||
}
|
||||
case "osrs":
|
||||
// Get the OSRS command and handle callback
|
||||
if cmd, ok := commands.All()["osrs"]; ok {
|
||||
if osrsCmd, ok := cmd.(interface {
|
||||
HandleCallback(tgbotapi.Update, *tgbotapi.BotAPI, []string)
|
||||
}); ok {
|
||||
osrsCmd.HandleCallback(update, bot, parts[1:])
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Printf("Unknown callback command: %s", commandName)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -25,16 +104,31 @@ func main() {
|
||||
updates := bot.GetUpdatesChan(u)
|
||||
|
||||
for update := range updates {
|
||||
// Handle callback queries (inline keyboard button presses)
|
||||
if update.CallbackQuery != nil {
|
||||
log.Printf("Received callback query: %s from user %d", update.CallbackQuery.Data, update.CallbackQuery.From.ID)
|
||||
handleCallbackQuery(update, bot)
|
||||
continue
|
||||
}
|
||||
|
||||
if update.Message == nil || !update.Message.IsCommand() {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("Received command: %s with args %s from chat %d (user %d)", update.Message.Command(), update.Message.CommandArguments(), 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 {
|
||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Unknown command.")
|
||||
bot.Send(msg)
|
||||
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