From 0559bbbf90f642ce3d650648784df33483f95d70 Mon Sep 17 00:00:00 2001 From: Tyler Sommer Date: Thu, 8 Mar 2018 03:15:30 -0700 Subject: [PATCH] Fix race in mail, update vendor --- Gopkg.lock | 4 +- mail/list.go | 6 +- vendor/github.com/motki/core/README.md | 29 +++ .../github.com/motki/core/eveapi/structure.go | 117 ++++----- .../github.com/motki/core/evedb/universe.go | 34 +++ vendor/github.com/motki/core/model/asset.go | 10 + .../github.com/motki/core/model/blueprint.go | 3 + .../core/model/{corporation.go => corp.go} | 162 ------------ .../github.com/motki/core/model/corp_auth.go | 193 ++++++++++++++ .../github.com/motki/core/model/industry.go | 3 + .../motki/core/model/internal/cache/cache.go | 164 ++++++++++++ .../core/model/internal/cache/cache_test.go | 99 +++++++ .../github.com/motki/core/model/inventory.go | 15 +- .../github.com/motki/core/model/location.go | 154 +++++++++++ vendor/github.com/motki/core/model/model.go | 7 +- vendor/github.com/motki/core/model/order.go | 7 + vendor/github.com/motki/core/model/product.go | 101 +++++++- .../github.com/motki/core/model/structure.go | 141 +++++++++- vendor/github.com/motki/core/model/user.go | 244 ------------------ .../github.com/motki/core/model/user_auth.go | 207 +++++++++++++++ .../motki/core/model/user_context.go | 51 ++++ .../motki/core/model/user_scopes.go | 48 ++++ .../motki/core/proto/server/inventory.go | 6 +- .../sirupsen/logrus/example_basic_test.go | 3 +- .../sirupsen/logrus/example_hook_test.go | 3 +- .../tyler-sommer/stick/value_test.go | 2 +- .../x/crypto/ssh/terminal/util_solaris.go | 3 +- vendor/golang.org/x/net/http2/transport.go | 4 +- .../x/text/unicode/norm/normalize_test.go | 2 +- views/industry/structures.html.twig | 6 +- 30 files changed, 1298 insertions(+), 530 deletions(-) rename vendor/github.com/motki/core/model/{corporation.go => corp.go} (60%) create mode 100644 vendor/github.com/motki/core/model/corp_auth.go create mode 100644 vendor/github.com/motki/core/model/internal/cache/cache.go create mode 100644 vendor/github.com/motki/core/model/internal/cache/cache_test.go create mode 100644 vendor/github.com/motki/core/model/location.go create mode 100644 vendor/github.com/motki/core/model/user_auth.go create mode 100644 vendor/github.com/motki/core/model/user_context.go create mode 100644 vendor/github.com/motki/core/model/user_scopes.go diff --git a/Gopkg.lock b/Gopkg.lock index 7af3de7..144f45c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -58,8 +58,8 @@ [[projects]] branch = "master" name = "github.com/motki/core" - packages = ["app","db","eveapi","evedb","evemarketer","log","model","proto","proto/client","proto/server","worker"] - revision = "211a50bc8d5e7c05ad2fd574a48da981a2d88773" + packages = ["app","db","eveapi","evedb","evemarketer","log","model","model/internal/cache","proto","proto/client","proto/server","worker"] + revision = "6ad72639966c9a2efdfe168f6a92d1e8196925d9" [[projects]] name = "github.com/pkg/errors" diff --git a/mail/list.go b/mail/list.go index 30bad81..aff4b5b 100644 --- a/mail/list.go +++ b/mail/list.go @@ -48,9 +48,9 @@ func NewModelList(m *model.Manager, key string) (*modelList, error) { } func (m *modelList) Add(rec Recipient) error { - m.mutex.RLock() + m.mutex.Lock() + defer m.mutex.Unlock() _, ok := m.subscribers[rec.Email] - m.mutex.RUnlock() if ok { return nil } @@ -61,9 +61,7 @@ func (m *modelList) Add(rec Recipient) error { if err != nil { return err } - m.mutex.Lock() m.subscribers[rec.Email] = struct{}{} - m.mutex.Unlock() return nil } diff --git a/vendor/github.com/motki/core/README.md b/vendor/github.com/motki/core/README.md index ac40545..0a2f906 100644 --- a/vendor/github.com/motki/core/README.md +++ b/vendor/github.com/motki/core/README.md @@ -2,4 +2,33 @@ The MOTKI EVE Corporation application libraries. +View [current documentation](https://godoc.org/github.com/motki/core) on GoDoc. + [![Build Status](https://travis-ci.org/motki/core.svg?branch=master)](https://travis-ci.org/motki/core) [![GoDoc](https://godoc.org/github.com/motki/core?status.svg)](https://godoc.org/github.com/motki/core) + +Subpackage Overview +------------------- + +| Name | Description +| ------------------ | ----------- +| [app][1] | Integration package that wires up all the dependencies in a MOTKI application. Use this package to bootstrap your own MOTKI client application. +| [db][2] | PostgreSQL database integration. Light wrapper around [jackc/pgx](https://github.com/jackc/pgx). +| [eveapi][3] | EVE API integration. Handles EVE SSO and fetching data from both ESI and XML APIs using [antihax/goesi](https://github.com/antihax/goesi). +| [evedb][4] | EVE Static Data Export interface. Queries the SDE for static type/universe information. MOTKI uses [Fuzzwork's Postgres dump](https://www.fuzzwork.co.uk/dump/). +| [evemarketer][5] | Provides region- and system-specific market statistics using [evemarketer.com](https://evemarketer.com). +| [log][6] | Wrapper around [sirupsen/logrus](https://github.com/sirupsen/logrus) providing a configuration API and a defacto `Logger` type. +| [model][7] | Encapsulates persistence of data to the database. General pattern is to fetch from DB, then from API if stale. The database schema for this package is defined in the [resources/ddl/ directory](https://github.com/motki/core/tree/master/resources/ddl). +| [proto][8] | Defines the protocol buffer (and [gRPC](https://grpc.io)) interface for MOTKI at large. +| [proto/client][9] | A golang gRPC client for interacting with a remote MOTKI application server. +| [proto/server][10] | A golang gRPC server for handling MOTKI client requests. + +[1]: https://godoc.org/github.com/motki/core/app +[2]: https://godoc.org/github.com/motki/core/db +[3]: https://godoc.org/github.com/motki/core/eveapi +[4]: https://godoc.org/github.com/motki/core/evedb +[5]: https://godoc.org/github.com/motki/core/evemarketer +[6]: https://godoc.org/github.com/motki/core/log +[7]: https://godoc.org/github.com/motki/core/model +[8]: https://godoc.org/github.com/motki/core/proto +[9]: https://godoc.org/github.com/motki/core/proto/client +[10]: https://godoc.org/github.com/motki/core/proto/server diff --git a/vendor/github.com/motki/core/eveapi/structure.go b/vendor/github.com/motki/core/eveapi/structure.go index 85915b6..1b7cbc6 100644 --- a/vendor/github.com/motki/core/eveapi/structure.go +++ b/vendor/github.com/motki/core/eveapi/structure.go @@ -1,74 +1,39 @@ package eveapi import ( - "database/sql/driver" - "encoding/json" - "fmt" - "strings" + "time" - "github.com/antihax/goesi/esi" "golang.org/x/net/context" ) +// A Structure is a player-owned citadel. type Structure struct { StructureID int64 + Name string SystemID int64 TypeID int64 - ProfileID int64 - CurrentVuln VulnSchedule - NextVuln VulnSchedule -} - -type VulnSchedule map[int][]int - -func (s *VulnSchedule) Scan(src interface{}) error { - if v, ok := src.(string); ok { - return json.Unmarshal([]byte(v), &s) - } - return fmt.Errorf("invalid vulnerability schedule: %v", src) } -func (s VulnSchedule) Value() (driver.Value, error) { - return json.Marshal(s) -} - -func (s VulnSchedule) String() string { - var out []string - for day, hrs := range s { - var d string - switch day { - case 0: - d = "Sunday" - case 1: - d = "Monday" - case 2: - d = "Tuesday" - case 3: - d = "Wednesday" - case 4: - d = "Thursday" - case 5: - d = "Friday" - default: - d = "Saturday" - } - var td []string - for _, hr := range hrs { - td = append(td, fmt.Sprintf("%02d00", hr)) - } - if len(td) != 0 { - out = append(out, fmt.Sprintf("%s: %s", d, strings.Join(td, ", "))) - } - } - return strings.Join(out, ", ") +// A CorporationStructure contains additional, sensitive information about a citadel. +type CorporationStructure struct { + Structure + ProfileID int64 + Services []string + FuelExpires time.Time + StateStart time.Time + StateEnd time.Time + UnanchorsAt time.Time + VulnWeekday int64 + VulnHour int64 + State string } -func (api *EveAPI) GetCorporationStructures(ctx context.Context, corpID int) ([]*Structure, error) { +func (api *EveAPI) GetCorporationStructures(ctx context.Context, corpID int) ([]*CorporationStructure, error) { res, _, err := api.client.ESI.CorporationApi.GetCorporationsCorporationIdStructures(ctx, int32(corpID), nil) if err != nil { return nil, err } - var structures []*Structure + var structures []*CorporationStructure for _, bp := range res { sched := map[int][]int{} for _, sch := range bp.CurrentVul { @@ -86,31 +51,41 @@ func (api *EveAPI) GetCorporationStructures(ctx context.Context, corpID int) ([] } nsched[d] = append(nsched[d], h) } - structures = append(structures, &Structure{ - StructureID: bp.StructureId, - SystemID: int64(bp.SystemId), - TypeID: int64(bp.TypeId), + // ESI doesn't return the structure name in this API call for some reason. + // Query the Universe ESI API for the structures name. + s, err := api.GetStructure(ctx, bp.StructureId) + if err != nil { + return nil, err + } + var srvs []string + for _, r := range bp.Services { + srvs = append(srvs, r.Name) + } + structures = append(structures, &CorporationStructure{ + Structure: *s, ProfileID: int64(bp.ProfileId), - CurrentVuln: sched, - NextVuln: nsched, + UnanchorsAt: bp.UnanchorsAt, + StateStart: bp.StateTimerStart, + StateEnd: bp.StateTimerEnd, + Services: srvs, + FuelExpires: bp.FuelExpires, + VulnWeekday: 0, // TODO: Update goesi + VulnHour: 0, // TODO: Update goesi + State: "", // TODO: Update goesi }) } return structures, nil } -func (api *EveAPI) UpdateCorporationStructureVulnSchedule(ctx context.Context, corpID int, structureID int, sched VulnSchedule) error { - var newSched []esi.PutCorporationsCorporationIdStructuresStructureIdNewSchedule - for day, hrs := range sched { - for _, hr := range hrs { - newSched = append(newSched, esi.PutCorporationsCorporationIdStructuresStructureIdNewSchedule{ - Day: int32(day), - Hour: int32(hr), - }) - } - } - _, err := api.client.ESI.CorporationApi.PutCorporationsCorporationIdStructuresStructureId(ctx, int32(corpID), newSched, int64(structureID), nil) +func (api *EveAPI) GetStructure(ctx context.Context, structureID int64) (*Structure, error) { + res, _, err := api.client.ESI.UniverseApi.GetUniverseStructuresStructureId(ctx, int64(structureID), nil) if err != nil { - return err + return nil, err } - return nil + return &Structure{ + StructureID: structureID, + Name: res.Name, + SystemID: int64(res.SolarSystemId), + TypeID: int64(res.TypeId), + }, nil } diff --git a/vendor/github.com/motki/core/evedb/universe.go b/vendor/github.com/motki/core/evedb/universe.go index 150c5eb..5e29659 100644 --- a/vendor/github.com/motki/core/evedb/universe.go +++ b/vendor/github.com/motki/core/evedb/universe.go @@ -19,6 +19,16 @@ type Region struct { Name string } +type Station struct { + StationID int + StationTypeID int + CorporationID int + SystemID int + ConstellationID int + RegionID int + Name string +} + func (e *EveDB) GetSystem(id int) (*System, error) { c, err := e.pool.Open() if err != nil { @@ -109,3 +119,27 @@ func (e *EveDB) GetAllRegions() ([]*Region, error) { } return res, nil } + +func (e *EveDB) GetStation(stationID int) (*Station, error) { + c, err := e.pool.Open() + if err != nil { + return nil, err + } + defer e.pool.Release(c) + r := c.QueryRow( + `SELECT s."stationID" + , s."stationTypeID" + , s."stationName" + , s."solarSystemID" + , s."constellationID" + , s."regionID" + , s."corporationID" + FROM evesde."staStations" s + WHERE s."stationID" = $1`, stationID) + s := &Station{} + err = r.Scan(&s.StationID, &s.StationTypeID, &s.Name, &s.SystemID, &s.ConstellationID, &s.RegionID, &s.CorporationID) + if err != nil { + return nil, err + } + return s, nil +} diff --git a/vendor/github.com/motki/core/model/asset.go b/vendor/github.com/motki/core/model/asset.go index d4e9c12..0e421e8 100644 --- a/vendor/github.com/motki/core/model/asset.go +++ b/vendor/github.com/motki/core/model/asset.go @@ -36,6 +36,9 @@ func assetFromEveAPI(bp *eveapi.Asset) *Asset { } func (m *Manager) GetCorporationAssets(ctx context.Context, corpID int) (res []*Asset, err error) { + if ctx, err = m.corporationAuthContext(ctx, corpID); err != nil { + return nil, err + } res, err = m.getCorporationAssetsFromDB(corpID) if err != nil { return nil, err @@ -47,6 +50,9 @@ func (m *Manager) GetCorporationAssets(ctx context.Context, corpID int) (res []* } func (m *Manager) GetCorporationAssetsByTypeAndLocationID(ctx context.Context, corpID, typeID, locationID int) (res []*Asset, err error) { + if ctx, err = m.corporationAuthContext(ctx, corpID); err != nil { + return nil, err + } assets, err := m.GetCorporationAssets(ctx, corpID) if err != nil { return nil, err @@ -60,6 +66,9 @@ func (m *Manager) GetCorporationAssetsByTypeAndLocationID(ctx context.Context, c } func (m *Manager) GetCorporationAsset(ctx context.Context, corpID int, itemID int) (res *Asset, err error) { + if ctx, err = m.corporationAuthContext(ctx, corpID); err != nil { + return nil, err + } res, err = m.getCorporationAssetFromDB(corpID, itemID) if err != nil { return nil, err @@ -88,6 +97,7 @@ func (m *Manager) GetAssetSystem(a *Asset) (*evedb.System, error) { // LocationID is a StationID case a.LocationID < 67000000: // LocationID is a conquerable station or outpost + default: // LocationID is in a container (or citadel) ca, err := m.getCorporationAssetFromDB(a.corpID, a.LocationID) diff --git a/vendor/github.com/motki/core/model/blueprint.go b/vendor/github.com/motki/core/model/blueprint.go index c26f453..f9b9db6 100644 --- a/vendor/github.com/motki/core/model/blueprint.go +++ b/vendor/github.com/motki/core/model/blueprint.go @@ -50,6 +50,9 @@ func blueprintFromEveAPI(bp *eveapi.Blueprint) *Blueprint { } func (m *Manager) GetCorporationBlueprints(ctx context.Context, corpID int) (jobs []*Blueprint, err error) { + if ctx, err = m.corporationAuthContext(ctx, corpID); err != nil { + return nil, err + } jobs, err = m.getCorporationBlueprintsFromDB(corpID) if err != nil { return nil, err diff --git a/vendor/github.com/motki/core/model/corporation.go b/vendor/github.com/motki/core/model/corp.go similarity index 60% rename from vendor/github.com/motki/core/model/corporation.go rename to vendor/github.com/motki/core/model/corp.go index b26e368..2a74a21 100644 --- a/vendor/github.com/motki/core/model/corporation.go +++ b/vendor/github.com/motki/core/model/corp.go @@ -8,15 +8,11 @@ import ( "time" "github.com/jackc/pgx" - "github.com/pkg/errors" "golang.org/x/net/context" "github.com/motki/core/eveapi" - "github.com/motki/core/log" ) -var ErrCorpNotRegistered = errors.New("ceo or director is not registered for the given corporation") - type Corporation struct { CorporationID int Name string @@ -281,161 +277,3 @@ func (m *Manager) apiCorporationDetailToDB(detail *CorporationDetail) (*Corporat } return detail, nil } - -type CorporationConfig struct { - OptIn bool - OptInBy int - OptInDate time.Time - - CreatedBy int - CreatedAt time.Time -} - -func (m *Manager) GetCorporationsOptedIn() ([]int, error) { - c, err := m.pool.Open() - if err != nil { - return nil, err - } - defer m.pool.Release(c) - rs, err := c.Query( - `SELECT - c.corporation_id - FROM app.corporation_settings c - WHERE c.opted_in = TRUE`) - if err != nil { - return nil, err - } - defer rs.Close() - var res []int - for rs.Next() { - i := 0 - if err = rs.Scan(&i); err != nil { - return nil, err - } - res = append(res, i) - } - return res, nil -} - -func (m *Manager) GetCorporationConfig(corpID int) (*CorporationConfig, error) { - c, err := m.pool.Open() - if err != nil { - return nil, err - } - defer m.pool.Release(c) - r := c.QueryRow( - `SELECT - c.opted_in - , c.opted_in_by - , c.opted_in_at - , c.created_by - , c.created_at - FROM app.corporation_settings c - WHERE c.corporation_id = $1`, corpID) - corp := &CorporationConfig{} - err = r.Scan( - &corp.OptIn, - &corp.OptInBy, - &corp.OptInDate, - &corp.CreatedBy, - &corp.CreatedAt, - ) - if err != nil { - if err == pgx.ErrNoRows { - return nil, ErrCorpNotRegistered - } - return nil, err - } - return corp, nil -} - -func (m *Manager) GetCorporationAuthorization(corpID int) (*Authorization, error) { - config, err := m.GetCorporationConfig(corpID) - if err != nil { - return nil, err - } - if !config.OptIn { - return nil, ErrCorpNotRegistered - } - user := &User{UserID: config.OptInBy} - return m.GetAuthorization(user, RoleDirector) -} - -func (m *Manager) SaveCorporationConfig(corpID int, detail *CorporationConfig) error { - db, err := m.pool.Open() - if err != nil { - return err - } - defer m.pool.Release(db) - _, err = db.Exec( - `INSERT INTO app.corporation_settings - ( - corporation_id - , opted_in - , opted_in_by - , opted_in_at - , created_by - , created_at - ) - VALUES($1, $2, $3, $4, $5, DEFAULT) - ON CONFLICT - ON CONSTRAINT "corporation_settings_pkey" - DO UPDATE - SET opted_in = EXCLUDED.opted_in - , opted_in_by = EXCLUDED.opted_in_by - , opted_in_at = EXCLUDED.opted_in_at`, - corpID, - detail.OptIn, - detail.OptInBy, - detail.OptInDate, - detail.CreatedBy, - ) - return err -} - -// UpdateCorporationData fetches updated data for all opted-in corporations. -// -// The function returned by this method is intended to be invoke in regular intervals. -func (m *Manager) UpdateCorporationDataFunc(logger log.Logger) func() error { - return func() error { - corps, err := m.GetCorporationsOptedIn() - if err != nil { - return err - } - if len(corps) == 0 { - logger.Debugf("no corporations opted in, not updating corp data") - return nil - } - for _, corpID := range corps { - logger.Debugf("updating data for corp %d", corpID) - a, err := m.GetCorporationAuthorization(corpID) - if err != nil { - logger.Errorf("error getting corp auth: %s", err.Error()) - continue - } - - ctx := a.Context() - if _, err := m.FetchCorporationDetail(ctx); err != nil { - logger.Errorf("error fetching corp details: %s", err.Error()) - } - if res, err := m.GetCorporationAssets(ctx, a.CorporationID); err != nil { - logger.Errorf("error fetching corp assets: %s", err.Error()) - } else { - logger.Debugf("fetched %d assets for corporation %d", len(res), a.CorporationID) - } - - if res, err := m.GetCorporationOrders(ctx, a.CorporationID); err != nil { - logger.Errorf("error fetching corp orders: %s", err.Error()) - } else { - logger.Debugf("fetched %d orders for corporation %d", len(res), a.CorporationID) - } - - if res, err := m.GetCorporationBlueprints(ctx, a.CorporationID); err != nil { - logger.Errorf("error fetching corp blueprints: %s", err.Error()) - } else { - logger.Debugf("fetched %d blueprints for corporation %d", len(res), a.CorporationID) - } - } - return nil - } -} diff --git a/vendor/github.com/motki/core/model/corp_auth.go b/vendor/github.com/motki/core/model/corp_auth.go new file mode 100644 index 0000000..1e001d4 --- /dev/null +++ b/vendor/github.com/motki/core/model/corp_auth.go @@ -0,0 +1,193 @@ +package model + +import ( + "time" + + "github.com/jackc/pgx" + "github.com/pkg/errors" + "golang.org/x/net/context" + + "github.com/motki/core/log" + "github.com/motki/core/model/internal/cache" +) + +var ErrCorpNotRegistered = errors.New("ceo or director is not registered for the given corporation") + +type CorporationConfig struct { + OptIn bool + OptInBy int + OptInDate time.Time + + CreatedBy int + CreatedAt time.Time +} + +func (m *Manager) GetCorporationsOptedIn() ([]int, error) { + c, err := m.pool.Open() + if err != nil { + return nil, err + } + defer m.pool.Release(c) + rs, err := c.Query( + `SELECT + c.corporation_id + FROM app.corporation_settings c + WHERE c.opted_in = TRUE`) + if err != nil { + return nil, err + } + defer rs.Close() + var res []int + for rs.Next() { + i := 0 + if err = rs.Scan(&i); err != nil { + return nil, err + } + res = append(res, i) + } + return res, nil +} + +func (m *Manager) GetCorporationConfig(corpID int) (*CorporationConfig, error) { + c, err := m.pool.Open() + if err != nil { + return nil, err + } + defer m.pool.Release(c) + r := c.QueryRow( + `SELECT + c.opted_in + , c.opted_in_by + , c.opted_in_at + , c.created_by + , c.created_at + FROM app.corporation_settings c + WHERE c.corporation_id = $1`, corpID) + corp := &CorporationConfig{} + err = r.Scan( + &corp.OptIn, + &corp.OptInBy, + &corp.OptInDate, + &corp.CreatedBy, + &corp.CreatedAt, + ) + if err != nil { + if err == pgx.ErrNoRows { + return nil, ErrCorpNotRegistered + } + return nil, err + } + return corp, nil +} + +func (m *Manager) GetCorporationAuthorization(corpID int) (*Authorization, error) { + v, err := m.cache.Memoize("corp_auth", corpID, func() (cache.Value, error) { + config, err := m.GetCorporationConfig(corpID) + if err != nil { + return nil, err + } + if !config.OptIn { + return nil, ErrCorpNotRegistered + } + user := &User{UserID: config.OptInBy} + return m.GetAuthorization(user, RoleDirector) + }) + if err != nil { + return nil, err + } else if a, ok := v.(*Authorization); ok { + return a, nil + } + return nil, errors.Errorf("expected *Authorization from cache, got %T", v) +} + +func (m *Manager) SaveCorporationConfig(corpID int, detail *CorporationConfig) error { + db, err := m.pool.Open() + if err != nil { + return err + } + defer m.pool.Release(db) + _, err = db.Exec( + `INSERT INTO app.corporation_settings + ( + corporation_id + , opted_in + , opted_in_by + , opted_in_at + , created_by + , created_at + ) + VALUES($1, $2, $3, $4, $5, DEFAULT) + ON CONFLICT + ON CONSTRAINT "corporation_settings_pkey" + DO UPDATE + SET opted_in = EXCLUDED.opted_in + , opted_in_by = EXCLUDED.opted_in_by + , opted_in_at = EXCLUDED.opted_in_at`, + corpID, + detail.OptIn, + detail.OptInBy, + detail.OptInDate, + detail.CreatedBy, + ) + return err +} + +func (m *Manager) corporationAuthContext(ctx context.Context, corpID int) (context.Context, error) { + if authctx, ok := ctx.(authContext); ok { + if authctx.CorporationID() != corpID { + return nil, errors.New("corpID mismatch") + } + } + a, err := m.GetCorporationAuthorization(corpID) + if err != nil { + return nil, err + } + return a.Context(), nil +} + +// UpdateCorporationData fetches updated data for all opted-in corporations. +// +// The function returned by this method is intended to be invoke in regular intervals. +func (m *Manager) UpdateCorporationDataFunc(logger log.Logger) func() error { + return func() error { + corps, err := m.GetCorporationsOptedIn() + if err != nil { + return err + } + if len(corps) == 0 { + logger.Debugf("no corporations opted in, not updating corp data") + return nil + } + for _, corpID := range corps { + logger.Debugf("updating data for corp %d", corpID) + a, err := m.GetCorporationAuthorization(corpID) + if err != nil { + logger.Errorf("error getting corp auth: %s", err.Error()) + continue + } + + ctx := a.Context() + if _, err := m.FetchCorporationDetail(ctx); err != nil { + logger.Errorf("error fetching corp details: %s", err.Error()) + } + if res, err := m.GetCorporationAssets(ctx, a.CorporationID); err != nil { + logger.Errorf("error fetching corp assets: %s", err.Error()) + } else { + logger.Debugf("fetched %d assets for corporation %d", len(res), a.CorporationID) + } + + if res, err := m.GetCorporationOrders(ctx, a.CorporationID); err != nil { + logger.Errorf("error fetching corp orders: %s", err.Error()) + } else { + logger.Debugf("fetched %d orders for corporation %d", len(res), a.CorporationID) + } + + if res, err := m.GetCorporationBlueprints(ctx, a.CorporationID); err != nil { + logger.Errorf("error fetching corp blueprints: %s", err.Error()) + } else { + logger.Debugf("fetched %d blueprints for corporation %d", len(res), a.CorporationID) + } + } + return nil + } +} diff --git a/vendor/github.com/motki/core/model/industry.go b/vendor/github.com/motki/core/model/industry.go index 058b457..b81fc9a 100644 --- a/vendor/github.com/motki/core/model/industry.go +++ b/vendor/github.com/motki/core/model/industry.go @@ -7,6 +7,9 @@ import ( ) func (m *Manager) GetCorporationIndustryJobs(ctx context.Context, corpID int) (jobs []*eveapi.IndustryJob, err error) { + if ctx, err = m.corporationAuthContext(ctx, corpID); err != nil { + return nil, err + } jobs, err = m.getCorporationIndustryJobsFromDB(corpID) if err != nil { return nil, err diff --git a/vendor/github.com/motki/core/model/internal/cache/cache.go b/vendor/github.com/motki/core/model/internal/cache/cache.go new file mode 100644 index 0000000..dd5d924 --- /dev/null +++ b/vendor/github.com/motki/core/model/internal/cache/cache.go @@ -0,0 +1,164 @@ +package cache + +import ( + "strconv" + "sync" + "time" +) + +type Value interface{} + +type key struct { + kind string + id int +} + +func (c key) String() string { + return c.kind + ":" + strconv.Itoa(c.id) +} + +type item struct { + value Value + expires time.Time +} + +func (c *item) expired() bool { + return time.Now().After(c.expires) +} + +type Bucket struct { + items map[key]*item + mu *sync.RWMutex + ttl time.Duration + quit chan struct{} +} + +func New(ttl time.Duration) *Bucket { + b := &Bucket{ + items: make(map[key]*item), + mu: &sync.RWMutex{}, + ttl: ttl, + quit: make(chan struct{}), + } + exp := newExpunger(b) + go exp.processTags() + go exp.expungeExpiredEntries() + return b +} + +func (c *Bucket) Shutdown() error { + close(c.quit) + return nil +} + +func (c *Bucket) Get(kind string, id int) (Value, bool) { + k := key{kind, id} + c.mu.RLock() + it, ok := c.items[k] + c.mu.RUnlock() + if !ok { + return nil, false + + } else if it.expired() { + c.remove(k) + return nil, false + } + return it.value, true +} + +func (c *Bucket) Put(kind string, id int, val Value) { + c.mu.Lock() + defer c.mu.Unlock() + c.items[key{kind, id}] = &item{ + value: val, + expires: time.Now().Add(c.ttl), + } +} + +func (c *Bucket) Memoize(kind string, id int, vfn func() (Value, error)) (v Value, err error) { + var ok bool + if v, ok = c.Get(kind, id); ok { + return v, nil + } + defer func() { + if err == nil { + c.Put(kind, id, v) + } + }() + return vfn() +} + +func (c *Bucket) remove(keys ...key) { + c.mu.Lock() + defer c.mu.Unlock() + for _, k := range keys { + delete(c.items, k) + } +} + +type expunger struct { + b *Bucket + + interval time.Duration + + recs map[time.Time][]key + tags chan tag + mu *sync.Mutex +} + +type tag struct { + K key + T time.Time +} + +const expungeInterval = 60 * time.Second + +func newExpunger(b *Bucket) *expunger { + return &expunger{ + b: b, + interval: expungeInterval, + recs: make(map[time.Time][]key), + tags: make(chan tag, 10), + mu: &sync.Mutex{}, + } +} + +func (c *expunger) tag(k key, t time.Time) { + c.tags <- tag{k, t} +} + +func (c *expunger) processTags() { + for { + select { + case t := <-c.tags: + tick := t.T.Truncate(c.interval).Add(c.interval) + c.mu.Lock() + c.recs[tick] = append(c.recs[tick], t.K) + c.mu.Unlock() + + case <-c.b.quit: + return + } + } +} + +func (c *expunger) expungeExpiredEntries() { + for { + tick := time.Now().Truncate(c.interval) + for t, ks := range c.recs { + if tick.After(t) { + c.b.remove(ks...) + c.mu.Lock() + delete(c.recs, t) + c.mu.Unlock() + } + } + select { + case <-time.After(c.interval): + continue + + case <-c.b.quit: + return + } + } +} diff --git a/vendor/github.com/motki/core/model/internal/cache/cache_test.go b/vendor/github.com/motki/core/model/internal/cache/cache_test.go new file mode 100644 index 0000000..187716b --- /dev/null +++ b/vendor/github.com/motki/core/model/internal/cache/cache_test.go @@ -0,0 +1,99 @@ +package cache_test + +import ( + "testing" + "time" + + "github.com/motki/core/model/internal/cache" +) + +func Test(t *testing.T) { + expected := "value" + + b := cache.New(10 * time.Millisecond) + defer func() { + if err := b.Shutdown(); err != nil { + t.Errorf("error shutting down bucket: %s", err.Error()) + } + }() + b.Put("test", 0, expected) + + v, ok := b.Get("test", 0) + if !ok { + t.Errorf("expected value from cache, got nothing") + return + } + actual, ok := v.(string) + if !ok { + t.Errorf("expected value to be string, got %T", v) + return + } + if actual != expected { + t.Errorf("expected \"%s\", got \"%s\"", expected, actual) + } + + <-time.After(20 * time.Millisecond) + + _, ok = b.Get("test", 0) + if ok { + t.Errorf("expected value to be expunged from cache") + return + } +} + +func TestMemoize(t *testing.T) { + b := cache.New(10 * time.Millisecond) + defer func() { + if err := b.Shutdown(); err != nil { + t.Errorf("error shutting down bucket: %s", err.Error()) + } + }() + + calls := new(int) + + get := func() (cache.Value, error) { + return b.Memoize("test", 0, func() (cache.Value, error) { + *calls++ + return *calls, nil + }) + } + + for i := 0; i < 5; i++ { + v, err := get() + if err != nil { + t.Errorf("error getting value from cache: %s", err.Error()) + return + } + vi, ok := v.(int) + if !ok { + t.Errorf("expected int, got %T", v) + return + } + if vi != 1 { + t.Errorf("expected func to be called once, but was called %d times", vi) + return + } + } + + if *calls != 1 { + t.Errorf("expected func to be called once, but was called %d times", *calls) + return + } + + <-time.After(20 * time.Millisecond) + + v, err := get() + if err != nil { + t.Errorf("error getting value from cache: %s", err.Error()) + return + } + vi, ok := v.(int) + if !ok { + t.Errorf("expected int, got %T", v) + return + } + if vi != 2 { + t.Errorf("expected func to be called twice, but was called %d times", vi) + return + } +} diff --git a/vendor/github.com/motki/core/model/inventory.go b/vendor/github.com/motki/core/model/inventory.go index f46c79c..a77acd9 100644 --- a/vendor/github.com/motki/core/model/inventory.go +++ b/vendor/github.com/motki/core/model/inventory.go @@ -17,6 +17,9 @@ type InventoryItem struct { } func (m *Manager) GetCorporationInventory(ctx context.Context, corpID int) (items []*InventoryItem, err error) { + if ctx, err = m.corporationAuthContext(ctx, corpID); err != nil { + return nil, err + } c, err := m.pool.Open() if err != nil { return nil, err @@ -52,7 +55,7 @@ func (m *Manager) GetCorporationInventory(ctx context.Context, corpID int) (item if err != nil { return nil, err } - err = m.SaveInventoryItem(r) + err = m.SaveInventoryItem(ctx, r) if err != nil { return nil, err } @@ -83,6 +86,10 @@ func (m *Manager) updateInventoryItemLevel(ctx context.Context, item *InventoryI } func (m *Manager) NewInventoryItem(ctx context.Context, corpID, typeID, locationID int) (*InventoryItem, error) { + var err error + if ctx, err = m.corporationAuthContext(ctx, corpID); err != nil { + return nil, err + } c, err := m.pool.Open() if err != nil { return nil, err @@ -115,7 +122,11 @@ func (m *Manager) NewInventoryItem(ctx context.Context, corpID, typeID, location return it, nil } -func (m *Manager) SaveInventoryItem(item *InventoryItem) error { +func (m *Manager) SaveInventoryItem(ctx context.Context, item *InventoryItem) error { + var err error + if ctx, err = m.corporationAuthContext(ctx, item.CorporationID); err != nil { + return err + } c, err := m.pool.Open() if err != nil { return err diff --git a/vendor/github.com/motki/core/model/location.go b/vendor/github.com/motki/core/model/location.go new file mode 100644 index 0000000..815e7b5 --- /dev/null +++ b/vendor/github.com/motki/core/model/location.go @@ -0,0 +1,154 @@ +package model + +import ( + "github.com/motki/core/eveapi" + "github.com/motki/core/evedb" + "github.com/pkg/errors" + "golang.org/x/net/context" +) + +// Location describes a station, structure, or solar system in the EVE universe. +// +// This is a basic abstraction over the loosely defined "location ID" found in various +// API responses. A Location may represent an NPC station, a player-owned Citadel, or +// simply a solar system. +// +// Any Location will contain, at a minimum, System, Constellation, and Region +// info. Station and Structure may be nil if the location is only as specific as a +// solar system. Otherwise, Station OR Structure will be populated, but never both. +type Location struct { + // The original LocationID this location represents. + LocationID int + + // The solar system this location is found in. + System *evedb.System + + // The constellation this location is found in. + Constellation *evedb.Constellation + + // The region this location is found in. + Region *evedb.Region + + // The NPC station at this location. May be nil. + Station *evedb.Station + + // The player-owned structure at this location. May be nil. + Structure *eveapi.Structure + + // Prevent other packages from creating this type. + noexport struct{} +} + +// IsStation returns true if the location is a NPC station. +// +// Use this method to determine if a location represents a NPC station. +func (l Location) IsStation() bool { + return l.Station != nil +} + +// IsCitadel returns true if the location is a player-controlled citadel. +// +// Use this method to determine if a location represents a player-controlled structure. +func (l Location) IsCitadel() bool { + return l.Structure != nil +} + +// IsSystem returns true if the location does not contain station or citadel information. +// +// Use this method to determine if a location is neither a station nor structure. In other +// words, this method returns true if the location is strictly just a solar system. +func (l Location) IsSystem() bool { + return l.Station == nil && l.Structure == nil +} + +// GetLocation attempts to resolve the given location. +func (m *Manager) GetLocation(ctx context.Context, locationID int) (*Location, error) { + // Magic numbers here are sourced from: + // - http://eveonline-third-party-documentation.readthedocs.io/en/latest/xmlapi/character/char_assetlist.html + // - https://oldforums.eveonline.com/?a=topic&threadID=667487 + const offsetOfficeIDToStationID = 6000001 + const legacyOutpostStart = 60014861 + const legacyOutpostEndInclusive = 60014928 + loc := &Location{} + var corpID int + if c, ok := authContextFromContext(ctx); ok { + corpID = c.CorporationID() + } + var err error + switch { + case locationID < 60000000: + // LocationID is a SystemID. + loc.System, err = m.evedb.GetSystem(locationID) + if err != nil { + return nil, err + } + + case locationID < 61000000: + // Location is a Station or legacy outpost. + if locationID >= legacyOutpostStart && locationID <= legacyOutpostEndInclusive { + // Conquerable outpost pre-dating player outposts. Not yet supported. + return nil, errors.Errorf("unable to determine details for locationID %d, conquerable outposts are not supported", locationID) + } + // Not a legacy outpost, must be a station. + loc.Station, err = m.evedb.GetStation(locationID) + + case locationID < 66000000: + // Location is a conquerable outpost. Not yet supported. + return nil, errors.Errorf("unable to determine details for locationID %d, conquerable outposts are not supported", locationID) + + case locationID < 67000000: + // LocationID is a rented office. + loc.Station, err = m.evedb.GetStation(locationID - offsetOfficeIDToStationID) + + default: + // LocationID is in a container or citadel. + if corpID != 0 { + // Corporation is opted-in, we can query for asset information. + if ca, err := m.GetCorporationAsset(ctx, corpID, locationID); err == nil { + // Found an asset with the given locationID, call GetLocation on the asset's location. + if loc, err = m.GetLocation(ctx, ca.LocationID); err != nil { + return nil, errors.Wrapf(err, "unable to determine details for locationID %d", locationID) + } + break + } else if err != ErrCorpNotRegistered { + return nil, errors.Wrapf(err, "unable to determine details for locationID %d", locationID) + } + } + // No corporation or corporation isn't opted-in, but locationID might be + // a public citadel. + s, err := m.GetStructure(ctx, locationID) + if err != nil { + return nil, err + } + loc.Structure = s + + } + + // Second round of resolution; ensure that loc.System gets populated. + switch { + case loc.System != nil: + // do nothing + + case loc.Structure != nil: + if loc.System, err = m.evedb.GetSystem(int(loc.Structure.SystemID)); err != nil { + return nil, err + } + + case loc.Station != nil: + if loc.System, err = m.evedb.GetSystem(loc.Station.SystemID); err != nil { + return nil, err + } + + default: + return nil, errors.Errorf("unable to determine details for locationID %d", locationID) + } + + // Finally, populate the Constellation and Region information. + if loc.Constellation, err = m.evedb.GetConstellation(loc.System.ConstellationID); err != nil { + return nil, err + } + if loc.Region, err = m.evedb.GetRegion(loc.System.RegionID); err != nil { + return nil, err + } + return loc, nil +} diff --git a/vendor/github.com/motki/core/model/model.go b/vendor/github.com/motki/core/model/model.go index d6c2800..91a4607 100644 --- a/vendor/github.com/motki/core/model/model.go +++ b/vendor/github.com/motki/core/model/model.go @@ -2,10 +2,13 @@ package model // import "github.com/motki/core/model" import ( + "time" + "github.com/motki/core/db" "github.com/motki/core/eveapi" "github.com/motki/core/evedb" "github.com/motki/core/evemarketer" + "github.com/motki/core/model/internal/cache" ) // A Manager is used to retrieve and save data. @@ -17,9 +20,11 @@ type Manager struct { evedb *evedb.EveDB eveapi *eveapi.EveAPI ec *evemarketer.EveMarketer + + cache *cache.Bucket } // NewManager creates a new Manager, ready for use. func NewManager(pool *db.ConnPool, evedb *evedb.EveDB, api *eveapi.EveAPI, ec *evemarketer.EveMarketer) *Manager { - return &Manager{pool: pool, evedb: evedb, eveapi: api, ec: ec} + return &Manager{pool: pool, evedb: evedb, eveapi: api, ec: ec, cache: cache.New(10 * time.Second)} } diff --git a/vendor/github.com/motki/core/model/order.go b/vendor/github.com/motki/core/model/order.go index 6bebb27..8c49fc7 100644 --- a/vendor/github.com/motki/core/model/order.go +++ b/vendor/github.com/motki/core/model/order.go @@ -31,6 +31,10 @@ type MarketOrder struct { } func (m *Manager) GetCorporationOrder(ctx context.Context, corpID, orderID int) (*MarketOrder, error) { + var err error + if ctx, err = m.corporationAuthContext(ctx, corpID); err != nil { + return nil, err + } order, err := m.getCorporationOrderFromDB(corpID, orderID) if err != nil && err != pgx.ErrNoRows { return nil, err @@ -127,6 +131,9 @@ func (m *Manager) getCorporationOrderFromAPI(ctx context.Context, corpID, orderI } func (m *Manager) GetCorporationOrders(ctx context.Context, corpID int) (orders []*MarketOrder, err error) { + if ctx, err = m.corporationAuthContext(ctx, corpID); err != nil { + return nil, err + } orders, err = m.getCorporationOrdersFromDB(corpID) if err != nil && err != pgx.ErrNoRows { return nil, err diff --git a/vendor/github.com/motki/core/model/product.go b/vendor/github.com/motki/core/model/product.go index 26ed53c..a3fa080 100644 --- a/vendor/github.com/motki/core/model/product.go +++ b/vendor/github.com/motki/core/model/product.go @@ -1,6 +1,7 @@ package model import ( + "bytes" "database/sql" "strconv" "strings" @@ -8,6 +9,7 @@ import ( "github.com/jackc/pgx" "github.com/pkg/errors" "github.com/shopspring/decimal" + "golang.org/x/net/context" "github.com/motki/core/evemarketer" ) @@ -83,6 +85,9 @@ func (p Product) Clone() *Product { // NewProduct creates a new production chain for the given corporation and type. func (m *Manager) NewProduct(corpID int, typeID int) (*Product, error) { + if _, err := m.corporationAuthContext(context.Background(), corpID); err != nil { + return nil, err + } bp, err := m.evedb.GetBlueprint(typeID) if err != nil { return nil, errors.Wrapf(err, "unable to create production chain for typeID %d", typeID) @@ -110,33 +115,92 @@ func (m *Manager) NewProduct(corpID int, typeID int) (*Product, error) { return p, nil } -// UpdateProductMarketPrices fetches the latest market data for the production chain. +// UpdateProductMarketPrices fetches the latest market data for the production +// chain in the specified region. +// +// This method updates the Product's regionID. To avoid this behavior, pass the +// current regionID in. +// +// err := m.UpdateProductMarketPrices(prod, prod.RegionID) func (m *Manager) UpdateProductMarketPrices(product *Product, regionID int) error { - stat, err := m.GetMarketStatRegion(regionID, product.TypeID) + if _, err := m.corporationAuthContext(context.Background(), product.CorporationID); err != nil { + return err + } + return m.updateProductsMarketPrices(regionID, product) +} + +func (m *Manager) updateProductsMarketPrices(regionID int, products ...*Product) error { + typeIDMap := make(map[int]struct{}) + var visitProduct func(*Product) + visitProduct = func(p *Product) { + typeIDMap[p.TypeID] = struct{}{} + for _, prod := range p.Materials { + visitProduct(prod) + } + } + for _, p := range products { + visitProduct(p) + } + var typeIDs []int + for id := range typeIDMap { + typeIDs = append(typeIDs, id) + } + firstID := typeIDs[0] + restIDs := typeIDs[1:] + stat, err := m.GetMarketStatRegion(regionID, firstID, restIDs...) if err != nil { - return errors.Wrapf(err, "unable to update production chain market price for typeID %d", product.TypeID) + return errors.Wrap(err, "unable to update production chain market prices") } - max := decimal.NewFromFloat(1000000000000) - var bestSell = max + bestSellMap := make(map[int]decimal.Decimal) for _, s := range stat { - if s.TypeID != product.TypeID { - continue - } if s.Kind != evemarketer.StatSell { continue } - if s.Min.LessThan(bestSell) { - bestSell = s.Min + if v, ok := bestSellMap[s.TypeID]; !ok || s.Min.LessThan(v) { + bestSellMap[s.TypeID] = s.Min } } - if bestSell.Equals(max) { - return errors.Errorf("no sell orders found for typeID %d in regionID %d", product.TypeID, regionID) + missing := make(map[int]struct{}) + for _, prod := range products { + if v, ok := bestSellMap[prod.TypeID]; ok { + prod.MarketPrice = v + prod.MarketRegionID = regionID + } else { + missing[prod.TypeID] = struct{}{} + } + } + if len(missing) > 0 { + buf := &bytes.Buffer{} + f := false + for id := range missing { + if f { + buf.WriteString(",") + } + f = true + buf.WriteString(strconv.Itoa(id)) + } + return errors.Errorf("unable to fetch market prices for type IDs: %s", buf.String()) } - product.MarketPrice = bestSell - product.MarketRegionID = regionID return nil } +func (m *Manager) UpdateProductMarketPricesRecursive(product *Product, regionID int) error { + if _, err := m.corporationAuthContext(context.Background(), product.CorporationID); err != nil { + return err + } + var prods []*Product + var visitProduct func(*Product) + visitProduct = func(p *Product) { + p.MarketRegionID = regionID + prods = append(prods, p) + for _, prod := range p.Materials { + visitProduct(prod) + } + } + visitProduct(product) + return m.updateProductsMarketPrices(regionID, prods...) +} + // saveProductWithTx attempts to insert or update the given product. // // This method does not commit or roll-back the transaction. @@ -201,6 +265,9 @@ func (m *Manager) saveProductWithTx(tx *pgx.Tx, product *Product) error { // // This function automatically handles both inserting and updating. func (m *Manager) SaveProduct(product *Product) error { + if _, err := m.corporationAuthContext(context.Background(), product.CorporationID); err != nil { + return err + } c, err := m.pool.Open() if err != nil { return err @@ -223,11 +290,17 @@ func (m *Manager) SaveProduct(product *Product) error { // GetAllProducts returns all production chains associated with the given corporation. func (m *Manager) GetAllProducts(corpID int) ([]*Product, error) { + if _, err := m.corporationAuthContext(context.Background(), corpID); err != nil { + return nil, err + } return m.getProducts(corpID) } // GetProduct returns a production chain for the given corporation and root product. func (m *Manager) GetProduct(corpID int, productID int) (*Product, error) { + if _, err := m.corporationAuthContext(context.Background(), corpID); err != nil { + return nil, err + } prods, err := m.getProducts(corpID, productID) if err != nil { return nil, err diff --git a/vendor/github.com/motki/core/model/structure.go b/vendor/github.com/motki/core/model/structure.go index 326d4d1..771bcb7 100644 --- a/vendor/github.com/motki/core/model/structure.go +++ b/vendor/github.com/motki/core/model/structure.go @@ -1,12 +1,19 @@ package model import ( + "database/sql" + "encoding/json" + "golang.org/x/net/context" "github.com/motki/core/eveapi" ) -func (m *Manager) GetCorporationStructures(ctx context.Context, corpID int) ([]*eveapi.Structure, error) { +func (m *Manager) GetCorporationStructures(ctx context.Context, corpID int) ([]*eveapi.CorporationStructure, error) { + var err error + if ctx, err = m.corporationAuthContext(ctx, corpID); err != nil { + return nil, err + } if jobs, err := m.getCorporationStructuresFromDB(corpID); err == nil && jobs != nil { return jobs, nil } else if err != nil { @@ -15,7 +22,33 @@ func (m *Manager) GetCorporationStructures(ctx context.Context, corpID int) ([]* return m.getCorporationStructuresFromAPI(ctx, corpID) } -func (m *Manager) getCorporationStructuresFromDB(corpID int) ([]*eveapi.Structure, error) { +func (m *Manager) GetStructure(ctx context.Context, structureID int) (*eveapi.Structure, error) { + c, err := m.pool.Open() + if err != nil { + return nil, err + } + defer m.pool.Release(c) + r := c.QueryRow( + `SELECT + c.structure_id + , c.name + , c.system_id + , c.type_id + FROM app.structures c + WHERE c.structure_id = $1 + AND c.fetched_at > (NOW() - INTERVAL '12 hours')`, structureID) + s := &eveapi.Structure{} + err = r.Scan(&s.StructureID, &s.Name, &s.SystemID, &s.TypeID) + if err != nil { + if err == sql.ErrNoRows { + return m.getStructureFromAPI(ctx, structureID) + } + return nil, err + } + return s, nil +} + +func (m *Manager) getCorporationStructuresFromDB(corpID int) ([]*eveapi.CorporationStructure, error) { c, err := m.pool.Open() if err != nil { return nil, err @@ -24,11 +57,18 @@ func (m *Manager) getCorporationStructuresFromDB(corpID int) ([]*eveapi.Structur rs, err := c.Query( `SELECT c.structure_id + , c.name , c.system_id , c.type_id , c.profile_id - , c.curr_vuln - , c.next_vuln + , c.fuel_expires + , c.services + , c.state_timer_start + , c.state_timer_end + , c.curr_state + , c.unanchors_at + , c.reinforce_weekday + , c.reinforce_hour FROM app.structures c WHERE c.corporation_id = $1 AND c.fetched_at > (NOW() - INTERVAL '12 hours')`, corpID) @@ -36,20 +76,33 @@ func (m *Manager) getCorporationStructuresFromDB(corpID int) ([]*eveapi.Structur return nil, err } defer rs.Close() - var res []*eveapi.Structure + var res []*eveapi.CorporationStructure for rs.Next() { - r := &eveapi.Structure{} + r := &eveapi.CorporationStructure{} + var s []byte err := rs.Scan( &r.StructureID, + &r.Name, &r.SystemID, &r.TypeID, &r.ProfileID, - &r.CurrentVuln, - &r.NextVuln, + &r.FuelExpires, + &s, + &r.StateStart, + &r.StateEnd, + &r.State, + &r.UnanchorsAt, + &r.VulnWeekday, + &r.VulnHour, ) if err != nil { return nil, err } + var srvs []string + if err = json.Unmarshal(s, &srvs); err != nil { + return nil, err + } + r.Services = srvs res = append(res, r) } if len(res) == 0 { @@ -58,7 +111,7 @@ func (m *Manager) getCorporationStructuresFromDB(corpID int) ([]*eveapi.Structur return res, nil } -func (m *Manager) getCorporationStructuresFromAPI(ctx context.Context, corpID int) ([]*eveapi.Structure, error) { +func (m *Manager) getCorporationStructuresFromAPI(ctx context.Context, corpID int) ([]*eveapi.CorporationStructure, error) { strucs, err := m.eveapi.GetCorporationStructures(ctx, corpID) if err != nil { return nil, err @@ -66,23 +119,51 @@ func (m *Manager) getCorporationStructuresFromAPI(ctx context.Context, corpID in return m.apiCorporationStructuresToDB(corpID, strucs) } -func (m *Manager) apiCorporationStructuresToDB(corpID int, strucs []*eveapi.Structure) ([]*eveapi.Structure, error) { +func (m *Manager) apiCorporationStructuresToDB(corpID int, strucs []*eveapi.CorporationStructure) ([]*eveapi.CorporationStructure, error) { db, err := m.pool.Open() if err != nil { return nil, err } defer m.pool.Release(db) for _, struc := range strucs { + b, err := json.Marshal(struc.Services) + if err != nil { + return nil, err + } _, err = db.Exec( `INSERT INTO app.structures - VALUES($1, $2, $3, $4, $5, $6, $7, DEFAULT)`, - corpID, + (structure_id, corporation_id, system_id, type_id, profile_id, + fuel_expires, services, state_timer_start, state_timer_end, + curr_state, unanchors_at, reinforce_weekday, reinforce_hour, + name, fetched_at) + VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, DEFAULT) + ON CONFLICT ON CONSTRAINT "structures_pkey" + DO UPDATE SET corporation_id = EXCLUDED.corporation_id, + name = EXCLUDED.name, + profile_id = EXCLUDED.profile_id, + unanchors_at = EXCLUDED.unanchors_at, + state_timer_start = EXCLUDED.state_timer_start, + state_timer_end = EXCLUDED.state_timer_end, + services = EXCLUDED.services, + fuel_expires = EXCLUDED.fuel_expires, + curr_state = EXCLUDED.curr_state, + reinforce_weekday = EXCLUDED.reinforce_weekday, + reinforce_hour = EXCLUDED.reinforce_hour, + fetched_at = DEFAULT`, struc.StructureID, + corpID, struc.SystemID, struc.TypeID, struc.ProfileID, - struc.CurrentVuln, - struc.NextVuln, + struc.FuelExpires, + b, + struc.StateStart, + struc.StateEnd, + struc.State, + struc.UnanchorsAt, + struc.VulnWeekday, + struc.VulnHour, + struc.Name, ) if err != nil { return nil, err @@ -90,3 +171,35 @@ func (m *Manager) apiCorporationStructuresToDB(corpID int, strucs []*eveapi.Stru } return strucs, nil } + +func (m *Manager) getStructureFromAPI(ctx context.Context, structureID int) (*eveapi.Structure, error) { + s, err := m.eveapi.GetStructure(ctx, int64(structureID)) + if err != nil { + return nil, err + } + return m.apiStructureToDB(s) +} + +func (m *Manager) apiStructureToDB(struc *eveapi.Structure) (*eveapi.Structure, error) { + db, err := m.pool.Open() + if err != nil { + return nil, err + } + defer m.pool.Release(db) + _, err = db.Exec( + `INSERT INTO app.structures + (structure_id, system_id, type_id, name, fetched_at) + VALUES($1, $2, $3, $4, DEFAULT) + ON CONFLICT ON CONSTRAINT "structures_pkey" + DO UPDATE SET name = EXCLUDED.name, + fetched_at = DEFAULT`, + struc.StructureID, + struc.SystemID, + struc.TypeID, + struc.Name, + ) + if err != nil { + return nil, err + } + return struc, nil +} diff --git a/vendor/github.com/motki/core/model/user.go b/vendor/github.com/motki/core/model/user.go index 4f5ebc5..705d1db 100644 --- a/vendor/github.com/motki/core/model/user.go +++ b/vendor/github.com/motki/core/model/user.go @@ -4,19 +4,12 @@ import ( "crypto/rand" "crypto/sha256" "database/sql/driver" - "encoding/base64" - "encoding/json" "fmt" "strings" - "github.com/antihax/goesi" "github.com/jackc/pgx" "github.com/pkg/errors" "golang.org/x/crypto/bcrypt" - "golang.org/x/net/context" - "golang.org/x/oauth2" - - "github.com/motki/core/eveapi" ) type Role int @@ -154,240 +147,3 @@ func (m *Manager) VerifyUserEmail(email string, hash []byte) (bool, error) { r := res.RowsAffected() return r == 1, nil } - -func (m *Manager) AuthenticateUser(name, password string) (*User, string, error) { - var emptyKey = "" - db, err := m.pool.Open() - if err != nil { - return nil, emptyKey, err - } - defer m.pool.Release(db) - u := &User{} - var p []byte - row := db.QueryRow(`SELECT id, username, email, password FROM app.users WHERE username = $1 AND verified = 1 AND disabled <> 1`, name) - err = row.Scan(&u.UserID, &u.Name, &u.Email, &p) - if err != nil { - return nil, emptyKey, err - } - err = bcrypt.CompareHashAndPassword(p, []byte(password)) - if err != nil { - return nil, emptyKey, err - } - bk := make([]byte, 32) - n, err := rand.Read(bk) - if err != nil || n != len(bk) { - return nil, emptyKey, errors.New("unable to securely generate user session key") - } - key := base64.RawURLEncoding.EncodeToString(bk) - _, err = db.Exec(`INSERT INTO app.user_sessions (user_id, key) VALUES($1, $2) - ON CONFLICT ON CONSTRAINT "user_sessions_pkey" DO - UPDATE SET key = EXCLUDED.key, - last_seen_at = DEFAULT, - created_at = DEFAULT`, u.UserID, key) - if err != nil { - return nil, emptyKey, err - } - return u, key, nil -} - -func (m *Manager) GetUserBySessionKey(key string) (*User, error) { - db, err := m.pool.Open() - if err != nil { - return nil, err - } - defer m.pool.Release(db) - u := &User{} - row := db.QueryRow(`UPDATE app.user_sessions us - SET last_seen_at = NOW() - FROM ( - SELECT u.id, u.username, u.email - FROM app.users u - JOIN app.user_sessions s ON s.user_id = u.id - WHERE s.key = $1 - AND s.last_seen_at >= NOW() - INTERVAL '30 mins' - ) u - WHERE us.user_id = u.id - RETURNING u.id, u.username, u.email`, key) - err = row.Scan(&u.UserID, &u.Name, &u.Email) - if err != nil { - return nil, err - } - return u, nil -} - -func (m *Manager) SaveAuthorization(u *User, r Role, characterID int, tok *oauth2.Token) error { - b, err := json.Marshal(tok) - if err != nil { - return err - } - db, err := m.pool.Open() - if err != nil { - return err - } - defer m.pool.Release(db) - _, err = db.Exec( - `INSERT INTO app.user_authorizations - ( - user_id - , character_id - , role - , token - ) - VALUES($1, $2, $3, $4) - ON CONFLICT - ON CONSTRAINT "user_authorizations_pkey" - DO UPDATE - SET character_id = EXCLUDED.character_id - , token = EXCLUDED.token`, - u.UserID, - characterID, - int(r), - b, - ) - if err != nil { - return err - } - return nil -} - -func (m *Manager) GetAuthorization(user *User, role Role) (*Authorization, error) { - db, err := m.pool.Open() - if err != nil { - return nil, err - } - defer m.pool.Release(db) - a := &Authorization{} - token := &oAuth2Token{} - var b []byte - ri := 0 - row := db.QueryRow( - `SELECT - user_id - , character_id - , "role" - , token - FROM app.user_authorizations - WHERE user_id = $1 - AND "role" = $2`, - user.UserID, - role) - err = row.Scan(&a.UserID, &a.CharacterID, &ri, &b) - a.Role = Role(ri) - if err != nil { - if err == pgx.ErrNoRows { - return nil, errors.New("not authorized") - } - return nil, err - } - err = token.Scan(b) - if err != nil { - return nil, err - } - a.Token = (*oauth2.Token)(token) - source, err := m.eveapi.TokenSource(a.Token) - if err != nil { - return nil, err - } - info, err := m.eveapi.Verify(source) - if err != nil { - return nil, err - } - if int(info.CharacterID) != a.CharacterID { - return nil, errors.New("expected character IDs to match!") - } - a.source = source - // Force retrieval of current char info from the API - char, err := m.getCharacterFromAPI(a.CharacterID) - if err != nil { - return nil, err - } - a.CorporationID = char.CorporationID - return a, nil -} - -func (m *Manager) RemoveAuthorization(user *User, role Role) error { - db, err := m.pool.Open() - if err != nil { - return err - } - defer m.pool.Release(db) - _, err = db.Exec( - `DELETE - FROM app.user_authorizations - WHERE user_id = $1 AND "role" = $2`, - user.UserID, - int(role)) - return err -} - -type oAuth2Token oauth2.Token - -func (r *oAuth2Token) Value() (driver.Value, error) { - return json.Marshal(r) -} - -func (r *oAuth2Token) Scan(src interface{}) error { - s, ok := src.([]byte) - if !ok { - return fmt.Errorf("invalid value for token: %v", src) - } - return json.Unmarshal(s, &r) -} - -type Authorization struct { - UserID int - CharacterID int - CorporationID int - Role Role - Token *oauth2.Token - source oauth2.TokenSource -} - -func (a *Authorization) Context() context.Context { - return context.WithValue(context.Background(), goesi.ContextOAuth2, a.source) -} - -var ( - userScopes = []string{ - eveapi.ScopePublicData, - eveapi.ScopeESISkillsReadSkills, - eveapi.ScopeESISkillsReadSkillQueue, - eveapi.ScopeESIKillmailsReadKillmails, - } - logisticsScopes = []string{ - eveapi.ScopeCharacterAssetsRead, - eveapi.ScopeCharacterIndustryJobsRead, - eveapi.ScopeCharacterMarketOrdersRead, - eveapi.ScopeCharacterWalletRead, - eveapi.ScopeCorporationMarketOrdersRead, - eveapi.ScopeCorporationIndustryJobsRead, - eveapi.ScopeCorporationWalletRead, - eveapi.ScopeESISkillsReadSkills, - eveapi.ScopeESIUniverseReadStructures, - eveapi.ScopeESIAssetsReadAssets, - eveapi.ScopeESIWalletReadCharacterWallet, - eveapi.ScopeESIMarketsStructureMarkets, - eveapi.ScopeESIIndustryReadCharacterJobs, - eveapi.ScopeESIMarketsReadCharacterOrders, - eveapi.ScopeESICharactersReadBlueprints, - } - directorScopes = []string{ - eveapi.ScopeESICorporationsReadStructures, - eveapi.ScopeESICorporationsWriteStructures, - } -) - -func APIScopesForRole(r Role) []string { - switch r { - case RoleUser: - return userScopes - case RoleLogistics: - return logisticsScopes - case RoleDirector: - s := make([]string, len(logisticsScopes)) - copy(s, logisticsScopes) - return append(s, directorScopes...) - default: - return []string{} - } -} diff --git a/vendor/github.com/motki/core/model/user_auth.go b/vendor/github.com/motki/core/model/user_auth.go new file mode 100644 index 0000000..df7c2df --- /dev/null +++ b/vendor/github.com/motki/core/model/user_auth.go @@ -0,0 +1,207 @@ +package model + +import ( + "crypto/rand" + "database/sql/driver" + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/jackc/pgx" + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" + "golang.org/x/net/context" + "golang.org/x/oauth2" +) + +func (m *Manager) AuthenticateUser(name, password string) (*User, string, error) { + var emptyKey = "" + db, err := m.pool.Open() + if err != nil { + return nil, emptyKey, err + } + defer m.pool.Release(db) + u := &User{} + var p []byte + row := db.QueryRow(`SELECT id, username, email, password FROM app.users WHERE username = $1 AND verified = 1 AND disabled <> 1`, name) + err = row.Scan(&u.UserID, &u.Name, &u.Email, &p) + if err != nil { + return nil, emptyKey, err + } + err = bcrypt.CompareHashAndPassword(p, []byte(password)) + if err != nil { + return nil, emptyKey, err + } + bk := make([]byte, 32) + n, err := rand.Read(bk) + if err != nil || n != len(bk) { + return nil, emptyKey, errors.New("unable to securely generate user session key") + } + key := base64.RawURLEncoding.EncodeToString(bk) + _, err = db.Exec(`INSERT INTO app.user_sessions (user_id, key) VALUES($1, $2) + ON CONFLICT ON CONSTRAINT "user_sessions_pkey" DO + UPDATE SET key = EXCLUDED.key, + last_seen_at = DEFAULT, + created_at = DEFAULT`, u.UserID, key) + if err != nil { + return nil, emptyKey, err + } + return u, key, nil +} + +func (m *Manager) GetUserBySessionKey(key string) (*User, error) { + db, err := m.pool.Open() + if err != nil { + return nil, err + } + defer m.pool.Release(db) + u := &User{} + row := db.QueryRow(`UPDATE app.user_sessions us + SET last_seen_at = NOW() + FROM ( + SELECT u.id, u.username, u.email + FROM app.users u + JOIN app.user_sessions s ON s.user_id = u.id + WHERE s.key = $1 + AND s.last_seen_at >= NOW() - INTERVAL '30 mins' + ) u + WHERE us.user_id = u.id + RETURNING u.id, u.username, u.email`, key) + err = row.Scan(&u.UserID, &u.Name, &u.Email) + if err != nil { + return nil, err + } + return u, nil +} + +func (m *Manager) SaveAuthorization(u *User, r Role, characterID int, tok *oauth2.Token) error { + b, err := json.Marshal(tok) + if err != nil { + return err + } + db, err := m.pool.Open() + if err != nil { + return err + } + defer m.pool.Release(db) + _, err = db.Exec( + `INSERT INTO app.user_authorizations + ( + user_id + , character_id + , role + , token + ) + VALUES($1, $2, $3, $4) + ON CONFLICT + ON CONSTRAINT "user_authorizations_pkey" + DO UPDATE + SET character_id = EXCLUDED.character_id + , token = EXCLUDED.token`, + u.UserID, + characterID, + int(r), + b, + ) + if err != nil { + return err + } + return nil +} + +func (m *Manager) GetAuthorization(user *User, role Role) (*Authorization, error) { + db, err := m.pool.Open() + if err != nil { + return nil, err + } + defer m.pool.Release(db) + a := &Authorization{} + token := &oAuth2Token{} + var b []byte + ri := 0 + row := db.QueryRow( + `SELECT + user_id + , character_id + , "role" + , token + FROM app.user_authorizations + WHERE user_id = $1 + AND "role" = $2`, + user.UserID, + role) + err = row.Scan(&a.UserID, &a.CharacterID, &ri, &b) + a.Role = Role(ri) + if err != nil { + if err == pgx.ErrNoRows { + return nil, errors.New("not authorized") + } + return nil, err + } + err = token.Scan(b) + if err != nil { + return nil, err + } + a.Token = (*oauth2.Token)(token) + source, err := m.eveapi.TokenSource(a.Token) + if err != nil { + return nil, err + } + info, err := m.eveapi.Verify(source) + if err != nil { + return nil, err + } + if int(info.CharacterID) != a.CharacterID { + return nil, errors.New("expected character IDs to match!") + } + a.source = source + // Force retrieval of current char info from the API + char, err := m.getCharacterFromAPI(a.CharacterID) + if err != nil { + return nil, err + } + a.CorporationID = char.CorporationID + return a, nil +} + +func (m *Manager) RemoveAuthorization(user *User, role Role) error { + db, err := m.pool.Open() + if err != nil { + return err + } + defer m.pool.Release(db) + _, err = db.Exec( + `DELETE + FROM app.user_authorizations + WHERE user_id = $1 AND "role" = $2`, + user.UserID, + int(role)) + return err +} + +type oAuth2Token oauth2.Token + +func (r *oAuth2Token) Value() (driver.Value, error) { + return json.Marshal(r) +} + +func (r *oAuth2Token) Scan(src interface{}) error { + s, ok := src.([]byte) + if !ok { + return fmt.Errorf("invalid value for token: %v", src) + } + return json.Unmarshal(s, &r) +} + +type Authorization struct { + UserID int + CharacterID int + CorporationID int + Role Role + Token *oauth2.Token + source oauth2.TokenSource +} + +func (a *Authorization) Context() context.Context { + return newAuthContext(a) +} diff --git a/vendor/github.com/motki/core/model/user_context.go b/vendor/github.com/motki/core/model/user_context.go new file mode 100644 index 0000000..95b0e74 --- /dev/null +++ b/vendor/github.com/motki/core/model/user_context.go @@ -0,0 +1,51 @@ +package model + +import ( + "github.com/antihax/goesi" + "golang.org/x/net/context" +) + +type authContext interface { + context.Context + + UserID() int + + Role() Role + CharacterID() int + CorporationID() int +} + +type authContextImpl struct { + context.Context + a *Authorization +} + +func (ctx authContextImpl) CorporationID() int { + return ctx.a.CorporationID +} + +func (ctx authContextImpl) CharacterID() int { + return ctx.a.CharacterID +} + +func (ctx authContextImpl) UserID() int { + return ctx.a.UserID +} + +func (ctx authContextImpl) Role() Role { + return ctx.a.Role +} + +func newAuthContext(a *Authorization) context.Context { + return authContextImpl{ + Context: context.WithValue(context.Background(), goesi.ContextOAuth2, a.source), + a: a, + } +} + +func authContextFromContext(ctx context.Context) (authContext, bool) { + if v, ok := ctx.(authContext); ok { + return v, true + } + return nil, false +} diff --git a/vendor/github.com/motki/core/model/user_scopes.go b/vendor/github.com/motki/core/model/user_scopes.go new file mode 100644 index 0000000..f2ac9e2 --- /dev/null +++ b/vendor/github.com/motki/core/model/user_scopes.go @@ -0,0 +1,48 @@ +package model + +import "github.com/motki/core/eveapi" + +var ( + userScopes = []string{ + eveapi.ScopePublicData, + eveapi.ScopeESISkillsReadSkills, + eveapi.ScopeESISkillsReadSkillQueue, + eveapi.ScopeESIKillmailsReadKillmails, + } + logisticsScopes = []string{ + eveapi.ScopeCharacterAssetsRead, + eveapi.ScopeCharacterIndustryJobsRead, + eveapi.ScopeCharacterMarketOrdersRead, + eveapi.ScopeCharacterWalletRead, + eveapi.ScopeCorporationMarketOrdersRead, + eveapi.ScopeCorporationIndustryJobsRead, + eveapi.ScopeCorporationWalletRead, + eveapi.ScopeESISkillsReadSkills, + eveapi.ScopeESIUniverseReadStructures, + eveapi.ScopeESIAssetsReadAssets, + eveapi.ScopeESIWalletReadCharacterWallet, + eveapi.ScopeESIMarketsStructureMarkets, + eveapi.ScopeESIIndustryReadCharacterJobs, + eveapi.ScopeESIMarketsReadCharacterOrders, + eveapi.ScopeESICharactersReadBlueprints, + } + directorScopes = []string{ + eveapi.ScopeESICorporationsReadStructures, + eveapi.ScopeESICorporationsWriteStructures, + } +) + +func APIScopesForRole(r Role) []string { + switch r { + case RoleUser: + return userScopes + case RoleLogistics: + return logisticsScopes + case RoleDirector: + s := make([]string, len(logisticsScopes)) + copy(s, logisticsScopes) + return append(s, directorScopes...) + default: + return []string{} + } +} diff --git a/vendor/github.com/motki/core/proto/server/inventory.go b/vendor/github.com/motki/core/proto/server/inventory.go index 4c67781..0f5c6b2 100644 --- a/vendor/github.com/motki/core/proto/server/inventory.go +++ b/vendor/github.com/motki/core/proto/server/inventory.go @@ -108,9 +108,13 @@ func (srv *grpcServer) SaveInventoryItem(ctx context.Context, req *proto.SaveInv if err != nil { return nil, err } + corpAuth, err := srv.model.GetCorporationAuthorization(char.CorporationID) + if err != nil { + return nil, err + } it := proto.ProtoToInventoryItem(req.Item) it.CorporationID = char.CorporationID - if err := srv.model.SaveInventoryItem(it); err != nil { + if err := srv.model.SaveInventoryItem(corpAuth.Context(), it); err != nil { return nil, err } return &proto.InventoryItemResponse{ diff --git a/vendor/github.com/sirupsen/logrus/example_basic_test.go b/vendor/github.com/sirupsen/logrus/example_basic_test.go index c7b2d63..a2acf55 100644 --- a/vendor/github.com/sirupsen/logrus/example_basic_test.go +++ b/vendor/github.com/sirupsen/logrus/example_basic_test.go @@ -1,9 +1,8 @@ package logrus_test import ( - "os" - "github.com/sirupsen/logrus" + "os" ) func Example_basic() { diff --git a/vendor/github.com/sirupsen/logrus/example_hook_test.go b/vendor/github.com/sirupsen/logrus/example_hook_test.go index 5ec5dbf..d4ddffc 100644 --- a/vendor/github.com/sirupsen/logrus/example_hook_test.go +++ b/vendor/github.com/sirupsen/logrus/example_hook_test.go @@ -1,10 +1,9 @@ package logrus_test import ( - "os" - "github.com/sirupsen/logrus" "gopkg.in/gemnasium/logrus-airbrake-hook.v2" + "os" ) func Example_hook() { diff --git a/vendor/github.com/tyler-sommer/stick/value_test.go b/vendor/github.com/tyler-sommer/stick/value_test.go index 7b796bf..0a807d9 100644 --- a/vendor/github.com/tyler-sommer/stick/value_test.go +++ b/vendor/github.com/tyler-sommer/stick/value_test.go @@ -164,7 +164,7 @@ type propStruct struct { func TestGetAttr(t *testing.T) { var getAttrTests = []getAttrTest{ - newGetAttrTest("map with non-string keys", map[int]string{1: "test"}, 1, "test"), + newGetAttrTest("map with non-string keys", map[int]string{1:"test"}, 1, "test"), newGetAttrTest("anon struct property", struct{ Name string }{"Tyler"}, "Name", "Tyler"), newGetAttrTest("struct property", propStruct{"Jackie"}, "Name", "Jackie"), newGetAttrTest("struct method (value, ptr receiver)", testStruct{"John"}, "Name", "John"), diff --git a/vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go b/vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go index 5dafc5a..a2e1b57 100644 --- a/vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go +++ b/vendor/golang.org/x/crypto/ssh/terminal/util_solaris.go @@ -7,10 +7,9 @@ package terminal // import "golang.org/x/crypto/ssh/terminal" import ( + "golang.org/x/sys/unix" "io" "syscall" - - "golang.org/x/sys/unix" ) // State contains the state of a terminal. diff --git a/vendor/golang.org/x/net/http2/transport.go b/vendor/golang.org/x/net/http2/transport.go index a3fe975..e6b321f 100644 --- a/vendor/golang.org/x/net/http2/transport.go +++ b/vendor/golang.org/x/net/http2/transport.go @@ -321,9 +321,7 @@ func (noCachedConnError) Error() string { return "http2: no cached c // or its equivalent renamed type in net/http2's h2_bundle.go. Both types // may coexist in the same running program. func isNoCachedConnError(err error) bool { - _, ok := err.(interface { - IsHTTP2NoCachedConnError() - }) + _, ok := err.(interface{ IsHTTP2NoCachedConnError() }) return ok } diff --git a/vendor/golang.org/x/text/unicode/norm/normalize_test.go b/vendor/golang.org/x/text/unicode/norm/normalize_test.go index 4f83737..e3c0ac7 100644 --- a/vendor/golang.org/x/text/unicode/norm/normalize_test.go +++ b/vendor/golang.org/x/text/unicode/norm/normalize_test.go @@ -720,7 +720,7 @@ var appendTestsNFC = []AppendTest{ } var appendTestsNFD = []AppendTest{ -// TODO: Move some of the tests here. + // TODO: Move some of the tests here. } var appendTestsNFKC = []AppendTest{ diff --git a/views/industry/structures.html.twig b/views/industry/structures.html.twig index d0b4045..63539b0 100644 --- a/views/industry/structures.html.twig +++ b/views/industry/structures.html.twig @@ -15,8 +15,7 @@ System Type - Curr Vuln - Next Vuln + Name @@ -24,8 +23,7 @@ {{ helper.GetSystem(s.SystemID) }} {{ helper.GetType(s.TypeID) }} - {{ s.CurrentVuln }} - {{ s.NextVuln }} + {{ s.Name }} {% endfor %}