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 " } // 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 `\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{}) }