OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
 
master @ 298 LINES
 
[ HISTORY ]  [ UP ]
 

package blizzard

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"sync"
	"time"
)

// PlayerInfo represents basic player information for profile fetching
type PlayerInfo struct {
	ID        int
	Name      string
	RealmSlug string
	Region    string
}

// PlayerProfileResult represents the result of a player profile fetch operation
type PlayerProfileResult struct {
	PlayerID   int
	PlayerName string
	RealmSlug  string
	Region     string
	Summary    *CharacterSummaryResponse
	Equipment  *CharacterEquipmentResponse
	Media      *CharacterMediaResponse
	Error      error
}

// FetchCharacterSummary fetches character summary data
func (c *Client) FetchCharacterSummary(playerName, realmSlug, region string) (*Cha
racterSummaryResponse, error) {
	namespace := fmt.Sprintf("profile-classic-%s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/profile/wow/character/%s/%s?namespace=%s&locale=en_
US",
		region, realmSlug, strings.ToLower(playerName), namespace,
	)

	return fetchPlayerProfileAPI[CharacterSummaryResponse](c, url)
}

// FetchCharacterEquipment fetches character equipment data
func (c *Client) FetchCharacterEquipment(playerName, realmSlug, region string) (*C
haracterEquipmentResponse, error) {
	namespace := fmt.Sprintf("profile-classic-%s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/profile/wow/character/%s/%s/equipment?namespace=%s&
locale=en_US",
		region, realmSlug, strings.ToLower(playerName), namespace,
	)

	return fetchPlayerProfileAPI[CharacterEquipmentResponse](c, url)
}

// FetchCharacterMedia fetches character media data (avatars)
func (c *Client) FetchCharacterMedia(playerName, realmSlug, region string) (*Chara
cterMediaResponse, error) {
	namespace := fmt.Sprintf("profile-classic-%s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/profile/wow/character/%s/%s/character-media?namespa
ce=%s&locale=en_US",
		region, realmSlug, strings.ToLower(playerName), namespace,
	)

	return fetchPlayerProfileAPI[CharacterMediaResponse](c, url)
}

// FetchCharacterStatus fetches the status response for a character (valid/moved/d
eleted).
func (c *Client) FetchCharacterStatus(playerName, realmSlug, region string) (*Char
acterStatusResponse, error) {
	namespace := fmt.Sprintf("profile-classic-%s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/profile/wow/character/%s/%s/status?namespace=%s&loc
ale=en_US",
		region, realmSlug, strings.ToLower(playerName), namespace,
	)

	return fetchPlayerProfileAPI[CharacterStatusResponse](c, url)
}

// FetchCharacterAchievements fetches the achievements summary for a character.
func (c *Client) FetchCharacterAchievements(playerName, realmSlug, region string) 
(*CharacterAchievementsResponse, error) {
	namespace := fmt.Sprintf("profile-classic-%s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/profile/wow/character/%s/%s/achievements?namespace=
%s&locale=en_US",
		region, realmSlug, strings.ToLower(playerName), namespace,
	)

	return fetchPlayerProfileAPI[CharacterAchievementsResponse](c, url)
}

// FetchItem fetches item details from the static game data API
func (c *Client) FetchItem(itemID int, region string) (*ItemDetailResponse, error)
 {
	// hardcoded namespace version - update when game patches
	namespace := fmt.Sprintf("static-5.5.3_64530-classic-%s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/data/wow/item/%d?namespace=%s&locale=en_US",
		region, itemID, namespace,
	)

	return fetchPlayerProfileAPI[ItemDetailResponse](c, url)
}

// FetchAchievement fetches an achievement definition (name + is_account_wide).
func (c *Client) FetchAchievement(achievementID int, region string) (*AchievementD
etailResponse, error) {
	namespace := fmt.Sprintf("static-5.5.3_64530-classic-%s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/data/wow/achievement/%d?namespace=%s&locale=en_US",
		region, achievementID, namespace,
	)

	return fetchPlayerProfileAPI[AchievementDetailResponse](c, url)
}

// fetchPlayerProfileAPI is a generic function for fetching player profile data
func fetchPlayerProfileAPI[T any](c *Client, url string) (*T, error) {
	const maxRetries = 3
	const baseDelay = 1 * time.Second

	var lastErr error
	attempt := 0
	for {
		if attempt > 0 {
			delay := time.Duration(1<<uint(attempt-1)) * baseDelay
			time.Sleep(delay)
		}

		result, err := fetchPlayerProfileAPIOnce[T](c, url)
		if err == nil {
			return result, nil
		}

		lastErr = err

		var apiErr *APIError
		if errors.As(err, &apiErr) {
			switch apiErr.Status {
			case http.StatusTooManyRequests:
				time.Sleep(apiErr.retryDelay())
				continue
			case http.StatusNotFound:
				return nil, err
			}
		}

		attempt++
		if attempt >= maxRetries {
			break
		}
	}

	return nil, fmt.Errorf("failed after %d attempts: %w", maxRetries, lastErr)
}

