Compare commits

...

5 Commits

Author SHA1 Message Date
14c12c2cdb Add WoW Connected Realm APIs and update Examples 2025-03-21 15:34:12 -05:00
a0fce7bac8 Move generics to their own file 2025-03-21 15:23:11 -05:00
8a99c6c184 Update example filename 2025-03-21 09:23:26 -05:00
f773677bcf Merge branch 'main' of github.com:Milhound/battlenetapi 2025-03-20 23:50:50 -05:00
Daniel Milholland
47f4e2f783 Initial commit 2025-03-20 23:43:48 -05:00
8 changed files with 291 additions and 53 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Daniel Milholland
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -33,59 +33,82 @@ type BattleNetAPIParams struct {
Namespace string Namespace string
Region string Region string
Token string Token string
Options interface{}
}
type URLFormatter interface {
FormatURL(baseURL, endpoint, namespace, region string, options interface{}) string
} }
func GetAccessToken(clientID string, clientSecret string) clientCredentialsAPI { func GetAccessToken(clientID string, clientSecret string) clientCredentialsAPI {
const authenticationUrl string = "https://oauth.battle.net/token" const authenticationUrl string = "https://oauth.battle.net/token"
resp, err := http.Post(authenticationUrl, "application/x-www-form-urlencoded", strings.NewReader(fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s", clientID, clientSecret))) resp, err := http.Post(authenticationUrl, "application/x-www-form-urlencoded", strings.NewReader(fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s", clientID, clientSecret)))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println("Error creating request:", err)
return clientCredentialsAPI{}
} }
defer resp.Body.Close() defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println("Error reading response body:", err)
return clientCredentialsAPI{}
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
fmt.Println(resp.Status) fmt.Println("Error response from server:", resp.Status)
panic("Failed to get access token") return clientCredentialsAPI{}
} }
var credentials clientCredentialsAPI var credentials clientCredentialsAPI
err = json.Unmarshal(respBody, &credentials) err = json.Unmarshal(respBody, &credentials)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println("Error parsing response body:", err)
panic("Failed to parse access token response") return clientCredentialsAPI{}
} }
fmt.Println("Access Token:", credentials.AccessToken) fmt.Println("Access Token:", credentials.AccessToken)
return credentials return credentials
} }
func BattleNetAPI(params BattleNetAPIParams) []byte { func BattleNetAPI(params BattleNetAPIParams, formatter URLFormatter) []byte {
if params.UrlOrEndpoint == "" || params.Namespace == "" || params.Region == "" || params.Token == "" {
fmt.Println("Invalid parameters")
return nil
}
var requestURL string var requestURL string
if strings.HasPrefix(params.UrlOrEndpoint, "http") { if strings.HasPrefix(params.UrlOrEndpoint, "http") {
requestURL = params.UrlOrEndpoint requestURL = params.UrlOrEndpoint
} else { } else {
var baseURL string = fmt.Sprintf("https://%s.api.blizzard.com", params.Region) baseURL := fmt.Sprintf("https://%s.api.blizzard.com", params.Region)
if formatter != nil {
requestURL = formatter.FormatURL(baseURL, params.UrlOrEndpoint, params.Namespace, params.Region, params.Options)
} else {
requestURL = fmt.Sprintf("%s%s?namespace=%s-%s", baseURL, params.UrlOrEndpoint, params.Namespace, params.Region) requestURL = fmt.Sprintf("%s%s?namespace=%s-%s", baseURL, params.UrlOrEndpoint, params.Namespace, params.Region)
} }
fmt.Println(requestURL) }
fmt.Println("Request URL:", requestURL)
req, err := http.NewRequest("GET", requestURL, nil) req, err := http.NewRequest("GET", requestURL, nil)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println("Error creating request:", err)
return nil
} }
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", params.Token)) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", params.Token))
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println("Error making request:", err)
return nil
} }
defer resp.Body.Close() defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println("Error reading response body:", err)
return nil
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
fmt.Println(resp.Status) fmt.Printf("Error response from server: %s\n", resp.Status)
panic("Failed to process the request") return nil
} }
return (respBody)
return respBody
} }

33
example/getLeaderboard.go Normal file
View File

