package main import ( "encoding/json" "sort" "time" "golang.org/x/net/context" "golang.org/x/oauth2" "io/ioutil" "net/http" "errors" "flag" "fmt" "log" "os" "strconv" "github.com/BurntSushi/toml" "github.com/go-openapi/strfmt" "github.com/leekchan/accounting" "github.com/logrusorgru/aurora" ESI "./client" ESILocation "./client/location" 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", "esi-location.read_location.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" cfgFilePath = flag.String("config", "configuration.toml", "Path to the configuration file.") cacheDBPath = flag.String("cache", "cache.db", "Path to the cache sqlite database.") ) var ctx = context.Background() var messages = make(chan *oauth2.Token) func main() { flag.Parse() 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 { 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)) } } posCallParam := ESILocation.NewGetCharactersCharacterIDLocationParams() posCallParam.WithCharacterID(m.CharacterID) posresponse, poserr := swaggerclient.Location.GetCharactersCharacterIDLocation(posCallParam, nil) if poserr != nil { fmt.Println("Error while getting the current character location.") log.Fatalf("Got error on GetCharactersCharacterIDLocation: %s", poserr) } position := posresponse.Payload itemIds := make([]int32, 0) itemIds = append(itemIds, *position.SolarSystemID) if position.StationID != nil { itemIds = append(itemIds, *position.StationID) } universeNames := getUniverseNames(swaggerclient, &itemIds) fmt.Printf("Currently in %s - %s\n", universeNames[*position.SolarSystemID], universeNames[*position.StationID], ) } type byPIPinType []*ESIPlanetaryInteraction.PinsItems0 func (s byPIPinType) Len() int { return len(s) } func (s byPIPinType) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s byPIPinType) Less(i, j int) bool { a := s[i] b := s[j] if *a.TypeID != *b.TypeID { return *a.TypeID < *b.TypeID } if a.ExtractorDetails != nil { return *a.ExtractorDetails.ProductTypeID < *a.ExtractorDetails.ProductTypeID } if a.SchematicID > 0 { return a.SchematicID < b.SchematicID } return *a.PinID < *b.PinID } 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 } pins := pesiresponse.Payload.Pins sort.Sort(byPIPinType(pins)) ptIds := make([]int32, 0, len(pins)) for _, pin := range pins { if pin.ExtractorDetails != nil { ptIds = append(ptIds, *pin.ExtractorDetails.ProductTypeID) } } pinNames := getUniverseNames(swaggerclient, &ptIds) for _, pin := range 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, %s, % 6d per cycle, %s %s (%02dd%02d:%02d)\n", status, *pin.ExtractorDetails.CycleTime, pinNames[*pin.ExtractorDetails.ProductTypeID], *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", *cacheDBPath) 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", *cacheDBPath) 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 }