// fetchPlayerProfileAPIOnce performs a single player profile API fetch attempt
func fetchPlayerProfileAPIOnce[T any](c *Client, url string) (*T, error) {
	c.waitForRateSlot()

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.Token))
	req.Header.Set("User-Agent", "WoWStatsDB/1.0")

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("HTTP request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, newAPIError(resp.StatusCode, bodyBytes, resp.Header.Get("Retry-After
"))
	}

	var result T
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}

	return &result, nil
}

// FetchPlayerProfilesConcurrent fetches player profiles concurrently with rate li
miting
func (c *Client) FetchPlayerProfilesConcurrent(ctx context.Context, players []Play
erInfo) <-chan PlayerProfileResult {
	results := make(chan PlayerProfileResult, len(players))
	var wg sync.WaitGroup

	for _, player := range players {
		wg.Add(1)
		go func(p PlayerInfo) {
			defer wg.Done()

			// Acquire concurrency slot
			select {
			case c.concurrency <- struct{}{}:
				defer func() { <-c.concurrency }() // Release slot
			case <-ctx.Done():
				results <- PlayerProfileResult{
					PlayerID:   p.ID,
					PlayerName: p.Name,
					RealmSlug:  p.RealmSlug,
					Region:     p.Region,
					Error:      ctx.Err(),
				}
				return
			}

			// minimal delay to respect API rate limits
			time.Sleep(20 * time.Millisecond)

			// fetch all profile data concurrently
			var summary *CharacterSummaryResponse
			var equipment *CharacterEquipmentResponse
			var media *CharacterMediaResponse
			var profileErr error

			// use a sub-context for the individual fetches
			subCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
			defer cancel()

			// launch concurrent fetches for this player
			summaryDone := make(chan struct{})
			equipmentDone := make(chan struct{})
			mediaDone := make(chan struct{})

			go func() {
				defer close(summaryDone)
				var err error
				summary, err = c.FetchCharacterSummary(p.Name, p.RealmSlug, p.Region)
				if err != nil && profileErr == nil {
					profileErr = fmt.Errorf("summary fetch failed: %w", err)
				}
			}()

			go func() {
				defer close(equipmentDone)
				var err error
				equipment, err = c.FetchCharacterEquipment(p.Name, p.RealmSlug, p.Region)
				if err != nil && profileErr == nil {
					profileErr = fmt.Errorf("equipment fetch failed: %w", err)
				}
			}()

			go func() {
				defer close(mediaDone)
				var err error
				media, err = c.FetchCharacterMedia(p.Name, p.RealmSlug, p.Region)
				if err != nil && profileErr == nil {
					profileErr = fmt.Errorf("media fetch failed: %w", err)
				}
			}()

			// wait for all fetches to complete or timeout
			select {
			case <-summaryDone:
			case <-subCtx.Done():
				profileErr = fmt.Errorf("summary fetch timeout")
			}

			select {
			case <-equipmentDone:
			case <-subCtx.Done():
				if profileErr == nil {
					profileErr = fmt.Errorf("equipment fetch timeout")
				}
			}

			select {
			case <-mediaDone:
			case <-subCtx.Done():
				if profileErr == nil {
					profileErr = fmt.Errorf("media fetch timeout")
				}
			}

			results <- PlayerProfileResult{
				PlayerID:   p.ID,
				PlayerName: p.Name,
				RealmSlug:  p.RealmSlug,
				Region:     p.Region,
				Summary:    summary,
				Equipment:  equipment,
				Media:      media,
				Error:      profileErr,
			}
		}(player)
	}

	// close channel when all goroutines complete
	go func() {
		wg.Wait()
		close(results)
	}()

	return results
}

package blizzard

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"sync"
	"time"
)

// PlayerInfo represents basic player inform
ation for profile fetching
type PlayerInfo struct {
	ID        int
	Name      string
	RealmSlug string
	Region    string
}

// PlayerProfileResult represents the result
 of a player profile fetch operation
type PlayerProfileResult struct {
	PlayerID   int
	PlayerName string
	RealmSlug  string
	Region     string
	Summary    *CharacterSummaryResponse
	Equipment  *CharacterEquipmentResponse
	Media      *CharacterMediaResponse
	Error      error
}