@@ -0,0 +1,33 @@
package example
import (
"encoding/json"
"fmt"
"os"
"battlenetapi/battlenet"
"battlenetapi/wow/gamedata"
)
func GetLeaderboard(params battlenet.BattleNetAPIParams, bracket string) {
// Get the current PVP season
response := battlenet.BattleNetAPI(params, nil)
var pvpIndex gamedata.PvpSeasonIndexAPI
json.Unmarshal(response, &pvpIndex)
// Get the leaderboards for the current PVP season and Bracket
params.UrlOrEndpoint = fmt.Sprintf(gamedata.PvpLeaderboardEndpoint, pvpIndex.CurrentSeason.ID, bracket)
response = battlenet.BattleNetAPI(params, nil)
file, err := os.Create(fmt.Sprintf("pvp_season_%d_leaderboard-bracket_%s.json", pvpIndex.CurrentSeason.ID, bracket))
if err != nil {
fmt.Println("Error creating file:", err)
return
}
defer file.Close()
_, err = file.Write(response)
if err != nil {
fmt.Println("Error writing data to file:", err)
return
}
}

74
example/getRealmStatus.go Normal file
View File

@@ -0,0 +1,74 @@
package example
import (
"battlenetapi/battlenet"
"battlenetapi/wow/gamedata"
"encoding/json"
"fmt"
"os"
)
func GetRealmStatus(params battlenet.BattleNetAPIParams, formatter battlenet.URLFormatter) {
// Get the realm status
response := battlenet.BattleNetAPI(params, formatter)
var searchResults gamedata.ConnectedRealmSearchAPI
var data []gamedata.RealmSearchResult
err := json.Unmarshal(response, &searchResults)
if err != nil {
fmt.Println("Unable to parse ConnectedRealmSearchAPI")
return
}
// Handle more than one page of data
currentPage := params.Options.(gamedata.RealmStatusParams).Page
if currentPage > 1 {
for currentPage <= searchResults.PageCount {
currentPage += 1
// Update page to current page
realmStatusParams := params.Options.(*gamedata.RealmStatusParams)
realmStatusParams.Page = currentPage
params.Options = realmStatusParams
// Call next page
response := battlenet.BattleNetAPI(params, formatter)
json.Unmarshal(response, &searchResults)
data = append(data, searchResults.Results...)
}
} else {
data = searchResults.Results
}
file, err := os.Create("realm_status.json")
if err != nil {
fmt.Println("Error creating file:", err)
return
}
defer file.Close()
// Print out select information from the data
for _, realm := range data {
info := realm.Data.Realms[0]
fmt.Printf(
"%s-%s (%s - %s): %s (%s) \n",
info.Name.US,
info.Type.Name.US,
info.Category.US,
info.Timezone,
realm.Data.Status.Name.US,
realm.Data.Population.Name.US,
)
}
// Save complete data
searchResults.Results = data
results, err := json.Marshal(searchResults)
if err != nil {
fmt.Println("Unable to convert search results to a seralized object")
}
_, err = file.Write(results)
if err != nil {
fmt.Println("Error writing data to file:", err)
return
}
}

View File

