osrs command

This commit is contained in:
2025-06-25 14:17:36 +02:00
parent 335912ea8a
commit 1eaef9f22f
2 changed files with 558 additions and 0 deletions

549
commands/osrs.go Normal file
View 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{})
}

View File

@ -58,6 +58,15 @@ func handleCallbackQuery(update tgbotapi.Update, bot *tgbotapi.BotAPI) {
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)
}