// FetchCharacterSummary fetches character s
ummary data
func (c *Client) FetchCharacterSummary(playe
rName, realmSlug, region string) (*Character
SummaryResponse, error) {
	namespace := fmt.Sprintf("profile-classic-%
s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/profile/wow/c
haracter/%s/%s?namespace=%s&locale=en_US",
		region, realmSlug, strings.ToLower(playerN
ame), namespace,
	)

	return fetchPlayerProfileAPI[CharacterSumma
ryResponse](c, url)
}

// FetchCharacterEquipment fetches character
 equipment data
func (c *Client) FetchCharacterEquipment(pla
yerName, realmSlug, region string) (*Charact
erEquipmentResponse, error) {
	namespace := fmt.Sprintf("profile-classic-%
s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/profile/wow/c
haracter/%s/%s/equipment?namespace=%s&locale
=en_US",
		region, realmSlug, strings.ToLower(playerN
ame), namespace,
	)

	return fetchPlayerProfileAPI[CharacterEquip
mentResponse](c, url)
}

// FetchCharacterMedia fetches character med
ia data (avatars)
func (c *Client) FetchCharacterMedia(playerN
ame, realmSlug, region string) (*CharacterMe
diaResponse, error) {
	namespace := fmt.Sprintf("profile-classic-%
s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/profile/wow/c
haracter/%s/%s/character-media?namespace=%s&
locale=en_US",
		region, realmSlug, strings.ToLower(playerN
ame), namespace,
	)

	return fetchPlayerProfileAPI[CharacterMedia
Response](c, url)
}

// FetchCharacterStatus fetches the status r
esponse for a character (valid/moved/deleted
).
func (c *Client) FetchCharacterStatus(player
Name, realmSlug, region string) (*CharacterS
tatusResponse, error) {
	namespace := fmt.Sprintf("profile-classic-%
s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/profile/wow/c
haracter/%s/%s/status?namespace=%s&locale=en
_US",
		region, realmSlug, strings.ToLower(playerN
ame), namespace,
	)

	return fetchPlayerProfileAPI[CharacterStatu
sResponse](c, url)
}

// FetchCharacterAchievements fetches the ac
hievements summary for a character.
func (c *Client) FetchCharacterAchievements(
playerName, realmSlug, region string) (*Char
acterAchievementsResponse, error) {
	namespace := fmt.Sprintf("profile-classic-%
s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/profile/wow/c
haracter/%s/%s/achievements?namespace=%s&loc
ale=en_US",
		region, realmSlug, strings.ToLower(playerN
ame), namespace,
	)

	return fetchPlayerProfileAPI[CharacterAchie
vementsResponse](c, url)
}

// FetchItem fetches item details from the s
tatic game data API
func (c *Client) FetchItem(itemID int, regio
n string) (*ItemDetailResponse, error) {
	// hardcoded namespace version - update whe
n game patches
	namespace := fmt.Sprintf("static-5.5.3_6453
0-classic-%s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/data/wow/item
/%d?namespace=%s&locale=en_US",
		region, itemID, namespace,
	)

	return fetchPlayerProfileAPI[ItemDetailResp
onse](c, url)
}

