class Notification(db.Model): """Our fancy notification class.""" id = db.Column(db.Integer(), primary_key=True) category = db.Column(db.String()) object_uri = db.Column(db.String()) icon = db.Column(db.String()) seen = db.Column(db.Boolean()) acknowledged = db.Column(db.Boolean()) created_by_actor_id = db.Column( db.Integer(), db.ForeignKey('actors.id', ondelete='SET NULL', name='fk_notification_by_actor'), nullable=True, ) for_identity_id = db.Column( db.Integer(), db.ForeignKey('identities.id', ondelete='CASCADE', name='fk_notification_for_identity'), ) created = db.Column(db.DateTime())
class Object(db.Model): """Objects are the Things in the fediverse. From the ActivityPub specifications: https://www.w3.org/TR/activitypub/#obj """ __tablename__ = 'objects' id = db.Column(db.Integer(), primary_key=True) uri = db.Column(db.String()) actor_uri = db.Column(db.String()) reply_to_uri = db.Column(db.String()) object_type = db.Column(db.String()) created = db.Column(db.DateTime()) created_by_actor_id = db.Column( db.Integer(), db.ForeignKey('actors.id', ondelete='SET NULL', name='fk_object_created_by_actor'), nullable=True, ) last_updated = db.Column(db.DateTime()) data = db.Column(JSONB())
class Blog(db.Model): """An account can have more than one blog. Each blog connects an account to a single ActivityPub actor. """ __tablename__ = 'blogs' id = db.Column(db.Integer(), primary_key=True) actor_id = db.Column( db.Integer(), db.ForeignKey('actors.id', ondelete='CASCADE', name='fk_blog_actor'), ) # Non-standard profile customizations page_background_color = db.Column(db.String()) page_background_image = db.Column(db.String()) page_background_tiled = db.Column(db.Boolean()) page_background_static = db.Column(db.Boolean()) section_background_color = db.Column(db.String()) section_header_image = db.Column(db.String()) section_text_color = db.Column(db.String()) section_text_shadow = db.Column(db.Boolean()) section_text_shadow_color = db.Column(db.Boolean()) # Can be disabled disabled = db.Column(db.Boolean()) # Can be soft deleted. To permanently delete, delete the associated account. # This is done to prevent abuse in the form of rapidly created temporary # accounts being used to torment and then removed after a block/mute. deleted = db.Column(db.Boolean()) created = db.Column(db.DateTime()) last_updated = db.Column(db.DateTime())
class Actor(db.Model): """Actually, actors are objects (or ActivityStreams), but I call them out explicitly, because they are a key component in how lamia sees the fediverse. From the ActivityPub specifications: https://www.w3.org/TR/activitypub/#actors All the world’s a stage, And all the identities and blogs merely actors - Snakespeare, As You Lamia It """ __tablename__ = 'actors' id = db.Column(db.Integer(), primary_key=True) actor_type = db.Column(db.String()) private_key = db.Column(db.String(), nullable=True) display_name = db.Column(db.String()) user_name = db.Column(db.String()) uri = db.Column(db.String()) local = db.Column(db.Boolean()) created = db.Column(db.DateTime()) last_updated = db.Column(db.DateTime()) data = db.Column(JSONB()) def generate_keys(self): """Create new keys and stuff them into an already created actor""" key = RSA.generate(2048) self.private_key = key.export_key("PEM").decode() try: del self.data['publicKey'] except KeyError: pass self.data['publicKey'] = { 'id': f'{BASE_URL}/u/{self.user_name}#main-key', 'owner': f'{BASE_URL}/u/{self.user_name}', 'publicKeyPem': key.publickey().export_key("PEM").decode() } # Convenience fields for local actors. identity_id = db.Column(db.Integer(), db.ForeignKey('identities.id', ondelete='SET NULL', name='fk_actor_identity'), nullable=True) blog_id = db.Column(db.Integer(), db.ForeignKey('blogs.id', ondelete='SET NULL', name='fk_actor_blog'), nullable=True)
class Account(db.Model): """Login details for user actors are stored here. Accounts in lamia are best explained as being containers for blogs and identities, where both of these are 1-to-1 connections to ActivityPub actors. Basically, an account on lamia can own multiple identities and blogs. These identities and blogs are what others see when someone with an account makes a post. TODO: to prevent intentional/unintentional abuse, this needs to be VERY transparent. """ __tablename__ = 'accounts' id = db.Column(db.Integer(), primary_key=True) primary_identity_id = db.Column( db.Integer(), db.ForeignKey('identities.id', ondelete='SET NULL', name='fk_actor_primary_identity'), nullable=True, ) email_address = db.Column(db.String()) # Should be a hash encrypted by scrypt password = db.Column(db.String()) created = db.Column(db.DateTime()) # Activates low bandwidth mode to control image transmissions low_bandwidth = db.Column(db.Boolean()) # Content should be hidden and attachments marked as sensitive by default sensitive_content = db.Column(db.Boolean()) # All follows require confirmation if this is true approval_for_follows = db.Column(db.Boolean()) # Either someone is banned or not banned. # Problematic users, nazis, etc should be banned without mercy or guilt. # Just f*****g do it, they aren't worth having around. # You shouldn't even have to think about it. Do it. Do it now. # Unless a deletion is desired. In which case, there is a way to do this. banned = db.Column(db.Boolean()) # Profile customizations enabled/disabled for this account disable_profile_customizations = db.Column(db.Boolean()) def set_password(self, password: str) -> None: """Hash a plaintext password and set the class property.""" self.password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() def check_password(self, password: str) -> bool: """Compare a plaintext password to the class property.""" return bcrypt.checkpw(password.encode(), self.password.encode())
class Emoji(db.Model): """A mapping of images, replacement text, and description text all of which exists for the purpose of proliferating blob emojis and upside down smiley faces throughout the fediverse. """ __tablename__ = 'emojis' id = db.Column(db.Integer(), primary_key=True) # A path to the image in /statics image = db.Column(db.String()) replacement = db.Column(db.String()) # Only needed if replacement doesn't do it justice description = db.Column(db.String()) # for linear algebra with emojis, in this essay, i will... set_name = db.Column(db.String())
class Tag(db.Model): """The metaphysical concept of the hashtag, made real and without conceit.""" __tablename__ = 'tags' id = db.Column(db.Integer(), primary_key=True) tag = db.Column(db.String()) created = db.Column(db.DateTime())
class Bookmark(db.Model): """A bookmark is a "saved" link to an ActivityPub object that is not an actor.""" __tablename__ = 'boookmarks' id = db.Column(db.Integer(), primary_key=True) description = db.Column(db.String()) object_id = db.Column( db.Integer(), db.ForeignKey('objects.id', ondelete='CASCADE', name='fk_bookmark_object')) actor_id = db.Column( db.Integer(), db.ForeignKey('actors.id', ondelete='CASCADE', name='fk_bookmark_actor')) group_id = db.Column(db.Integer(), db.ForeignKey('bookmark_groups.id', ondelete='SET NULL', name='fk_bookmark_group'), nullable=True) created = db.Column(db.DateTime())
class BookmarkGroup(db.Model): """Bookmark groups can be implicitly created to organize bookmarks""" __tablename__ = 'bookmark_groups' id = db.Column(db.Integer(), primary_key=True) group = db.Column(db.String()) created = db.Column(db.DateTime())
class Activity(db.Model): """Activities are things that happen to other things on the fediverse. From the ActivityPub specifications: * https://www.w3.org/TR/activitypub/#client-to-server-interactions * https://www.w3.org/TR/activitypub/#server-to-server-interactions """ __tablename__ = 'activities' id = db.Column(db.Integer(), primary_key=True) uri = db.Column(db.String()) object_uri = db.Column(db.String()) actor_uri = db.Column(db.String()) activity_type = db.Column(db.String()) created = db.Column(db.DateTime()) data = db.Column(JSONB())
class Report(db.Model): """Reports are a necessary part of any online social environment. They are a way to flag local content for moderators on a local site, and they can be created and sent to this site from other sites. """ __tablename__ = 'reports' id = db.Column(db.Integer(), primary_key=True) original_content = db.Column(db.String()) content_uri = db.Column(db.String()) target_actor_id = db.Column( db.Integer(), db.ForeignKey( 'actors.id', ondelete='SET NULL', name='fk_report_target_actor'), nullable=True, ) report_by_actor_id = db.Column( db.Integer(), db.ForeignKey( 'actors.id', ondelete='SET NULL', name='fk_report_created_by_actor'), nullable=True, ) current_status = db.Column(db.String()) assigned_to_account_id = db.Column( db.Integer(), db.ForeignKey( 'accounts.id', ondelete='SET NULL', name='fk_report_assigned_to_account'), nullable=True, ) comment_count = db.Column(db.Integer()) created = db.Column(db.DateTime()) last_updated = db.Column(db.DateTime()) resolved = db.Column(db.Boolean()) marked_resolved_by_account_id = db.Column( db.Integer(), db.ForeignKey( 'accounts.id', ondelete='SET NULL', name='fk_report_marked_resolved_by_actor'), nullable=True)
class Attachments(db.Model): """An attachment is an image tied to some kind of ActivityPub object. TODO: We can look into non-image attachments and probably dismiss the possibility of them later on. """ __tablename__ = 'attachments' id = db.Column(db.Integer(), primary_key=True) uploaded_by_actor_uri = db.Column(db.String()) alt_text = db.Column(db.String()) storage_path = db.Column(db.String()) storage_uri = db.Column(db.String()) remote_uri = db.Column(db.String()) size_in_bytes = db.Column(db.Integer()) local = db.Column(db.Boolean()) created = db.Column(db.DateTime())
class OauthToken(db.Model): """A token created for our lovely API.""" __tablename__ = 'oauth_tokens' id = db.Column(db.Integer(), primary_key=True) app_id = db.Column( db.Integer(), db.ForeignKey( 'oauth_applications.id', ondelete='CASCADE')) account_id = db.Column( db.Integer(), db.ForeignKey( 'accounts.id', ondelete='CASCADE', name='fk_oauthtoken_account')) access_token = db.Column(db.String()) token_secret = db.Column(db.String()) expires = db.Column(db.DateTime()) created = db.Column(db.DateTime()) def set_access_token(self, payload: dict, days: int = 7) -> str: """Encode a payload for this token.""" now = pendulum.now() duration = pendulum.duration(days=days) expires = now + duration payload['created'] = now.to_iso8601_string() payload['expiration'] = expires.to_iso8601_string() self.expires = expires.naive() self.created = now.naive() secret = os.urandom(24).hex() self.token_secret = secret self.access_token = jwt.encode( payload, secret, algorithm='HS256').decode() return self.access_token def decode_access_token(self, token: str) -> dict: """Decode a previously encoded token.""" return jwt.decode(token, self.token_secret, algorithms=['HS256'])
class Setting(db.Model): """A basic key and value storage for settings. Note: From a philosophical standpoint, settings should all be optional, and built around non-essential functionality. The installation of lamia should require as little gymnastics as possible and should be, dare I say, Fun. TODO: figure out settings we may need here. """ __tablename__ = 'settings' id = db.Column(db.Integer(), primary_key=True) key = db.Column(db.String()) value = db.Column(JSONB())
class DomainEmailBlock(db.Model): """Email domain blocks prevent harmful registrations and registrations from spammers. """ __tablename__ = 'domain_email_block' id = db.Column(db.Integer(), primary_key=True) domain = db.Column(db.String()) created = db.Column(db.DateTime()) created_by_account_id = db.Column( db.Integer(), db.ForeignKey( 'accounts.id', ondelete='SET NULL', name='fk_domainemailblock_created_by_account'), nullable=True)
class ModerationLog(db.Model): """Quis custodiet ipsos custodes? Instance admins and other moderators should keep an eye on log entries and log entries should result in notifications, in theory. """ __tablename__ = 'moderation_logs' description = db.Column(db.String()) created = db.Column(db.DateTime()) created_by_account_id = db.Column( db.Integer(), db.ForeignKey( 'accounts.id', ondelete='SET NULL', name='fk_moderationlog_account'))
class Feed(db.Model): """A feed is a set of actors that you want to create a custom timeline for. As an example, you could create a feed labeled "friends" for calling out your friends' blog posts and statuses. It's worth noting that you need to subscribe to an actor before this works. """ __tablename__ = 'feeds' id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String()) identity_id = db.Column( db.Integer(), db.ForeignKey('identities.id', ondelete='CASCADE', name='fk_feed_identity'), ) created = db.Column(db.DateTime())
class DomainCensor(db.Model): """A domain censor is a light server to server moderation action. An domain censor is a created at the server level, and forces all activities from a specific domain to either appear content warned or minimized. """ __tablename__ = 'domain_censors' id = db.Column(db.Integer(), primary_key=True) domain = db.Column(db.String()) created = db.Column(db.DateTime()) created_by_account_id = db.Column( db.Integer(), db.ForeignKey( 'accounts.id', ondelete='SET NULL', name='fk_domaincensor_created_by_account'), nullable=True)
class OauthApplication(db.Model): """An external application or service.""" __tablename__ = 'oauth_applications' id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String()) website = db.Column(db.String()) redirect_uri = db.Column(db.String()) scopes = db.Column(db.String()) client_id = db.Column(db.String()) client_secret = db.Column(db.String()) owner_id = db.Column(db.Integer()) created = db.Column(db.DateTime()) last_updated = db.Column(db.String())
class DomainBlock(db.Model): """A domain block is a severe server to client moderation action. A domain block is a created at the server level, and probits all actors on a blocked site from interacting with the local site. It also breaks follows between actors on this site and the site that is blocked. """ __tablename__ = 'domain_blocks' id = db.Column(db.Integer(), primary_key=True) domain = db.Column(db.String()) created = db.Column(db.DateTime()) created_by_account_id = db.Column( db.Integer(), db.ForeignKey( 'accounts.id', ondelete='SET NULL', name='fk_domainblock_created_by_account'), nullable=True)
class DomainMute(db.Model): """A domain mute is a moderate server to server moderation action. A domain mute is a created at the server level, and either hides the domains' activities from the federated timeline or minimizes them. """ __tablename__ = 'domain_mutes' id = db.Column(db.Integer(), primary_key=True) domain = db.Column(db.String()) created = db.Column(db.DateTime()) duration = db.Column(db.Interval()) forever = db.Column(db.Boolean()) created_by_account_id = db.Column( db.Integer(), db.ForeignKey( 'accounts.id', ondelete='SET NULL', name='fk_domainmute_created_by_account'), nullable=True)
class ReportComment(db.Model): """Allow moderator to moderator talk in a report, should work like chat messages.""" __tablename__ = 'report_comments' message = db.Column(db.String()) created_by_account_id = db.Column( db.Integer(), db.ForeignKey( 'accounts.id', ondelete='SET NULL', name='fk_reportcomment_created_by_account'), nullable=True) created = db.Column(db.DateTime()) report_id = db.Column( db.Integer(), db.ForeignKey( 'reports.id', ondelete='CASCADE', name='fk_reportcomment_report')) # Changing the status of a report should create a comment where the message # is something like 'changed status from ignored to open'. status_change = db.Column(db.Boolean())