A corporation manager and dashboard for EVE Online
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 

233 lignes
8.8 KiB

  1. # -*- coding: utf-8 -*-
  2. from datetime import datetime
  3. import random
  4. import sqlite3
  5. from flask import g
  6. from werkzeug.local import LocalProxy
  7. __all__ = ["Database"]
  8. class Database:
  9. """Database manager for low-level authentication actions."""
  10. MAX_SESSION_STALENESS = 2 * 60 * 60 # 2 hours
  11. MAX_SESSION_AGE = 24 * 60 * 60 # 24 hours
  12. SESSION_GRACE = 60 * 60 # 1 hour
  13. path = None
  14. def __init__(self):
  15. if self.path is None:
  16. raise RuntimeError("Database.path not set")
  17. self._conn = sqlite3.connect(self.path)
  18. def __enter__(self):
  19. return self._conn.__enter__()
  20. def __exit__(self, exc_type, exc_value, trace):
  21. return self._conn.__exit__(exc_type, exc_value, trace)
  22. @classmethod
  23. def _get(cls):
  24. """Return the current database, or allocate a new one if necessary."""
  25. if not hasattr(g, "_db"):
  26. g._db = cls()
  27. return g._db
  28. @classmethod
  29. def pre_hook(cls):
  30. """Hook to be called before a request context.
  31. Sets up the g.db proxy.
  32. """
  33. g.db = LocalProxy(cls._get)
  34. @classmethod
  35. def post_hook(cls, exc):
  36. """Hook to be called when tearing down an application context.
  37. Closes the database if necessary.
  38. """
  39. if hasattr(g, "_db"):
  40. g._db.close()
  41. def close(self):
  42. """Close the database connection."""
  43. return self._conn.close()
  44. def _clear_old_sessions(self):
  45. """Remove old sessions from the database.
  46. Sessions can expire if they are not touched (accessed) in a certain
  47. period of time, or if their absolute age exceeds some number. We don't
  48. actually remove them until a bit after this time.
  49. """
  50. query = """DELETE FROM session WHERE
  51. strftime("%s", "now") - strftime("%s", session_created) >= {} OR
  52. strftime("%s", "now") - strftime("%s", session_touched) >= {}"""
  53. create_thresh = self.MAX_SESSION_AGE + self.SESSION_GRACE
  54. touch_thresh = self.MAX_SESSION_STALENESS + self.SESSION_GRACE
  55. with self._conn as conn:
  56. conn.execute(query.format(create_thresh, touch_thresh))
  57. def new_session(self):
  58. """Allocate a new session in the database.
  59. Return its ID as an integer and creation timestamp as a naive UTC
  60. datetime.
  61. """
  62. created = datetime.utcnow().replace(microsecond=0)
  63. query = "INSERT INTO session (session_created) VALUES (?)"
  64. with self._conn as conn:
  65. cur = conn.execute(query, (created,))
  66. return cur.lastrowid, created
  67. def has_session(self, sid):
  68. """Return the creation timestamp for the given session ID, or None.
  69. Will only return a timestamp for non-expired sessions. This function
  70. randomly does database maintenance; very old expired sessions may be
  71. cleared.
  72. """
  73. if random.random() <= 0.2:
  74. self._clear_old_sessions()
  75. query = """SELECT session_created FROM session
  76. WHERE session_id = ? AND
  77. strftime("%s", "now") - strftime("%s", session_created) < {} AND
  78. strftime("%s", "now") - strftime("%s", session_touched) < {}"""
  79. query = query.format(self.MAX_SESSION_AGE, self.MAX_SESSION_STALENESS)
  80. res = self._conn.execute(query, (sid,)).fetchall()
  81. if not res:
  82. return None
  83. return datetime.strptime(res[0][0], "%Y-%m-%d %H:%M:%S")
  84. def read_session(self, sid):
  85. """Return the character associated with the given session, or None."""
  86. query = """SELECT session_character FROM session
  87. WHERE session_id = ? AND
  88. strftime("%s", "now") - strftime("%s", session_created) < {} AND
  89. strftime("%s", "now") - strftime("%s", session_touched) < {}"""
  90. query = query.format(self.MAX_SESSION_AGE, self.MAX_SESSION_STALENESS)
  91. res = self._conn.execute(query, (sid,)).fetchall()
  92. return res[0][0] if res else None
  93. def touch_session(self, sid):
  94. """Update the given session's last access timestamp."""
  95. query = """UPDATE session
  96. SET session_touched = CURRENT_TIMESTAMP
  97. WHERE session_id = ?"""
  98. with self._conn as conn:
  99. conn.execute(query, (sid,))
  100. def attach_session(self, sid, cid):
  101. """Attach the given session to a character. Does not touch it."""
  102. query = """UPDATE session
  103. SET session_character = ?
  104. WHERE session_id = ?"""
  105. with self._conn as conn:
  106. conn.execute(query, (cid, sid))
  107. def drop_session(self, sid):
  108. """Remove the given session from the database."""
  109. with self._conn as conn:
  110. conn.execute("DELETE FROM session WHERE session_id = ?", (sid,))
  111. def put_character(self, cid, name):
  112. """Put a character into the database if they don't already exist."""
  113. with self._conn as conn:
  114. cur = conn.execute("BEGIN TRANSACTION")
  115. cur.execute(
  116. """UPDATE character SET character_name = ?
  117. WHERE character_id = ?""", (name, cid))
  118. if cur.rowcount == 0:
  119. cur.execute(
  120. """INSERT INTO character (character_id, character_name)
  121. VALUES (?, ?)""", (cid, name))
  122. def read_character(self, cid):
  123. """Return a dictionary of properties for the given character."""
  124. query = """SELECT character_name, character_style
  125. FROM character WHERE character_id = ?"""
  126. res = self._conn.execute(query, (cid,)).fetchall()
  127. return {"name": res[0][0], "style": res[0][1]} if res else {}
  128. def update_character(self, cid, prop, value):
  129. """Update a property for the given character."""
  130. props = {"name": "character_name", "style": "character_style"}
  131. field = props[prop]
  132. with self._conn as conn:
  133. conn.execute("""UPDATE character SET {} = ?
  134. WHERE character_id = ?""".format(field), (value, cid))
  135. def set_auth(self, cid, token, expires, refresh):
  136. """Set the authentication info for the given character."""
  137. with self._conn as conn:
  138. conn.execute("""INSERT OR REPLACE INTO auth
  139. (auth_character, auth_token, auth_token_expiry, auth_refresh)
  140. VALUES (?, ?, ?, ?)""", (cid, token, expires, refresh))
  141. def update_auth(self, cid, token, expires, refresh):
  142. """Update the authentication info for the given character.
  143. Functionally equivalent to set_auth provided that the character has an
  144. existing auth entry, but is more efficient.
  145. """
  146. with self._conn as conn:
  147. conn.execute("""UPDATE auth
  148. SET auth_token = ?, auth_token_expiry = ?, auth_refresh = ?
  149. WHERE auth_character = ?""", (token, expires, refresh, cid))
  150. def get_auth(self, cid):
  151. """Return authentication info for the given character.
  152. Return a 3-tuple of (access_token, token_expiry, refresh_token), or
  153. None if there is no auth info.
  154. """
  155. query = """SELECT auth_token, auth_token_expiry, auth_refresh
  156. FROM auth WHERE auth_character = ?"""
  157. res = self._conn.execute(query, (cid,)).fetchall()
  158. if not res:
  159. return None
  160. token, expiry, refresh = res[0]
  161. expires = datetime.strptime(expiry, "%Y-%m-%d %H:%M:%S")
  162. return token, expires, refresh
  163. def drop_auth(self, cid):
  164. """Drop any authentication info for the given character."""
  165. with self._conn as conn:
  166. conn.execute("DELETE FROM auth WHERE auth_character = ?", (cid,))
  167. def get_authed_characters(self):
  168. """Return a list of characters with authentication info.
  169. Each list item is a 4-tuple of (character_id, access_token,
  170. token_expiry, refresh_token).
  171. """
  172. query = """SELECT auth_character, auth_token, auth_token_expiry,
  173. auth_refresh FROM auth"""
  174. res = self._conn.execute(query).fetchall()
  175. dtparse = lambda dt: datetime.strptime(dt, "%Y-%m-%d %H:%M:%S")
  176. return [(cid, token, dtparse(expiry), refresh)
  177. for (cid, token, expiry, refresh) in res]
  178. def set_character_modprop(self, cid, module, prop, value):
  179. """Add or update a character module property."""
  180. with self._conn as conn:
  181. conn.execute("""INSERT OR REPLACE INTO character_prop
  182. (cprop_character, cprop_module, cprop_key, cprop_value)
  183. VALUES (?, ?, ?, ?)""", (cid, module, prop, value))
  184. def get_character_modprop(self, cid, module, prop):
  185. """Return the value of a character module property, or None."""
  186. query = """SELECT cprop_value FROM character_prop
  187. WHERE cprop_character = ? AND cprop_module = ? AND cprop_key = ?"""
  188. res = self._conn.execute(query, (cid, module, prop)).fetchall()
  189. return res[0][0] if res else None