// FetchAchievement fetches an achievement d
efinition (name + is_account_wide).
func (c *Client) FetchAchievement(achievemen
tID int, region string) (*AchievementDetailR
esponse, error) {
	namespace := fmt.Sprintf("static-5.5.3_6453
0-classic-%s", region)
	url := fmt.Sprintf(
		"https://%s.api.blizzard.com/data/wow/achi
evement/%d?namespace=%s&locale=en_US",
		region, achievementID, namespace,
	)

	return fetchPlayerProfileAPI[AchievementDet
ailResponse](c, url)
}

// fetchPlayerProfileAPI is a generic functi
on for fetching player profile data
func fetchPlayerProfileAPI[T any](c *Client,
 url string) (*T, error) {
	const maxRetries = 3
	const baseDelay = 1 * time.Second

	var lastErr error
	attempt := 0
	for {
		if attempt > 0 {
			delay := time.Duration(1<<uint(attempt-1)
) * baseDelay
			time.Sleep(delay)
		}

		result, err := fetchPlayerProfileAPIOnce[T
](c, url)
		if err == nil {
			return result, nil
		}

		lastErr = err

		var apiErr *APIError
		if errors.As(err, &apiErr) {
			switch apiErr.Status {
			case http.StatusTooManyRequests:
				time.Sleep(apiErr.retryDelay())
				continue
			case http.StatusNotFound:
				return nil, err
			}
		}

		attempt++
		if attempt >= maxRetries {
			break
		}
	}

	return nil, fmt.Errorf("failed after %d att
empts: %w", maxRetries, lastErr)
}

// fetchPlayerProfileAPIOnce performs a sing
le player profile API fetch attempt
func fetchPlayerProfileAPIOnce[T any](c *Cli
ent, url string) (*T, error) {
	c.waitForRateSlot()

	req, err := http.NewRequest("GET", url, nil
)
	if err != nil {
		return nil, fmt.Errorf("failed to create r
equest: %w", err)
	}

	req.Header.Set("Authorization", fmt.Sprintf
("Bearer %s", c.Token))
	req.Header.Set("User-Agent", "WoWStatsDB/1.
0")

	resp, err := c.HTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("HTTP request faile
d: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, newAPIError(resp.StatusCode, b
odyBytes, resp.Header.Get("Retry-After"))
	}

	var result T
	if err := json.NewDecoder(resp.Body).Decode
(&result); err != nil {
		return nil, fmt.Errorf("failed to decode r
esponse: %w", err)
	}

	return &result, nil
}

// FetchPlayerProfilesConcurrent fetches pla
yer profiles concurrently with rate limiting
func (c *Client) FetchPlayerProfilesConcurre
nt(ctx context.Context, players []PlayerInfo
) <-chan PlayerProfileResult {
	results := make(chan PlayerProfileResult, l
en(players))
	var wg sync.WaitGroup

	for _, player := range players {
		wg.Add(1)
		go func(p PlayerInfo) {
			defer wg.Done()

			// Acquire concurrency slot
			select {
			case c.concurrency <- struct{}{}:
				defer func() { <-c.concurrency }() // Re
lease slot
			case <-ctx.Done():
				results <- PlayerProfileResult{
					PlayerID:   p.ID,
					PlayerName: p.Name,
					RealmSlug:  p.RealmSlug,
					Region:     p.Region,
					Error:      ctx.Err(),
				}
				return
			}

			// minimal delay to respect API rate limi
ts
			time.Sleep(20 * time.Millisecond)

			// fetch all profile data concurrently
			var summary *CharacterSummaryResponse
			var equipment *CharacterEquipmentResponse
			var media *CharacterMediaResponse
			var profileErr error

			// use a sub-context for the individual f
etches
			subCtx, cancel := context.WithTimeout(ctx
, 30*time.Second)
			defer cancel()

			// launch concurrent fetches for this pla
yer
			summaryDone := make(chan struct{})
			equipmentDone := make(chan struct{})
			mediaDone := make(chan struct{})

			go func() {
				defer close(summaryDone)
				var err error
				summary, err = c.FetchCharacterSummary(p
.Name, p.RealmSlug, p.Region)
				if err != nil && profileErr == nil {
					profileErr = fmt.Errorf("summary fetch 
failed: %w", err)
				}
			}()

			go func() {
				defer close(equipmentDone)
				var err error
				equipment, err = c.FetchCharacterEquipme
nt(p.Name, p.RealmSlug, p.Region)
				if err != nil && profileErr == nil {
					profileErr = fmt.Errorf("equipment fetc
h failed: %w", err)
				}
			}()

			go func() {
				defer close(mediaDone)
				var err error
				media, err = c.FetchCharacterMedia(p.Nam
e, p.RealmSlug, p.Region)
				if err != nil && profileErr == nil {
					profileErr = fmt.Errorf("media fetch fa
iled: %w", err)
				}
			}()

			// wait for all fetches to complete or ti
meout
			select {
			case <-summaryDone:
			case <-subCtx.Done():
				profileErr = fmt.Errorf("summary fetch t
imeout")
			}

			select {
			case <-equipmentDone:
			case <-subCtx.Done():
				if profileErr == nil {
					profileErr = fmt.Errorf("equipment fetc
h timeout")
				}
			}

			select {
			case <-mediaDone:
			case <-subCtx.Done():
				if profileErr == nil {
					profileErr = fmt.Errorf("media fetch ti
meout")
				}
			}

			results <- PlayerProfileResult{
				PlayerID:   p.ID,
				PlayerName: p.Name,
				RealmSlug:  p.RealmSlug,
				Region:     p.Region,
				Summary:    summary,
				Equipment:  equipment,
				Media:      media,
				Error:      profileErr,
			}
		}(player)
	}

	// close channel when all goroutines comple
te
	go func() {
		wg.Wait()
		close(results)
	}()

	return results
}
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET