550 lines
16 KiB
Go
550 lines
16 KiB
Go
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{})
|
||
}
|