Medchem command
This commit is contained in:
@ -17,7 +17,7 @@ func (d DebugCommand) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d DebugCommand) Help() 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) {
|
func (d DebugCommand) Execute(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
|
||||||
|
609
commands/medchem.go
Normal file
609
commands/medchem.go
Normal file
@ -0,0 +1,609 @@
|
|||||||
|
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 CompoundData struct {
|
||||||
|
CID int
|
||||||
|
Name string
|
||||||
|
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)
|
||||||
|
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 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")
|
||||||
|
|
||||||
|
if c.IUPACName != "" {
|
||||||
|
fmt.Fprintf(b, "*IUPAC Name:* %s\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], fmt.Sprintf("CID %d", cid))
|
||||||
|
return compound, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(MedchemCommand{})
|
||||||
|
}
|
45
main.go
45
main.go
@ -5,6 +5,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"nignoggobot/commands"
|
"nignoggobot/commands"
|
||||||
@ -31,6 +32,37 @@ func notifyShutdown(reason string) {
|
|||||||
bot.Send(msg)
|
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:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Printf("Unknown callback command: %s", commandName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Signal handling for SIGINT/SIGTERM
|
// Signal handling for SIGINT/SIGTERM
|
||||||
sigs := make(chan os.Signal, 1)
|
sigs := make(chan os.Signal, 1)
|
||||||
@ -63,11 +95,18 @@ func main() {
|
|||||||
updates := bot.GetUpdatesChan(u)
|
updates := bot.GetUpdatesChan(u)
|
||||||
|
|
||||||
for update := range updates {
|
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() {
|
if update.Message == nil || !update.Message.IsCommand() {
|
||||||
continue
|
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()
|
cmdName := update.Message.Command()
|
||||||
isGroup := update.Message.Chat.IsGroup() || update.Message.Chat.IsSuperGroup()
|
isGroup := update.Message.Chat.IsGroup() || update.Message.Chat.IsSuperGroup()
|
||||||
@ -79,8 +118,8 @@ func main() {
|
|||||||
cmd.Execute(update, bot)
|
cmd.Execute(update, bot)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Unknown command: %s", cmdName)
|
log.Printf("Unknown command: %s", cmdName)
|
||||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Unknown command.")
|
// msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Unknown command.")
|
||||||
bot.Send(msg)
|
// bot.Send(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user