model.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. # Useful documentation for SQLAlchemy ORM:
  2. # https://docs.sqlalchemy.org/en/13/orm/tutorial.html
  3. # https://docs.sqlalchemy.org/en/13/orm/basic_relationships.html
  4. import flask
  5. import json
  6. import pagure.lib.model
  7. import random
  8. import string
  9. from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, \
  10. LargeBinary, UnicodeText, func
  11. from sqlalchemy import PrimaryKeyConstraint
  12. from sqlalchemy.orm import relationship
  13. from sqlalchemy.ext.declarative import declarative_base
  14. from . import activitypub
  15. from . import database
  16. from . import settings
  17. from . import tasks
  18. from Crypto.PublicKey import RSA
  19. from pagure.config import config as pagure_config
  20. # The app URL defined in the Pagure configuration, eg. "https://example.org/"
  21. # We need this for generating Actor IDs.
  22. APP_URL = pagure_config['APP_URL'].rstrip('/')
  23. assert APP_URL and len(APP_URL) > 0, 'APP_URL missing from Pagure configuration.'
  24. # We use SQLAlchemy "declarative" mappings: https://docs.sqlalchemy.org/en/13/orm/tutorial.html#declare-a-mapping
  25. BASE = declarative_base()
  26. def new_random_id(length=32):
  27. """
  28. Generate a random string to use as an Activity ID when a new one is
  29. created.
  30. """
  31. symbols = string.ascii_lowercase + string.digits
  32. return ''.join([ random.choice(symbols) for i in range(length) ])
  33. class Actor:
  34. @property
  35. def inbox_id(self):
  36. return self.actor_id + '/inbox'
  37. @property
  38. def outbox_id(self):
  39. return self.actor_id + '/outbox'
  40. @property
  41. def followers_id(self):
  42. return self.actor_id + '/followers'
  43. @property
  44. def following_id(self):
  45. return self.actor_id + '/following'
  46. @property
  47. def publickey_id(self):
  48. return self.actor_id + '/key.pub'
  49. def _deliver(self, activity):
  50. """
  51. Send an activity.
  52. https://www.w3.org/TR/activitypub/#delivery
  53. :param activity: Python dictionary of the Activity to send.
  54. """
  55. activity_document = json.dumps(activity)
  56. db = database.start_database_session()
  57. # Save a copy of the Activity in the database before sending
  58. db.add(Activity(id = activity['id'],
  59. actor_id = self.actor_id,
  60. document = activity_document))
  61. # Save the Activity to the Actor OUTBOX
  62. # outbox = Outbox(actor_id = self.actor_id,
  63. # activity_id = activity.id)
  64. #db.add(outbox)
  65. # Close db session
  66. db.commit()
  67. db.remove()
  68. # Now we are ready to POST to the remote actors
  69. # First off, we extract the list of recipients and remove the bto:
  70. # and bcc: recipients from the Activity before it's sent.
  71. recipients = []
  72. if 'to' in activity:
  73. if isinstance(activity['to'], str):
  74. activity['to'] = [ activity['to'] ]
  75. recipients.extend(activity['to'])
  76. if 'cc' in activity:
  77. if isinstance(activity['cc'], str):
  78. activity['cc'] = [ activity['cc'] ]
  79. recipients.extend(activity['cc'])
  80. if 'bto' in activity:
  81. if isinstance(activity['bto'], str):
  82. activity['bto'] = [ activity['bto'] ]
  83. recipients.extend(activity['bto'])
  84. # Remove according to spec.
  85. # https://www.w3.org/TR/activitypub/#client-to-server-interactions
  86. del activity['bto']
  87. if 'bcc' in activity:
  88. if isinstance(activity['bcc'], str):
  89. activity['bcc'] = [ activity['bcc'] ]
  90. recipients.extend(activity['bcc'])
  91. # Remove according to spec.
  92. # https://www.w3.org/TR/activitypub/#client-to-server-interactions
  93. del activity['bcc']
  94. # Stop here if there are no recipients.
  95. # https://www.w3.org/TR/activitypub/#h-note-8
  96. if len(recipients) == 0:
  97. return
  98. # Create a new Celery task individual delivery to each recipient
  99. for recipient in recipients:
  100. tasks.post_activity.delay(
  101. activity_document = activity_document,
  102. recipient_id = recipient,
  103. indirections = settings.DELIVERY_INDIRECTIONS)
  104. def accept(self, object_id):
  105. """
  106. Accept an ActivityPub object.
  107. """
  108. # Create the RDF document of the "Accept" Activity
  109. activity = {
  110. 'id': self.actor_id + '/outbox/' + new_random_id(),
  111. 'type': 'Accept',
  112. 'actor': self.actor_id,
  113. 'object': object_id
  114. }
  115. # Send the "Accept" Activity to the remote actors
  116. self._deliver(activity)
  117. class Person(pagure.lib.model.User, Actor):
  118. @property
  119. def actor_id(self):
  120. return APP_URL + '/' + self.url_path
  121. def to_rdf(self):
  122. return {
  123. '@context': activitypub.jsonld_context,
  124. 'type': 'Person',
  125. 'name': self.fullname,
  126. 'preferredUsername': self.username,
  127. 'id': self.actor_id,
  128. 'inbox': self.inbox_id,
  129. 'outbox': self.outbox_id,
  130. 'followers': self.followers_id,
  131. 'following': self.following_id,
  132. 'publicKey': self.publickey_id,
  133. }
  134. class Repository(pagure.lib.model.Project, Actor):
  135. @property
  136. def actor_id(self):
  137. return APP_URL + '/' + self.url_path + '.git'
  138. def to_rdf(self):
  139. return {
  140. '@context': activitypub.jsonld_context,
  141. 'type': 'Repository',
  142. 'name': self.name,
  143. 'id': self.actor_id,
  144. 'inbox': self.inbox_id,
  145. 'outbox': self.outbox_id,
  146. 'followers': self.followers_id,
  147. 'following': self.following_id,
  148. 'publicKey': self.publickey_id,
  149. 'team': None,
  150. }
  151. class GpgKey(BASE):
  152. """
  153. This class represents a Person GPG key that is used to sign HTTP POST
  154. requests.
  155. """
  156. __tablename__ = 'forgefed_gpg_key'
  157. __table_args__ = (
  158. PrimaryKeyConstraint('actor_id', 'private'),
  159. )
  160. # The actor who owns the key
  161. actor_id = Column(UnicodeText, nullable=False)
  162. # The private part of the key
  163. private = Column(LargeBinary, nullable=False)
  164. # The public part of the key
  165. public = Column(LargeBinary, nullable=False)
  166. # When was this key created
  167. created = Column(DateTime, nullable=False, default=func.now())
  168. @staticmethod
  169. def new_key():
  170. """
  171. Returns a new private/public key pair.
  172. """
  173. key = RSA.generate(settings.HTTP_SIGNATURE_KEY_BITS)
  174. return {
  175. 'private': key.export_key('PEM'),
  176. 'public': key.publickey().export_key('PEM')
  177. }
  178. @staticmethod
  179. def test_or_set(actor_id):
  180. """
  181. Test if an Actor already has a GPG key, otherwise automatically generate
  182. a new one.
  183. :param actor_id: ID (URL) of the ActivityPub Actor.
  184. """
  185. db = database.start_database_session()
  186. key = db.query(GpgKey) \
  187. .filter(GpgKey.actor_id == actor_id) \
  188. .one_or_none()
  189. if not key:
  190. key = GpgKey.new_key()
  191. db.add(GpgKey(actor_id = actor_id,
  192. private = key['private'],
  193. public = key['public']))
  194. db.commit()
  195. db.remove()
  196. class Follower(BASE):
  197. """
  198. This class represents a "Follow" relationship.
  199. """
  200. __tablename__ = 'forgefed_follower'
  201. __table_args__ = (
  202. PrimaryKeyConstraint('subject_id', 'object_id'),
  203. )
  204. # Actor that is following
  205. subject_id = Column(UnicodeText, nullable=False)
  206. # Actor that is followed
  207. object_id = Column(UnicodeText, nullable=False)
  208. # When was this relation established
  209. established = Column(DateTime, nullable=False, default=func.now())
  210. class Activity(BASE):
  211. """
  212. This class represents an ActivityPub Activity.
  213. """
  214. __tablename__ = 'forgefed_activity'
  215. # The ID of the Activity (a URL)
  216. id = Column(UnicodeText, primary_key=True, default=new_random_id)
  217. # The ID (URL) of the Actor who sent the Activity.
  218. actor_id = Column(UnicodeText, nullable=False)
  219. # The JSON-LD document of the activity
  220. document = Column(UnicodeText, nullable=False)
  221. # When an Activity was received
  222. received = Column(DateTime, nullable=False, default=func.now())
  223. deliveries = relationship('Delivery', back_populates='activity')
  224. #inboxes = relationship('Inbox', back_populates='activity')
  225. #outboxes = relationship('Outbox', back_populates='activity')
  226. class Delivery(BASE):
  227. """
  228. This class/table records which Activity was delivered to which Actor.
  229. It records both incoming and outgoing activities.
  230. """
  231. __tablename__ = 'forgefed_delivery'
  232. __table_args__ = (
  233. PrimaryKeyConstraint('activity_id', 'recipient_id'),
  234. )
  235. # The ID of the Activity
  236. activity_id = Column(
  237. UnicodeText,
  238. ForeignKey('forgefed_activity.id'),
  239. nullable=False)
  240. # The ID (URL) of the Actor who received the Activity.
  241. recipient_id = Column(UnicodeText, nullable=False)
  242. # The ID of the INBOX used to send the Activity to the Actor. An Actor can
  243. # have both an "inbox" and "sharedInbox".
  244. recipient_inbox = Column(UnicodeText, nullable=False)
  245. # DateTime of when the Activity was delivered
  246. delivered = Column(DateTime, nullable=False, default=func.now())
  247. activity = relationship('Activity', back_populates='deliveries')
  248. """
  249. class Inbox(BASE):
  250. # This class represents an Actor INBOX messages.
  251. __tablename__ = 'forgefed_inbox'
  252. __table_args__ = (
  253. PrimaryKeyConstraint('actor_id', 'activity_id'),
  254. )
  255. # The ID of the Actor (a URL)
  256. actor_id = Column(UnicodeText, nullable=False)
  257. # The ID of the Activity
  258. activity_id = Column(
  259. UnicodeText,
  260. ForeignKey('forgefed_activity.id'),
  261. nullable=False)
  262. # When an Activity was added to the Actor INBOX
  263. received = Column(DateTime, nullable=False, default=func.now())
  264. activity = relationship('Activity', back_populates='inboxes')
  265. class Outbox(BASE):
  266. # This class represents an Actor OUTBOX messages.
  267. __tablename__ = 'forgefed_outbox'
  268. __table_args__ = (
  269. PrimaryKeyConstraint('actor_id', 'activity_id'),
  270. )
  271. # The ID of the Actor (a URL)
  272. actor_id = Column(UnicodeText, nullable=False)
  273. # The ID of the Activity
  274. activity_id = Column(
  275. UnicodeText,
  276. ForeignKey('forgefed_activity.id'),
  277. nullable=False)
  278. # When an Activity was added to the Actor OUTBOX
  279. received = Column(DateTime, nullable=False, default=func.now())
  280. activity = relationship('Activity', back_populates='outboxes')
  281. """
  282. # Automatically create the database tables on startup. The tables are first
  283. # checked for existence before any CREATE TABLE command is issued.
  284. # https://docs.sqlalchemy.org/en/13/orm/tutorial.html#create-a-schema
  285. BASE.metadata.create_all(database.engine)