Compare commits
4 Commits
279a94c754
...
master
Author | SHA1 | Date | |
---|---|---|---|
e7a8cbc2a1 | |||
1eaef9f22f | |||
335912ea8a | |||
a0f0918504 |
@ -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
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{})
|
||||
}
|
@ -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
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{})
|
||||
}
|
@ -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
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{})
|
||||
}
|
54
main.go
54
main.go
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user