feat(app): implement character login and destination setting for multiple characters
This commit introduces several key features: - **Multiple Character Support**: The application can now handle multiple logged-in EVE Online characters. - **List Characters**: A new function `ListCharacters` allows retrieving a list of all authenticated characters. - **Set Destination for All**: The `SetDestinationForAll` function enables setting a destination for all logged-in characters simultaneously. - **UI Updates**: The frontend has been updated to display logged-in characters and to allow setting destinations for all characters. - **Backend Refinements**: The ESI SSO logic has been improved to support refreshing tokens for multiple characters and to handle the new multi-character functionality. - **Dependency Updates**: Dependencies have been updated to their latest versions. chore: update go module dependencies
This commit is contained in:
377
esi_sso.go
377
esi_sso.go
@@ -24,22 +24,18 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// SSO endpoints
|
||||
issuerAuthorizeURL = "https://login.eveonline.com/v2/oauth/authorize"
|
||||
issuerTokenURL = "https://login.eveonline.com/v2/oauth/token"
|
||||
|
||||
// ESI base
|
||||
esiBase = "https://esi.evetech.net"
|
||||
esiBase = "https://esi.evetech.net"
|
||||
)
|
||||
|
||||
// ESISSO encapsulates a minimal PKCE SSO client and token store
|
||||
type ESISSO struct {
|
||||
clientID string
|
||||
redirectURI string
|
||||
scopes []string
|
||||
|
||||
state string
|
||||
codeVerifier string
|
||||
state string
|
||||
codeVerifier string
|
||||
codeChallenge string
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -74,6 +70,11 @@ type ESIToken struct {
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type CharacterInfo struct {
|
||||
CharacterID int64 `json:"character_id"`
|
||||
CharacterName string `json:"character_name"`
|
||||
}
|
||||
|
||||
func NewESISSO(clientID string, redirectURI string, scopes []string) *ESISSO {
|
||||
s := &ESISSO{
|
||||
clientID: clientID,
|
||||
@@ -94,7 +95,6 @@ func (s *ESISSO) initDB() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Ensure tokens table exists in same DB; safe to AutoMigrate
|
||||
if err := db.AutoMigrate(&ESIToken{}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -128,7 +128,6 @@ func (s *ESISSO) loadToken() {
|
||||
}
|
||||
}
|
||||
|
||||
// BuildAuthorizeURL prepares state and PKCE challenge and returns the browser URL
|
||||
func (s *ESISSO) BuildAuthorizeURL() (string, error) {
|
||||
if s.clientID == "" {
|
||||
return "", errors.New("EVE_SSO_CLIENT_ID not set")
|
||||
@@ -155,7 +154,6 @@ func (s *ESISSO) BuildAuthorizeURL() (string, error) {
|
||||
return issuerAuthorizeURL + "?" + q.Encode(), nil
|
||||
}
|
||||
|
||||
// StartCallbackServerAsync starts the callback server in the background and returns immediately
|
||||
func (s *ESISSO) StartCallbackServerAsync() error {
|
||||
u, err := url.Parse(s.redirectURI)
|
||||
if err != nil {
|
||||
@@ -209,7 +207,6 @@ func (s *ESISSO) StartCallbackServerAsync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: blocking start; prefer StartCallbackServerAsync
|
||||
func (s *ESISSO) StartCallbackServer() error {
|
||||
u, err := url.Parse(s.redirectURI)
|
||||
if err != nil {
|
||||
@@ -290,7 +287,6 @@ func (s *ESISSO) exchangeToken(ctx context.Context, code string) error {
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if tr.AccessToken != "" {
|
||||
s.accessToken = tr.AccessToken
|
||||
}
|
||||
@@ -300,7 +296,7 @@ func (s *ESISSO) exchangeToken(ctx context.Context, code string) error {
|
||||
if tr.ExpiresIn > 0 {
|
||||
s.expiresAt = time.Now().Add(time.Duration(tr.ExpiresIn-30) * time.Second)
|
||||
}
|
||||
// Parse basic claims for display
|
||||
s.mu.Unlock()
|
||||
name, cid := parseTokenCharacter(tr.AccessToken)
|
||||
s.characterName = name
|
||||
s.characterID = cid
|
||||
@@ -340,7 +336,6 @@ func (s *ESISSO) refresh(ctx context.Context) error {
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.accessToken = tr.AccessToken
|
||||
if tr.RefreshToken != "" {
|
||||
s.refreshToken = tr.RefreshToken
|
||||
@@ -348,6 +343,7 @@ func (s *ESISSO) refresh(ctx context.Context) error {
|
||||
if tr.ExpiresIn > 0 {
|
||||
s.expiresAt = time.Now().Add(time.Duration(tr.ExpiresIn-30) * time.Second)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
name, cid := parseTokenCharacter(tr.AccessToken)
|
||||
s.characterName = name
|
||||
s.characterID = cid
|
||||
@@ -355,8 +351,50 @@ func (s *ESISSO) refresh(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ESISSO) refreshForToken(ctx context.Context, t *ESIToken) (*ESIToken, error) {
|
||||
form := url.Values{}
|
||||
form.Set("grant_type", "refresh_token")
|
||||
form.Set("refresh_token", t.RefreshToken)
|
||||
form.Set("client_id", s.clientID)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, issuerTokenURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("token refresh failed: %s: %s", resp.Status, string(b))
|
||||
}
|
||||
var tr tokenResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.AccessToken = tr.AccessToken
|
||||
if tr.RefreshToken != "" {
|
||||
t.RefreshToken = tr.RefreshToken
|
||||
}
|
||||
if tr.ExpiresIn > 0 {
|
||||
t.ExpiresAt = time.Now().Add(time.Duration(tr.ExpiresIn-30) * time.Second)
|
||||
}
|
||||
if s.db != nil {
|
||||
_ = s.db.Save(t).Error
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (s *ESISSO) ensureAccessToken(ctx context.Context) (string, error) {
|
||||
s.mu.Lock()
|
||||
if s.accessToken == "" && s.db != nil && s.characterID != 0 {
|
||||
s.mu.Unlock()
|
||||
s.loadToken()
|
||||
s.mu.Lock()
|
||||
}
|
||||
tok := s.accessToken
|
||||
exp := s.expiresAt
|
||||
s.mu.Unlock()
|
||||
@@ -374,12 +412,26 @@ func (s *ESISSO) ensureAccessToken(ctx context.Context) (string, error) {
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
// PostWaypoint calls ESI to set destination or add waypoint
|
||||
func (s *ESISSO) ensureAccessTokenFor(ctx context.Context, t *ESIToken) (string, error) {
|
||||
if t.AccessToken == "" || time.Now().After(t.ExpiresAt.Add(-60*time.Second)) {
|
||||
nt, err := s.refreshForToken(ctx, t)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return nt.AccessToken, nil
|
||||
}
|
||||
return t.AccessToken, nil
|
||||
}
|
||||
|
||||
func (s *ESISSO) PostWaypoint(destinationID int64, clearOthers bool, addToBeginning bool) error {
|
||||
tok, err := s.ensureAccessToken(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.postWaypointWithToken(tok, destinationID, clearOthers, addToBeginning)
|
||||
}
|
||||
|
||||
func (s *ESISSO) postWaypointWithToken(tok string, destinationID int64, clearOthers bool, addToBeginning bool) error {
|
||||
q := url.Values{}
|
||||
q.Set("destination_id", strconv.FormatInt(destinationID, 10))
|
||||
q.Set("add_to_beginning", strconv.FormatBool(addToBeginning))
|
||||
@@ -410,12 +462,30 @@ func (s *ESISSO) PostWaypoint(destinationID int64, clearOthers bool, addToBeginn
|
||||
return fmt.Errorf("waypoint failed: %s: %s", resp.Status, string(b))
|
||||
}
|
||||
|
||||
// Status reports current login state and character details
|
||||
type SSOStatus struct {
|
||||
LoggedIn bool
|
||||
CharacterID int64
|
||||
CharacterName string
|
||||
ExpiresAt time.Time
|
||||
func (s *ESISSO) PostWaypointForAll(destinationID int64, clearOthers bool, addToBeginning bool) error {
|
||||
if s.db == nil {
|
||||
return errors.New("db not initialised")
|
||||
}
|
||||
var tokens []ESIToken
|
||||
if err := s.db.Find(&tokens).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var firstErr error
|
||||
for i := range tokens {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
tok, err := s.ensureAccessTokenFor(ctx, &tokens[i])
|
||||
cancel()
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := s.postWaypointWithToken(tok, destinationID, clearOthers, addToBeginning); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (s *ESISSO) Status() SSOStatus {
|
||||
@@ -429,151 +499,13 @@ func (s *ESISSO) Status() SSOStatus {
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveSystemIDByName first checks local DB via GORM, then falls back to ESI
|
||||
func (s *ESISSO) ResolveSystemIDByName(ctx context.Context, name string) (int64, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return 0, errors.New("empty system name")
|
||||
}
|
||||
// 0) Try DB first
|
||||
if s.db != nil {
|
||||
var sys SolarSystem
|
||||
if err := s.db.Where("solarSystemName = ?", name).First(&sys).Error; err == nil && sys.SolarSystemID != 0 {
|
||||
fmt.Printf("DB: resolved %q -> %d\n", name, sys.SolarSystemID)
|
||||
return sys.SolarSystemID, nil
|
||||
}
|
||||
}
|
||||
// Fallback to ESI logic
|
||||
// 1) Prefer universe/ids (name->id) for accuracy
|
||||
type idsReq struct {
|
||||
Names []string `json:"names"`
|
||||
}
|
||||
body, _ := json.Marshal(idsReq{Names: []string{name}})
|
||||
idsURL := esiBase + "/v3/universe/ids/?datasource=tranquility"
|
||||
fmt.Printf("ESI: resolve system id via universe/ids for name=%q\n", name)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, idsURL, strings.NewReader(string(body)))
|
||||
if err == nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, errDo := http.DefaultClient.Do(req)
|
||||
if errDo == nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
var idsResp struct {
|
||||
Systems []struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"systems"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&idsResp); err == nil {
|
||||
if len(idsResp.Systems) > 0 {
|
||||
fmt.Printf("ESI: universe/ids hit: %s -> %d\n", idsResp.Systems[0].Name, idsResp.Systems[0].ID)
|
||||
return idsResp.Systems[0].ID, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Fallback: strict search
|
||||
q := url.Values{}
|
||||
q.Set("categories", "solar_system")
|
||||
q.Set("search", name)
|
||||
q.Set("strict", "true")
|
||||
searchURL := esiBase + "/v3/search/?" + q.Encode()
|
||||
fmt.Printf("ESI: strict search for %q\n", name)
|
||||
req2, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
req2.Header.Set("Accept", "application/json")
|
||||
resp2, err := http.DefaultClient.Do(req2)
|
||||
if err == nil {
|
||||
defer resp2.Body.Close()
|
||||
if resp2.StatusCode == http.StatusOK {
|
||||
var payload struct {
|
||||
SolarSystem []int64 `json:"solar_system"`
|
||||
}
|
||||
if err := json.NewDecoder(resp2.Body).Decode(&payload); err == nil && len(payload.SolarSystem) > 0 {
|
||||
fmt.Printf("ESI: strict search hit: %d\n", payload.SolarSystem[0])
|
||||
return payload.SolarSystem[0], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Fallback: non-strict search then best-effort name match via universe/names
|
||||
q.Set("strict", "false")
|
||||
searchURL2 := esiBase + "/v3/search/?" + q.Encode()
|
||||
fmt.Printf("ESI: non-strict search for %q\n", name)
|
||||
req3, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL2, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
req3.Header.Set("Accept", "application/json")
|
||||
resp3, err := http.DefaultClient.Do(req3)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp3.Body.Close()
|
||||
if resp3.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(resp3.Body)
|
||||
return 0, fmt.Errorf("search failed: %s: %s", resp3.Status, string(b))
|
||||
}
|
||||
var payload struct {
|
||||
SolarSystem []int64 `json:"solar_system"`
|
||||
}
|
||||
if err := json.NewDecoder(resp3.Body).Decode(&payload); err != nil || len(payload.SolarSystem) == 0 {
|
||||
return 0, fmt.Errorf("system not found: %s", name)
|
||||
}
|
||||
// If one result, return it
|
||||
if len(payload.SolarSystem) == 1 {
|
||||
fmt.Printf("ESI: non-strict search single hit: %d\n", payload.SolarSystem[0])
|
||||
return payload.SolarSystem[0], nil
|
||||
}
|
||||
// Multiple: resolve names and pick exact case-insensitive match if possible
|
||||
ids := payload.SolarSystem
|
||||
var idNamesReq = make([]int64, 0, len(ids))
|
||||
idNamesReq = append(idNamesReq, ids...)
|
||||
|
||||
namesURL := esiBase + "/v3/universe/names/?datasource=tranquility"
|
||||
idsBody, _ := json.Marshal(idNamesReq)
|
||||
req4, err := http.NewRequestWithContext(ctx, http.MethodPost, namesURL, strings.NewReader(string(idsBody)))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
req4.Header.Set("Content-Type", "application/json")
|
||||
req4.Header.Set("Accept", "application/json")
|
||||
resp4, err := http.DefaultClient.Do(req4)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp4.Body.Close()
|
||||
if resp4.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(resp4.Body)
|
||||
return 0, fmt.Errorf("names lookup failed: %s: %s", resp4.Status, string(b))
|
||||
}
|
||||
var namesResp []struct {
|
||||
Category string `json:"category"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.NewDecoder(resp4.Body).Decode(&namesResp); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
lower := strings.ToLower(name)
|
||||
for _, n := range namesResp {
|
||||
if n.Category == "solar_system" && strings.ToLower(n.Name) == lower {
|
||||
fmt.Printf("ESI: names resolved exact: %s -> %d\n", n.Name, n.ID)
|
||||
return n.ID, nil
|
||||
}
|
||||
}
|
||||
// Fallback: return first
|
||||
fmt.Printf("ESI: names resolved fallback: returning %d for %q\n", namesResp[0].ID, name)
|
||||
return namesResp[0].ID, nil
|
||||
type SSOStatus struct {
|
||||
LoggedIn bool
|
||||
CharacterID int64
|
||||
CharacterName string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
type tokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
@@ -615,7 +547,6 @@ func parseTokenCharacter(jwt string) (name string, id int64) {
|
||||
name = v
|
||||
}
|
||||
if v, ok := m["sub"].(string); ok {
|
||||
// format EVE:CHARACTER:<id>
|
||||
if idx := strings.LastIndexByte(v, ':'); idx > -1 {
|
||||
if idv, err := strconv.ParseInt(v[idx+1:], 10, 64); err == nil {
|
||||
id = idv
|
||||
@@ -623,4 +554,120 @@ func parseTokenCharacter(jwt string) (name string, id int64) {
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ResolveSystemIDByName ... DB-first then ESI fallbacks
|
||||
func (s *ESISSO) ResolveSystemIDByName(ctx context.Context, name string) (int64, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return 0, errors.New("empty system name")
|
||||
}
|
||||
if s.db != nil {
|
||||
var sys SolarSystem
|
||||
if err := s.db.Where("solarSystemName = ?", name).First(&sys).Error; err == nil && sys.SolarSystemID != 0 {
|
||||
fmt.Printf("DB: resolved %q -> %d\n", name, sys.SolarSystemID)
|
||||
return sys.SolarSystemID, nil
|
||||
}
|
||||
}
|
||||
// Fallbacks (universe/ids -> strict search -> non-strict + names)
|
||||
type idsReq struct{ Names []string `json:"names"` }
|
||||
body, _ := json.Marshal(idsReq{Names: []string{name}})
|
||||
idsURL := esiBase + "/v3/universe/ids/?datasource=tranquility"
|
||||
fmt.Printf("ESI: resolve system id via universe/ids for name=%q\n", name)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, idsURL, strings.NewReader(string(body)))
|
||||
if err == nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, errDo := http.DefaultClient.Do(req)
|
||||
if errDo == nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
var idsResp struct{ Systems []struct{ ID int64 `json:"id"`; Name string `json:"name"` } `json:"systems"` }
|
||||
if err := json.NewDecoder(resp.Body).Decode(&idsResp); err == nil {
|
||||
if len(idsResp.Systems) > 0 {
|
||||
fmt.Printf("ESI: universe/ids hit: %s -> %d\n", idsResp.Systems[0].Name, idsResp.Systems[0].ID)
|
||||
return idsResp.Systems[0].ID, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("categories", "solar_system")
|
||||
q.Set("search", name)
|
||||
q.Set("strict", "true")
|
||||
searchURL := esiBase + "/v3/search/?" + q.Encode()
|
||||
fmt.Printf("ESI: strict search for %q\n", name)
|
||||
req2, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
req2.Header.Set("Accept", "application/json")
|
||||
resp2, err := http.DefaultClient.Do(req2)
|
||||
if err == nil {
|
||||
defer resp2.Body.Close()
|
||||
if resp2.StatusCode == http.StatusOK {
|
||||
var payload struct{ SolarSystem []int64 `json:"solar_system"` }
|
||||
if err := json.NewDecoder(resp2.Body).Decode(&payload); err == nil && len(payload.SolarSystem) > 0 {
|
||||
fmt.Printf("ESI: strict search hit: %d\n", payload.SolarSystem[0])
|
||||
return payload.SolarSystem[0], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
q.Set("strict", "false")
|
||||
searchURL2 := esiBase + "/v3/search/?" + q.Encode()
|
||||
fmt.Printf("ESI: non-strict search for %q\n", name)
|
||||
req3, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL2, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
req3.Header.Set("Accept", "application/json")
|
||||
resp3, err := http.DefaultClient.Do(req3)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp3.Body.Close()
|
||||
if resp3.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(resp3.Body)
|
||||
return 0, fmt.Errorf("search failed: %s: %s", resp3.Status, string(b))
|
||||
}
|
||||
var payload struct{ SolarSystem []int64 `json:"solar_system"` }
|
||||
if err := json.NewDecoder(resp3.Body).Decode(&payload); err != nil || len(payload.SolarSystem) == 0 {
|
||||
return 0, fmt.Errorf("system not found: %s", name)
|
||||
}
|
||||
if len(payload.SolarSystem) == 1 {
|
||||
fmt.Printf("ESI: non-strict search single hit: %d\n", payload.SolarSystem[0])
|
||||
return payload.SolarSystem[0], nil
|
||||
}
|
||||
ids := payload.SolarSystem
|
||||
namesURL := esiBase + "/v3/universe/names/?datasource=tranquility"
|
||||
idsBody, _ := json.Marshal(ids)
|
||||
req4, err := http.NewRequestWithContext(ctx, http.MethodPost, namesURL, strings.NewReader(string(idsBody)))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
req4.Header.Set("Content-Type", "application/json")
|
||||
req4.Header.Set("Accept", "application/json")
|
||||
resp4, err := http.DefaultClient.Do(req4)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp4.Body.Close()
|
||||
if resp4.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(resp4.Body)
|
||||
return 0, fmt.Errorf("names lookup failed: %s: %s", resp4.Status, string(b))
|
||||
}
|
||||
var namesResp []struct{ Category string `json:"category"`; ID int64 `json:"id"`; Name string `json:"name"` }
|
||||
if err := json.NewDecoder(resp4.Body).Decode(&namesResp); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
lower := strings.ToLower(name)
|
||||
for _, n := range namesResp {
|
||||
if n.Category == "solar_system" && strings.ToLower(n.Name) == lower {
|
||||
fmt.Printf("ESI: names resolved exact: %s -> %d\n", n.Name, n.ID)
|
||||
return n.ID, nil
|
||||
}
|
||||
}
|
||||
fmt.Printf("ESI: names resolved fallback: returning %d for %q\n", namesResp[0].ID, name)
|
||||
return namesResp[0].ID, nil
|
||||
}
|
||||
Reference in New Issue
Block a user