Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-wcl

Go Reference

go-wcl is a Go client for the Warcraft Logs v2 GraphQL API.

It handles OAuth 2.0 authentication, request retries, and rate-limit inspection. Typed methods are generated from the API schema, and Client.Execute runs arbitrary queries for anything the typed methods do not cover.

Features

  • OAuth 2.0 client-credentials, authorization-code, and PKCE flows.
  • Type-safe methods and models generated from the live schema with genqlient.
  • Execute runs any raw GraphQL query the generated methods don't cover.
  • Automatic retries with exponential backoff, honoring Retry-After.
  • Rate-limit inspection and typed error helpers.
  • Minimal runtime dependencies.

Requirements

  • Go 1.25 or later.
  • A Warcraft Logs API client. Create one on the client management page to get a client ID and secret.

Installation

go get github.com/math280h/go-wcl

Usage

package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	warcraftlogs "github.com/math280h/go-wcl"
)

func main() {
	ctx := context.Background()

	client, err := warcraftlogs.New(ctx,
		warcraftlogs.WithClientCredentials("client-id", "client-secret"))
	if err != nil {
		log.Fatal(err)
	}

	char, err := client.CharacterByName(ctx, "Asmongold", "area-52", "us")
	if errors.Is(err, warcraftlogs.ErrNotFound) {
		log.Fatal("character not found")
	} else if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s - %s (%s)\n", char.Name, char.Server.Name, char.Server.Region.Name)
}

Lookups that resolve to a missing entity return ErrNotFound, as shown above. An unknown report code is rejected by the API itself and arrives as a GraphQL error instead. See GraphQLErrors.

Authentication

Public data is accessed with the client-credentials flow against ClientEndpoint:

client, err := warcraftlogs.New(ctx,
	warcraftlogs.WithClientCredentials(id, secret))

