Ejemplo n.º 1
0
class APObjectTag(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    type = db.Column(db.Enum(APObjectType))
    href = db.Column(db.String(256))
    name = db.Column(db.String(64))
    ap_object_id = db.Column(db.ForeignKey('ap_object.id'), nullable=False)

    ap_object = db.relationship('APObject',
                                foreign_keys=[ap_object_id],
                                backref=db.backref(
                                    'tags', cascade='all, delete-orphan'))

    def __init__(self, ap_object_id, _type, href, name):
        self.ap_object_id = ap_object_id
        self.type = _type
        self.href = href
        self.name = name

    def to_dict(self):

        output = {'type': self.type.value}
        if self.href is not None:
            output['href'] = self.href
        if self.name is not None:
            output['name'] = self.name

        return output
Ejemplo n.º 2
0
class Following(db.Model):
    '''
        Representation of who a user is following both locally
        and federally.

        This model also records which ActivityPub outbox page the
        follower most recently requested to view for a given leader.
    '''
    id = db.Column(db.Integer, nullable=False, primary_key=True)
    approved = db.Column(db.Boolean, nullable=False)
    leader = db.Column(db.String(256), nullable=False)
    followers_collection = db.Column(db.String(256), nullable=False)

    follower_id = db.Column(db.Integer,
                            db.ForeignKey('actor.id'),
                            nullable=False)
    follower = db.relationship('Actor', backref='following')

    def __init__(self,
                 follower_id,
                 leader,
                 followers_collection,
                 approved=False):
        self.follower_id = follower_id
        self.leader = leader
        self.followers_collection = followers_collection
        self.approved = approved
Ejemplo n.º 3
0
class Notification(db.Model):

    id = db.Column(db.Integer, primary_key=True)

    actor_id = db.Column(db.Integer, db.ForeignKey('ap_object.id'))
    content = db.Column(db.String(256), nullable=False)
    date = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
    type = db.Column(db.String(16), nullable=False)

    actor = db.relationship('Actor')

    def __init__(self, actor, content, _type):
        '''
            actor: Actor to which this notification belongs
            content (str): Message contents of the notification
            _type (str): Type of notification (ie, Follow, Dislike, Mention, etc.)
        '''
        self.actor_id = actor.id
        self.content = content
        self.type = _type

    def to_dict(self):
        return {
            'id': self.id,
            'content': self.content,
            'published': xsd_datetime(self.date),
            'type': self.type
        }
Ejemplo n.º 4
0
class User(db.Model):
    id = db.Column(db.Integer, nullable=False, primary_key=True)
    username = db.Column(db.String(255), nullable=False)
    password_hash = db.Column(db.String(255), nullable=False)
    actors = db.relationship('Actor')

    def __init__(self, username, password):
        self.username = username.lower()
        self.password_hash = hashpw(bytes(password, 'utf-8'), gensalt())
Ejemplo n.º 5
0
class User(db.Model):
    id = db.Column(db.Integer, nullable=False, primary_key=True)
    username = db.Column(db.String(32), unique=True, nullable=False)
    password_hash = db.Column(db.String(255), nullable=False)

    primary_actor_id = db.Column(db.Integer, db.ForeignKey('actor.id'))
    primary_actor = db.relationship('Actor', foreign_keys=[primary_actor_id])

    def __init__(self, username, password):
        self.username = username.lower()
        self.password_hash = hashpw(bytes(password, 'utf-8'), gensalt())
Ejemplo n.º 6
0
class APObjectRecipient(db.Model):

    id = db.Column(db.Integer, primary_key=True)
    ap_object_id = db.Column(db.Integer,
                             db.ForeignKey('ap_object.id'),
                             nullable=False)
    method = db.Column(db.String(3), nullable=False)
    recipient = db.Column(db.String(256), nullable=False)

    ap_object = db.relationship('APObject', backref='recipients')

    def __init__(self, ap_object_id, method, recipient):
        self.ap_object_id = ap_object_id
        self.method = method
        self.recipient = recipient
Ejemplo n.º 7
0
class APObjectRecipient(db.Model):

    id = db.Column(db.Integer, primary_key=True)
    ap_object_id = db.Column(db.Integer,
                             db.ForeignKey('ap_object.id'),
                             nullable=False)
    method = db.Column(db.String(3), nullable=False)
    recipient = db.Column(db.String(256), nullable=False)

    ap_object = db.relationship(
        'APObject',
        foreign_keys=[ap_object_id],
        backref=db.backref('recipients', cascade='all, delete-orphan'))

    def __init__(self, method, recipient):
        self.method = method
        self.recipient = recipient
Ejemplo n.º 8
0
class Note(db.Model):
    id = db.Column(db.Integer, nullable=False, primary_key=True)
    actor_id = db.Column(db.Integer, db.ForeignKey('actor.id'), nullable=False)
    content = db.Column(db.String(1024), nullable=False)
    author = db.relationship('Actor')
    published = db.Column(db.DateTime, default=datetime.utcnow)

    def to_dict(self):
        return {}
Ejemplo n.º 9
0
class Actor(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    public_key = db.Column(db.Text(16639))  #PEM format
    private_key = db.Column(db.Text(16639))  #PEM format
    username = db.Column(db.String(32), nullable=False)
    user = db.relationship('User')
    notes = db.relationship('Note')

    def __init__(self, *args, **kwargs):
        if kwargs.get('user_id') != None:
            self.user_id = kwargs.get('user_id')
        elif kwargs.get('user') != None:
            self.user_id = user.id
        else:
            raise Exception(
                'Instantiating an Actor requires either a user object or user id. '
            )

        if kwargs.get('username') != None:
            self.username = kwargs.get('username')

    def to_dict(self):

        api_url = config['api_url']
        username = self.username

        return {
            '@context': [
                'https://www.w3.org/ns/activitystreams',
                'https://w3id.org/security/v1'
            ],
            'id':
            f'{api_url}/actors/{username}',
            'type':
            'Person',
            'inbox':
            f'{api_url}/actors/{username}/inbox',
            'outbox':
            f'{api_url}/actors/{username}/outbox',
            'followers':
            f'{api_url}/actors/{username}/followers',
            'following':
            f'{api_url}/actors/{username}/following',
            'liked':
            f'{api_url}/actors/{username}/liked',
            'preferredUsername':
            self.username,
            'publicKey': {
                'actor': f'{api_url}/actors/{username}',
                'id': f'{api_url}/actors/{username}#main-key',
                'publicKeyPem': self.public_key
            }
        }
Ejemplo n.º 10
0
class APObjectAttributedTo(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    internal_actor_id = db.Column(db.ForeignKey('actor.id'))
    external_actor_id = db.Column(db.String(1024))
    ap_object_id = db.Column(db.Integer,
                             db.ForeignKey('ap_object.id'),
                             nullable=False)

    ap_object = db.relationship('APObject',
                                uselist=False,
                                foreign_keys=[ap_object_id])
    internal_actor = db.relationship('Actor', foreign_keys=[internal_actor_id])
Ejemplo n.º 11
0
class FollowedBy(db.Model):
    '''
        Representation of who is following a local Vagabond actor. The inboxes
        (and shared inboxes, if they exist) of the followers are stored in this table
        as well.
    '''

    id = db.Column(db.Integer, primary_key=True)

    leader_id = db.Column(db.Integer,
                          db.ForeignKey('ap_object.id'),
                          nullable=False)
    leader = db.relationship('Actor', backref='followed_by')

    approved = db.Column(db.Boolean, nullable=False)

    follower = db.Column(db.String(256), nullable=False)  #URL of follower

    follower_inbox = db.Column(db.String(256), nullable=False)
    follower_shared_inbox = db.Column(db.String(256), nullable=True)

    def __init__(self,
                 leader_id,
                 follower,
                 follower_inbox,
                 follower_shared_inbox=None,
                 approved=True):
        '''
            leader_id: int
            follower: str
                URL of the follower
            follower_inbox: str
            ?follower_shared_inbox: str
        '''
        self.leader_id = leader_id
        self.follower = follower
        self.follower_inbox = follower_inbox
        self.follower_shared_inbox = follower_shared_inbox
        self.approved = approved
Ejemplo n.º 12
0
class APObjectAttributedTo(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    internal_actor_id = db.Column(db.ForeignKey('ap_object.id'))
    external_actor_id = db.Column(db.String(1024))
    ap_object_id = db.Column(db.Integer,
                             db.ForeignKey('ap_object.id'),
                             nullable=False)

    ap_object = db.relationship('APObject',
                                foreign_keys=[ap_object_id],
                                backref=db.backref(
                                    'attributions',
                                    cascade='all, delete-orphan'))
Ejemplo n.º 13
0
class Actor(APObject):
    id = db.Column(db.Integer, db.ForeignKey('ap_object.id'), primary_key=True)
    username = db.Column(db.String(32), unique=True, nullable=False)
    public_key = db.Column(db.Text(16639))
    private_key = db.Column(db.Text(16639))

    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    user = db.relationship('User', backref='actors', foreign_keys=[user_id])

    __mapper_args__ = {'polymorphic_identity': APObjectType.PERSON}

    def __init__(self, username, user=None, user_id=None):

        self.username = username

        if user_id is not None:
            self.user_id = user_id
        elif user is not None:
            self.user_id = user.id
        else:
            raise Exception(
                'Instantiating an Actor requires either a user object or user id. '
            )

        key = RSA.generate(2048)
        self.private_key = key.export_key()
        self.public_key = key.publickey().export_key()
        self.type = APObjectType.PERSON

    def to_dict(self):

        api_url = config['api_url']
        username = self.username
        output = super().to_dict()

        output['id'] = f'{api_url}/actors/{username}'
        output['inbox'] = f'{api_url}/actors/{username}/inbox'
        output['outbox'] = f'{api_url}/actors/{username}/outbox'
        output['followers'] = f'{api_url}/actors/{username}/followers'
        output['following'] = f'{api_url}/actors/{username}/following'
        output['liked'] = f'{api_url}/actors/{username}/liked'
        output['preferredUsername'] = self.username
        output['endpoints'] = {'sharedInbox': f'{api_url}/inbox'}

        output['publicKey'] = {
            'actor': f'{api_url}/actors/{username}',
            'id': f'{api_url}/actors/{username}#main-key',
            'publicKeyPem': self.public_key
        }

        return output
Ejemplo n.º 14
0
class Actor(APObject):

    username = db.Column(db.String(32), unique=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    user = db.relationship('User', backref='actors', foreign_keys=[user_id])

    __mapper_args__ = {'polymorphic_identity': APObjectType.PERSON}

    def __init__(self, username, user=None, user_id=None):

        self.username = username

        if user_id is not None:
            self.user_id = user_id
        elif user is not None:
            self.user_id = user.id
        else:
            raise Exception(
                'Instantiating an Actor requires either a user object or user id. '
            )

        self.type = APObjectType.PERSON

    def to_dict(self):

        api_url = os.environ['API_URL']
        username = self.username
        output = super().to_dict()

        output['id'] = f'{api_url}/actors/{username}'
        output['inbox'] = f'{api_url}/actors/{username}/inbox'
        output['outbox'] = f'{api_url}/actors/{username}/outbox'
        output['followers'] = f'{api_url}/actors/{username}/followers'
        output['following'] = f'{api_url}/actors/{username}/following'
        output['liked'] = f'{api_url}/actors/{username}/liked'
        output['preferredUsername'] = self.username
        output['endpoints'] = {'sharedInbox': f'{api_url}/inbox'}

        output['publicKey'] = {
            'actor': f'{api_url}/actors/{username}',
            'id': f'{api_url}/actors/{username}#main-key',
            'publicKeyPem': os.environ['PUBLIC_KEY']
        }

        return output
Ejemplo n.º 15
0
class APObject(db.Model):
    '''
        Superclass for all ActivityPub objects including instances of Activity
    '''
    id = db.Column(db.Integer, primary_key=True)
    external_id = db.Column(db.String(256), unique=True)

    in_reply_to_internal_id = db.Column(db.ForeignKey('ap_object.id'))
    in_reply_to_external_id = db.Column(db.String(256))

    content = db.Column(db.String(4096))
    published = db.Column(db.DateTime, default=datetime.utcnow)
    type = db.Column(db.Enum(APObjectType))

    #attributedTo property
    internal_author_id = db.Column(db.Integer, db.ForeignKey('ap_object.id'))
    external_author_id = db.Column(db.String(256))

    __mapper_args__ = {
        'polymorphic_identity': APObjectType.OBJECT,
        'polymorphic_on': type
    }

    @staticmethod
    def get_object_from_url(url):
        '''
            Takes the provided URL and attempts to locate an object with the matching external_id.
            This method works even for locally stored objects without an external_id property by parsing
            the provided URL and extracting the ID. 
        '''
        api_url = os.environ['API_URL']
        if url.replace(api_url, '') != url:
            url = url.replace(api_url, '')
            splits = url.split('/')
            if len(splits) == 3:
                if splits[1] == 'actors':
                    try:
                        username = splits[2]
                        return db.session.query(vagabond.models.Actor).filter(
                            db.func.lower(username) == db.func.lower(
                                vagabond.models.Actor.username)).first()
                    except:
                        return None
                elif splits[1] == 'objects':
                    try:
                        _id = int(splits[2])
                        return db.session.query(
                            vagabond.models.APObject).get(_id)
                    except:
                        return None
        else:
            obj = db.session.query(APObject).filter(
                APObject.external_id == url).first()
            return obj

    def set_in_reply_to(self, in_reply_to):
        '''
            Sets the inReplyTo propety of this APObject
        '''

        if isinstance(in_reply_to, dict):
            if 'id' in in_reply_to:
                self.in_reply_to_external_id = in_reply_to['id']
                obj = APObject.get_object_from_url(in_reply_to['id'])
                if obj is not None:
                    self.in_reply_to_internal_id = obj.id
            else:
                raise Exception(
                    'Cannot set inReplyTo property: provided dictionary lacks \'id\' property.'
                )

        elif isinstance(in_reply_to, str):
            self.in_reply_to_external_id = in_reply_to
            obj = APObject.get_object_from_url(in_reply_to)
            if obj is not None:
                self.in_reply_to_internal_id = obj.id

        elif (isinstance(in_reply_to, db.Model)):
            self.in_reply_to_external_id = in_reply_to.to_dict()['id']
            self.in_reply_to_internal_id = in_reply_to.id

    def to_dict(self):

        api_url = os.environ['API_URL']

        output = {
            '@context': ["https://www.w3.org/ns/activitystreams"],
            'type': self.type.value,
        }

        if self.external_id is not None:
            output['id'] = self.external_id
        else:
            output['id'] = f'{api_url}/objects/{self.id}'

        #inReplyTo - what this object is in reply to
        if self.in_reply_to_internal_id is not None:
            in_reply_to = db.session.query(APObject).get(
                self.in_reply_to_internal_id)
            if in_reply_to is not None:
                output['inReplyTo'] = in_reply_to.to_dict()
        elif self.in_reply_to_external_id is not None:
            output['inReplyTo'] = self.in_reply_to_external_id

        #inReplyTo - what objects are a reply to this one
        if self.type == APObjectType.NOTE and self.id is not None:
            total_items = db.session.query(APObject).filter(
                APObject.in_reply_to_internal_id == self.id).count()
            if total_items > 0:
                output['replies'] = f'{api_url}/objects/{self.id}/replies'

        #attributedTo
        if hasattr(
                self,
                'external_author_id') and self.external_author_id is not None:
            output['attributedTo'] = self.external_author_id
        elif hasattr(
                self,
                'internal_author_id') and self.internal_author_id is not None:
            attributed_to = db.session.query(APObject).get(
                self.internal_author_id)
            if attributed_to is not None:
                output['attributedTo'] = attributed_to.to_dict()['id']

        #tag property
        if len(self.tags) > 0:
            output['tag'] = []
            for tag in self.tags:
                output['tag'].append(tag.to_dict())

        if hasattr(self, 'content') and self.content is not None:
            output['content'] = self.content

        if hasattr(self, 'published'):
            output['published'] = xsd_datetime(self.published)

        if hasattr(self, 'recipients'):
            for recipient in self.recipients:

                if recipient.method not in output:
                    output[recipient.method] = []

                output[recipient.method].append(recipient.recipient)

        return output

    def add_tag(self, tag):
        '''
         def __init__(self, ap_object_id, type, href, name):
            Adds a tag to this object.
            tag: dict | APObjectTag
        '''
        if isinstance(tag, dict):
            _type = None
            if tag['type'] == 'Mention':
                self.tags.append(
                    APObjectTag(self.id, APObjectType.MENTION, tag.get('href'),
                                tag.get('name')))
            else:
                raise Exception('Only supported tag type is Mention')
        elif isinstance(tag, APObjectTag):
            self.tags.append(tag)
        else:
            raise Exception(
                'APObject#add_tag method only accepts dictionaries and instances of APObjectTag as input. Some other type was provided.'
            )

    def add_recipient(self, method, recipient):
        '''
            method = 'to' | 'bto' | 'cc' | 'bcc'
            recipient = Actor URL ID

            Adds an actor as a recipient of this object using either the to, bto, cc, or bcc
            ActivityPub fields. 
        '''
        method = method.lower()
        if method != 'to' and method != 'bto' and method != 'cc' and method != 'bcc':
            raise Exception(
                "Only acceptable values for APObject#add_recipient are 'to', 'bto', 'cc', and 'bcc'"
            )

        self.recipients.append(APObjectRecipient(method, recipient))

    def add_all_recipients(self, obj: dict):
        '''
            Takes a dictionary object which contains some combination of 
            the 'to', 'bto', 'cc', and 'bcc' fields and adds the intended recipients 
            as recipients of this object.

            The four aformentioned fields can be either strings or lists.
        '''
        keys = ['to', 'bto', 'cc', 'bcc']
        for key in keys:
            recipients = obj.get(key)
            if recipients is not None:
                if isinstance(recipients, str):
                    recipients = [recipients]
                elif isinstance(recipients, list) is not True:
                    raise Exception(
                        f'APObject#add_all_recipients method given an object whose {key} value was neither a string nor an array.'
                    )

                #prevent duplicate entries
                uniques = []
                for recipient in recipients:
                    if recipients not in uniques:
                        uniques.append(recipient)

                for recipient in uniques:
                    self.add_recipient(key, recipient)

    def attribute_to(self, author):
        '''
            author: str | Model | int

            A string indicates that the object is being attributed to an external actor while
            a SQLAlchemy model or integer indicates a local actor.

        '''
        #TODO: use get_object_from_url to set internal author id
        if isinstance(author, str):
            self.external_author_id = author
        elif isinstance(author, db.Model):
            self.internal_author_id = author.id
        elif isinstance(author, int):
            self.internal_author_id = author
Ejemplo n.º 16
0
class Activity(APObject):

    internal_object_id = db.Column(db.ForeignKey('ap_object.id'))
    external_object_id = db.Column(db.String(1024))
    internal_actor_id = db.Column(db.ForeignKey('ap_object.id'))
    external_actor_id = db.Column(db.String(1024))
    
    internal_object = db.relationship('APObject', foreign_keys=[internal_object_id], remote_side=APObject.id, backref=db.backref('activities', cascade='all, delete-orphan'))


    __mapper_args__ = {
        'polymorphic_identity': APObjectType.ACTIVITY
    }


    def set_object(self, obj):
        '''
            Input: vagabond.models.APObject | str | dict

            Takes an instance of APObject, a URL representing the object,
            or a dictionary representing the object and wraps this instance
            of Activity around it.
        '''
        if isinstance(obj, db.Model):
            self.internal_object_id = obj.id
            

        elif isinstance(obj, str):
            interal_object = APObject.get_object_from_url(obj)
            if interal_object is not None:
                self.internal_object_id = interal_object.id

            self.external_object_id = obj

        elif isinstance(obj, dict) and 'id' in obj:
            interal_object = APObject.get_object_from_url(obj['id'])
            if interal_object is not None:
                self.internal_object_id = interal_object.id      
            self.external_object_id = obj['id']

        else:
            raise Exception('Activity#set_object method requires an APObject, a string, or a dictionary')
        

    def set_actor(self, actor):
        '''
            Input: vagabond.models.Actor | str | dict
        '''
        if isinstance(actor, db.Model):
            self.internal_actor_id = actor.id
        elif isinstance(actor, str):
            self.external_actor_id = actor
        elif isinstance(actor, dict) and 'id' in actor:
            self.external_actor_id = actor['id']
        else:
            raise Exception('Activity#set_actor method requires an Actor, a string, or a dictionary')


    def to_dict(self):
        output = super().to_dict()

        if self.internal_object_id is not None:
            _object = db.session.query(APObject).get(self.internal_object_id)
            if _object is not None:
                    output['object'] = _object.to_dict()

        elif self.external_object_id is not None:
            output['object'] = self.external_object_id
        else:
            raise Exception('Activites must have an internal or external object.')

        if self.internal_actor_id is not None:
            output['actor'] = db.session.query(APObject).get(self.internal_actor_id).to_dict()['id']
        elif self.external_actor_id is not None:
            output['actor'] = self.external_actor_id
        else:
            raise Exception('Activies must have an internal or external actor')

        return output
Ejemplo n.º 17
0
class Activity(APObject):
    id = db.Column(db.Integer, db.ForeignKey('ap_object.id'), primary_key=True)

    internal_object_id = db.Column(db.ForeignKey('ap_object.id'))
    external_object_id = db.Column(db.String(1024))
    internal_actor_id = db.Column(db.ForeignKey('actor.id'))
    external_actor_id = db.Column(db.String(1024))

    actor = db.relationship('Actor',
                            foreign_keys=[internal_actor_id
                                          ])  #Person performing the activity
    object = db.relationship('APObject',
                             foreign_keys=internal_object_id,
                             uselist=False)

    __mapper_args__ = {
        'polymorphic_identity': APObjectType.ACTIVITY,
        'inherit_condition': id == APObject.id
    }

    def set_object(self, obj):
        '''
            Input: vagabond.models.APObject | str | dict

            Takes an instance of APObject, a URL representing the object,
            or a dictionary representing the object and wraps this instance
            of Activity around it.
        '''
        if isinstance(obj, db.Model):
            self.internal_object_id = obj.id
        elif isinstance(obj, str):
            self.external_object_id = obj
        elif isinstance(obj, dict) and 'id' in obj:
            self.external_object_id = obj['id']
        else:
            raise Exception(
                'Activity#set_object method requires an APObject, a string, or a dictionary'
            )

    def set_actor(self, actor):
        '''
            Input: vagabond.models.Actor | str | dict
        '''
        if isinstance(actor, db.Model):
            self.internal_actor_id = actor.id
        elif isinstance(actor, str):
            self.external_actor_id = actor
        elif isinstance(actor, dict) and 'id' in actor:
            self.external_actor_id = actor['id']
        else:
            raise Exception(
                'Activity#set_actor method requires an Actor, a string, or a dictionary'
            )

    def to_dict(self):
        output = super().to_dict()

        if self.object is not None:
            output['object'] = self.object.to_dict()
        elif self.external_object_id is not None:
            output['object'] = self.external_object_id
        else:
            raise Exception(
                'Activites must have an internal or external object.')

        if self.internal_actor_id is not None:
            output[
                'actor'] = f'{config["api_url"]}/actors/{self.actor.username}'
        elif self.external_actor_id is not None:
            output['actor'] = self.external_actor_id
        else:
            raise Exception('Activies must have an internal or external actor')

        return output
Ejemplo n.º 18
0
class APObject(db.Model):
    '''
        Superclass for all ActivityPub objects including instances of Activity
    '''
    id = db.Column(db.Integer, primary_key=True)
    external_id = db.Column(db.String(256), unique=True)
    context = ["https://www.w3.org/ns/activitystreams"]
    content = db.Column(db.String(4096))
    published = db.Column(db.DateTime, default=datetime.utcnow)
    type = db.Column(db.Enum(APObjectType))
    attributed_to = db.relationship('APObjectAttributedTo', uselist=False)

    __mapper_args__ = {
        'polymorphic_identity': APObjectType.OBJECT,
        'polymorphic_on': type
    }

    def to_dict(self):

        api_url = config['api_url']

        output = {
            '@context': self.context,
            'type': self.type.value,
        }

        if self.external_id is not None:
            output['id'] = self.external_id
        else:
            output['id'] = f'{api_url}/objects/{self.id}'

        if self.attributed_to is not None:
            if self.attributed_to.internal_actor_id is not None:
                output[
                    'attributedTo'] = f'{api_url}/actors/{self.attributed_to.internal_actor.username}'
            elif self.attributed_to.external_actor_id is not None:
                output['attributedTo'] = self.attributed_to.external_actor_id

        if self.content is not None:
            output['content'] = self.content

        if self.published is not None:
            output['published'] = xsd_datetime(self.published)

        if hasattr(self, 'recipients'):
            for recipient in self.recipients:
                if output.get(recipient.method) is None:
                    output[recipient.method] = [recipient.recipient]
                else:
                    output[recipient.method].append(recipient.recipient)

        return output

    def add_recipient(self, method: str, recipient):
        '''
            method = 'to' | 'bto' | 'cc' | 'bcc'
            recipient = Actor URL ID

            Adds an actor as a recipient of this object using either the to, bto, cc, or bcc
            ActivityPub fields. This method adds records to the database but does not commit or flush
            them.
        '''
        method = method.lower()
        if method != 'to' and method != 'bto' and method != 'cc' and method != 'bcc':
            raise Exception(
                "Only acceptable values for APObject#add_recipient are 'to', 'bto', 'cc', and 'bcc'"
            )

        db.session.add((APObjectRecipient(self.id, method, recipient)))

    def add_all_recipients(self, obj: dict):
        '''
            Takes a dictionary object which contains some combination of 
            the 'to', 'bto', 'cc', and 'bcc' fields and adds the intended recipients 
            as recipients of this object.

            The four aformentioned fields can be either strings or lists.
        '''
        keys = ['to', 'bto', 'cc', 'bcc']
        for key in keys:
            value = obj.get(key)
            if value is not None:
                if isinstance(value, str):
                    value = [value]
                elif isinstance(value, list) is not True:
                    raise Exception(
                        f'APObject#add_all_recipients method given an object whose {key} value was neither a string nor an array.'
                    )

                for _value in value:
                    self.add_recipient(key, _value)

    def add_to_inbox(self, actor):
        '''
            actor: Vagabond.models.Actor | int
            Puts this object into the inbox of a local actor.

        '''

    def attribute_to(self, author):
        '''
            author: str | Model | int

            Creates an instance of APObjectAttributedTo that represents an attribution
            to the input id, external actor URL, or vagabond.models.Actor instance.

            A string indicates that the object is being attributed to an external actor while
            a SQLAlchemy model or integer indicates a local actor.

            The newly created instance of APObjectAttributedTo is added to the database session,
            but not committed or flushed.
        '''
        attribution = APObjectAttributedTo()
        attribution.ap_object_id = self.id

        if isinstance(author, str):
            attribution.external_actor_id = author
        elif isinstance(author, db.Model):
            attribution.internal_actor_id = author.id
        elif isinstance(author, int):
            attribution.internal_actor_id = author

        db.session.add(attribution)