123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- # -*- coding: utf-8 -*-
- """
- (c) 2021 - Copyright ...
-
- Authors:
- zPlus <zplus@peers.community>
-
- Notes
- This module contains Actors classes. Each class contains a set of actions
- that can be performed by an Actor, for example .follow() or .accept().
- These classes cannot be instantiated on their own, directly. Instead they
- serve as parent classes for the model defined in model.py. The module.py
- module extends the Pagure database model. Actors always correspond to an
- entity in the database for example a user or a repository, therefore an
- Actor instance is always created starting from SQLAlchemy classes (defined
- in model.py). These functions could have been added to the SQLAlchemy
- classes in model.py but I think it's a better idea to keep the model
- definition separate.
- """
- import datetime
- import flask
- import json
- import logging
- from . import APP_URL
- from . import activitypub
- from . import model
- from . import settings
- from . import tasks
- log = logging.getLogger(__name__)
- """
- def action(func):
- " ""
- This function creates a decorator to be applied to the methods of class
- Actor. It represents an ActivityStream Action.
- The decorator will first execute the decorated function, and then schedule
- the delivery of the returned Activity.
-
- NOTE The function that is decorated with this decorator MUST return an
- Activity.
-
- :param func: The function to be decorated.
- " ""
-
- @functools.wraps(func)
- def decorator(self, *args, **kwargs):
- " ""
- Send an activity.
- By default, the Activity is sent to the Actor followers collection.
- To override this behavior it's possible to call the function like this:
- actor.follow(..., to=[], cc=[], bto=[], bcc=[])
-
- https://www.w3.org/TR/activitypub/#delivery
- " ""
-
- forgefed_db = database.start_database_session()
-
- # Create the activity from the Actor by executing the action (function)
- activity = func(self, *args, **kwargs)
-
- # Assign an ID to the Activity
- activity['id'] = '{}/federation/activity/{}'.format(APP_URL, activitypub.new_activity_id())
-
- # Add publishing datetime
- # - use UTC
- # - remove microseconds, use HH:MM:SS only
- # - add timezone info. There is also .astimezone() but it seems to
- # return the wrong value when used with .utcnow(). Bug?
- # - convert to ISO 8601 format
- activity['published'] = activitypub.format_datetime(datetime.datetime.utcnow())
-
- # By default we send the activity to the as:followers collection
- activity['to'] = [ self.followers_uri ]
-
- # Create the list of recipients
- recipients = []
-
- # TODO Check if it's possible to simplify this loop
- for field in [ 'to', 'cc', 'bto', 'bcc' ]:
- if field in kwargs:
- activity[field] = kwargs[field]
-
- if field in activity:
- if isinstance(activity[field], str):
- recipients.append(activity[field])
- else:
- recipients.extend(activity[field])
-
- # Save a copy of the Activity in the database and add it to the Actor's
- # OUTBOX before sending it
- forgefed_db.add(database.Activity(
- uri = activity['id'],
- actor_uri = activity['actor'],
- document = json.dumps(activity)))
-
- forgefed_db.add(database.Collection(
- uri = self.outbox_uri,
- item = activity['id']))
-
- forgefed_db.commit()
- forgefed_db.remove()
-
- # Now we are ready to POST to the remote actors
-
- # Before sending, remove bto and bcc according to spec.
- # https://www.w3.org/TR/activitypub/#client-to-server-interactions
- activity.pop('bto', None)
- activity.pop('bcc', None)
-
- # Stop here if there are no recipients.
- # https://www.w3.org/TR/activitypub/#h-note-8
- if len(recipients) == 0:
- log.debug('No recipients. Activity will not be sent.')
- return
-
- # Make sure the local actor has a GPG key before POSTing anything. The
- # remote Actor will use the key for verifying the Activity.
- database.GpgKey.test_or_set(self.local_uri, self.publickey_uri)
-
- # Create a new Celery task for each recipient. Activities are POSTed
- # individually because if one request fails we don't want to resend
- # the same Activity to *all* the recipients.
- for recipient in recipients:
- log.debug('Scheduling new activity: id={} recipient={}'.format(
- activity['id'], recipient))
-
- tasks.activity.post.delay(
- activity = activity,
- recipient_uri = recipient,
- depth = settings.DELIVERY_DEPTH)
-
- return decorator
- """
- class Actor:
- #@action
- def accept(self, object_uri, *args, **kwargs):
- """
- Accept Activity.
-
- :param object_uri: URI of the ActivityPub object that was accepted.
- """
-
- return {
- 'type': 'Accept',
- 'actor': self.local_uri,
- 'object': object_uri,
- **kwargs }
-
- #@action
- def create(self, object_uri, *args, **kwargs):
- """
- Create Activity.
-
- :param object_uri: URI of the ActivityPub object that was created.
- """
-
- return {
- 'type': 'Create',
- 'actor': self.local_uri,
- 'object': object_uri }
-
- # DONE
- def follow(self, object_uri, *args, **kwargs):
- """
- Follow Activity.
-
- :param object_uri: URI of the ActivityPub actor to follow.
- """
-
- forgefed = self._get_database_session()
- remote_actor = activitypub.fetch(object_uri)
-
- if not remote_actor:
- raise Exception('Could not fetch remote actor.')
-
- log.debug('Remote Actor {} fetched.'.format(object_uri))
-
- # We cache a copy of the remote actor for quick lookups. This is used for
- # example when listing "following/followers" collections. If we only
- # stored the actors' URI we would need to GET the JSON data every time.
- forgefed.merge(model.Cache(
- uri = object_uri,
- document = json.dumps(remote_actor)))
-
- # Add the remote actor to the "following" collection
- forgefed.merge(model.Collection(
- uri = self.following_uri,
- item = object_uri))
-
- # Add a feed
- forgefed.add(model.Feed(
- actor_uri = self.local_uri,
- content = flask.render_template('federation/feed/follow.html', actor=self.jsonld, object=remote_actor)
- ))
-
- self.send_activity({
- 'type': 'Follow',
- 'object': object_uri,
- 'to': object_uri
- })
-
- #@action
- def offer(self, object, *args, **kwargs):
- """
- Offer Activity.
-
- :param object: Object to offer. Either a URI or a dictionary.
- """
-
- return {
- 'type': 'Offer',
- 'actor': self.local_uri,
- 'object': object }
-
- #@action
- def resolve(self, object, *args, **kwargs):
- """
- Resolve Activity.
-
- :param object: Object that has been resolved.
- """
-
- return {
- 'type': 'Resolve',
- 'actor': self.local_uri,
- 'object': object }
-
- #@action
- def update(self, object, *args, **kwargs):
- """
- Update Activity.
-
- :param object: The object that was updated.
- """
-
- return {
- 'type': 'Update',
- 'actor': self.local_uri,
- 'object': object }
|