A corporation manager and dashboard for EVE Online
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

173 lines
6.8 KiB

  1. # -*- coding: utf-8 -*-
  2. import sys
  3. import textwrap
  4. from flask import g
  5. from .._provided import config, logger
  6. from ...exceptions import EVEAPIForbiddenError
  7. __all__ = ["update_operation"]
  8. def _save_operation(cname, opname, primary, secondary, key=None):
  9. """Save the given campaign/operation overview info in the database."""
  10. secstr = "" if secondary is None else (" secondary=%d" % secondary)
  11. logger.debug("Setting overview primary=%d%s campaign=%s operation=%s",
  12. primary, secstr, cname, opname)
  13. g.campaign_db.set_overview(cname, opname, primary, secondary)
  14. g.campaign_db.touch_operation(cname, opname, key=key)
  15. def _build_filter(qualifiers, arg):
  16. """Given a qualifiers string from the config, return a filter function.
  17. This function is extremely sensitive since it executes arbitrary Python
  18. code. It should never be run with a user-provided argument! We trust the
  19. contents of a config file because it originates from a known place on the
  20. filesystem.
  21. """
  22. namespace = {"g": g}
  23. body = ("def _func(%s):\n" % arg) + textwrap.indent(qualifiers, " " * 4)
  24. exec(body, namespace)
  25. return namespace["_func"]
  26. def _store_kill(cname, opnames, kill):
  27. """Store the given kill and its associations into the database."""
  28. kid = kill["killmail_id"]
  29. if g.campaign_db.has_kill(kid):
  30. current = g.campaign_db.get_kill_associations(cname, kid)
  31. opnames -= set(current)
  32. if opnames:
  33. logger.debug("Adding operations=%s to kill id=%d campaign=%s",
  34. ",".join(opnames), kid, cname)
  35. else:
  36. logger.debug("Adding kill id=%d campaign=%s operations=%s", kid, cname,
  37. ",".join(opnames))
  38. g.campaign_db.add_kill(kill)
  39. g.campaign_db.associate_kill(cname, kid, opnames)
  40. def _update_killboard_operations(cname, opnames, min_kill_id):
  41. """Update all killboard-type operations in the given campaign subset."""
  42. operations = config["campaigns"][cname]["operations"]
  43. filters = []
  44. for opname in opnames:
  45. qualif = operations[opname]["qualifiers"]
  46. filters.append((_build_filter(qualif, "kill"), opname))
  47. args = ["kills", "corporationID", g.config.get("corp.id"), "no-items",
  48. "no-attackers", "orderDirection", "desc"]
  49. max_kill_id = min_kill_id
  50. for kill in g.eve.zkill.iter_killmails(*args, extended=True):
  51. kid = kill["killmail_id"]
  52. if min_kill_id > 0 and kid == min_kill_id:
  53. # TODO: This fails if ZKill receives kills out of order.
  54. # Should look ahead for kills within, say, 12 hours of the
  55. # min_kill_id, and extend code below to ignore kills aleady
  56. # included instead of re-adding.
  57. break
  58. ktime = kill["killmail_time"]
  59. logger.debug("Evaluating kill date=%s id=%d for campaign=%s "
  60. "operations=%s", ktime, kid, cname, ",".join(opnames))
  61. max_kill_id = max(max_kill_id, kid)
  62. ops = set()
  63. for filt, opname in filters:
  64. if filt(kill):
  65. ops.add(opname)
  66. if ops:
  67. _store_kill(cname, ops, kill)
  68. for opname in opnames:
  69. primary, secondary = g.campaign_db.count_kills(cname, opname)
  70. show_isk = operations[opname].get("isk", True)
  71. if not show_isk:
  72. secondary = None
  73. _save_operation(cname, opname, primary, secondary, key=max_kill_id)
  74. def _get_prices():
  75. """Return a dict mapping type IDs to ISK prices."""
  76. pricelist = g.eve.esi().v1.markets.prices.get()
  77. return {entry["type_id"]: entry["average_price"]
  78. for entry in pricelist if "average_price" in entry}
  79. def _save_collection_overview(cname, opnames, data):
  80. """Save collection overview data to the database."""
  81. operations = config["campaigns"][cname]["operations"]
  82. for opname in opnames:
  83. primary = sum(count for d in data[opname].values()
  84. for (count, _) in d.values())
  85. show_isk = operations[opname].get("isk", True)
  86. if show_isk:
  87. secondary = sum(value for d in data[opname].values()
  88. for (_, value) in d.values())
  89. else:
  90. secondary = None
  91. _save_operation(cname, opname, primary, secondary)
  92. def _update_collection_operations(cname, opnames):
  93. """Update all collection-type operations in the given campaign subset."""
  94. operations = config["campaigns"][cname]["operations"]
  95. filters = []
  96. for opname in opnames:
  97. qualif = operations[opname]["qualifiers"]
  98. filters.append((_build_filter(qualif, "asset"), opname))
  99. prices = _get_prices()
  100. data = {opname: {} for opname in opnames}
  101. for char_id, token in g.auth.get_valid_characters():
  102. logger.debug("Fetching assets for char id=%d campaign=%s "
  103. "operations=%s", char_id, cname, ",".join(opnames))
  104. try:
  105. assets = g.eve.esi(token).v1.characters(char_id).assets.get()
  106. except EVEAPIForbiddenError:
  107. logger.debug("Asset access denied for char id=%d", char_id)
  108. continue
  109. for opname in opnames:
  110. data[opname][char_id] = {}
  111. logger.debug("Evaluating %d assets for char id=%d",
  112. len(assets), char_id)
  113. for asset in assets:
  114. for filt, opname in filters:
  115. if filt(asset):
  116. typeid = asset["type_id"]
  117. count = 1 if asset["is_singleton"] else asset["quantity"]
  118. value = prices.get(typeid, 0.0)
  119. char = data[opname][char_id]
  120. if typeid in char:
  121. char[typeid][0] += count
  122. char[typeid][1] += count * value
  123. else:
  124. char[typeid] = [count, count * value]
  125. g.campaign_db.update_items(cname, data)
  126. _save_collection_overview(cname, opnames, data)
  127. def update_operation(cname, opname, new=False):
  128. """Update a campaign/operation. Assumes a thread-exclusive lock is held."""
  129. campaign = config["campaigns"][cname]
  130. operations = campaign["operations"]
  131. optype = operations[opname]["type"]
  132. opnames = [opn for opn in campaign["enabled"]
  133. if operations[opn]["type"] == optype]
  134. if optype == "killboard":
  135. opsubset = []
  136. min_key = 0 if new else sys.maxsize
  137. for opn in opnames:
  138. last_updated, key = g.campaign_db.check_operation(cname, opn)
  139. if new and last_updated is None:
  140. opsubset.append(opn)
  141. elif not new and last_updated is not None:
  142. min_key = min(min_key, key)
  143. opsubset.append(opn)
  144. _update_killboard_operations(cname, opsubset, min_key)
  145. elif optype == "collection":
  146. _update_collection_operations(cname, opnames)
  147. else:
  148. raise RuntimeError("Unknown operation type: %s" % optype)