pyfedi/app/models.py

1188 lines
52 KiB
Python
Raw Normal View History

from datetime import datetime, timedelta, date, timezone
2023-08-05 02:26:24 -07:00
from time import time
2023-09-17 02:19:51 -07:00
from typing import List
2024-02-18 18:01:53 -08:00
from flask import current_app, escape, url_for, render_template_string
from flask_login import UserMixin, current_user
2023-10-20 19:49:01 -07:00
from sqlalchemy import or_, text
2023-08-05 02:26:24 -07:00
from werkzeug.security import generate_password_hash, check_password_hash
from flask_babel import _, lazy_gettext as _l
from sqlalchemy.orm import backref
from sqlalchemy_utils.types import TSVectorType # https://sqlalchemy-searchable.readthedocs.io/en/latest/installation.html
2023-10-03 02:29:13 -07:00
from flask_sqlalchemy import BaseQuery
from sqlalchemy_searchable import SearchQueryMixin
2023-11-29 10:12:17 -08:00
from app import db, login, cache
2023-08-05 02:26:24 -07:00
import jwt
import os
2023-08-05 02:26:24 -07:00
from app.constants import SUBSCRIPTION_NONMEMBER, SUBSCRIPTION_MEMBER, SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER, \
SUBSCRIPTION_BANNED, SUBSCRIPTION_PENDING
2023-08-05 02:26:24 -07:00
2023-12-16 23:33:27 -08:00
# datetime.utcnow() is depreciated in Python 3.12 so it will need to be swapped out eventually
def utcnow():
2023-12-16 23:33:27 -08:00
return datetime.utcnow()
2023-10-03 02:29:13 -07:00
class FullTextSearchQuery(BaseQuery, SearchQueryMixin):
pass
class BannedInstances(db.Model):
id = db.Column(db.Integer, primary_key=True)
domain = db.Column(db.String(256), index=True)
reason = db.Column(db.String(256))
initiator = db.Column(db.String(256))
created_at = db.Column(db.DateTime, default=utcnow)
class AllowedInstances(db.Model):
id = db.Column(db.Integer, primary_key=True)
domain = db.Column(db.String(256), index=True)
created_at = db.Column(db.DateTime, default=utcnow)
class Instance(db.Model):
id = db.Column(db.Integer, primary_key=True)
domain = db.Column(db.String(256), index=True)
inbox = db.Column(db.String(256))
shared_inbox = db.Column(db.String(256))
outbox = db.Column(db.String(256))
vote_weight = db.Column(db.Float, default=1.0)
software = db.Column(db.String(50))
version = db.Column(db.String(50))
created_at = db.Column(db.DateTime, default=utcnow)
updated_at = db.Column(db.DateTime, default=utcnow)
last_seen = db.Column(db.DateTime, default=utcnow) # When an Activity was received from them
last_successful_send = db.Column(db.DateTime) # When we successfully sent them an Activity
failures = db.Column(db.Integer, default=0) # How many times we failed to send (reset to 0 after every successful send)
most_recent_attempt = db.Column(db.DateTime) # When the most recent failure was
2024-01-03 19:09:22 -08:00
dormant = db.Column(db.Boolean, default=False) # True once this instance is considered offline and not worth sending to any more
start_trying_again = db.Column(db.DateTime) # When to start trying again. Should grow exponentially with each failure.
2024-01-03 19:09:22 -08:00
gone_forever = db.Column(db.Boolean, default=False) # True once this instance is considered offline forever - never start trying again
ip_address = db.Column(db.String(50))
2024-02-22 19:52:17 -08:00
trusted = db.Column(db.Boolean, default=False)
posts = db.relationship('Post', backref='instance', lazy='dynamic')
post_replies = db.relationship('PostReply', backref='instance', lazy='dynamic')
communities = db.relationship('Community', backref='instance', lazy='dynamic')
def online(self):
return not self.dormant and not self.gone_forever
def user_is_admin(self, user_id):
role = InstanceRole.query.filter_by(instance_id=self.id, user_id=user_id).first()
return role and role.role == 'admin'
class InstanceRole(db.Model):
instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
role = db.Column(db.String(50), default='admin')
user = db.relationship('User', lazy='joined')
class InstanceBlock(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), primary_key=True)
created_at = db.Column(db.DateTime, default=utcnow)
2024-02-18 18:01:53 -08:00
class Conversation(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
reported = db.Column(db.Boolean, default=False)
read = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=utcnow)
updated_at = db.Column(db.DateTime, default=utcnow)
initiator = db.relationship('User', backref=db.backref('conversations_initiated', lazy='dynamic'),
foreign_keys=[user_id])
messages = db.relationship('ChatMessage', backref=db.backref('conversation'), cascade='all,delete',
lazy='dynamic')
def member_names(self, user_id):
retval = []
for member in self.members:
if member.id != user_id:
retval.append(member.display_name())
return ', '.join(retval)
def is_member(self, user):
for member in self.members:
if member.id == user.id:
return True
return False
def instances(self):
retval = []
for member in self.members:
if member.instance.id != 1 and member.instance not in retval:
retval.append(member.instance)
return retval
2024-02-18 18:56:56 -08:00
@staticmethod
def find_existing_conversation(recipient, sender):
sql = """SELECT
c.id AS conversation_id,
c.created_at AS conversation_created_at,
c.updated_at AS conversation_updated_at,
cm1.user_id AS user1_id,
cm2.user_id AS user2_id
FROM
public.conversation AS c
JOIN
public.conversation_member AS cm1 ON c.id = cm1.conversation_id
JOIN
public.conversation_member AS cm2 ON c.id = cm2.conversation_id
WHERE
cm1.user_id = :user_id_1 AND
cm2.user_id = :user_id_2 AND
cm1.user_id <> cm2.user_id;"""
ec = db.session.execute(text(sql), {'user_id_1': recipient.id, 'user_id_2': sender.id}).fetchone()
return Conversation.query.get(ec[0]) if ec else None
2024-02-18 18:01:53 -08:00
conversation_member = db.Table('conversation_member',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('conversation_id', db.Integer, db.ForeignKey('conversation.id')),
db.PrimaryKeyConstraint('user_id', 'conversation_id')
)
class ChatMessage(db.Model):
id = db.Column(db.Integer, primary_key=True)
sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
recipient_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
conversation_id = db.Column(db.Integer, db.ForeignKey('conversation.id'), index=True)
body = db.Column(db.Text)
body_html = db.Column(db.Text)
reported = db.Column(db.Boolean, default=False)
read = db.Column(db.Boolean, default=False)
encrypted = db.Column(db.String(15))
created_at = db.Column(db.DateTime, default=utcnow)
sender = db.relationship('User', foreign_keys=[sender_id])
2023-08-05 02:26:24 -07:00
class File(db.Model):
id = db.Column(db.Integer, primary_key=True)
file_path = db.Column(db.String(255))
file_name = db.Column(db.String(255))
width = db.Column(db.Integer)
height = db.Column(db.Integer)
2024-03-03 15:13:14 -08:00
alt_text = db.Column(db.String(300))
2024-02-12 09:50:25 -08:00
source_url = db.Column(db.String(1024))
2023-11-27 01:05:35 -08:00
thumbnail_path = db.Column(db.String(255))
thumbnail_width = db.Column(db.Integer)
thumbnail_height = db.Column(db.Integer)
def view_url(self):
if self.source_url:
return self.source_url
elif self.file_path:
file_path = self.file_path[4:] if self.file_path.startswith('app/') else self.file_path
return f"https://{current_app.config['SERVER_NAME']}/{file_path}"
else:
return ''
def medium_url(self):
if self.file_path is None:
return self.thumbnail_url()
file_path = self.file_path[4:] if self.file_path.startswith('app/') else self.file_path
return f"https://{current_app.config['SERVER_NAME']}/{file_path}"
2023-11-27 01:05:35 -08:00
def thumbnail_url(self):
if self.thumbnail_path is None:
if self.source_url:
return self.source_url
else:
return ''
2023-11-27 01:05:35 -08:00
thumbnail_path = self.thumbnail_path[4:] if self.thumbnail_path.startswith('app/') else self.thumbnail_path
return f"https://{current_app.config['SERVER_NAME']}/{thumbnail_path}"
2023-08-05 02:26:24 -07:00
def delete_from_disk(self):
if self.file_path and os.path.isfile(self.file_path):
os.unlink(self.file_path)
if self.thumbnail_path and os.path.isfile(self.thumbnail_path):
os.unlink(self.thumbnail_path)
2024-03-08 00:40:47 -08:00
if self.source_url and not self.source_url.startswith('http') and os.path.isfile(self.source_url):
os.unlink(self.source_url)
2024-02-09 14:42:18 -08:00
def filesize(self):
size = 0
if self.file_path and os.path.exists(self.file_path):
size += os.path.getsize(self.file_path)
if self.thumbnail_path and os.path.exists(self.thumbnail_path):
size += os.path.getsize(self.thumbnail_path)
return size
2023-08-05 02:26:24 -07:00
class Topic(db.Model):
id = db.Column(db.Integer, primary_key=True)
2024-01-27 21:11:32 -08:00
machine_name = db.Column(db.String(50), index=True)
name = db.Column(db.String(50))
num_communities = db.Column(db.Integer, default=0)
2024-02-29 23:32:29 -08:00
parent_id = db.Column(db.Integer)
communities = db.relationship('Community', lazy='dynamic', backref='topic', cascade="all, delete-orphan")
2023-08-05 02:26:24 -07:00
class Community(db.Model):
2023-10-03 02:29:13 -07:00
query_class = FullTextSearchQuery
2023-08-05 02:26:24 -07:00
id = db.Column(db.Integer, primary_key=True)
icon_id = db.Column(db.Integer, db.ForeignKey('file.id'))
image_id = db.Column(db.Integer, db.ForeignKey('file.id'))
2023-10-20 19:49:01 -07:00
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
2023-08-05 02:26:24 -07:00
name = db.Column(db.String(256), index=True)
title = db.Column(db.String(256))
description = db.Column(db.Text) # markdown
description_html = db.Column(db.Text) # html equivalent of above markdown
2023-08-05 02:26:24 -07:00
rules = db.Column(db.Text)
rules_html = db.Column(db.Text)
content_warning = db.Column(db.Text) # "Are you sure you want to view this community?"
2023-08-05 02:26:24 -07:00
subscriptions_count = db.Column(db.Integer, default=0)
post_count = db.Column(db.Integer, default=0)
post_reply_count = db.Column(db.Integer, default=0)
nsfw = db.Column(db.Boolean, default=False)
nsfl = db.Column(db.Boolean, default=False)
instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), index=True)
low_quality = db.Column(db.Boolean, default=False) # upvotes earned in low quality communities don't improve reputation
created_at = db.Column(db.DateTime, default=utcnow)
last_active = db.Column(db.DateTime, default=utcnow)
2023-08-05 02:26:24 -07:00
public_key = db.Column(db.Text)
private_key = db.Column(db.Text)
2023-12-30 15:09:20 -08:00
content_retention = db.Column(db.Integer, default=-1)
topic_id = db.Column(db.Integer, db.ForeignKey('topic.id'), index=True)
2024-01-20 18:44:13 -08:00
default_layout = db.Column(db.String(15))
2023-08-05 02:26:24 -07:00
ap_id = db.Column(db.String(255), index=True)
2023-08-22 02:24:11 -07:00
ap_profile_id = db.Column(db.String(255), index=True)
2023-08-05 02:26:24 -07:00
ap_followers_url = db.Column(db.String(255))
ap_preferred_username = db.Column(db.String(255))
ap_discoverable = db.Column(db.Boolean, default=False)
ap_public_url = db.Column(db.String(255))
ap_fetched_at = db.Column(db.DateTime)
ap_deleted_at = db.Column(db.DateTime)
ap_inbox_url = db.Column(db.String(255))
2024-02-22 19:52:17 -08:00
ap_outbox_url = db.Column(db.String(255))
ap_featured_url = db.Column(db.String(255))
ap_moderators_url = db.Column(db.String(255))
2023-08-05 02:26:24 -07:00
ap_domain = db.Column(db.String(255))
banned = db.Column(db.Boolean, default=False)
restricted_to_mods = db.Column(db.Boolean, default=False)
local_only = db.Column(db.Boolean, default=False) # only users on this instance can post
new_mods_wanted = db.Column(db.Boolean, default=False)
2023-08-05 02:26:24 -07:00
searchable = db.Column(db.Boolean, default=True)
2023-09-05 01:25:02 -07:00
private_mods = db.Column(db.Boolean, default=False)
2023-08-05 02:26:24 -07:00
2023-12-30 15:09:20 -08:00
# Which feeds posts from this community show up in
show_home = db.Column(db.Boolean, default=False) # For anonymous users. When logged in, the home feed shows posts from subscribed communities
show_popular = db.Column(db.Boolean, default=True)
show_all = db.Column(db.Boolean, default=True)
search_vector = db.Column(TSVectorType('name', 'title', 'description', 'rules'))
2023-08-05 02:26:24 -07:00
posts = db.relationship('Post', lazy='dynamic', cascade="all, delete-orphan")
replies = db.relationship('PostReply', lazy='dynamic', cascade="all, delete-orphan")
icon = db.relationship('File', foreign_keys=[icon_id], single_parent=True, backref='community', cascade="all, delete-orphan")
image = db.relationship('File', foreign_keys=[image_id], single_parent=True, cascade="all, delete-orphan")
2023-11-29 10:12:17 -08:00
@cache.memoize(timeout=500)
def icon_image(self, size='default') -> str:
if self.icon_id is not None:
if size == 'default':
if self.icon.file_path is not None:
if self.icon.file_path.startswith('app/'):
return self.icon.file_path.replace('app/', '/')
else:
return self.icon.file_path
if self.icon.source_url is not None:
if self.icon.source_url.startswith('app/'):
return self.icon.source_url.replace('app/', '/')
else:
return self.icon.source_url
elif size == 'tiny':
if self.icon.thumbnail_path is not None:
if self.icon.thumbnail_path.startswith('app/'):
return self.icon.thumbnail_path.replace('app/', '/')
else:
return self.icon.thumbnail_path
if self.icon.source_url is not None:
if self.icon.source_url.startswith('app/'):
return self.icon.source_url.replace('app/', '/')
else:
return self.icon.source_url
2024-01-04 19:41:50 -08:00
return '/static/images/1px.gif'
2023-11-29 10:12:17 -08:00
@cache.memoize(timeout=500)
def header_image(self) -> str:
if self.image_id is not None:
if self.image.file_path is not None:
if self.image.file_path.startswith('app/'):
return self.image.file_path.replace('app/', '/')
else:
return self.image.file_path
if self.image.source_url is not None:
if self.image.source_url.startswith('app/'):
return self.image.source_url.replace('app/', '/')
else:
return self.image.source_url
return ''
def display_name(self) -> str:
if self.ap_id is None:
return self.title
else:
return f"{self.title}@{self.ap_domain}"
def link(self) -> str:
if self.ap_id is None:
return self.name
else:
2024-03-04 00:39:56 -08:00
return self.ap_id.lower()
@cache.memoize(timeout=3)
2023-09-17 02:19:51 -07:00
def moderators(self):
return CommunityMember.query.filter((CommunityMember.community_id == self.id) &
(or_(
CommunityMember.is_owner,
CommunityMember.is_moderator
))
).all()
2023-12-25 15:36:02 -08:00
def is_moderator(self, user=None):
if user is None:
return any(moderator.user_id == current_user.id for moderator in self.moderators())
else:
return any(moderator.user_id == user.id for moderator in self.moderators())
2023-12-25 15:36:02 -08:00
def is_owner(self, user=None):
if user is None:
return any(moderator.user_id == current_user.id and moderator.is_owner for moderator in self.moderators())
else:
return any(moderator.user_id == user.id and moderator.is_owner for moderator in self.moderators())
def is_instance_admin(self, user):
if self.instance_id:
instance_role = InstanceRole.query.filter(InstanceRole.instance_id == self.instance_id,
InstanceRole.user_id == user.id,
InstanceRole.role == 'admin').first()
return instance_role is not None
else:
return False
2024-01-06 15:47:06 -08:00
def user_is_banned(self, user):
membership = CommunityMember.query.filter(CommunityMember.community_id == self.id, CommunityMember.user_id == user.id).first()
2024-03-16 06:17:47 -07:00
if membership and membership.is_banned:
2024-03-14 18:24:45 -07:00
return True
banned = CommunityBan.query.filter(CommunityBan.community_id == self.id, CommunityBan.user_id == user.id).first()
if banned:
return True
return False
2024-01-06 15:47:06 -08:00
def profile_id(self):
2024-03-04 00:39:56 -08:00
retval = self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/c/{self.name}"
return retval.lower()
def is_local(self):
return self.ap_id is None or self.profile_id().startswith('https://' + current_app.config['SERVER_NAME'])
def local_url(self):
if self.is_local():
return self.ap_profile_id
else:
return f"https://{current_app.config['SERVER_NAME']}/c/{self.ap_id}"
2024-01-06 15:47:06 -08:00
def notify_new_posts(self, user_id: int) -> bool:
member_info = CommunityMember.query.filter(CommunityMember.community_id == self.id,
CommunityMember.is_banned == False,
CommunityMember.user_id == user_id).first()
if not member_info:
return False
return member_info.notify_new_posts
# instances that have users which are members of this community. (excluding the current instance)
def following_instances(self, include_dormant=False) -> List[Instance]:
instances = Instance.query.join(User, User.instance_id == Instance.id).join(CommunityMember, CommunityMember.user_id == User.id)
instances = instances.filter(CommunityMember.community_id == self.id, CommunityMember.is_banned == False)
if not include_dormant:
instances = instances.filter(Instance.dormant == False)
instances = instances.filter(Instance.id != 1, Instance.gone_forever == False)
return instances.all()
def delete_dependencies(self):
for post in self.posts:
post.delete_dependencies()
db.session.delete(post)
db.session.query(CommunityBan).filter(CommunityBan.community_id == self.id).delete()
db.session.query(CommunityBlock).filter(CommunityBlock.community_id == self.id).delete()
db.session.query(CommunityJoinRequest).filter(CommunityJoinRequest.community_id == self.id).delete()
db.session.query(CommunityMember).filter(CommunityMember.community_id == self.id).delete()
db.session.query(Report).filter(Report.suspect_community_id == self.id).delete()
2023-08-05 02:26:24 -07:00
2023-10-18 02:23:59 -07:00
user_role = db.Table('user_role',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('role_id', db.Integer, db.ForeignKey('role.id')),
db.PrimaryKeyConstraint('user_id', 'role_id')
)
2023-08-05 02:26:24 -07:00
class User(UserMixin, db.Model):
2023-10-03 02:29:13 -07:00
query_class = FullTextSearchQuery
2023-08-05 02:26:24 -07:00
id = db.Column(db.Integer, primary_key=True)
2023-11-21 23:48:27 -08:00
user_name = db.Column(db.String(255), index=True)
2023-12-31 17:49:15 -08:00
title = db.Column(db.String(256))
2023-08-05 02:26:24 -07:00
email = db.Column(db.String(255), index=True)
password_hash = db.Column(db.String(128))
verified = db.Column(db.Boolean, default=False)
verification_token = db.Column(db.String(16), index=True)
2023-08-05 02:26:24 -07:00
banned = db.Column(db.Boolean, default=False)
deleted = db.Column(db.Boolean, default=False)
about = db.Column(db.Text) # markdown
about_html = db.Column(db.Text) # html
2023-08-05 02:26:24 -07:00
keywords = db.Column(db.String(256))
2023-12-28 00:00:26 -08:00
matrix_user_id = db.Column(db.String(256))
2023-08-05 02:26:24 -07:00
show_nsfw = db.Column(db.Boolean, default=False)
show_nsfl = db.Column(db.Boolean, default=False)
created = db.Column(db.DateTime, default=utcnow)
last_seen = db.Column(db.DateTime, default=utcnow, index=True)
2024-02-12 20:22:03 -08:00
avatar_id = db.Column(db.Integer, db.ForeignKey('file.id'), index=True)
cover_id = db.Column(db.Integer, db.ForeignKey('file.id'), index=True)
2023-08-05 02:26:24 -07:00
public_key = db.Column(db.Text)
private_key = db.Column(db.Text)
newsletter = db.Column(db.Boolean, default=True)
2024-02-22 19:52:17 -08:00
email_unread = db.Column(db.Boolean, default=True) # True if they want to receive 'unread notifications' emails
email_unread_sent = db.Column(db.Boolean) # True after a 'unread notifications' email has been sent. None for remote users
receive_message_mode = db.Column(db.String(20), default='Closed') # possible values: Open, TrustedOnly, Closed
2023-08-05 02:26:24 -07:00
bounces = db.Column(db.SmallInteger, default=0)
timezone = db.Column(db.String(20))
2023-09-10 01:20:53 -07:00
reputation = db.Column(db.Float, default=0.0)
attitude = db.Column(db.Float, default=1.0) # (upvotes cast - downvotes cast) / (upvotes + downvotes). A number between 1 and -1 is the ratio between up and down votes they cast
2023-08-05 02:26:24 -07:00
stripe_customer_id = db.Column(db.String(50))
stripe_subscription_id = db.Column(db.String(50))
searchable = db.Column(db.Boolean, default=True)
indexable = db.Column(db.Boolean, default=False)
2023-10-07 01:32:19 -07:00
bot = db.Column(db.Boolean, default=False)
ignore_bots = db.Column(db.Boolean, default=False)
2023-11-30 02:21:37 -08:00
unread_notifications = db.Column(db.Integer, default=0)
ip_address = db.Column(db.String(50))
instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), index=True)
2023-12-31 19:26:57 -08:00
reports = db.Column(db.Integer, default=0) # how many times this user has been reported.
default_sort = db.Column(db.String(25), default='hot')
theme = db.Column(db.String(20), default='')
2024-02-22 19:52:17 -08:00
referrer = db.Column(db.String(256))
2024-02-26 00:26:19 -08:00
markdown_editor = db.Column(db.Boolean, default=False)
2023-08-05 02:26:24 -07:00
avatar = db.relationship('File', lazy='joined', foreign_keys=[avatar_id], single_parent=True, cascade="all, delete-orphan")
cover = db.relationship('File', lazy='joined', foreign_keys=[cover_id], single_parent=True, cascade="all, delete-orphan")
instance = db.relationship('Instance', lazy='joined', foreign_keys=[instance_id])
2024-02-18 18:01:53 -08:00
conversations = db.relationship('Conversation', lazy='dynamic', secondary=conversation_member, backref=db.backref('members', lazy='joined'))
ap_id = db.Column(db.String(255), index=True) # e.g. username@server
ap_profile_id = db.Column(db.String(255), index=True) # e.g. https://server/u/username
ap_public_url = db.Column(db.String(255)) # e.g. https://server/u/username
2023-08-05 02:26:24 -07:00
ap_fetched_at = db.Column(db.DateTime)
ap_followers_url = db.Column(db.String(255))
ap_preferred_username = db.Column(db.String(255))
ap_manually_approves_followers = db.Column(db.Boolean)
ap_deleted_at = db.Column(db.DateTime)
ap_inbox_url = db.Column(db.String(255))
ap_domain = db.Column(db.String(255))
2024-02-29 23:32:29 -08:00
search_vector = db.Column(TSVectorType('user_name', 'about', 'keywords'))
2023-08-05 02:26:24 -07:00
activity = db.relationship('ActivityLog', backref='account', lazy='dynamic', cascade="all, delete-orphan")
2023-11-29 10:12:17 -08:00
posts = db.relationship('Post', lazy='dynamic', cascade="all, delete-orphan")
2023-12-26 18:47:17 -08:00
post_replies = db.relationship('PostReply', lazy='dynamic', cascade="all, delete-orphan")
2023-08-05 02:26:24 -07:00
2023-10-18 02:23:59 -07:00
roles = db.relationship('Role', secondary=user_role, lazy='dynamic', cascade="all, delete")
2023-08-05 02:26:24 -07:00
def __repr__(self):
2024-01-17 17:56:23 -08:00
return '<User {}_{}>'.format(self.user_name, self.id)
2023-08-05 02:26:24 -07:00
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
try:
result = check_password_hash(self.password_hash, password)
return result
except Exception:
return False
2024-01-11 15:34:08 -08:00
def get_id(self):
if self.is_authenticated:
return self.id
else:
2024-01-11 16:49:40 -08:00
return 0
2024-01-11 15:34:08 -08:00
2023-10-20 19:49:01 -07:00
def display_name(self):
if self.deleted is False:
2023-12-31 17:49:15 -08:00
if self.title:
return self.title
else:
return self.user_name
2023-10-20 19:49:01 -07:00
else:
return '[deleted]'
@cache.memoize(timeout=10)
def avatar_thumbnail(self) -> str:
if self.avatar_id is not None:
if self.avatar.thumbnail_path is not None:
if self.avatar.thumbnail_path.startswith('app/'):
return self.avatar.thumbnail_path.replace('app/', '/')
else:
return self.avatar.thumbnail_path
else:
return self.avatar_image()
return ''
@cache.memoize(timeout=10)
2023-10-07 01:32:19 -07:00
def avatar_image(self) -> str:
if self.avatar_id is not None:
if self.avatar.file_path is not None:
if self.avatar.file_path.startswith('app/'):
return self.avatar.file_path.replace('app/', '/')
else:
return self.avatar.file_path
2023-10-07 01:32:19 -07:00
if self.avatar.source_url is not None:
if self.avatar.source_url.startswith('app/'):
return self.avatar.source_url.replace('app/', '/')
else:
return self.avatar.source_url
2023-10-07 01:32:19 -07:00
return ''
def cover_image(self) -> str:
if self.cover_id is not None:
2024-02-09 09:41:24 -08:00
if self.cover.thumbnail_path is not None:
if self.cover.thumbnail_path.startswith('app/'):
return self.cover.thumbnail_path.replace('app/', '/')
else:
2024-02-09 09:41:24 -08:00
return self.cover.thumbnail_path
2023-10-07 01:32:19 -07:00
if self.cover.source_url is not None:
if self.cover.source_url.startswith('app/'):
return self.cover.source_url.replace('app/', '/')
else:
return self.cover.source_url
2023-10-07 01:32:19 -07:00
return ''
2024-02-09 14:42:18 -08:00
def filesize(self):
size = 0
if self.avatar_id:
size += self.avatar.filesize()
if self.cover_id:
size += self.cover.filesize()
return size
def num_content(self):
content = 0
2024-03-20 15:07:11 -07:00
content += db.session.execute(text('SELECT COUNT(id) as c FROM "post" WHERE user_id = :user_id'), {'user_id': self.id}).scalar()
content += db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE user_id = :user_id'), {'user_id': self.id}).scalar()
2024-02-09 14:42:18 -08:00
return content
def is_local(self):
return self.ap_id is None or self.ap_profile_id.startswith('https://' + current_app.config['SERVER_NAME'])
2024-02-01 18:30:03 -08:00
def waiting_for_approval(self):
application = UserRegistration.query.filter_by(user_id=self.id, status=0).first()
return application is not None
@cache.memoize(timeout=30)
def is_admin(self):
for role in self.roles:
if role.name == 'Admin':
return True
return False
def trustworthy(self):
if self.is_admin():
return True
if self.created_recently() or self.reputation < 100:
return False
return True
2023-10-10 02:25:37 -07:00
def link(self) -> str:
if self.is_local():
2023-10-10 02:25:37 -07:00
return self.user_name
else:
2024-03-04 00:46:23 -08:00
return self.ap_id
2023-10-10 02:25:37 -07:00
def followers_url(self):
if self.ap_followers_url:
return self.ap_followers_url
else:
return self.profile_id() + '/followers'
2023-08-05 02:26:24 -07:00
def get_reset_password_token(self, expires_in=600):
return jwt.encode(
{'reset_password': self.id, 'exp': time() + expires_in},
current_app.config['SECRET_KEY'],
2023-09-02 21:30:20 -07:00
algorithm='HS256')
2023-08-05 02:26:24 -07:00
def another_account_using_email(self, email):
another_account = User.query.filter(User.email == email, User.id != self.id).first()
return another_account is not None
def expires_soon(self):
if self.expires is None:
return False
return self.expires < utcnow() + timedelta(weeks=1)
2023-08-05 02:26:24 -07:00
def is_expired(self):
if self.expires is None:
return True
return self.expires < utcnow()
2023-08-05 02:26:24 -07:00
def expired_ages_ago(self):
if self.expires is None:
return True
return self.expires < datetime(2019, 9, 1)
2023-12-26 22:51:07 -08:00
def recalculate_attitude(self):
upvotes = db.session.execute(text('SELECT COUNT(id) as c FROM "post_vote" WHERE user_id = :user_id AND effect > 0'),
{'user_id': self.id}).scalar()
downvotes = db.session.execute(text('SELECT COUNT(id) as c FROM "post_vote" WHERE user_id = :user_id AND effect < 0'),
{'user_id': self.id}).scalar()
if upvotes is None:
upvotes = 0
if downvotes is None:
downvotes = 0
comment_upvotes = db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply_vote" WHERE user_id = :user_id AND effect > 0'),
{'user_id': self.id}).scalar()
comment_downvotes = db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply_vote" WHERE user_id = :user_id AND effect < 0'),
{'user_id': self.id}).scalar()
if comment_upvotes is None:
comment_upvotes = 0
if comment_downvotes is None:
comment_downvotes = 0
total_upvotes = upvotes + comment_upvotes
total_downvotes = downvotes + comment_downvotes
if total_downvotes == 0: # guard against division by zero
self.attitude = 1.0
else:
self.attitude = (total_upvotes - total_downvotes) / (total_upvotes + total_downvotes)
def subscribed(self, community_id: int) -> int:
if community_id is None:
return False
subscription:CommunityMember = CommunityMember.query.filter_by(user_id=self.id, community_id=community_id).first()
if subscription:
if subscription.is_banned:
return SUBSCRIPTION_BANNED
elif subscription.is_owner:
return SUBSCRIPTION_OWNER
elif subscription.is_moderator:
return SUBSCRIPTION_MODERATOR
else:
return SUBSCRIPTION_MEMBER
else:
join_request = CommunityJoinRequest.query.filter_by(user_id=self.id, community_id=community_id).first()
if join_request:
return SUBSCRIPTION_PENDING
else:
return SUBSCRIPTION_NONMEMBER
2023-09-17 02:19:51 -07:00
def communities(self) -> List[Community]:
return Community.query.filter(Community.banned == False).\
2024-01-27 21:11:32 -08:00
join(CommunityMember).filter(CommunityMember.is_banned == False, CommunityMember.user_id == self.id).all()
2023-09-17 02:19:51 -07:00
def profile_id(self):
2024-03-04 00:39:56 -08:00
result = self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}"
2024-03-04 00:46:23 -08:00
return result
2023-11-29 08:14:22 -08:00
def created_recently(self):
return self.created and self.created > utcnow() - timedelta(days=7)
2023-11-29 08:14:22 -08:00
2023-12-31 19:26:57 -08:00
def has_blocked_instance(self, instance_id: int):
instance_block = InstanceBlock.query.filter_by(user_id=self.id, instance_id=instance_id).first()
return instance_block is not None
2023-12-31 19:26:57 -08:00
def has_blocked_user(self, user_id: int):
existing_block = UserBlock.query.filter_by(blocker_id=self.id, blocked_id=user_id).first()
return existing_block is not None
2023-08-05 02:26:24 -07:00
@staticmethod
def verify_reset_password_token(token):
try:
id = jwt.decode(token, current_app.config['SECRET_KEY'],
algorithms=['HS256'])['reset_password']
except:
return
return User.query.get(id)
2023-12-09 18:10:09 -08:00
def flush_cache(self):
cache.delete('/u/' + self.user_name + '_False')
cache.delete('/u/' + self.user_name + '_True')
2023-12-31 17:49:15 -08:00
def delete_dependencies(self):
if self.cover_id:
file = File.query.get(self.cover_id)
file.delete_from_disk()
self.cover_id = None
db.session.delete(file)
if self.avatar_id:
file = File.query.get(self.avatar_id)
file.delete_from_disk()
self.avatar_id = None
db.session.delete(file)
2024-02-01 18:30:03 -08:00
if self.waiting_for_approval():
db.session.query(UserRegistration).filter(UserRegistration.user_id == self.id).delete()
2023-12-31 17:49:15 -08:00
2023-10-20 19:49:01 -07:00
def purge_content(self):
2023-11-30 02:21:37 -08:00
files = File.query.join(Post).filter(Post.user_id == self.id).all()
for file in files:
file.delete_from_disk()
2023-12-31 17:49:15 -08:00
self.delete_dependencies()
2024-01-08 23:44:08 -08:00
posts = Post.query.filter_by(user_id=self.id).all()
for post in posts:
post.delete_dependencies()
post.flush_cache()
db.session.delete(post)
2024-02-08 15:52:16 -08:00
post_replies = PostReply.query.filter_by(user_id=self.id).all()
for reply in post_replies:
2024-02-08 15:54:05 -08:00
reply.body = reply.body_html = ''
2024-01-08 23:44:08 -08:00
db.session.commit()
2023-10-20 19:49:01 -07:00
2023-08-05 02:26:24 -07:00
class ActivityLog(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
activity_type = db.Column(db.String(64))
activity = db.Column(db.String(255))
timestamp = db.Column(db.DateTime, index=True, default=utcnow)
2023-08-05 02:26:24 -07:00
class Post(db.Model):
2023-10-03 02:29:13 -07:00
query_class = FullTextSearchQuery
2023-08-05 02:26:24 -07:00
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
community_id = db.Column(db.Integer, db.ForeignKey('community.id'), index=True)
image_id = db.Column(db.Integer, db.ForeignKey('file.id'), index=True)
domain_id = db.Column(db.Integer, db.ForeignKey('domain.id'), index=True)
instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), index=True)
2023-08-05 02:26:24 -07:00
slug = db.Column(db.String(255))
title = db.Column(db.String(255))
url = db.Column(db.String(2048))
body = db.Column(db.Text)
body_html = db.Column(db.Text)
2023-08-05 02:26:24 -07:00
type = db.Column(db.Integer)
comments_enabled = db.Column(db.Boolean, default=True)
2023-12-14 00:22:46 -08:00
mea_culpa = db.Column(db.Boolean, default=False)
2023-08-05 02:26:24 -07:00
has_embed = db.Column(db.Boolean, default=False)
reply_count = db.Column(db.Integer, default=0)
2023-12-14 20:35:11 -08:00
score = db.Column(db.Integer, default=0, index=True) # used for 'top' ranking
2024-02-12 20:22:03 -08:00
nsfw = db.Column(db.Boolean, default=False, index=True)
nsfl = db.Column(db.Boolean, default=False, index=True)
2023-08-05 02:26:24 -07:00
sticky = db.Column(db.Boolean, default=False)
notify_author = db.Column(db.Boolean, default=True)
2024-03-12 00:58:47 -07:00
indexable = db.Column(db.Boolean, default=True)
2024-02-12 20:22:03 -08:00
from_bot = db.Column(db.Boolean, default=False, index=True)
created_at = db.Column(db.DateTime, index=True, default=utcnow) # this is when the content arrived here
posted_at = db.Column(db.DateTime, index=True, default=utcnow) # this is when the original server created it
last_active = db.Column(db.DateTime, index=True, default=utcnow)
2023-08-05 02:26:24 -07:00
ip = db.Column(db.String(50))
up_votes = db.Column(db.Integer, default=0)
down_votes = db.Column(db.Integer, default=0)
2024-02-12 20:22:03 -08:00
ranking = db.Column(db.Integer, default=0, index=True) # used for 'hot' ranking
2023-08-05 02:26:24 -07:00
language = db.Column(db.String(10))
edited_at = db.Column(db.DateTime)
reports = db.Column(db.Integer, default=0) # how many times this post has been reported. Set to -1 to ignore reports
2023-08-05 02:26:24 -07:00
ap_id = db.Column(db.String(255), index=True)
ap_create_id = db.Column(db.String(100))
ap_announce_id = db.Column(db.String(100))
2023-08-05 02:26:24 -07:00
search_vector = db.Column(TSVectorType('title', 'body'))
2023-11-29 10:12:17 -08:00
image = db.relationship(File, lazy='joined', foreign_keys=[image_id], cascade="all, delete")
domain = db.relationship('Domain', lazy='joined', foreign_keys=[domain_id])
author = db.relationship('User', lazy='joined', overlaps='posts', foreign_keys=[user_id])
community = db.relationship('Community', lazy='joined', overlaps='posts', foreign_keys=[community_id])
2023-12-09 18:10:09 -08:00
replies = db.relationship('PostReply', lazy='dynamic', backref='post')
def is_local(self):
return self.ap_id is None or self.ap_id.startswith('https://' + current_app.config['SERVER_NAME'])
@classmethod
def get_by_ap_id(cls, ap_id):
return cls.query.filter_by(ap_id=ap_id).first()
def delete_dependencies(self):
db.session.query(Report).filter(Report.suspect_post_id == self.id).delete()
db.session.execute(text('DELETE FROM post_reply_vote WHERE post_reply_id IN (SELECT id FROM post_reply WHERE post_id = :post_id)'),
{'post_id': self.id})
db.session.execute(text('DELETE FROM post_reply WHERE post_id = :post_id'), {'post_id': self.id})
db.session.execute(text('DELETE FROM post_vote WHERE post_id = :post_id'), {'post_id': self.id})
if self.image_id:
file = File.query.get(self.image_id)
file.delete_from_disk()
def youtube_embed(self):
if self.url:
vpos = self.url.find('v=')
if vpos != -1:
return self.url[vpos + 2:vpos + 13]
def profile_id(self):
if self.ap_id:
return self.ap_id
else:
return f"https://{current_app.config['SERVER_NAME']}/post/{self.id}"
2024-01-10 23:39:22 -08:00
def blocked_by_content_filter(self, content_filters):
lowercase_title = self.title.lower()
for name, keywords in content_filters.items() if content_filters else {}:
for keyword in keywords:
if keyword in lowercase_title:
return name
return False
2023-12-09 18:10:09 -08:00
def flush_cache(self):
cache.delete(f'/post/{self.id}_False')
cache.delete(f'/post/{self.id}_True')
2023-08-05 02:26:24 -07:00
class PostReply(db.Model):
2023-10-03 02:29:13 -07:00
query_class = FullTextSearchQuery
2023-08-05 02:26:24 -07:00
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), index=True)
community_id = db.Column(db.Integer, db.ForeignKey('community.id'), index=True)
2023-10-10 02:25:37 -07:00
domain_id = db.Column(db.Integer, db.ForeignKey('domain.id'), index=True)
2023-08-05 02:26:24 -07:00
image_id = db.Column(db.Integer, db.ForeignKey('file.id'), index=True)
2024-02-08 18:14:39 -08:00
parent_id = db.Column(db.Integer, index=True)
2023-08-05 02:26:24 -07:00
root_id = db.Column(db.Integer)
2023-10-10 02:25:37 -07:00
depth = db.Column(db.Integer, default=0)
2023-12-27 23:00:07 -08:00
instance_id = db.Column(db.Integer, db.ForeignKey('instance.id'), index=True)
2023-08-05 02:26:24 -07:00
body = db.Column(db.Text)
body_html = db.Column(db.Text)
body_html_safe = db.Column(db.Boolean, default=False)
2023-12-14 20:35:11 -08:00
score = db.Column(db.Integer, default=0, index=True) # used for 'top' sorting
2023-08-05 02:26:24 -07:00
nsfw = db.Column(db.Boolean, default=False)
nsfl = db.Column(db.Boolean, default=False)
notify_author = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, index=True, default=utcnow)
posted_at = db.Column(db.DateTime, index=True, default=utcnow)
2023-08-05 02:26:24 -07:00
ip = db.Column(db.String(50))
2023-10-07 01:32:19 -07:00
from_bot = db.Column(db.Boolean, default=False)
2023-08-05 02:26:24 -07:00
up_votes = db.Column(db.Integer, default=0)
down_votes = db.Column(db.Integer, default=0)
ranking = db.Column(db.Float, default=0.0, index=True) # used for 'hot' sorting
2023-08-05 02:26:24 -07:00
language = db.Column(db.String(10))
edited_at = db.Column(db.DateTime)
reports = db.Column(db.Integer, default=0) # how many times this post has been reported. Set to -1 to ignore reports
2023-08-05 02:26:24 -07:00
ap_id = db.Column(db.String(255), index=True)
ap_create_id = db.Column(db.String(100))
ap_announce_id = db.Column(db.String(100))
2023-08-05 02:26:24 -07:00
search_vector = db.Column(TSVectorType('body'))
2023-12-26 19:58:30 -08:00
author = db.relationship('User', lazy='joined', foreign_keys=[user_id], single_parent=True, overlaps="post_replies")
community = db.relationship('Community', lazy='joined', overlaps='replies', foreign_keys=[community_id])
2023-12-26 18:47:17 -08:00
def is_local(self):
return self.ap_id is None or self.ap_id.startswith('https://' + current_app.config['SERVER_NAME'])
@classmethod
def get_by_ap_id(cls, ap_id):
return cls.query.filter_by(ap_id=ap_id).first()
def profile_id(self):
2023-12-09 18:10:09 -08:00
if self.ap_id:
return self.ap_id
else:
return f"https://{current_app.config['SERVER_NAME']}/comment/{self.id}"
# the ap_id of the parent object, whether it's another PostReply or a Post
def in_reply_to(self):
if self.parent_id is None:
return self.post.ap_id
else:
parent = PostReply.query.get(self.parent_id)
return parent.ap_id
# the AP profile of the person who wrote the parent object, which could be another PostReply or a Post
def to(self):
if self.parent_id is None:
return self.post.author.profile_id()
else:
parent = PostReply.query.get(self.parent_id)
return parent.author.profile_id()
2023-12-25 15:36:02 -08:00
def delete_dependencies(self):
db.session.query(Report).filter(Report.suspect_post_reply_id == self.id).delete()
db.session.execute(text('DELETE FROM post_reply_vote WHERE post_reply_id = :post_reply_id'),
{'post_reply_id': self.id})
if self.image_id:
file = File.query.get(self.image_id)
file.delete_from_disk()
def has_replies(self):
reply = PostReply.query.filter_by(parent_id=self.id).first()
return reply is not None
2024-01-10 23:39:22 -08:00
def blocked_by_content_filter(self, content_filters):
lowercase_body = self.body.lower()
for name, keywords in content_filters.items() if content_filters else {}:
for keyword in keywords:
if keyword in lowercase_body:
return name
return False
2023-08-05 02:26:24 -07:00
class Domain(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), index=True)
post_count = db.Column(db.Integer, default=0)
banned = db.Column(db.Boolean, default=False, index=True) # Domains can be banned site-wide (by admin) or DomainBlock'ed by users
notify_mods = db.Column(db.Boolean, default=False, index=True)
notify_admins = db.Column(db.Boolean, default=False, index=True)
2023-08-05 02:26:24 -07:00
2024-02-01 19:52:23 -08:00
def blocked_by(self, user):
block = DomainBlock.query.filter_by(domain_id=self.id, user_id=user.id).first()
return block is not None
def purge_content(self):
files = File.query.join(Post).filter(Post.domain_id == self.id).all()
for file in files:
file.delete_from_disk()
posts = Post.query.filter_by(domain_id=self.id).all()
for post in posts:
post.delete_dependencies()
post.flush_cache()
db.session.delete(post)
db.session.commit()
2023-08-05 02:26:24 -07:00
class DomainBlock(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
domain_id = db.Column(db.Integer, db.ForeignKey('domain.id'), primary_key=True)
created_at = db.Column(db.DateTime, default=utcnow)
2023-08-05 02:26:24 -07:00
class CommunityBlock(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
community_id = db.Column(db.Integer, db.ForeignKey('community.id'), primary_key=True)
created_at = db.Column(db.DateTime, default=utcnow)
2023-08-05 02:26:24 -07:00
class CommunityMember(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
community_id = db.Column(db.Integer, db.ForeignKey('community.id'), primary_key=True)
is_moderator = db.Column(db.Boolean, default=False)
is_owner = db.Column(db.Boolean, default=False)
2024-02-12 20:22:03 -08:00
is_banned = db.Column(db.Boolean, default=False, index=True)
2024-01-06 15:47:06 -08:00
notify_new_posts = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=utcnow)
2023-08-05 02:26:24 -07:00
# people banned from communities
class CommunityBan(db.Model):
2024-03-14 18:24:45 -07:00
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) # person who is banned, not the banner
community_id = db.Column(db.Integer, db.ForeignKey('community.id'), primary_key=True)
banned_by = db.Column(db.Integer, db.ForeignKey('user.id'))
reason = db.Column(db.String(50))
created_at = db.Column(db.DateTime, default=utcnow)
ban_until = db.Column(db.DateTime)
2023-08-05 02:26:24 -07:00
class UserNote(db.Model):
id = db.Column(db.Integer, primary_key=True)
2024-01-24 00:17:36 -08:00
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
target_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
2023-08-05 02:26:24 -07:00
body = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=utcnow)
2023-08-05 02:26:24 -07:00
class UserBlock(db.Model):
id = db.Column(db.Integer, primary_key=True)
2024-01-24 00:17:36 -08:00
blocker_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
blocked_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
created_at = db.Column(db.DateTime, default=utcnow)
2023-08-05 02:26:24 -07:00
class Settings(db.Model):
name = db.Column(db.String(50), primary_key=True)
value = db.Column(db.String(1024))
2023-09-05 01:25:02 -07:00
class Interest(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
communities = db.Column(db.Text)
class CommunityJoinRequest(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
2024-01-24 00:17:36 -08:00
community_id = db.Column(db.Integer, db.ForeignKey('community.id'), index=True)
class UserFollowRequest(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
follow_id = db.Column(db.Integer, db.ForeignKey('user.id'))
2024-02-01 18:30:03 -08:00
class UserRegistration(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
answer = db.Column(db.String(512))
status = db.Column(db.Integer, default=0, index=True) # 0 = unapproved, 1 = approved
created_at = db.Column(db.DateTime, default=utcnow)
approved_at = db.Column(db.DateTime)
approved_by = db.Column(db.Integer, db.ForeignKey('user.id'))
user = db.relationship('User', foreign_keys=[user_id], lazy='joined')
2023-09-10 01:20:53 -07:00
class PostVote(db.Model):
id = db.Column(db.Integer, primary_key=True)
2024-01-24 00:17:36 -08:00
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
2023-09-10 01:20:53 -07:00
author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
2024-01-24 00:17:36 -08:00
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), index=True)
2024-01-09 13:18:11 -08:00
effect = db.Column(db.Float, index=True)
created_at = db.Column(db.DateTime, default=utcnow)
post = db.relationship('Post', foreign_keys=[post_id])
2023-09-10 01:20:53 -07:00
class PostReplyVote(db.Model):
id = db.Column(db.Integer, primary_key=True)
2024-01-24 00:17:36 -08:00
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True) # who voted
author_id = db.Column(db.Integer, db.ForeignKey('user.id')) # the author of the reply voted on - who's reputation is affected
2024-01-24 00:17:36 -08:00
post_reply_id = db.Column(db.Integer, db.ForeignKey('post_reply.id'), index=True)
2023-09-10 01:20:53 -07:00
effect = db.Column(db.Float)
created_at = db.Column(db.DateTime, default=utcnow)
2023-09-10 01:20:53 -07:00
# save every activity to a log, to aid debugging
class ActivityPubLog(db.Model):
id = db.Column(db.Integer, primary_key=True)
direction = db.Column(db.String(3)) # 'in' or 'out'
2024-01-18 10:45:48 -08:00
activity_id = db.Column(db.String(256), index=True)
activity_type = db.Column(db.String(50)) # e.g. 'Follow', 'Accept', 'Like', etc
activity_json = db.Column(db.Text) # the full json of the activity
result = db.Column(db.String(10)) # 'success' or 'failure'
exception_message = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=utcnow)
class Filter(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(50))
2024-01-10 23:39:22 -08:00
filter_home = db.Column(db.Boolean, default=True)
filter_posts = db.Column(db.Boolean, default=True)
filter_replies = db.Column(db.Boolean, default=False)
hide_type = db.Column(db.Integer, default=0) # 0 = hide with warning, 1 = hide completely
2024-01-24 00:17:36 -08:00
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
2024-01-10 23:39:22 -08:00
expire_after = db.Column(db.Date)
keywords = db.Column(db.String(500))
2024-01-10 23:39:22 -08:00
def keywords_string(self):
if self.keywords is None or self.keywords == '':
return ''
split_keywords = [kw.strip() for kw in self.keywords.split('\n')]
return ', '.join(split_keywords)
2023-10-18 02:23:59 -07:00
class Role(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
weight = db.Column(db.Integer, default=0)
permissions = db.relationship('RolePermission')
class RolePermission(db.Model):
role_id = db.Column(db.Integer, db.ForeignKey('role.id'), primary_key=True)
permission = db.Column(db.String, primary_key=True, index=True)
class Notification(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(50))
url = db.Column(db.String(512))
read = db.Column(db.Boolean, default=False)
2024-01-24 00:17:36 -08:00
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True) # who the notification should go to
author_id = db.Column(db.Integer, db.ForeignKey('user.id')) # the person who caused the notification to happen
created_at = db.Column(db.DateTime, default=utcnow)
class Report(db.Model):
id = db.Column(db.Integer, primary_key=True)
reasons = db.Column(db.String(256))
description = db.Column(db.String(256))
status = db.Column(db.Integer, default=0)
2024-02-18 18:01:53 -08:00
type = db.Column(db.Integer, default=0) # 0 = user, 1 = post, 2 = reply, 3 = community, 4 = conversation
reporter_id = db.Column(db.Integer, db.ForeignKey('user.id'))
suspect_community_id = db.Column(db.Integer, db.ForeignKey('community.id'))
suspect_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
suspect_post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
2023-12-25 15:36:02 -08:00
suspect_post_reply_id = db.Column(db.Integer, db.ForeignKey('post_reply.id'))
2024-02-18 18:01:53 -08:00
suspect_conversation_id = db.Column(db.Integer, db.ForeignKey('conversation.id'))
in_community_id = db.Column(db.Integer, db.ForeignKey('community.id'))
created_at = db.Column(db.DateTime, default=utcnow)
updated = db.Column(db.DateTime, default=utcnow)
# textual representation of self.type
def type_text(self):
2024-02-18 18:01:53 -08:00
types = ('User', 'Post', 'Comment', 'Community', 'Conversation')
if self.type is None:
return ''
else:
return types[self.type]
def is_local(self):
return True
class IpBan(db.Model):
id = db.Column(db.Integer, primary_key=True)
ip_address = db.Column(db.String(50), index=True)
notes = db.Column(db.String(150))
created_at = db.Column(db.DateTime, default=utcnow)
class Site(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(256))
description = db.Column(db.String(256))
icon_id = db.Column(db.Integer, db.ForeignKey('file.id'))
sidebar = db.Column(db.Text, default='')
legal_information = db.Column(db.Text, default='')
public_key = db.Column(db.Text)
private_key = db.Column(db.Text)
enable_downvotes = db.Column(db.Boolean, default=True)
allow_local_image_posts = db.Column(db.Boolean, default=True)
remote_image_cache_days = db.Column(db.Integer, default=30)
enable_nsfw = db.Column(db.Boolean, default=False)
enable_nsfl = db.Column(db.Boolean, default=False)
community_creation_admin_only = db.Column(db.Boolean, default=False)
reports_email_admins = db.Column(db.Boolean, default=True)
2024-02-01 18:30:03 -08:00
registration_mode = db.Column(db.String(20), default='Closed') # possible values: Open, RequireApplication, Closed
application_question = db.Column(db.Text, default='')
allow_or_block_list = db.Column(db.Integer, default=2) # 1 = allow list, 2 = block list
allowlist = db.Column(db.Text, default='')
blocklist = db.Column(db.Text, default='')
2024-02-16 23:05:57 -08:00
auto_decline_referrers = db.Column(db.Text, default='rdrama.net')
created_at = db.Column(db.DateTime, default=utcnow)
updated = db.Column(db.DateTime, default=utcnow)
last_active = db.Column(db.DateTime, default=utcnow)
2024-01-12 14:12:31 -08:00
log_activitypub_json = db.Column(db.Boolean, default=False)
default_theme = db.Column(db.String(20), default='')
@staticmethod
def admins() -> List[User]:
return User.query.filter_by(deleted=False, banned=False).join(user_role).filter(user_role.c.role_id == 4).all()
2023-08-05 02:26:24 -07:00
@login.user_loader
def load_user(id):
return User.query.get(int(id))