Compare commits

...

4 Commits

Author SHA1 Message Date
e7a8cbc2a1 More commands 2025-06-25 15:53:42 +02:00
1eaef9f22f osrs command 2025-06-25 14:17:36 +02:00
335912ea8a Medchem command improvements 2025-06-25 12:00:29 +02:00
a0f0918504 Medchem command 2025-06-25 11:43:55 +02:00
11 changed files with 1724 additions and 7 deletions

View File

@ -17,7 +17,7 @@ func (d DebugCommand) Name() string {
}
func (d DebugCommand) Help() string {
return "Show debug info (admin only)"
return "Show debug info"
}
func (d DebugCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {

76
commands/fortune.go Normal file
View 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{})
}

View File

@ -17,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 dumb stuff and chemistry. Now version 2.0, rewritten in Go.")
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
View 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
View 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
View 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{})
}

View File

@ -29,13 +29,13 @@ func (m MolCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
}
cid, err := fetchPubchemCID(args)
if err != nil {
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Structure not found")
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
photo.Caption = args + "\nIf you want more info, try the /medchem command instead"
bot.Send(photo)
}

549
commands/osrs.go Normal file
View 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
View 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
View 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{})
}

54
main.go
View File

@ -5,6 +5,7 @@ import (
"log"
"os"
"os/signal"
"strings"
"syscall"
"nignoggobot/commands"
@ -31,6 +32,46 @@ func notifyShutdown(reason string) {
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)
@ -63,11 +104,18 @@ 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 from chat %d (user %d)", update.Message.Command(), update.Message.Chat.ID, update.Message.From.ID)
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()
@ -79,8 +127,8 @@ func main() {
cmd.Execute(update, bot)
} else {
log.Printf("Unknown command: %s", cmdName)
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Unknown command.")
bot.Send(msg)
// msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Unknown command.")
// bot.Send(msg)
}
}
}