This repository has been archived on 2025-02-01. You can view files and clone it, but cannot push or open issues or pull requests.
eve-goclient/main.go

558 lines
15 KiB
Go

package main
import (
"encoding/json"
"time"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"io/ioutil"
"net/http"
"errors"
"fmt"
"log"
"os"
"strconv"
"github.com/BurntSushi/toml"
"github.com/go-openapi/strfmt"
"github.com/leekchan/accounting"
"github.com/logrusorgru/aurora"
ESI "./client"
ESIPlanetaryInteraction "./client/planetary_interaction"
ESISkills "./client/skills"
ESIUniverse "./client/universe"
ESIWallet "./client/wallet"
"database/sql"
_ "github.com/mattn/go-sqlite3"
httptransport "github.com/go-openapi/runtime/client"
)
// Character - Structure to save the verification data.
type Character struct {
CharacterID int32
CharacterName string
ExpiresOn string
}
type configurationFile struct {
ClientID string
ClientSecret string
}
var (
googleOauthConfig = &oauth2.Config{
RedirectURL: "http://localhost:3000/callback",
ClientID: "CLIENTKEY",
ClientSecret: "SECRETKEY",
Scopes: []string{
"esi-skills.read_skillqueue.v1",
"esi-skills.read_skills.v1",
"esi-planets.manage_planets.v1",
"esi-wallet.read_character_wallet.v1",
},
Endpoint: oauth2.Endpoint{
AuthURL: "https://login.eveonline.com/oauth/authorize/",
TokenURL: "https://login.eveonline.com/oauth/token/",
},
}
// Some random string, random for each request
oauthStateString = "random"
)
var ctx = context.Background()
var messages = make(chan *oauth2.Token)
func main() {
logfilename := fmt.Sprintf("log-%s.log", time.Now().Format("2006-01-02_15_04_05"))
f, err := os.OpenFile(logfilename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("error opening file: %v", err)
}
defer f.Close()
log.SetOutput(f)
cErr := readConfigurationFile()
if cErr != nil {
fmt.Println("Missing configuration file.")
log.Fatal(cErr)
}
cToken := getDatabaseToken()
if cToken == nil {
cToken = getNewAuthorizationToken()
}
client := googleOauthConfig.Client(oauth2.NoContext, cToken)
m, err := getCharacterInfo(client)
if err != nil {
cToken = getNewAuthorizationToken()
client = googleOauthConfig.Client(oauth2.NoContext, cToken)
m, err = getCharacterInfo(client)
if err != nil {
log.Fatal(err)
}
}
transport := httptransport.NewWithClient("esi.tech.ccp.is", "/latest", []string{"https"}, client)
swaggerclient := ESI.New(transport, strfmt.Default)
printCharacterInformation(swaggerclient, m)
fmt.Printf("\n\nPlanetary interaction\n")
printCharacterPlanets(swaggerclient, m)
fmt.Printf("\n\nSkill queue\n")
printCharacterSkillQueue(swaggerclient, m)
}
func readConfigurationFile() error {
cfgFilePath := "configuration.toml"
var config configurationFile
if _, err := toml.DecodeFile(cfgFilePath, &config); err != nil {
log.Println(err)
return err
}
if config.ClientID == "" || config.ClientSecret == "" {
log.Println("Missing ClientID or ClientSecret configuration option in configuration file" + cfgFilePath + ".")
return errors.New("Missing ClientID or ClientSecret in configuration file " + cfgFilePath + ".")
}
googleOauthConfig.ClientID = config.ClientID
googleOauthConfig.ClientSecret = config.ClientSecret
return nil
}
func getCharacterInfo(client *http.Client) (*Character, error) {
req, _ := http.NewRequest("GET", "https://login.eveonline.com/oauth/verify", nil)
response, errDo := client.Do(req)
if errDo != nil {
return nil, errDo
}
defer response.Body.Close()
contents, _ := ioutil.ReadAll(response.Body)
var m Character
errJSON := json.Unmarshal(contents, &m)
if errJSON != nil {
log.Printf("Unmarshalling error: %s\n", errJSON)
return nil, errJSON
}
if m.CharacterID == 0 || m.CharacterName == "" {
log.Printf("Invalid return value from verify call: %s", contents)
return nil, errors.New("Invalid token")
}
return &m, nil
}
func printCharacterSkillQueue(swaggerclient *ESI.App, m *Character) {
callParam := ESISkills.NewGetCharactersCharacterIDSkillqueueParams()
callParam.WithCharacterID(m.CharacterID)
skillqueueresp, skillerr := swaggerclient.Skills.GetCharactersCharacterIDSkillqueue(callParam, nil)
if skillerr != nil {
log.Fatalf("Error on GetCharactersCharacterIDSkillqueue\n%s\n", skillerr)
}
skillqueue := skillqueueresp.Payload
now := time.Now()
snIds := make([]int32, 0, len(skillqueue))
for _, skill := range skillqueue {
snIds = append(snIds, *skill.SkillID)
}
skillNames := getUniverseNames(swaggerclient, &snIds)
for _, skill := range skillqueue {
// element is the element from someSlice for where we are
name := skillNames[*skill.SkillID]
finishDate := time.Time(skill.FinishDate)
duration := finishDate.Sub(now)
// see https://github.com/ccpgames/esi-issues/issues/113
// The queue is only updated when the user logs in with the client
// we thus need to do the computations and filtering ourselves
if finishDate.Before(time.Now()) {
fmt.Printf(" ✔ % 32s - L%d\n",
name,
*skill.FinishedLevel,
)
continue
}
fmt.Printf("% 35s - L%d - %s to %s (% 4dd %02d:%02d)\n",
name,
*skill.FinishedLevel,
time.Time(skill.StartDate).Format("_2 Jan 2006, 15:04"),
time.Time(skill.FinishDate).Format("_2 Jan 2006, 15:04"),
int32(duration.Hours())/24,
int32(duration.Minutes())/60%24,
int32(duration.Minutes())%60,
)
}
}
func printCharacterInformation(swaggerclient *ESI.App, m *Character) {
callParam := ESIWallet.NewGetCharactersCharacterIDWalletsParams()
callParam.WithCharacterID(m.CharacterID)
esiresponse, esierr := swaggerclient.Wallet.GetCharactersCharacterIDWallets(callParam, nil)
if esierr != nil {
fmt.Println("Error while getting the wallet information")
log.Fatalf("Got error on GetCharactersCharacterIDWallets: %s", esierr)
}
wallets := esiresponse.Payload
ac := accounting.Accounting{Symbol: "ISK ", Precision: 0, Thousand: "'"}
fmt.Printf("Name: %s\n", m.CharacterName)
for _, wallet := range wallets {
if wallet.Balance > 0 {
fmt.Printf("Wallet: %s\n", ac.FormatMoney(wallet.Balance/100))
}
}
}
func printCharacterPlanets(swaggerclient *ESI.App, m *Character) {
callParam := ESIPlanetaryInteraction.NewGetCharactersCharacterIDPlanetsParams()
callParam.WithCharacterID(m.CharacterID)
esiresponse, esierr := swaggerclient.PlanetaryInteraction.GetCharactersCharacterIDPlanets(callParam, nil)
if esierr != nil {
fmt.Println("Error while getting the planetary interaction information.")
log.Fatalf("Got error on GetCharactersCharacterIDPlanets: %s", esierr)
}
planets := esiresponse.Payload
now := time.Now()
snIds := make([]int32, 0, len(planets))
for _, planet := range planets {
snIds = append(snIds, *planet.PlanetID)
}
planetNames := getUniverseNames(swaggerclient, &snIds)
for _, planet := range planets {
pcallParam := ESIPlanetaryInteraction.NewGetCharactersCharacterIDPlanetsPlanetIDParams()
pcallParam.WithCharacterID(m.CharacterID).WithPlanetID(*planet.PlanetID)
solarSystemInfo := getSolarSystemInformation(swaggerclient, *planet.SolarSystemID)
planetName := planetNames[*planet.PlanetID]
fmt.Printf(" Planet %s, %s - %s, level %d with %d structures - Updated %s\n",
planetName,
solarSystemInfo.SolarSystemName,
*planet.PlanetType,
*planet.UpgradeLevel, *planet.NumPins,
time.Time(*planet.LastUpdate).Format("_2 Jan 2006, 15:04"),
)
pesiresponse, pesierr := swaggerclient.PlanetaryInteraction.GetCharactersCharacterIDPlanetsPlanetID(pcallParam, nil)
if pesierr != nil {
fmt.Println(" Error while getting the planetary interaction information")
log.Printf("Error on CharactersCharacterIDPlanetsPlanetID: %s\n", pesierr)
continue
}
for _, pin := range pesiresponse.Payload.Pins {
if pin.ExtractorDetails != nil {
status := fmt.Sprint(aurora.Red("✘").Bold())
statuscomment := fmt.Sprint(aurora.Red("expired").Bold())
duration := now.Sub(time.Time(pin.ExpiryTime))
if time.Time(pin.ExpiryTime).After(now) {
status = fmt.Sprint(aurora.Green("✔").Bold())
statuscomment = "expires"
duration = time.Time(pin.ExpiryTime).Sub(now)
}
fmt.Printf(" %s Extractor % 5ds cycle, % 6d per cycle, %s %s (%02dd%02d:%02d)\n",
status,
*pin.ExtractorDetails.CycleTime,
*pin.ExtractorDetails.QtyPerCycle,
statuscomment,
time.Time(pin.ExpiryTime).Format("_2 Jan 2006, 15:04"),
(int32(duration.Minutes()) / (60 * 24)),
(int32(duration.Minutes())/60)%24,
(int32(duration.Minutes()) % 60),
)
} else if pin.SchematicID != 0 {
// Get the schematic from ESI and cache it
schematicInfo := getSchematicsInformation(swaggerclient, pin.SchematicID)
fmt.Printf(" ✔ Factory % 5ds cycle, %s\n",
schematicInfo.CycleTime,
schematicInfo.SchematicName,
)
}
}
}
}
func getUniverseNames(swaggerclient *ESI.App, itemIds *[]int32) map[int32]string {
itemNames := make(map[int32]string)
itemMissingNames := make(map[int32]string)
for _, itemID := range *itemIds {
itemName := getCachedData(fmt.Sprintf("%d.name", itemID))
if itemName != "" {
itemNames[itemID] = itemName
} else {
itemMissingNames[itemID] = ""
}
}
if len(itemMissingNames) > 0 {
snIds := make([]int32, 0, len(itemMissingNames))
for itemID := range itemMissingNames {
snIds = append(snIds, itemID)
}
sncallParam := ESIUniverse.NewPostUniverseNamesParams()
sncallParam.WithIds(snIds)
skillNameResp, skillNameErr := swaggerclient.Universe.PostUniverseNames(sncallParam)
if skillNameErr != nil {
log.Printf("Error on PostUniverseNames on items: %v\n", snIds)
log.Printf("Error on PostUniverseNames: %s\n", skillNameErr)
} else {
for _, searchResult := range skillNameResp.Payload {
itemName := searchResult.Name
itemID := searchResult.ID
putCacheData(fmt.Sprintf("%d.name", *itemID), *itemName)
itemNames[*itemID] = *itemName
}
}
}
return itemNames
}
// SolarSystemInfo - Structure to store and cache the Solar System information from ESI
type SolarSystemInfo struct {
SolarSystemName string
}
func getSolarSystemInformation(swaggerclient *ESI.App, solarSystemID int32) SolarSystemInfo {
systemName := getCachedData(fmt.Sprintf("%d.name", solarSystemID))
if systemName == "" {
scallParams := ESIUniverse.NewGetUniverseSystemsSystemIDParams()
scallParams.WithSystemID(solarSystemID)
sesiresponse, _ := swaggerclient.Universe.GetUniverseSystemsSystemID(scallParams)
solarSystemESIInfo := sesiresponse.Payload
var solarSystemInfo SolarSystemInfo
solarSystemInfo.SolarSystemName = *solarSystemESIInfo.SolarSystemName
putCacheData(fmt.Sprintf("%d.name", solarSystemID), solarSystemInfo.SolarSystemName)
return solarSystemInfo
}
var solarSystemInfo SolarSystemInfo
solarSystemInfo.SolarSystemName = systemName
return solarSystemInfo
}
// SchematicInfo - Structure to store and cache the schematics information from ESI
type SchematicInfo struct {
CycleTime int32
SchematicName string
}
func getSchematicsInformation(swaggerclient *ESI.App, schematicID int32) SchematicInfo {
schematicName := getCachedData(fmt.Sprintf("%d.name", schematicID))
schematicCycle := getCachedData(fmt.Sprintf("%d.cycle", schematicID))
if schematicName == "" {
scallParams := ESIPlanetaryInteraction.NewGetUniverseSchematicsSchematicIDParams()
scallParams.WithSchematicID(schematicID)
sesiresponse, _ := swaggerclient.PlanetaryInteraction.GetUniverseSchematicsSchematicID(scallParams)
schematicsESIInfo := sesiresponse.Payload
var schematicInfo SchematicInfo
schematicInfo.CycleTime = *schematicsESIInfo.CycleTime
schematicInfo.SchematicName = *schematicsESIInfo.SchematicName
putCacheData(fmt.Sprintf("%d.name", schematicID), schematicInfo.SchematicName)
putCacheData(fmt.Sprintf("%d.cycle", schematicID), fmt.Sprintf("%d", schematicInfo.CycleTime))
return schematicInfo
}
var schematicInfo SchematicInfo
pCycleTime, _ := strconv.ParseInt(schematicCycle, 10, 32)
schematicInfo.CycleTime = int32(pCycleTime)
schematicInfo.SchematicName = schematicName
return schematicInfo
}
func getNewAuthorizationToken() *oauth2.Token {
http.HandleFunc("/", handleLogin)
http.HandleFunc("/callback", handleAuthenticationCallback)
go func() {
fmt.Println("No available token. Please visit http://localhost:3000 to renew.")
http.ListenAndServe(":3000", nil)
}()
return <-messages
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
url := googleOauthConfig.AuthCodeURL(oauthStateString, oauth2.AccessTypeOffline)
// https://eveonline-third-party-documentation.readthedocs.io/en/latest/sso/authentication.html
// response_type: Must be set to “code”.
url = url + "&response_type=code"
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
func handleAuthenticationCallback(w http.ResponseWriter, r *http.Request) {
state := r.FormValue("state")
if state != oauthStateString {
log.Printf("invalid oauth state, expected '%s', got '%s'\n", oauthStateString, state)
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
// No token to pass, we will get one on the second pass
return
}
code := r.FormValue("code")
token, err := googleOauthConfig.Exchange(oauth2.NoContext, code)
if err != nil {
log.Printf("Code exchange failed with '%s'\n", err)
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
// No token to pass, we will get one on the second pass
return
}
client := googleOauthConfig.Client(oauth2.NoContext, token)
req, _ := http.NewRequest("GET", "https://login.eveonline.com/oauth/verify", nil)
response, _ := client.Do(req)
defer response.Body.Close()
contents, _ := ioutil.ReadAll(response.Body)
var m Character
errJSON := json.Unmarshal(contents, &m)
if errJSON != nil {
fmt.Printf("JSON read error with '%s'\n", errJSON)
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
fmt.Fprintf(w, "Got token for character %s.\n", m.CharacterName)
fmt.Fprintf(w, "You can now close this navigator tab.\n")
log.Printf("Refresh token is %s\n", token.RefreshToken)
putCacheData("accessToken", token.AccessToken)
putCacheData("refreshToken", token.RefreshToken)
messages <- token
}
func getCachedData(key string) string {
db, err := sql.Open("sqlite3", "./foo.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.Exec("CREATE TABLE IF NOT EXISTS properties (id text NOT NULL PRIMARY KEY, value TEXT);")
if err != nil {
log.Fatal(err)
}
stmt, err := db.Prepare("SELECT value FROM properties WHERE id = ?")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
var response string
err = stmt.QueryRow(key).Scan(&response)
if err != nil {
return ""
}
return response
}
func putCacheData(key string, value string) {
db, err := sql.Open("sqlite3", "./foo.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.Exec("CREATE TABLE IF NOT EXISTS properties (id text NOT NULL PRIMARY KEY, value TEXT);")
if err != nil {
log.Fatal(err)
}
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
stmt, err := tx.Prepare("INSERT OR REPLACE INTO properties(id, value) values(?, ?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
_, err = stmt.Exec(key, value)
if err != nil {
log.Fatal(err)
}
tx.Commit()
}
func getDatabaseToken() *oauth2.Token {
refreshToken := getCachedData("refreshToken")
//accessToken := getCachedData("accessToken")
token := new(oauth2.Token)
token.RefreshToken = refreshToken
//token.AccessToken = accessToken
token.TokenType = "Bearer"
return token
}