actors.py.deprecated 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2021 - Copyright ...
  4. Authors:
  5. zPlus <zplus@peers.community>
  6. Notes
  7. This module contains Actors classes. Each class contains a set of actions
  8. that can be performed by an Actor, for example .follow() or .accept().
  9. These classes cannot be instantiated on their own, directly. Instead they
  10. serve as parent classes for the model defined in model.py. The module.py
  11. module extends the Pagure database model. Actors always correspond to an
  12. entity in the database for example a user or a repository, therefore an
  13. Actor instance is always created starting from SQLAlchemy classes (defined
  14. in model.py). These functions could have been added to the SQLAlchemy
  15. classes in model.py but I think it's a better idea to keep the model
  16. definition separate.
  17. """
  18. import datetime
  19. import flask
  20. import json
  21. import logging
  22. from . import APP_URL
  23. from . import activitypub
  24. from . import model
  25. from . import settings
  26. from . import tasks
  27. log = logging.getLogger(__name__)
  28. """
  29. def action(func):
  30. " ""
  31. This function creates a decorator to be applied to the methods of class
  32. Actor. It represents an ActivityStream Action.
  33. The decorator will first execute the decorated function, and then schedule
  34. the delivery of the returned Activity.
  35. NOTE The function that is decorated with this decorator MUST return an
  36. Activity.
  37. :param func: The function to be decorated.
  38. " ""
  39. @functools.wraps(func)
  40. def decorator(self, *args, **kwargs):
  41. " ""
  42. Send an activity.
  43. By default, the Activity is sent to the Actor followers collection.
  44. To override this behavior it's possible to call the function like this:
  45. actor.follow(..., to=[], cc=[], bto=[], bcc=[])
  46. https://www.w3.org/TR/activitypub/#delivery
  47. " ""
  48. forgefed_db = database.start_database_session()
  49. # Create the activity from the Actor by executing the action (function)
  50. activity = func(self, *args, **kwargs)
  51. # Assign an ID to the Activity
  52. activity['id'] = '{}/federation/activity/{}'.format(APP_URL, activitypub.new_activity_id())
  53. # Add publishing datetime
  54. # - use UTC
  55. # - remove microseconds, use HH:MM:SS only
  56. # - add timezone info. There is also .astimezone() but it seems to
  57. # return the wrong value when used with .utcnow(). Bug?
  58. # - convert to ISO 8601 format
  59. activity['published'] = activitypub.format_datetime(datetime.datetime.utcnow())
  60. # By default we send the activity to the as:followers collection
  61. activity['to'] = [ self.followers_uri ]
  62. # Create the list of recipients
  63. recipients = []
  64. # TODO Check if it's possible to simplify this loop
  65. for field in [ 'to', 'cc', 'bto', 'bcc' ]:
  66. if field in kwargs:
  67. activity[field] = kwargs[field]
  68. if field in activity:
  69. if isinstance(activity[field], str):
  70. recipients.append(activity[field])
  71. else:
  72. recipients.extend(activity[field])
  73. # Save a copy of the Activity in the database and add it to the Actor's
  74. # OUTBOX before sending it
  75. forgefed_db.add(database.Activity(
  76. uri = activity['id'],
  77. actor_uri = activity['actor'],
  78. document = json.dumps(activity)))
  79. forgefed_db.add(database.Collection(
  80. uri = self.outbox_uri,
  81. item = activity['id']))
  82. forgefed_db.commit()
  83. forgefed_db.remove()
  84. # Now we are ready to POST to the remote actors
  85. # Before sending, remove bto and bcc according to spec.
  86. # https://www.w3.org/TR/activitypub/#client-to-server-interactions
  87. activity.pop('bto', None)
  88. activity.pop('bcc', None)
  89. # Stop here if there are no recipients.
  90. # https://www.w3.org/TR/activitypub/#h-note-8
  91. if len(recipients) == 0:
  92. log.debug('No recipients. Activity will not be sent.')
  93. return
  94. # Make sure the local actor has a GPG key before POSTing anything. The
  95. # remote Actor will use the key for verifying the Activity.
  96. database.GpgKey.test_or_set(self.local_uri, self.publickey_uri)
  97. # Create a new Celery task for each recipient. Activities are POSTed
  98. # individually because if one request fails we don't want to resend
  99. # the same Activity to *all* the recipients.
  100. for recipient in recipients:
  101. log.debug('Scheduling new activity: id={} recipient={}'.format(
  102. activity['id'], recipient))
  103. tasks.activity.post.delay(
  104. activity = activity,
  105. recipient_uri = recipient,
  106. depth = settings.DELIVERY_DEPTH)
  107. return decorator
  108. """
  109. class Actor:
  110. #@action
  111. def accept(self, object_uri, *args, **kwargs):
  112. """
  113. Accept Activity.
  114. :param object_uri: URI of the ActivityPub object that was accepted.
  115. """
  116. return {
  117. 'type': 'Accept',
  118. 'actor': self.local_uri,
  119. 'object': object_uri,
  120. **kwargs }
  121. #@action
  122. def create(self, object_uri, *args, **kwargs):
  123. """
  124. Create Activity.
  125. :param object_uri: URI of the ActivityPub object that was created.
  126. """
  127. return {
  128. 'type': 'Create',
  129. 'actor': self.local_uri,
  130. 'object': object_uri }
  131. # DONE
  132. def follow(self, object_uri, *args, **kwargs):
  133. """
  134. Follow Activity.
  135. :param object_uri: URI of the ActivityPub actor to follow.
  136. """
  137. forgefed = self._get_database_session()
  138. remote_actor = activitypub.fetch(object_uri)
  139. if not remote_actor:
  140. raise Exception('Could not fetch remote actor.')
  141. log.debug('Remote Actor {} fetched.'.format(object_uri))
  142. # We cache a copy of the remote actor for quick lookups. This is used for
  143. # example when listing "following/followers" collections. If we only
  144. # stored the actors' URI we would need to GET the JSON data every time.
  145. forgefed.merge(model.Cache(
  146. uri = object_uri,
  147. document = json.dumps(remote_actor)))
  148. # Add the remote actor to the "following" collection
  149. forgefed.merge(model.Collection(
  150. uri = self.following_uri,
  151. item = object_uri))
  152. # Add a feed
  153. forgefed.add(model.Feed(
  154. actor_uri = self.local_uri,
  155. content = flask.render_template('federation/feed/follow.html', actor=self.jsonld, object=remote_actor)
  156. ))
  157. self.send_activity({
  158. 'type': 'Follow',
  159. 'object': object_uri,
  160. 'to': object_uri
  161. })
  162. #@action
  163. def offer(self, object, *args, **kwargs):
  164. """
  165. Offer Activity.
  166. :param object: Object to offer. Either a URI or a dictionary.
  167. """
  168. return {
  169. 'type': 'Offer',
  170. 'actor': self.local_uri,
  171. 'object': object }
  172. #@action
  173. def resolve(self, object, *args, **kwargs):
  174. """
  175. Resolve Activity.
  176. :param object: Object that has been resolved.
  177. """
  178. return {
  179. 'type': 'Resolve',
  180. 'actor': self.local_uri,
  181. 'object': object }
  182. #@action
  183. def update(self, object, *args, **kwargs):
  184. """
  185. Update Activity.
  186. :param object: The object that was updated.
  187. """
  188. return {
  189. 'type': 'Update',
  190. 'actor': self.local_uri,
  191. 'object': object }