@@ -1,13 +1,12 @@
package main package main
import ( import (
"encoding/json" "battlenetapi/battlenet"
"battlenetapi/example"
"battlenetapi/wow/gamedata"
"fmt" "fmt"
"os" "os"
"battlenetapi/battlenet"
"battlenetapi/wow/gamedata"
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
@@ -23,30 +22,28 @@ func main() {
clientSecret := os.Getenv("CLIENT_SECRET") clientSecret := os.Getenv("CLIENT_SECRET")
credentials := battlenet.GetAccessToken(clientID, clientSecret) credentials := battlenet.GetAccessToken(clientID, clientSecret)
// Get the current PVP season // Get the leaderboard for the shuffle bracket
params := battlenet.BattleNetAPIParams{ params := battlenet.BattleNetAPIParams{
UrlOrEndpoint: gamedata.PvpSeasonIndexEndpoint, UrlOrEndpoint: gamedata.PvpSeasonIndexEndpoint,
Namespace: battlenet.DYNAMIC, Namespace: battlenet.DYNAMIC,
Region: battlenet.US, Region: battlenet.US,
Token: credentials.AccessToken, Token: credentials.AccessToken,
} }
response := battlenet.BattleNetAPI(params)
var pvpIndex gamedata.PvpSeasonIndexAPI
json.Unmarshal(response, &pvpIndex)
// Get the leaderboards for the current PVP season and Bracket
shuffleOrBlitz := fmt.Sprintf("blitz-%s", gamedata.ClassDemonHunterHavoc) shuffleOrBlitz := fmt.Sprintf("blitz-%s", gamedata.ClassDemonHunterHavoc)
params.UrlOrEndpoint = fmt.Sprintf(gamedata.PvpLeaderboardEndpoint, pvpIndex.CurrentSeason.ID, shuffleOrBlitz) example.GetLeaderboard(params, shuffleOrBlitz)
response = battlenet.BattleNetAPI(params)
file, err := os.Create(fmt.Sprintf("pvp_season_%d_leaderboard-bracket_%s.json", pvpIndex.CurrentSeason.ID, shuffleOrBlitz)) // Get current realm status
if err != nil { params = battlenet.BattleNetAPIParams{
fmt.Println("Error creating file:", err) UrlOrEndpoint: gamedata.ConnectedRealmSearchEndpoint,
return Namespace: battlenet.DYNAMIC,
} Region: battlenet.US,
defer file.Close() Token: credentials.AccessToken,
_, err = file.Write(response) Options: gamedata.RealmStatusParams{
if err != nil { Status: gamedata.UP,
fmt.Println("Error writing data to file:", err) OrderBy: "id",
return Page: 1,
},
} }
formatter := gamedata.URLFormatterImpl{}
example.GetRealmStatus(params, formatter)
} }

View File

@@ -0,0 +1,84 @@
package gamedata
import "fmt"
const (
ConnectedRealmsIndexEndpoint = "/data/wow/connected-realm/index"
ConnectedRealmEndpoint = "/data/wow/connected-realm/%d"
ConnectedRealmSearchEndpoint = "/data/wow/search/connected-realm"
)
type Realm struct {
ID int `json:"id"`
Name localized `json:"name"`
Region struct {
Name localized `json:"name"`
ID int `json:"id"`
} `json:"region"`
Category localized `json:"category"`
Locale string `json:"locale"`
Type struct {
Name localized `json:"name"`
Type string `json:"type"`
} `json:"type"`
Slug string `json:"slug"`
Timezone string `json:"timezone"`
Tournament bool `json:"is_tournament"`
}
type RealmSearchResult struct {
Key href `json:"key"`
Data struct {
Realms []Realm `json:"realms"`
ID int `json:"id"`
Queue bool `json:"has_queue"`
Status struct {
Name localized `json:"name"`
Type string `json:"type"`
} `json:"status"`
Population struct {
Name localized `json:"name"`
Type string `json:"type"`
} `json:"population"`
} `json:"data"`
}
type RealmStatusParams struct {
Status string
Timezone string
OrderBy string
Page int
}
type URLFormatterImpl struct{}
func (u URLFormatterImpl) FormatURL(baseURL, endpoint, namespace, region string, options interface{}) string {
o := options.(RealmStatusParams)
requestURL := fmt.Sprintf("%s%s?namespace=%s-%s", baseURL, endpoint, namespace, region)
requestURL = fmt.Sprintf("%s&status.type=%s&realms.timezone=%s&orderby=%s&_page=%d", requestURL, o.Status, o.Timezone, o.OrderBy, o.Page)
return requestURL
}
type localized struct {
IT string `json:"it_IT"`
RU string `json:"ru_RU"`
GB string `json:"en_GB"`
TW string `json:"zh_TW"`
KR string `json:"ko_KR"`
US string `json:"en_US"`
MX string `json:"es_MX"`
BR string `json:"pt_BR"`
ES string `json:"es_ES"`
CN string `json:"zh_CN"`
FR string `json:"fr_FR"`
DE string `json:"de_DE"`
}
type ConnectedRealmSearchAPI struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
MaxPageSize int `json:"maxPageSize"`
PageCount int `json:"pageCount"`
Results []RealmSearchResult `json:"results"`
}

20
wow/gamedata/generics.go Normal file
View File

@@ -0,0 +1,20 @@
package gamedata
const (
UP string = "UP"
DOWN string = "DOWN"
)
type href struct {
Href string `json:"href"`
}
type idAndKey struct {
ID int `json:"id"`
Key href `json:"key"`
}
type idAndType struct {
ID int `json:"id"`
Type string `json:"type"`
}

View File

@@ -53,20 +53,6 @@ const (
ClassWarriorProtection string = "warrior-protection" ClassWarriorProtection string = "warrior-protection"
) )
type href struct {
Href string `json:"href"`
}
type idAndKey struct {
ID int `json:"id"`
Key href `json:"key"`
}
type idAndType struct {
ID int `json:"id"`
Type string `json:"type"`
}
type PvpSeasonIndexAPI struct { type PvpSeasonIndexAPI struct {
Seasons []idAndKey `json:"seasons"` Seasons []idAndKey `json:"seasons"`
CurrentSeason idAndKey `json:"current_season"` CurrentSeason idAndKey `json:"current_season"`