123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- # Useful documentation for SQLAlchemy ORM:
- # https://docs.sqlalchemy.org/en/13/orm/tutorial.html
- # https://docs.sqlalchemy.org/en/13/orm/basic_relationships.html
- import flask
- import json
- import pagure.lib.model
- import random
- import string
- from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, \
- LargeBinary, UnicodeText, func
- from sqlalchemy import PrimaryKeyConstraint
- from sqlalchemy.orm import relationship
- from sqlalchemy.ext.declarative import declarative_base
- from . import activitypub
- from . import database
- from . import settings
- from . import tasks
- from Crypto.PublicKey import RSA
- from pagure.config import config as pagure_config
- # The app URL defined in the Pagure configuration, eg. "https://example.org/"
- # We need this for generating Actor IDs.
- APP_URL = pagure_config['APP_URL'].rstrip('/')
- assert APP_URL and len(APP_URL) > 0, 'APP_URL missing from Pagure configuration.'
- # We use SQLAlchemy "declarative" mappings: https://docs.sqlalchemy.org/en/13/orm/tutorial.html#declare-a-mapping
- BASE = declarative_base()
- def new_random_id(length=32):
- """
- Generate a random string to use as an Activity ID when a new one is
- created.
- """
-
- symbols = string.ascii_lowercase + string.digits
- return ''.join([ random.choice(symbols) for i in range(length) ])
- class Actor:
- @property
- def inbox_id(self):
- return self.actor_id + '/inbox'
-
- @property
- def outbox_id(self):
- return self.actor_id + '/outbox'
-
- @property
- def followers_id(self):
- return self.actor_id + '/followers'
-
- @property
- def following_id(self):
- return self.actor_id + '/following'
-
- @property
- def publickey_id(self):
- return self.actor_id + '/key.pub'
-
- def _deliver(self, activity):
- """
- Send an activity.
- https://www.w3.org/TR/activitypub/#delivery
-
- :param activity: Python dictionary of the Activity to send.
- """
-
- activity_document = json.dumps(activity)
-
- db = database.start_database_session()
-
- # Save a copy of the Activity in the database before sending
- db.add(Activity(id = activity['id'],
- actor_id = self.actor_id,
- document = activity_document))
-
- # Save the Activity to the Actor OUTBOX
- # outbox = Outbox(actor_id = self.actor_id,
- # activity_id = activity.id)
- #db.add(outbox)
-
- # Close db session
- db.commit()
- db.remove()
-
- # Now we are ready to POST to the remote actors
-
- # First off, we extract the list of recipients and remove the bto:
- # and bcc: recipients from the Activity before it's sent.
- recipients = []
-
- if 'to' in activity:
- if isinstance(activity['to'], str):
- activity['to'] = [ activity['to'] ]
-
- recipients.extend(activity['to'])
-
- if 'cc' in activity:
- if isinstance(activity['cc'], str):
- activity['cc'] = [ activity['cc'] ]
-
- recipients.extend(activity['cc'])
-
- if 'bto' in activity:
- if isinstance(activity['bto'], str):
- activity['bto'] = [ activity['bto'] ]
-
- recipients.extend(activity['bto'])
-
- # Remove according to spec.
- # https://www.w3.org/TR/activitypub/#client-to-server-interactions
- del activity['bto']
-
- if 'bcc' in activity:
- if isinstance(activity['bcc'], str):
- activity['bcc'] = [ activity['bcc'] ]
-
- recipients.extend(activity['bcc'])
-
- # Remove according to spec.
- # https://www.w3.org/TR/activitypub/#client-to-server-interactions
- del activity['bcc']
-
- # Stop here if there are no recipients.
- # https://www.w3.org/TR/activitypub/#h-note-8
- if len(recipients) == 0:
- return
-
- # Create a new Celery task individual delivery to each recipient
- for recipient in recipients:
- tasks.post_activity.delay(
- activity_document = activity_document,
- recipient_id = recipient,
- indirections = settings.DELIVERY_INDIRECTIONS)
-
- def accept(self, object_id):
- """
- Accept an ActivityPub object.
- """
-
- # Create the RDF document of the "Accept" Activity
- activity = {
- 'id': self.actor_id + '/outbox/' + new_random_id(),
- 'type': 'Accept',
- 'actor': self.actor_id,
- 'object': object_id
- }
-
- # Send the "Accept" Activity to the remote actors
- self._deliver(activity)
- class Person(pagure.lib.model.User, Actor):
- @property
- def actor_id(self):
- return APP_URL + '/' + self.url_path
-
- def to_rdf(self):
- return {
- '@context': activitypub.jsonld_context,
- 'type': 'Person',
- 'name': self.fullname,
- 'preferredUsername': self.username,
- 'id': self.actor_id,
- 'inbox': self.inbox_id,
- 'outbox': self.outbox_id,
- 'followers': self.followers_id,
- 'following': self.following_id,
- 'publicKey': self.publickey_id,
- }
- class Repository(pagure.lib.model.Project, Actor):
- @property
- def actor_id(self):
- return APP_URL + '/' + self.url_path + '.git'
-
- def to_rdf(self):
- return {
- '@context': activitypub.jsonld_context,
- 'type': 'Repository',
- 'name': self.name,
- 'id': self.actor_id,
- 'inbox': self.inbox_id,
- 'outbox': self.outbox_id,
- 'followers': self.followers_id,
- 'following': self.following_id,
- 'publicKey': self.publickey_id,
- 'team': None,
- }
- class GpgKey(BASE):
- """
- This class represents a Person GPG key that is used to sign HTTP POST
- requests.
- """
-
- __tablename__ = 'forgefed_gpg_key'
- __table_args__ = (
- PrimaryKeyConstraint('actor_id', 'private'),
- )
-
- # The actor who owns the key
- actor_id = Column(UnicodeText, nullable=False)
-
- # The private part of the key
- private = Column(LargeBinary, nullable=False)
-
- # The public part of the key
- public = Column(LargeBinary, nullable=False)
-
- # When was this key created
- created = Column(DateTime, nullable=False, default=func.now())
-
- @staticmethod
- def new_key():
- """
- Returns a new private/public key pair.
- """
-
- key = RSA.generate(settings.HTTP_SIGNATURE_KEY_BITS)
-
- return {
- 'private': key.export_key('PEM'),
- 'public': key.publickey().export_key('PEM')
- }
-
- @staticmethod
- def test_or_set(actor_id):
- """
- Test if an Actor already has a GPG key, otherwise automatically generate
- a new one.
-
- :param actor_id: ID (URL) of the ActivityPub Actor.
- """
-
- db = database.start_database_session()
-
- key = db.query(GpgKey) \
- .filter(GpgKey.actor_id == actor_id) \
- .one_or_none()
-
- if not key:
- key = GpgKey.new_key()
-
- db.add(GpgKey(actor_id = actor_id,
- private = key['private'],
- public = key['public']))
-
- db.commit()
-
- db.remove()
- class Follower(BASE):
- """
- This class represents a "Follow" relationship.
- """
-
- __tablename__ = 'forgefed_follower'
- __table_args__ = (
- PrimaryKeyConstraint('subject_id', 'object_id'),
- )
-
- # Actor that is following
- subject_id = Column(UnicodeText, nullable=False)
-
- # Actor that is followed
- object_id = Column(UnicodeText, nullable=False)
-
- # When was this relation established
- established = Column(DateTime, nullable=False, default=func.now())
- class Activity(BASE):
- """
- This class represents an ActivityPub Activity.
- """
-
- __tablename__ = 'forgefed_activity'
-
- # The ID of the Activity (a URL)
- id = Column(UnicodeText, primary_key=True, default=new_random_id)
-
- # The ID (URL) of the Actor who sent the Activity.
- actor_id = Column(UnicodeText, nullable=False)
-
- # The JSON-LD document of the activity
- document = Column(UnicodeText, nullable=False)
-
- # When an Activity was received
- received = Column(DateTime, nullable=False, default=func.now())
-
- deliveries = relationship('Delivery', back_populates='activity')
- #inboxes = relationship('Inbox', back_populates='activity')
- #outboxes = relationship('Outbox', back_populates='activity')
- class Delivery(BASE):
- """
- This class/table records which Activity was delivered to which Actor.
- It records both incoming and outgoing activities.
- """
-
- __tablename__ = 'forgefed_delivery'
- __table_args__ = (
- PrimaryKeyConstraint('activity_id', 'recipient_id'),
- )
-
- # The ID of the Activity
- activity_id = Column(
- UnicodeText,
- ForeignKey('forgefed_activity.id'),
- nullable=False)
-
- # The ID (URL) of the Actor who received the Activity.
- recipient_id = Column(UnicodeText, nullable=False)
-
- # The ID of the INBOX used to send the Activity to the Actor. An Actor can
- # have both an "inbox" and "sharedInbox".
- recipient_inbox = Column(UnicodeText, nullable=False)
-
- # DateTime of when the Activity was delivered
- delivered = Column(DateTime, nullable=False, default=func.now())
-
- activity = relationship('Activity', back_populates='deliveries')
- """
- class Inbox(BASE):
- # This class represents an Actor INBOX messages.
-
- __tablename__ = 'forgefed_inbox'
- __table_args__ = (
- PrimaryKeyConstraint('actor_id', 'activity_id'),
- )
-
- # The ID of the Actor (a URL)
- actor_id = Column(UnicodeText, nullable=False)
-
- # The ID of the Activity
- activity_id = Column(
- UnicodeText,
- ForeignKey('forgefed_activity.id'),
- nullable=False)
-
- # When an Activity was added to the Actor INBOX
- received = Column(DateTime, nullable=False, default=func.now())
-
- activity = relationship('Activity', back_populates='inboxes')
- class Outbox(BASE):
- # This class represents an Actor OUTBOX messages.
-
- __tablename__ = 'forgefed_outbox'
- __table_args__ = (
- PrimaryKeyConstraint('actor_id', 'activity_id'),
- )
-
- # The ID of the Actor (a URL)
- actor_id = Column(UnicodeText, nullable=False)
-
- # The ID of the Activity
- activity_id = Column(
- UnicodeText,
- ForeignKey('forgefed_activity.id'),
- nullable=False)
-
- # When an Activity was added to the Actor OUTBOX
- received = Column(DateTime, nullable=False, default=func.now())
-
- activity = relationship('Activity', back_populates='outboxes')
- """
- # Automatically create the database tables on startup. The tables are first
- # checked for existence before any CREATE TABLE command is issued.
- # https://docs.sqlalchemy.org/en/13/orm/tutorial.html#create-a-schema
- BASE.metadata.create_all(database.engine)
|