Files
nignoggobot/commands/osrs.go
2025-06-25 14:17:36 +02:00

550 lines
16 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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{})
}