Private data (a user's own reports, CurrentUser) requires the authorization-code or PKCE flow against UserEndpoint. Use OAuthConfig to build the authorization URL and exchange the returned code, then pass the token to the client:

cfg := warcraftlogs.OAuthConfig(id, secret, "https://example.com/callback")

// 1. Redirect the user to the authorization URL.
url := cfg.AuthCodeURL("state-token")

// 2. On the callback, exchange the code for a token.
tok, err := cfg.Exchange(ctx, codeFromCallback)
if err != nil {
	log.Fatal(err)
}

// 3. Build a client for the private API.
client, err := warcraftlogs.New(ctx,
	warcraftlogs.WithTokenSource(cfg.TokenSource(ctx, tok)),
	warcraftlogs.WithEndpoint(warcraftlogs.UserEndpoint))

For PKCE (clients that cannot hold a secret), pass an empty secret and use the verifier options from golang.org/x/oauth2:

cfg := warcraftlogs.OAuthConfig(id, "", "https://example.com/callback")
verifier := oauth2.GenerateVerifier()
url := cfg.AuthCodeURL("state-token", oauth2.S256ChallengeOption(verifier))
tok, err := cfg.Exchange(ctx, codeFromCallback, oauth2.VerifierOption(verifier))

examples/userauth is a runnable version of this flow: a local redirect server, state validation, the PKCE exchange, and a CurrentUser call against UserEndpoint.

go run ./examples/userauth
go run ./examples/userauth -redirect http://localhost:9000/callback

Typed queries

Methods cover characters, guilds, reports, world data, game data, and users.

// Report header only.
report, err := client.Report(ctx, "aBcDeFgHiJkLmN01", false)

// World data.
zones, err := client.Zones(ctx, 0) // 0 = all expansions
encounter, err := client.Encounter(ctx, 3009)

Listing reports

Reports lists a guild's uploaded logs. Identify the guild by GuildID, or by the name/slug/region trio; UserID lists a user's personal logs instead.

page, err := client.Reports(ctx, warcraftlogs.ReportsParams{
	GuildName:         "Skill Issue",
	GuildServerSlug:   "tarren-mill",
	GuildServerRegion: "eu",
})
for _, r := range page.Data {
	fmt.Printf("%s  %s\n", r.Code, r.Title)
}

page.Data holds ReportSummary values: code, title, start and end time, zone, guild and owner. Call Client.Report or Client.ReportWithFights with a code for the rest. Walk the pages by incrementing Page while page.HasMorePages is true; Total, PerPage, CurrentPage, LastPage, From and To are also available.

Reports and fights

ReportWithFights returns the header, the fights and the encounter phases together:

report, err := client.ReportWithFights(ctx, warcraftlogs.ReportWithFightsParams{
	Code:     "aBcDeFgHiJkLmN01",
	KillType: warcraftlogs.KillTypeKills,
})
fmt.Println(report.Title, len(report.Fights))

Fields the schema marks nullable are pointers, because for several of them the Go zero value is also a legal answer. On a trash fight Kill, Difficulty and Size are all nil; on a boss fight Kill is false for a wipe:

for _, f := range report.Fights {
	switch {
	case f.Kill == nil:
		fmt.Printf("%s (trash)\n", f.Name)
	case *f.Kill:
		fmt.Printf("%s (kill)\n", f.Name)
	default:
		fmt.Printf("%s (wipe at %.1f%%)\n", f.Name, *f.FightPercentage)
	}
}

report.Phases carries the phase metadata for every encounter in the log. Join it against a fight's observed transitions to answer which phase the raid was in at a given report-relative timestamp:

fight := report.Fights[0]
if pt, ok := fight.PhaseAt(fight.EndTime); ok {
	for _, p := range report.PhasesFor(fight.EncounterID) {
		if p.Id == pt.Id {
			fmt.Printf("ended in %s\n", p.Name) // e.g. "Stage Three"
		}
	}
}

Prefer PhaseTransitions over the LastPhase* fields: a fight can re-enter a phase it has already been in, and LastPhase numbers normal phases and intermissions separately.

Actors

ReportMasterData returns every actor in a report alongside the full ability table. ReportActors filters server-side:

players, err := client.ReportActors(ctx, warcraftlogs.ReportActorsParams{
	Code: "aBcDeFgHiJkLmN01",
	Type: warcraftlogs.ActorPlayer,
})

SubType narrows further: a class for players ("DeathKnight"), or ActorBoss for NPCs. Leaving a field empty omits that filter.

Analysis endpoints

Rankings, tables, graphs, events, and player details are returned as json.RawMessage, matching the API's JSON type. Decode them into your own structs.

data, err := client.CharacterZoneRankings(ctx, warcraftlogs.ZoneRankingsParams{
	Character: warcraftlogs.CharacterRef{Name: "Asmongold", ServerSlug: "area-52", ServerRegion: "us"},
	Metric:    warcraftlogs.CharacterPageRankingMetricTypeDps,
})

table, err := client.ReportTable(ctx, warcraftlogs.TableDataTypeDamageDone,
	warcraftlogs.ReportAnalysisParams{Code: "aBcDeFgHiJkLmN01"})

CharacterZoneRankings and CharacterEncounterRankings rank a named character. EncounterLeaderboard is the inverse - the leaderboard for one boss:

top, err := client.EncounterLeaderboard(ctx, warcraftlogs.EncounterLeaderboardParams{
	EncounterID: 3009,
	ClassName:   "Mage",
	SpecName:    "Fire",
	Metric:      warcraftlogs.CharacterRankingMetricTypeDps,
})

Events are paginated. ReportEventsAll follows the cursor for you and yields one event at a time, so you never hold more than a page in memory:

params := warcraftlogs.ReportEventsParams{Code: "aBcDeFgHiJkLmN01", FightIDs: []int{12}}
for raw, err := range client.ReportEventsAll(ctx, warcraftlogs.EventDataTypeDeaths, params) {
	if err != nil {
		log.Fatal(err)
	}
	var e deathEvent
	if err := json.Unmarshal(raw, &e); err != nil {
		log.Fatal(err)
	}
	// ... handle e ...
}

Breaking out of the loop stops immediately without fetching another page. If the API ever returns a cursor that does not move forward, iteration stops with ErrPageNotAdvancing instead of re-requesting the same page.

ReportEvents returns a single page if you want to drive pagination yourself; NextPageTimestamp is zero on the last page. Either way, events require either FightIDs or an explicit StartTime/EndTime range.

examples/analysis is a runnable walkthrough of a real log: report metadata, a per-boss pull summary, the damage breakdown of a kill, and every death joined against report master data to resolve actor and ability names.

go run ./examples/analysis
go run ./examples/analysis -report aBcDeFgHiJkLmN01

Raw queries

Execute runs any query and decodes the data field into a pointer. Use it for operations not covered by the typed methods.

var resp struct {
	WorldData struct {
		Regions []struct {
			ID   int    `json:"id"`
			Name string `json:"name"`
		} `json:"regions"`
	} `json:"worldData"`
}
err := client.Execute(ctx, `query { worldData { regions { id name } } }`, nil, &resp)

Rate limiting

The API uses an hourly point budget. Inspect it at any time:

limit, err := client.RateLimit(ctx)
fmt.Printf("%.1f / %d points used, resets in %ds\n",
	limit.PointsSpentThisHour, limit.LimitPerHour, limit.PointsResetIn)

Requests that return HTTP 429 or 5xx are retried automatically with backoff (configurable via WithMaxRetries), including the Cloudflare-specific 520-527 range. A Cloudflare challenge page is reported as a CDNError rather than surfacing as a JSON decode failure.

Errors

Helpers classify errors returned by any method:

if _, err := client.Report(ctx, code, false); err != nil {
	switch {
	case warcraftlogs.IsRateLimited(err):
		// HTTP 429.
	case warcraftlogs.IsUnauthorized(err):
		// HTTP 401 or 403.
	case warcraftlogs.IsBlocked(err):
		// Cloudflare served a challenge page; the request never reached the API.
		var cdn *warcraftlogs.CDNError
		errors.As(err, &cdn)
		log.Printf("blocked: HTTP %d %q", cdn.StatusCode, cdn.Title)
	}
	for _, ge := range warcraftlogs.GraphQLErrors(err) {
		// e.g. "graphql: reportData.report: This report does not exist."
		log.Printf("graphql: %s: %s", ge.Path, ge.Message)
	}
	if status, ok := warcraftlogs.HTTPStatus(err); ok {
		log.Printf("http status: %d", status)
	}
}

The classifiers key off HTTP status only. The API sends no extensions on its GraphQL errors and reports a report you may not read as though it does not exist, so a failure that carries only a GraphQL error is not classifiable beyond its message. Read GraphQLErrors directly for those.

Client options

New accepts functional options:

Option Purpose
WithClientCredentials(id, secret) Client-credentials authentication.
WithTokenSource(ts) Authenticate with a caller-provided oauth2.TokenSource.
WithHTTPClient(hc) Use a preconfigured *http.Client verbatim. Supersedes the other auth and transport options, so combining them is an error.
WithEndpoint(url) Override the GraphQL endpoint (e.g. UserEndpoint).
WithTokenURL(url) Override the OAuth token endpoint (default TokenURL).
WithScopes(scopes...) Scopes for the client-credentials flow.
WithUserAgent(ua) Set the User-Agent header.
WithMaxRetries(n) Retry attempts for 429/5xx responses (default 3).
WithTimeout(d) Per-request timeout (default 60s).
WithBaseTransport(rt) http.RoundTripper beneath the retry and auth layers.
WithLogger(l) Log retried requests to a *slog.Logger. Silent by default.

Development

The typed layer is generated from a committed copy of the schema (schema/schema.graphql) and the operations under operations/.

Copy .env.example to .env and fill in your credentials:

task              # list tasks
task check        # fmt check, vet, build, and unit tests
task regenerate   # refresh the schema, then regenerate the typed client
task test:integration  # run tests against the live API

Each task maps to plain Go commands if you prefer to run them directly:

go generate ./...                 # regenerate from operations + schema
go -C tools run ./introspect      # refresh schema/schema.graphql
go test ./...                     # unit tests
go test -tags integration ./...   # live API tests (skipped without credentials)

Disclaimer

This project is not affiliated with or endorsed by Warcraft Logs or Blizzard Entertainment. It's an unofficial client, maintained independently. All trademarks belong to their respective owners.

You're bound by the Warcraft Logs terms of service when using their API through this library.

License

See LICENSE.

About

Go client for the Warcraft Logs v2 GraphQL API.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages