@@ -181,19 +181,30 @@ class AuthManager: | |||||
g.db.update_auth(cid, token, expires, refresh) | g.db.update_auth(cid, token, expires, refresh) | ||||
return token | return token | ||||
def _is_corp_member(self, token, char_id): | |||||
""""Return whether the given character is in the site's corp.""" | |||||
resp = self._eve.esi(token).v3.characters(char_id).get() | |||||
return resp.get("corporation_id") == self._config.get("corp.id") | |||||
def _check_access(self, token, char_id): | def _check_access(self, token, char_id): | ||||
""""Check whether the given character is allowed to access this site. | """"Check whether the given character is allowed to access this site. | ||||
If allowed, do nothing. If not, raise AccessDeniedError. | If allowed, do nothing. If not, raise AccessDeniedError. | ||||
""" | """ | ||||
resp = self._eve.esi(token).v3.characters(char_id).get() | |||||
if resp.get("corporation_id") != self._config.get("corp.id"): | |||||
if not self._is_corp_member(token, char_id): | |||||
self._debug("Access denied per corp membership for char id=%d " | self._debug("Access denied per corp membership for char id=%d " | ||||
"session id=%d", char_id, session["id"]) | "session id=%d", char_id, session["id"]) | ||||
g.db.drop_auth(char_id) | g.db.drop_auth(char_id) | ||||
self._invalidate_session() | self._invalidate_session() | ||||
raise AccessDeniedError() | raise AccessDeniedError() | ||||
def _cache_token(self, cid, token): | |||||
"""Cache the given token for this request.""" | |||||
if hasattr(g, "_cached_tokens"): | |||||
g._cached_tokens[cid] = token | |||||
else: | |||||
g._cached_tokens = {cid: token} | |||||
def _update_prop_cache(self, module, prop, value): | def _update_prop_cache(self, module, prop, value): | ||||
"""Update the value of a character module property in the cache.""" | """Update the value of a character module property in the cache.""" | ||||
if hasattr(g, "_character_modprops"): | if hasattr(g, "_character_modprops"): | ||||
@@ -282,21 +293,29 @@ class AuthManager: | |||||
self._update_prop_cache(module, prop, value) | self._update_prop_cache(module, prop, value) | ||||
return True | return True | ||||
def get_token(self): | |||||
"""Return a valid token for the current character, or None. | |||||
def get_token(self, cid=None): | |||||
"""Return a valid token for the given character. | |||||
If no character is given, we use the current session's character. If a | |||||
token couldn't be retrieved, return None. | |||||
Assuming this is called in a restricted route (following a True result | |||||
from is_authenticated), this function makes no API calls and should | |||||
always succeed. If it is called in other circumstances, it may fail and | |||||
return None. | |||||
Assuming we want the current character's token and this is called in a | |||||
restricted route (following a True result from is_authenticated), this | |||||
function makes no API calls and should always succeed. If it is called | |||||
in other circumstances, it may return None or raise EVEAPIError. | |||||
""" | """ | ||||
cid = self.get_character_id() | |||||
if cid is None: | |||||
cid = self.get_character_id() | |||||
if not cid: | if not cid: | ||||
return None | return None | ||||
if not hasattr(g, "_cached_token"): | |||||
g._cached_token = self._get_token(cid) | |||||
return g._cached_token | |||||
if hasattr(g, "_cached_tokens"): | |||||
if cid in g._cached_tokens: | |||||
return g._cached_tokens[cid] | |||||
token = self._get_token(cid) | |||||
self._cache_token(cid, token) | |||||
return token | |||||
def is_authenticated(self): | def is_authenticated(self): | ||||
"""Return whether the user has permission to access this site. | """Return whether the user has permission to access this site. | ||||
@@ -324,7 +343,7 @@ class AuthManager: | |||||
self._debug("Access granted for char id=%d session id=%d", cid, | self._debug("Access granted for char id=%d session id=%d", cid, | ||||
session["id"]) | session["id"]) | ||||
g.db.touch_session(session["id"]) | g.db.touch_session(session["id"]) | ||||
g._cached_token = token | |||||
self._cache_token(cid, token) | |||||
return True | return True | ||||
def make_login_link(self): | def make_login_link(self): | ||||
@@ -382,3 +401,35 @@ class AuthManager: | |||||
if "id" in session: | if "id" in session: | ||||
self._debug("Logging out session id=%d", session["id"]) | self._debug("Logging out session id=%d", session["id"]) | ||||
self._invalidate_session() | self._invalidate_session() | ||||
def get_valid_characters(self): | |||||
"""Iterate over all valid corp members that we have tokens for. | |||||
Each character is returned as a 2-tuple of (char_id, token). | |||||
This function may make a large number of API queries (up to three per | |||||
character in the corp), hence it is a generator. | |||||
""" | |||||
chars = g.db.get_authed_characters() | |||||
for cid, token, expires, refresh in chars: | |||||
seconds_til_expiry = (expires - datetime.utcnow()).total_seconds() | |||||
if seconds_til_expiry < self.EXPIRY_THRESHOLD: | |||||
result = self._fetch_new_token(refresh, refresh=True) | |||||
if not result: | |||||
self._debug("Couldn't refresh token for char id=%d", cid) | |||||
g.db.drop_auth(cid) | |||||
continue | |||||
token, expires, refresh, char_id, char_name = result | |||||
if char_id != cid: | |||||
self._debug("Refreshed token has incorrect char id=%d for " | |||||
"char id=%d", char_id, cid) | |||||
g.db.drop_auth(cid) | |||||
continue | |||||
g.db.put_character(cid, char_name) | |||||
g.db.update_auth(cid, token, expires, refresh) | |||||
if self._is_corp_member(token, cid): | |||||
yield cid, token |
@@ -204,6 +204,19 @@ class Database: | |||||
with self._conn as conn: | with self._conn as conn: | ||||
conn.execute("DELETE FROM auth WHERE auth_character = ?", (cid,)) | conn.execute("DELETE FROM auth WHERE auth_character = ?", (cid,)) | ||||
def get_authed_characters(self): | |||||
"""Return a list of characters with authentication info. | |||||
Each list item is a 4-tuple of (character_id, access_token, | |||||
token_expiry, refresh_token). | |||||
""" | |||||
query = """SELECT auth_character, auth_token, auth_token_expiry, | |||||
auth_refresh FROM auth""" | |||||
res = self._conn.execute(query).fetchall() | |||||
dtparse = lambda dt: datetime.strptime(dt, "%Y-%m-%d %H:%M:%S") | |||||
return [(cid, token, dtparse(expiry), refresh) | |||||
for (cid, token, expiry, refresh) in res] | |||||
def set_character_modprop(self, cid, module, prop, value): | def set_character_modprop(self, cid, module, prop, value): | ||||
"""Add or update a character module property.""" | """Add or update a character module property.""" | ||||
with self._conn as conn: | with self._conn as conn: | ||||
@@ -163,7 +163,7 @@ class EVESwaggerInterface: | |||||
self._data_source = "tranquility" | self._data_source = "tranquility" | ||||
self._cache = _ESICache() | self._cache = _ESICache() | ||||
def __call__(self, token): | |||||
def __call__(self, token=None): | |||||
return _ESIQueryBuilder(self, token) | return _ESIQueryBuilder(self, token) | ||||
def _do(self, method, query, params, data, token, can_cache=False): | def _do(self, method, query, params, data, token, can_cache=False): | ||||
@@ -183,10 +183,9 @@ class EVESwaggerInterface: | |||||
if cached is not None: | if cached is not None: | ||||
return cached | return cached | ||||
headers = { | |||||
"Accept": "application/json", | |||||
"Authorization": "Bearer " + token | |||||
} | |||||
headers = {"Accept": "application/json"} | |||||
if token is not None: | |||||
headers["Authorization"] = "Bearer " + token | |||||
params = params.copy() if params else {} | params = params.copy() if params else {} | ||||
params["datasource"] = self._data_source | params["datasource"] = self._data_source | ||||
url = self._base_url + query | url = self._base_url + query | ||||
@@ -198,7 +197,8 @@ class EVESwaggerInterface: | |||||
result = resp.json() if resp.content else None | result = resp.json() if resp.content else None | ||||
except (requests.RequestException, ValueError) as exc: | except (requests.RequestException, ValueError) as exc: | ||||
self._logger.exception("ESI request failed") | self._logger.exception("ESI request failed") | ||||
if hasattr(exc, "response") and exc.response.status_code == 403: | |||||
if hasattr(exc, "response") and (exc.response and | |||||
exc.response.status_code == 403): | |||||
raise EVEAPIForbiddenError() | raise EVEAPIForbiddenError() | ||||
raise EVEAPIError() | raise EVEAPIError() | ||||
@@ -113,6 +113,25 @@ class _Faction(_UniqueObject): | |||||
return self._data["name"] | return self._data["name"] | ||||
class _Type(_UniqueObject): | |||||
"""Represents any type, including ships and materials.""" | |||||
@property | |||||
def name(self): | |||||
"""The item's name, as a string.""" | |||||
return self._data["name"] | |||||
@property | |||||
def group_id(self): | |||||
"""The item's group ID, as an integer.""" | |||||
return self._data["group_id"] | |||||
@property | |||||
def market_group_id(self): | |||||
"""The item's market group ID, as an integer, or None.""" | |||||
return self._data.get("market_group_id") | |||||
class _Killable(_UniqueObject): | class _Killable(_UniqueObject): | ||||
"""Represents a killable object, like a ship, structure, or fighter.""" | """Represents a killable object, like a ship, structure, or fighter.""" | ||||
@@ -186,6 +205,17 @@ class _DummyFaction(_Faction): | |||||
}) | }) | ||||
class _DummyType(_Type): | |||||
"""Represents an unknown or invalid type.""" | |||||
def __init__(self, universe): | |||||
super().__init__(universe, -1, { | |||||
"name": "Unknown", | |||||
"group_id": -1, | |||||
"market_group_id": -1 | |||||
}) | |||||
class _DummyKillable(_Killable): | class _DummyKillable(_Killable): | ||||
"""Represents an unknown or invalid killable object.""" | """Represents an unknown or invalid killable object.""" | ||||
@@ -209,6 +239,7 @@ class Universe: | |||||
self._constellations = {} | self._constellations = {} | ||||
self._regions = {} | self._regions = {} | ||||
self._factions = {} | self._factions = {} | ||||
self._types = {} | |||||
self._killable_idx = {} | self._killable_idx = {} | ||||
self._killable_tab = {} | self._killable_tab = {} | ||||
@@ -237,11 +268,13 @@ class Universe: | |||||
self._factions = entities["factions"] | self._factions = entities["factions"] | ||||
del entities | del entities | ||||
types = self._load_yaml(self._dir / "types.yml.gz") | |||||
self._killable_idx = {kid: cat for cat, kids in types.items() | |||||
self._types = self._load_yaml(self._dir / "types.yml.gz") | |||||
killables = self._load_yaml(self._dir / "killables.yml.gz") | |||||
self._killable_idx = {kid: cat for cat, kids in killables.items() | |||||
for kid in kids} | for kid in kids} | ||||
self._killable_tab = types | |||||
del types | |||||
self._killable_tab = killables | |||||
del killables | |||||
self._loaded = True | self._loaded = True | ||||
@@ -285,6 +318,16 @@ class Universe: | |||||
return _DummyFaction(self) | return _DummyFaction(self) | ||||
return _Faction(self, fid, self._factions[fid]) | return _Faction(self, fid, self._factions[fid]) | ||||
def type(self, tid): | |||||
"""Return a _Type with the given ID. | |||||
If the ID is invalid, return a dummy unknown object with ID -1. | |||||
""" | |||||
self._load() | |||||
if tid not in self._types: | |||||
return _DummyKillable(self) | |||||
return _Type(self, tid, self._types[tid]) | |||||
def killable(self, kid): | def killable(self, kid): | ||||
"""Return a _Killable with the given ID. | """Return a _Killable with the given ID. | ||||
@@ -181,3 +181,43 @@ class CampaignDB: | |||||
}, | }, | ||||
"value": row[12] | "value": row[12] | ||||
} for row in res] | } for row in res] | ||||
def update_items(self, campaign, data): | |||||
"""Update all item details in the database for the given campaign. | |||||
The data should be a multi-layered dictionary. It maps operation names | |||||
to a dict that maps character IDs to a dict that maps type IDs to | |||||
integer counts. | |||||
""" | |||||
with self._conn as conn: | |||||
cur = conn.execute("BEGIN TRANSACTION") | |||||
query = "DELETE FROM oper_item WHERE oi_campaign = ?" | |||||
cur.execute(query, (campaign,)) | |||||
query = """INSERT INTO oper_item ( | |||||
oi_campaign, oi_operation, oi_character, oi_type, oi_count) | |||||
VALUES (?, ?, ?, ?, ?)""" | |||||
cur.executemany(query, [ | |||||
(campaign, operation, int(char_id), int(type_id), int(count)) | |||||
for operation, chars in data.items() | |||||
for char_id, types in chars.items() | |||||
for type_id, count in types.items()]) | |||||
def get_associated_items(self, campaign, operation, limit=5, offset=0): | |||||
"""Return a list of items associated with a campaign/operation. | |||||
Items are returned as 2-tuples of (item_type, item_count), most recent | |||||
first, up to a limit. Use -1 for no limit. | |||||
""" | |||||
if not isinstance(limit, int): | |||||
raise ValueError(limit) | |||||
if not isinstance(offset, int): | |||||
raise ValueError(offset) | |||||
query = """SELECT oi_type, SUM(oi_count) as total_count | |||||
FROM oper_item | |||||
WHERE oi_campaign = ? AND oi_operation = ? | |||||
GROUP BY oi_type ORDER BY total_count DESC LIMIT {} OFFSET {}""" | |||||
qform = query.format(limit, offset) | |||||
res = self._conn.execute(qform, (campaign, operation)).fetchall() | |||||
return [(type_id, count or 0) for type_id, count in res] |
@@ -62,8 +62,8 @@ def get_summary(cname, opname, limit=5): | |||||
kills = g.campaign_db.get_associated_kills(cname, opname, limit=limit) | kills = g.campaign_db.get_associated_kills(cname, opname, limit=limit) | ||||
return kills, "killboard_recent" | return kills, "killboard_recent" | ||||
elif optype == "collection": | elif optype == "collection": | ||||
... | |||||
return [], None | |||||
items = g.campaign_db.get_associated_items(cname, opname, limit=limit) | |||||
return items, "collection_items" | |||||
else: | else: | ||||
raise RuntimeError("Unknown operation type: %s" % optype) | raise RuntimeError("Unknown operation type: %s" % optype) | ||||
@@ -6,6 +6,7 @@ import textwrap | |||||
from flask import g | from flask import g | ||||
from .._provided import config, logger | from .._provided import config, logger | ||||
from ...exceptions import EVEAPIForbiddenError | |||||
__all__ = ["update_operation"] | __all__ = ["update_operation"] | ||||
@@ -17,7 +18,7 @@ def _save_operation(cname, opname, primary, secondary, key=None): | |||||
g.campaign_db.set_overview(cname, opname, primary, secondary) | g.campaign_db.set_overview(cname, opname, primary, secondary) | ||||
g.campaign_db.touch_operation(cname, opname, key=key) | g.campaign_db.touch_operation(cname, opname, key=key) | ||||
def _build_filter(qualifiers): | |||||
def _build_filter(qualifiers, arg): | |||||
"""Given a qualifiers string from the config, return a filter function. | """Given a qualifiers string from the config, return a filter function. | ||||
This function is extremely sensitive since it executes arbitrary Python | This function is extremely sensitive since it executes arbitrary Python | ||||
@@ -26,7 +27,7 @@ def _build_filter(qualifiers): | |||||
filesystem. | filesystem. | ||||
""" | """ | ||||
namespace = {"g": g} | namespace = {"g": g} | ||||
body = "def _func(kill):\n" + textwrap.indent(qualifiers, " " * 4) | |||||
body = ("def _func(%s):\n" % arg) + textwrap.indent(qualifiers, " " * 4) | |||||
exec(body, namespace) | exec(body, namespace) | ||||
return namespace["_func"] | return namespace["_func"] | ||||
@@ -52,7 +53,7 @@ def _update_killboard_operations(cname, opnames, min_kill_id): | |||||
filters = [] | filters = [] | ||||
for opname in opnames: | for opname in opnames: | ||||
qualif = operations[opname]["qualifiers"] | qualif = operations[opname]["qualifiers"] | ||||
filters.append((_build_filter(qualif), opname)) | |||||
filters.append((_build_filter(qualif, "kill"), opname)) | |||||
args = ["kills", "corporationID", g.config.get("corp.id"), "no-items", | args = ["kills", "corporationID", g.config.get("corp.id"), "no-items", | ||||
"no-attackers", "orderDirection", "asc"] | "no-attackers", "orderDirection", "asc"] | ||||
@@ -80,21 +81,64 @@ def _update_killboard_operations(cname, opnames, min_kill_id): | |||||
secondary = None | secondary = None | ||||
_save_operation(cname, opname, primary, secondary, key=max_kill_id) | _save_operation(cname, opname, primary, secondary, key=max_kill_id) | ||||
def _save_collection_overview(cname, opnames, data): | |||||
"""Save collection overview data to the database.""" | |||||
operations = config["campaigns"][cname]["operations"] | |||||
if any(operations[opname].get("isk", True) for opname in opnames): | |||||
pricelist = g.eve.esi().v1.markets.prices.get() | |||||
prices = {entry["type_id"]: entry["average_price"] | |||||
for entry in pricelist if "average_price" in entry} | |||||
else: | |||||
prices = {} | |||||
for opname in opnames: | |||||
primary = sum(sum(d.values()) for d in data[opname].values()) | |||||
show_isk = operations[opname].get("isk", True) | |||||
if show_isk: | |||||
secondary = sum(prices.get(typeid, 0.0) * count | |||||
for d in data[opname].values() | |||||
for typeid, count in d.items()) | |||||
else: | |||||
secondary = None | |||||
_save_operation(cname, opname, primary, secondary) | |||||
def _update_collection_operations(cname, opnames): | def _update_collection_operations(cname, opnames): | ||||
"""Update all collection-type operations in the given campaign subset.""" | """Update all collection-type operations in the given campaign subset.""" | ||||
campaign = config["campaigns"][cname] | |||||
operations = config["campaigns"][cname]["operations"] | |||||
filters = [] | |||||
for opname in opnames: | for opname in opnames: | ||||
operation = campaign["operations"][opname] | |||||
show_isk = operation.get("isk", True) | |||||
qualif = operations[opname]["qualifiers"] | |||||
filters.append((_build_filter(qualif, "asset"), opname)) | |||||
# store per-user counts; update for all users in corp who have fresh | |||||
# API keys and leave other data stale | |||||
... | |||||
primary = __import__("random").randint(10, 99) | |||||
secondary = __import__("random").randint(10000000, 5000000000) / 100 \ | |||||
if show_isk else None | |||||
data = {opname: {} for opname in opnames} | |||||
_save_operation(cname, opname, primary, secondary) | |||||
for char_id, token in g.auth.get_valid_characters(): | |||||
logger.debug("Fetching assets for char id=%d campaign=%s " | |||||
"operations=%s", char_id, cname, ",".join(opnames)) | |||||
try: | |||||
assets = g.eve.esi(token).v1.characters(char_id).assets.get() | |||||
except EVEAPIForbiddenError: | |||||
logger.debug("Asset access denied for char id=%d", char_id) | |||||
continue | |||||
for opname in opnames: | |||||
data[opname][char_id] = {} | |||||
logger.debug("Evaluating %d assets for char id=%d", | |||||
len(assets), char_id) | |||||
for asset in assets: | |||||
for filt, opname in filters: | |||||
if filt(asset): | |||||
typeid = asset["type_id"] | |||||
count = 1 if asset["is_singleton"] else asset["quantity"] | |||||
char = data[opname][char_id] | |||||
if typeid in char: | |||||
char[typeid] += count | |||||
else: | |||||
char[typeid] = count | |||||
g.campaign_db.update_items(cname, data) | |||||
_save_collection_overview(cname, opnames, data) | |||||
def update_operation(cname, opname, new=False): | def update_operation(cname, opname, new=False): | ||||
"""Update a campaign/operation. Assumes a thread-exclusive lock is held.""" | """Update a campaign/operation. Assumes a thread-exclusive lock is held.""" | ||||
@@ -48,7 +48,8 @@ campaigns: | |||||
# Report as "10 units" / "1 unit" of Tritanium | # Report as "10 units" / "1 unit" of Tritanium | ||||
unit: unit|units | unit: unit|units | ||||
qualifiers: |- | qualifiers: |- | ||||
return item_type == "Tritanium" # ... | |||||
type = g.eve.universe.type(asset["type_id"]) | |||||
return type.name == "Tritanium" | |||||
bar: | bar: | ||||
title: Save the Bar | title: Save the Bar | ||||
operations: [] | operations: [] |
@@ -56,7 +56,11 @@ DROP TABLE IF EXISTS oper_item; | |||||
CREATE TABLE oper_item ( | CREATE TABLE oper_item ( | ||||
oi_campaign TEXT, | oi_campaign TEXT, | ||||
oi_operation TEXT | |||||
-- ... | |||||
-- UNIQUE (oi_campaign, oi_operation, ...) | |||||
oi_operation TEXT, | |||||
oi_character INTEGER, | |||||
oi_type INTEGER, | |||||
oi_count INTEGER, | |||||
UNIQUE (oi_campaign, oi_operation, oi_character, oi_type) | |||||
); | ); | ||||
CREATE INDEX oi_campaign_operation ON oper_item (oi_campaign, oi_operation); |
@@ -66,23 +66,37 @@ def _load_typeids(sde_dir, groups): | |||||
assert data[_SOLAR_SYSTEM]["groupID"] == _SOLAR_SYSTEM | assert data[_SOLAR_SYSTEM]["groupID"] == _SOLAR_SYSTEM | ||||
assert data[_SOLAR_SYSTEM]["name"]["en"] == "Solar System" | assert data[_SOLAR_SYSTEM]["name"]["en"] == "Solar System" | ||||
types = {"ships": {}, "structures": {}, "fighters": {}} | |||||
types = {} | |||||
killables = {"ships": {}, "structures": {}, "fighters": {}} | |||||
cat_conv = {_SHIP_CAT: "ships", _FIGHTER_CAT: "fighters"} | cat_conv = {_SHIP_CAT: "ships", _FIGHTER_CAT: "fighters"} | ||||
cat_conv.update({cid: "structures" for cid in _STRUCT_CATS}) | cat_conv.update({cid: "structures" for cid in _STRUCT_CATS}) | ||||
group_conv = {gid: cid for cid, gids in groups.items() for gid in gids} | group_conv = {gid: cid for cid, gids in groups.items() for gid in gids} | ||||
for tid, type_ in data.items(): | for tid, type_ in data.items(): | ||||
name = type_["name"].get("en", "Unknown") | |||||
gid = type_["groupID"] | gid = type_["groupID"] | ||||
assert isinstance(tid, int) | |||||
assert isinstance(gid, int) | |||||
assert tid >= 0 | |||||
assert gid >= 0 | |||||
types[tid] = {"name": name, "group_id": gid} | |||||
if "marketGroupID" in type_: | |||||
mgid = type_["marketGroupID"] | |||||
assert isinstance(mgid, int) | |||||
assert mgid >= 0 | |||||
types[tid]["market_group_id"] = mgid | |||||
if gid in group_conv: | if gid in group_conv: | ||||
cid = group_conv[gid] | cid = group_conv[gid] | ||||
cname = cat_conv[cid] | cname = cat_conv[cid] | ||||
name = type_["name"]["en"] | |||||
group = groups[cid][gid] | group = groups[cid][gid] | ||||
assert isinstance(tid, int) | |||||
types[cname][tid] = {"name": name, "group": group} | |||||
killables[cname][tid] = {"name": name, "group": group} | |||||
print("done.") | print("done.") | ||||
return types | |||||
return types, killables | |||||
def _load_ids(sde_dir): | def _load_ids(sde_dir): | ||||
print("Loading itemIDs... ", end="", flush=True) | print("Loading itemIDs... ", end="", flush=True) | ||||
@@ -248,28 +262,27 @@ def _load_factions(sde_dir): | |||||
def _dump_types(out_dir, types): | def _dump_types(out_dir, types): | ||||
print("Dumping types... ", end="", flush=True) | print("Dumping types... ", end="", flush=True) | ||||
_save_yaml(out_dir / "types.yml", types) | _save_yaml(out_dir / "types.yml", types) | ||||
print("done.") | |||||
def _dump_killables(out_dir, killables): | |||||
print("Dumping killables... ", end="", flush=True) | |||||
_save_yaml(out_dir / "killables.yml", killables) | |||||
print("done.") | print("done.") | ||||
def _dump_galaxy(out_dir, galaxy): | def _dump_galaxy(out_dir, galaxy): | ||||
print("Dumping galaxy... ", end="", flush=True) | print("Dumping galaxy... ", end="", flush=True) | ||||
_save_yaml(out_dir / "galaxy.yml", galaxy) | _save_yaml(out_dir / "galaxy.yml", galaxy) | ||||
print("done.") | print("done.") | ||||
def _dump_entities(out_dir, factions): | def _dump_entities(out_dir, factions): | ||||
print("Dumping entities... ", end="", flush=True) | print("Dumping entities... ", end="", flush=True) | ||||
entities = {"factions": factions} | entities = {"factions": factions} | ||||
_save_yaml(out_dir / "entities.yml", entities) | _save_yaml(out_dir / "entities.yml", entities) | ||||
print("done.") | print("done.") | ||||
def _compress(out_dir): | def _compress(out_dir): | ||||
targets = ["types", "galaxy", "entities"] | |||||
targets = ["types", "killables", "galaxy", "entities"] | |||||
for basename in targets: | for basename in targets: | ||||
print("Compressing %s... " % basename, end="", flush=True) | print("Compressing %s... " % basename, end="", flush=True) | ||||
@@ -285,7 +298,7 @@ def _compress(out_dir): | |||||
def _cleanup(out_dir): | def _cleanup(out_dir): | ||||
print("Cleaning up... ", end="", flush=True) | print("Cleaning up... ", end="", flush=True) | ||||
targets = ["types", "galaxy", "entities"] | |||||
targets = ["types", "killables", "galaxy", "entities"] | |||||
for basename in targets: | for basename in targets: | ||||
(out_dir / (basename + ".yml")).unlink() | (out_dir / (basename + ".yml")).unlink() | ||||
@@ -299,9 +312,10 @@ def import_sde(sde_dir, out_dir): | |||||
_verify_categoryids(sde_dir) | _verify_categoryids(sde_dir) | ||||
groups = _load_groupids(sde_dir) | groups = _load_groupids(sde_dir) | ||||
types = _load_typeids(sde_dir, groups) | |||||
types, killables = _load_typeids(sde_dir, groups) | |||||
_dump_types(out_dir, types) | _dump_types(out_dir, types) | ||||
del groups, types | |||||
_dump_killables(out_dir, killables) | |||||
del groups, types, killables | |||||
ids = _load_ids(sde_dir) | ids = _load_ids(sde_dir) | ||||
print("Counts: regions=%d, constellations=%d, systems=%d" % ( | print("Counts: regions=%d, constellations=%d, systems=%d" % ( | ||||
@@ -17,35 +17,35 @@ | |||||
</a> | </a> | ||||
</td> | </td> | ||||
<td class="fluid extra"> | <td class="fluid extra"> | ||||
<a href="https://zkillboard.com/system/${system.id}/">${system.name}</a> <abbr title="${system.security}" class="${get_security_class(system.security)}">${format_security(system.security)}</abbr><br/> | |||||
<a href="https://zkillboard.com/region/${system.region.id}/">${system.region.name}</a> | |||||
<a href="https://zkillboard.com/system/${system.id}/">${system.name | h}</a> <abbr title="${system.security}" class="${get_security_class(system.security)}">${format_security(system.security)}</abbr><br/> | |||||
<a href="https://zkillboard.com/region/${system.region.id}/">${system.region.name | h}</a> | |||||
</td> | </td> | ||||
<td class="icon"> | <td class="icon"> | ||||
<a href="https://zkillboard.com/kill/${kill['id']}/"> | <a href="https://zkillboard.com/kill/${kill['id']}/"> | ||||
<img title="Kill ${kill['id']}: ${killed.name}" alt="Kill ${kill['id']}: ${killed.name}" src="${g.eve.image.inventory(victim["ship_id"], 64)}"/> | |||||
<img title="Kill ${kill['id']}: ${killed.name | h}" alt="Kill ${kill['id']}: ${killed.name | h}" src="${g.eve.image.inventory(victim["ship_id"], 64)}"/> | |||||
</a> | </a> | ||||
</td> | </td> | ||||
<td class="icon extra"> | <td class="icon extra"> | ||||
<a href="https://zkillboard.com/character/${victim['char_id']}/"> | <a href="https://zkillboard.com/character/${victim['char_id']}/"> | ||||
<img title="${victim['char_name']}" alt="${victim['char_name']}" src="${g.eve.image.character(victim["char_id"], 128)}"/> | |||||
<img title="${victim['char_name'] | h}" alt="${victim['char_name'] | h}" src="${g.eve.image.character(victim["char_id"], 128)}"/> | |||||
</a> | </a> | ||||
</td> | </td> | ||||
<td class="icon${' extra' if victim["alliance_id"] and victim["faction_id"] else ''}"> | <td class="icon${' extra' if victim["alliance_id"] and victim["faction_id"] else ''}"> | ||||
<a href="https://zkillboard.com/corporation/${victim['corp_id']}/"> | <a href="https://zkillboard.com/corporation/${victim['corp_id']}/"> | ||||
<img title="${victim['corp_name']}" alt="${victim['corp_name']}" src="${g.eve.image.corp(victim["corp_id"], 128)}"/> | |||||
<img title="${victim['corp_name'] | h}" alt="${victim['corp_name'] | h}" src="${g.eve.image.corp(victim["corp_id"], 128)}"/> | |||||
</a> | </a> | ||||
</td> | </td> | ||||
<td class="icon${'' if victim["alliance_id"] else ' extra'}"> | <td class="icon${'' if victim["alliance_id"] else ' extra'}"> | ||||
% if victim["alliance_id"]: | % if victim["alliance_id"]: | ||||
<a href="https://zkillboard.com/alliance/${victim['alliance_id']}/"> | <a href="https://zkillboard.com/alliance/${victim['alliance_id']}/"> | ||||
<img title="${victim['alliance_name']}" alt="${victim['alliance_name']}" src="${g.eve.image.alliance(victim["alliance_id"], 128)}"/> | |||||
<img title="${victim['alliance_name'] | h}" alt="${victim['alliance_name'] | h}" src="${g.eve.image.alliance(victim["alliance_id"], 128)}"/> | |||||
</a> | </a> | ||||
% endif | % endif | ||||
</td> | </td> | ||||
<td class="icon${'' if victim["faction_id"] else ' extra'}"> | <td class="icon${'' if victim["faction_id"] else ' extra'}"> | ||||
% if victim["faction_id"]: | % if victim["faction_id"]: | ||||
<a href="https://zkillboard.com/faction/${victim['faction_id']}/"> | <a href="https://zkillboard.com/faction/${victim['faction_id']}/"> | ||||
<img title="${victim['faction_name']}" alt="${victim['faction_name']}" src="${g.eve.image.faction(victim["faction_id"], 128)}"/> | |||||
<img title="${victim['faction_name'] | h}" alt="${victim['faction_name'] | h}" src="${g.eve.image.faction(victim["faction_id"], 128)}"/> | |||||
</a> | </a> | ||||
% endif | % endif | ||||
</td> | </td> | ||||
@@ -64,8 +64,17 @@ | |||||
</table> | </table> | ||||
</div> | </div> | ||||
</%def> | </%def> | ||||
<%def name="_collection_items(summary)"> | |||||
<div class="head">XXX:</div> | |||||
<div class="contents"> | |||||
${summary | h} | |||||
</div> | |||||
</%def> | |||||
<%def name="render_summary(renderer, summary)"><% | <%def name="render_summary(renderer, summary)"><% | ||||
if renderer == "killboard_recent": | if renderer == "killboard_recent": | ||||
return _killboard_recent(summary) | return _killboard_recent(summary) | ||||
if renderer == "collection_items": | |||||
return _collection_items(summary) | |||||
raise RuntimeError("Unknown renderer: %s" % renderer) | raise RuntimeError("Unknown renderer: %s" % renderer) | ||||
%></%def> | %></%def> |