storage.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import sqlite3
  2. from Crypto.PublicKey import RSA
  3. from . import settings
  4. # Open (or create database file)
  5. db = sqlite3.connect(settings.DATABASE_PATH)
  6. # Returns SQLite rows as dictionaries instead of tuples.
  7. # https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.row_factory
  8. db.row_factory = sqlite3.Row
  9. # Create database tables if they do not exist, for example if it's a
  10. # new database.
  11. with db:
  12. db.executescript(
  13. """
  14. -- GPG keys of actors used for signing HTTP requests
  15. CREATE TABLE IF NOT EXISTS gpg_key
  16. (
  17. actor TEXT NOT NULL, -- The actor who owns the key
  18. private BLOB NOT NULL, -- The private part of the key
  19. public BLOB NOT NULL, -- The public part of the key
  20. PRIMARY KEY (actor)
  21. );
  22. -- List of "Follow" relationships.
  23. CREATE TABLE IF NOT EXISTS follow
  24. (
  25. subject TEXT NOT NULL, -- Actor that is following
  26. object TEXT NOT NULL, -- Actor that is followed
  27. PRIMARY KEY (subject, object)
  28. );
  29. CREATE INDEX IF NOT EXISTS subject_idx ON follow (subject ASC);
  30. CREATE INDEX IF NOT EXISTS object_idx ON follow (object ASC);
  31. -- Copies of activities
  32. CREATE TABLE IF NOT EXISTS activity
  33. (
  34. id TEXT NOT NULL, -- The ID of the Activity
  35. document TEXT NOT NULL, -- The JSON-LD document of the activity
  36. PRIMARY KEY (id)
  37. );
  38. -- Actor INBOX
  39. CREATE TABLE IF NOT EXISTS inbox
  40. (
  41. actor TEXT NOT NULL, -- The ID of the Actor
  42. activity TEXT NOT NULL, -- The ID of the Activity
  43. received TEXT NOT NULL, -- When the Activity was added to the INBOX
  44. PRIMARY KEY (actor, activity)
  45. );
  46. CREATE INDEX IF NOT EXISTS actor_idx ON inbox (actor ASC);
  47. CREATE INDEX IF NOT EXISTS activity_idx ON inbox (activity ASC);
  48. -- Actor OUTBOX
  49. CREATE TABLE IF NOT EXISTS outbox
  50. (
  51. actor TEXT NOT NULL, -- The ID of the Actor
  52. activity TEXT NOT NULL, -- The ID of the Activity
  53. sent TEXT NOT NULL, -- When the Activity was added to the OUTBOX
  54. PRIMARY KEY (actor, activity)
  55. );
  56. CREATE INDEX IF NOT EXISTS actor_idx ON inbox (actor ASC);
  57. CREATE INDEX IF NOT EXISTS activity_idx ON inbox (activity ASC);
  58. """)
  59. db.close()
  60. def get_gpg_key(self, actor_id, autogenerate=True):
  61. """
  62. ActivityPub activities are signed and verified using HTTP signatures.
  63. This function automatically generates new keys for actors that don't
  64. have one, and returns a private+public keys pair.
  65. :param actor_id: The ID of the Actor to search.
  66. :param autogenerate: Automatically generate a new key pair if none exist.
  67. """
  68. keys = None
  69. # Retrieve the keys
  70. with self.db as db:
  71. cursor = db.execute (
  72. """
  73. SELECT private, public
  74. FROM gpg_key
  75. WHERE actor = ?
  76. """,
  77. [ actor_id ]
  78. )
  79. results = cursor.fetchone()
  80. if results:
  81. keys = { 'private': results['private'],
  82. 'public': results['public'] }
  83. if not keys:
  84. # Create one
  85. key = RSA.generate(settings.HTTP_SIGNATURES_KEY_BITS)
  86. keys = { 'private': key.export_key('PEM'),
  87. 'public': key.publickey().export_key('PEM') }
  88. # Save it in the database
  89. self.set_gpg_key(actor_id = actor_id,
  90. private_key = keys['private'],
  91. public_key = keys['public'])
  92. return keys
  93. def set_gpg_key(self, actor_id, private_key, public_key):
  94. with self.db as db:
  95. db.execute (
  96. """
  97. INSERT OR IGNORE INTO gpg_key (actor, private, public)
  98. VALUES (?, ?, ?)
  99. """,
  100. [ actor_id, private_key, public_key ]
  101. )
  102. def follow(self, subjet_id, object_id):
  103. with self.db as db:
  104. db.execute (
  105. """
  106. INSERT OR IGNORE INTO follow (subject, object)
  107. VALUES (?, ?)
  108. """,
  109. [ subjet_id, object_id ]
  110. )
  111. def get_followers(self, actor_id):
  112. with self.db as db:
  113. cursor = db.execute (
  114. """
  115. SELECT subject
  116. FROM follow
  117. WHERE object = ?
  118. """,
  119. [ actor_id ]
  120. )
  121. results = cursor.fetchall()
  122. followers = [ follower['subject'] for follower in results ]
  123. return followers
  124. def get_following(self, actor_id):
  125. with self.db as db:
  126. cursor = db.execute (
  127. """
  128. SELECT object
  129. FROM follow
  130. WHERE subject = ?
  131. """,
  132. [ actor_id ]
  133. )
  134. results = cursor.fetchall()
  135. following = [ follower['object'] for follower in results ]
  136. return following
  137. def get_activity(self, id):
  138. with self.db as db:
  139. cursor = db.execute (
  140. """
  141. SELECT document
  142. FROM activity
  143. WHERE id = ?
  144. """,
  145. [ id ]
  146. )
  147. result = cursor.fetchone()
  148. return None if not result else json.loads(result['documnent'])
  149. def get_inbox(self, actor_id, offset, limit):
  150. with self.db as db:
  151. cursor = db.execute (
  152. """
  153. SELECT activity.document AS document
  154. FROM inbox
  155. JOIN activity ON activity.id = inbox.activity
  156. WHERE inbox.actor = ?
  157. ORDER BY inbox.received DESC
  158. OFFSET ?
  159. LIMIT ?
  160. """,
  161. [ actor_id, offset, limit ]
  162. )
  163. results = cursor.fetchall()
  164. activities = [ json.loads(result['document']) for result in results ]
  165. return activities
  166. def get_outbox(self, actor_id, offset, limit):
  167. with self.db as db:
  168. cursor = db.execute (
  169. """
  170. SELECT activity.document AS document
  171. FROM outbox
  172. JOIN activity ON activity.id = outbox.activity
  173. WHERE outbox.actor = ?
  174. ORDER BY outbox.received DESC
  175. OFFSET ?
  176. LIMIT ?
  177. """,
  178. [ actor_id, offset, limit ]
  179. )
  180. results = cursor.fetchall()
  181. activities = [ json.loads(result['document']) for result in results ]
  182. return activities
  183. def store_inbox_activity(self, actor_id, activity_document):
  184. if 'id' not in activity_document:
  185. return
  186. # Add the Activity to the Actor's INBOX
  187. with self.db as db:
  188. db.execute (
  189. """
  190. INSERT OR IGNORE INTO activity (id, document)
  191. VALUES (?, ?)
  192. """,
  193. [ activity_document['id'], json.dumps(activity_document) ]
  194. )
  195. db.execute (
  196. """
  197. INSERT OR IGNORE INTO inbox (actor, activity, received)
  198. VALUES (?, ?, ?)
  199. """,
  200. [
  201. actor_id,
  202. activity_document['id'],
  203. datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat()
  204. ]
  205. )
  206. def store_outbox_activity(self, actor_id, activity_document):
  207. if 'id' not in activity_document:
  208. return
  209. # Add the Activity to the Actor's INBOX
  210. with self.db as db:
  211. db.execute (
  212. """
  213. INSERT OR IGNORE INTO activity (id, document)
  214. VALUES (?, ?)
  215. """,
  216. [ activity_document['id'], json.dumps(activity_document) ]
  217. )
  218. db.execute (
  219. """
  220. INSERT OR IGNORE INTO outbox (actor, activity, sent)
  221. VALUES (?, ?, ?)
  222. """,
  223. [
  224. actor_id,
  225. activity_document['id'],
  226. datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat()
  227. ]
  228. )