diff --git a/app/models.py b/app/models.py index e69de29b..beb1d509 100644 --- a/app/models.py +++ b/app/models.py @@ -0,0 +1,274 @@ +from datetime import datetime, timedelta, date +from hashlib import md5 +from time import time +from flask import current_app, escape +from flask_login import UserMixin +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 +from app import db, login +import jwt + + +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) + alt_text = db.Column(db.String(256)) + source_url = db.Column(db.String(256)) + + +class Community(db.Model): + id = db.Column(db.Integer, primary_key=True) + icon_id = db.Column(db.Integer, db.ForeignKey('file.id')) + name = db.Column(db.String(256), index=True) + title = db.Column(db.String(256)) + description = db.Column(db.Text) + rules = db.Column(db.Text) + 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) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + last_active = db.Column(db.DateTime, default=datetime.utcnow) + public_key = db.Column(db.Text) + private_key = db.Column(db.Text) + + ap_id = db.Column(db.String(255), index=True) + ap_profile_id = db.Column(db.String(255)) + 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)) + ap_domain = db.Column(db.String(255)) + + banned = db.Column(db.Boolean, default=False) + searchable = db.Column(db.Boolean, default=True) + + search_vector = db.Column(TSVectorType('name', 'title', 'description')) + + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + user_name = db.Column(db.String(255), unique=True, index=True) + email = db.Column(db.String(255), index=True) + password_hash = db.Column(db.String(128)) + verified = db.Column(db.Boolean, default=False) + banned = db.Column(db.Boolean, default=False) + deleted = db.Column(db.Boolean, default=False) + about = db.Column(db.Text) + keywords = db.Column(db.String(256)) + show_nsfw = db.Column(db.Boolean, default=False) + show_nsfl = db.Column(db.Boolean, default=False) + created = db.Column(db.DateTime, default=datetime.utcnow) + last_seen = db.Column(db.DateTime, default=datetime.utcnow, index=True) + role = db.Column(db.Integer, default=0) + avatar_id = db.Column(db.Integer, db.ForeignKey('file.id')) + cover_id = db.Column(db.Integer, db.ForeignKey('file.id')) + public_key = db.Column(db.Text) + private_key = db.Column(db.Text) + newsletter = db.Column(db.Boolean, default=True) + bounces = db.Column(db.SmallInteger, default=0) + timezone = db.Column(db.String(20)) + stripe_customer_id = db.Column(db.String(50)) + stripe_subscription_id = db.Column(db.String(50)) + searchable = db.Column(db.Boolean, default=True) + + ap_id = db.Column(db.String(255), index=True) + ap_profile_id = db.Column(db.String(255)) + ap_public_url = db.Column(db.String(255)) + 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)) + + search_vector = db.Column(TSVectorType('user_name', 'bio', 'keywords')) + activity = db.relationship('ActivityLog', backref='account', lazy='dynamic', cascade="all, delete-orphan") + + def __repr__(self): + return ''.format(self.user_name) + + 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 + + def avatar(self, size): + digest = md5(self.email.lower().encode('utf-8')).hexdigest() + return 'https://www.gravatar.com/avatar/{}?d=identicon&s={}'.format( + digest, size) + + 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'], + algorithm='HS256').decode('utf-8') + + 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 < datetime.utcnow() + timedelta(weeks=1) + + def is_expired(self): + if self.expires is None: + return True + return self.expires < datetime.utcnow() + + def expired_ages_ago(self): + if self.expires is None: + return True + return self.expires < datetime(2019, 9, 1) + + @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) + + +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=datetime.utcnow) + + +class Post(db.Model): + 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) + slug = db.Column(db.String(255)) + title = db.Column(db.String(255)) + url = db.Column(db.String(2048)) + body = db.Column(db.Text) + type = db.Column(db.Integer) + has_embed = db.Column(db.Boolean, default=False) + reply_count = db.Column(db.Integer, default=0) + score = db.Column(db.Integer, default=0, index=True) + nsfw = db.Column(db.Boolean, default=False) + nsfl = db.Column(db.Boolean, default=False) + sticky = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow) + posted_at = db.Column(db.DateTime, index=True, default=datetime.utcnow) + last_active = db.Column(db.DateTime, index=True, default=datetime.utcnow) + ip = db.Column(db.String(50)) + up_votes = db.Column(db.Integer, default=0) + down_votes = db.Column(db.Integer, default=0) + ranking = db.Column(db.Integer, default=0) + language = db.Column(db.String(10)) + edited_at = db.Column(db.DateTime) + + ap_id = db.Column(db.String(255), index=True) + + search_vector = db.Column(TSVectorType('title', 'body')) + + +class PostReply(db.Model): + 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) + image_id = db.Column(db.Integer, db.ForeignKey('file.id'), index=True) + parent_id = db.Column(db.Integer) + root_id = db.Column(db.Integer) + body = db.Column(db.Text) + score = db.Column(db.Integer, default=0, index=True) + nsfw = db.Column(db.Boolean, default=False) + nsfl = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow) + posted_at = db.Column(db.DateTime, index=True, default=datetime.utcnow) + ip = db.Column(db.String(50)) + up_votes = db.Column(db.Integer, default=0) + down_votes = db.Column(db.Integer, default=0) + ranking = db.Column(db.Integer, default=0) + language = db.Column(db.String(10)) + edited_at = db.Column(db.DateTime) + + ap_id = db.Column(db.String(255), index=True) + + search_vector = db.Column(TSVectorType('body')) + + +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) + + +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=datetime.utcnow) + + +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=datetime.utcnow) + + +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) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + +class UserNote(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + target_id = db.Column(db.Integer, db.ForeignKey('user.id')) + body = db.Column(db.Text) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + +class UserBlock(db.Model): + id = db.Column(db.Integer, primary_key=True) + blocker_id = db.Column(db.Integer, db.ForeignKey('user.id')) + blocked_id = db.Column(db.Integer, db.ForeignKey('user.id')) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + +class BannedInstances(db.Model): + id = db.Column(db.Integer, primary_key=True) + domain = db.Column(db.String(256)) + reason = db.Column(db.String(256)) + initiator = db.Column(db.String(256)) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + +class Settings(db.Model): + name = db.Column(db.String(50), primary_key=True) + value = db.Column(db.String(1024)) + + +@login.user_loader +def load_user(id): + return User.query.get(int(id)) diff --git a/migrations/README b/migrations/README new file mode 100644 index 00000000..0e048441 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 00000000..ec9d45c2 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 00000000..89f80b21 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,110 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except TypeError: + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 00000000..2c015630 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/1a9507704262_add_user_deleted_and_indexes.py b/migrations/versions/1a9507704262_add_user_deleted_and_indexes.py new file mode 100644 index 00000000..dd5be79b --- /dev/null +++ b/migrations/versions/1a9507704262_add_user_deleted_and_indexes.py @@ -0,0 +1,56 @@ +"""add user.deleted and indexes + +Revision ID: 1a9507704262 +Revises: 54f1dd40e066 +Create Date: 2023-08-05 21:09:39.158879 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1a9507704262' +down_revision = '54f1dd40e066' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('community', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_community_ap_id'), ['ap_id'], unique=False) + batch_op.create_index(batch_op.f('ix_community_name'), ['name'], unique=False) + + with op.batch_alter_table('post', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_post_ap_id'), ['ap_id'], unique=False) + + with op.batch_alter_table('post_reply', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_post_reply_ap_id'), ['ap_id'], unique=False) + + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.add_column(sa.Column('deleted', sa.Boolean(), nullable=True)) + batch_op.create_index(batch_op.f('ix_user_ap_id'), ['ap_id'], unique=False) + batch_op.create_index(batch_op.f('ix_user_last_seen'), ['last_seen'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_user_last_seen')) + batch_op.drop_index(batch_op.f('ix_user_ap_id')) + batch_op.drop_column('deleted') + + with op.batch_alter_table('post_reply', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_post_reply_ap_id')) + + with op.batch_alter_table('post', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_post_ap_id')) + + with op.batch_alter_table('community', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_community_name')) + batch_op.drop_index(batch_op.f('ix_community_ap_id')) + + # ### end Alembic commands ### diff --git a/migrations/versions/54f1dd40e066_initial_tables.py b/migrations/versions/54f1dd40e066_initial_tables.py new file mode 100644 index 00000000..87ee64ec --- /dev/null +++ b/migrations/versions/54f1dd40e066_initial_tables.py @@ -0,0 +1,320 @@ +"""initial tables + +Revision ID: 54f1dd40e066 +Revises: +Create Date: 2023-08-04 21:27:35.452094 + +""" +import sqlalchemy_utils +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '54f1dd40e066' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('banned_instances', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('domain', sa.String(length=256), nullable=True), + sa.Column('reason', sa.String(length=256), nullable=True), + sa.Column('initiator', sa.String(length=256), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('domain', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('post_count', sa.Integer(), nullable=True), + sa.Column('banned', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('domain', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_domain_banned'), ['banned'], unique=False) + batch_op.create_index(batch_op.f('ix_domain_name'), ['name'], unique=False) + + op.create_table('file', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('file_path', sa.String(length=255), nullable=True), + sa.Column('file_name', sa.String(length=255), nullable=True), + sa.Column('width', sa.Integer(), nullable=True), + sa.Column('height', sa.Integer(), nullable=True), + sa.Column('alt_text', sa.String(length=256), nullable=True), + sa.Column('source_url', sa.String(length=256), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('settings', + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('value', sa.String(length=1024), nullable=True), + sa.PrimaryKeyConstraint('name') + ) + op.create_table('community', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('icon_id', sa.Integer(), nullable=True), + sa.Column('name', sa.String(length=256), nullable=True), + sa.Column('title', sa.String(length=256), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('rules', sa.Text(), nullable=True), + sa.Column('subscriptions_count', sa.Integer(), nullable=True), + sa.Column('post_count', sa.Integer(), nullable=True), + sa.Column('post_reply_count', sa.Integer(), nullable=True), + sa.Column('nsfw', sa.Boolean(), nullable=True), + sa.Column('nsfl', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('last_active', sa.DateTime(), nullable=True), + sa.Column('public_key', sa.Text(), nullable=True), + sa.Column('private_key', sa.Text(), nullable=True), + sa.Column('ap_id', sa.String(length=255), nullable=True), + sa.Column('ap_profile_id', sa.String(length=255), nullable=True), + sa.Column('ap_followers_url', sa.String(length=255), nullable=True), + sa.Column('ap_preferred_username', sa.String(length=255), nullable=True), + sa.Column('ap_discoverable', sa.Boolean(), nullable=True), + sa.Column('ap_public_url', sa.String(length=255), nullable=True), + sa.Column('ap_fetched_at', sa.DateTime(), nullable=True), + sa.Column('ap_deleted_at', sa.DateTime(), nullable=True), + sa.Column('ap_inbox_url', sa.String(length=255), nullable=True), + sa.Column('ap_domain', sa.String(length=255), nullable=True), + sa.Column('banned', sa.Boolean(), nullable=True), + sa.Column('searchable', sa.Boolean(), nullable=True), + sa.Column('search_vector', sqlalchemy_utils.types.ts_vector.TSVectorType(), nullable=True), + sa.ForeignKeyConstraint(['icon_id'], ['file.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_name', sa.String(length=255), nullable=True), + sa.Column('email', sa.String(length=255), nullable=True), + sa.Column('password_hash', sa.String(length=128), nullable=True), + sa.Column('verified', sa.Boolean(), nullable=True), + sa.Column('banned', sa.Boolean(), nullable=True), + sa.Column('about', sa.Text(), nullable=True), + sa.Column('keywords', sa.String(length=256), nullable=True), + sa.Column('show_nsfw', sa.Boolean(), nullable=True), + sa.Column('show_nsfl', sa.Boolean(), nullable=True), + sa.Column('created', sa.DateTime(), nullable=True), + sa.Column('last_seen', sa.DateTime(), nullable=True), + sa.Column('role', sa.Integer(), nullable=True), + sa.Column('avatar_id', sa.Integer(), nullable=True), + sa.Column('cover_id', sa.Integer(), nullable=True), + sa.Column('public_key', sa.Text(), nullable=True), + sa.Column('private_key', sa.Text(), nullable=True), + sa.Column('newsletter', sa.Boolean(), nullable=True), + sa.Column('bounces', sa.SmallInteger(), nullable=True), + sa.Column('timezone', sa.String(length=20), nullable=True), + sa.Column('stripe_customer_id', sa.String(length=50), nullable=True), + sa.Column('stripe_subscription_id', sa.String(length=50), nullable=True), + sa.Column('searchable', sa.Boolean(), nullable=True), + sa.Column('ap_id', sa.String(length=255), nullable=True), + sa.Column('ap_profile_id', sa.String(length=255), nullable=True), + sa.Column('ap_public_url', sa.String(length=255), nullable=True), + sa.Column('ap_fetched_at', sa.DateTime(), nullable=True), + sa.Column('ap_followers_url', sa.String(length=255), nullable=True), + sa.Column('ap_preferred_username', sa.String(length=255), nullable=True), + sa.Column('ap_manually_approves_followers', sa.Boolean(), nullable=True), + sa.Column('ap_deleted_at', sa.DateTime(), nullable=True), + sa.Column('ap_inbox_url', sa.String(length=255), nullable=True), + sa.Column('ap_domain', sa.String(length=255), nullable=True), + sa.Column('search_vector', sqlalchemy_utils.types.ts_vector.TSVectorType(), nullable=True), + sa.ForeignKeyConstraint(['avatar_id'], ['file.id'], ), + sa.ForeignKeyConstraint(['cover_id'], ['file.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_user_email'), ['email'], unique=False) + batch_op.create_index(batch_op.f('ix_user_user_name'), ['user_name'], unique=True) + + op.create_table('activity_log', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('activity_type', sa.String(length=64), nullable=True), + sa.Column('activity', sa.String(length=255), nullable=True), + sa.Column('timestamp', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('activity_log', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_activity_log_timestamp'), ['timestamp'], unique=False) + batch_op.create_index(batch_op.f('ix_activity_log_user_id'), ['user_id'], unique=False) + + op.create_table('community_block', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('community_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['community_id'], ['community.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('user_id', 'community_id') + ) + op.create_table('community_member', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('community_id', sa.Integer(), nullable=False), + sa.Column('is_moderator', sa.Boolean(), nullable=True), + sa.Column('is_owner', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['community_id'], ['community.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('user_id', 'community_id') + ) + op.create_table('domain_block', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('domain_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['domain_id'], ['domain.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('user_id', 'domain_id') + ) + op.create_table('post', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('community_id', sa.Integer(), nullable=True), + sa.Column('image_id', sa.Integer(), nullable=True), + sa.Column('domain_id', sa.Integer(), nullable=True), + sa.Column('slug', sa.String(length=255), nullable=True), + sa.Column('title', sa.String(length=255), nullable=True), + sa.Column('url', sa.String(length=2048), nullable=True), + sa.Column('body', sa.Text(), nullable=True), + sa.Column('type', sa.Integer(), nullable=True), + sa.Column('has_embed', sa.Boolean(), nullable=True), + sa.Column('reply_count', sa.Integer(), nullable=True), + sa.Column('score', sa.Integer(), nullable=True), + sa.Column('nsfw', sa.Boolean(), nullable=True), + sa.Column('nsfl', sa.Boolean(), nullable=True), + sa.Column('sticky', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('posted_at', sa.DateTime(), nullable=True), + sa.Column('last_active', sa.DateTime(), nullable=True), + sa.Column('ip', sa.String(length=50), nullable=True), + sa.Column('up_votes', sa.Integer(), nullable=True), + sa.Column('down_votes', sa.Integer(), nullable=True), + sa.Column('ranking', sa.Integer(), nullable=True), + sa.Column('language', sa.String(length=10), nullable=True), + sa.Column('edited_at', sa.DateTime(), nullable=True), + sa.Column('ap_id', sa.String(length=255), nullable=True), + sa.Column('search_vector', sqlalchemy_utils.types.ts_vector.TSVectorType(), nullable=True), + sa.ForeignKeyConstraint(['community_id'], ['community.id'], ), + sa.ForeignKeyConstraint(['domain_id'], ['domain.id'], ), + sa.ForeignKeyConstraint(['image_id'], ['file.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('post', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_post_community_id'), ['community_id'], unique=False) + batch_op.create_index(batch_op.f('ix_post_created_at'), ['created_at'], unique=False) + batch_op.create_index(batch_op.f('ix_post_domain_id'), ['domain_id'], unique=False) + batch_op.create_index(batch_op.f('ix_post_image_id'), ['image_id'], unique=False) + batch_op.create_index(batch_op.f('ix_post_last_active'), ['last_active'], unique=False) + batch_op.create_index(batch_op.f('ix_post_posted_at'), ['posted_at'], unique=False) + batch_op.create_index(batch_op.f('ix_post_score'), ['score'], unique=False) + batch_op.create_index(batch_op.f('ix_post_user_id'), ['user_id'], unique=False) + + op.create_table('user_block', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('blocker_id', sa.Integer(), nullable=True), + sa.Column('blocked_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['blocked_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['blocker_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user_note', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('target_id', sa.Integer(), nullable=True), + sa.Column('body', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['target_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('post_reply', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('post_id', sa.Integer(), nullable=True), + sa.Column('community_id', sa.Integer(), nullable=True), + sa.Column('image_id', sa.Integer(), nullable=True), + sa.Column('parent_id', sa.Integer(), nullable=True), + sa.Column('root_id', sa.Integer(), nullable=True), + sa.Column('body', sa.Text(), nullable=True), + sa.Column('score', sa.Integer(), nullable=True), + sa.Column('nsfw', sa.Boolean(), nullable=True), + sa.Column('nsfl', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('posted_at', sa.DateTime(), nullable=True), + sa.Column('ip', sa.String(length=50), nullable=True), + sa.Column('up_votes', sa.Integer(), nullable=True), + sa.Column('down_votes', sa.Integer(), nullable=True), + sa.Column('ranking', sa.Integer(), nullable=True), + sa.Column('language', sa.String(length=10), nullable=True), + sa.Column('edited_at', sa.DateTime(), nullable=True), + sa.Column('ap_id', sa.String(length=255), nullable=True), + sa.Column('search_vector', sqlalchemy_utils.types.ts_vector.TSVectorType(), nullable=True), + sa.ForeignKeyConstraint(['community_id'], ['community.id'], ), + sa.ForeignKeyConstraint(['image_id'], ['file.id'], ), + sa.ForeignKeyConstraint(['post_id'], ['post.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('post_reply', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_post_reply_community_id'), ['community_id'], unique=False) + batch_op.create_index(batch_op.f('ix_post_reply_created_at'), ['created_at'], unique=False) + batch_op.create_index(batch_op.f('ix_post_reply_image_id'), ['image_id'], unique=False) + batch_op.create_index(batch_op.f('ix_post_reply_post_id'), ['post_id'], unique=False) + batch_op.create_index(batch_op.f('ix_post_reply_posted_at'), ['posted_at'], unique=False) + batch_op.create_index(batch_op.f('ix_post_reply_score'), ['score'], unique=False) + batch_op.create_index(batch_op.f('ix_post_reply_user_id'), ['user_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('post_reply', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_post_reply_user_id')) + batch_op.drop_index(batch_op.f('ix_post_reply_score')) + batch_op.drop_index(batch_op.f('ix_post_reply_posted_at')) + batch_op.drop_index(batch_op.f('ix_post_reply_post_id')) + batch_op.drop_index(batch_op.f('ix_post_reply_image_id')) + batch_op.drop_index(batch_op.f('ix_post_reply_created_at')) + batch_op.drop_index(batch_op.f('ix_post_reply_community_id')) + + op.drop_table('post_reply') + op.drop_table('user_note') + op.drop_table('user_block') + with op.batch_alter_table('post', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_post_user_id')) + batch_op.drop_index(batch_op.f('ix_post_score')) + batch_op.drop_index(batch_op.f('ix_post_posted_at')) + batch_op.drop_index(batch_op.f('ix_post_last_active')) + batch_op.drop_index(batch_op.f('ix_post_image_id')) + batch_op.drop_index(batch_op.f('ix_post_domain_id')) + batch_op.drop_index(batch_op.f('ix_post_created_at')) + batch_op.drop_index(batch_op.f('ix_post_community_id')) + + op.drop_table('post') + op.drop_table('domain_block') + op.drop_table('community_member') + op.drop_table('community_block') + with op.batch_alter_table('activity_log', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_activity_log_user_id')) + batch_op.drop_index(batch_op.f('ix_activity_log_timestamp')) + + op.drop_table('activity_log') + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_user_user_name')) + batch_op.drop_index(batch_op.f('ix_user_email')) + + op.drop_table('user') + op.drop_table('community') + op.drop_table('settings') + op.drop_table('file') + with op.batch_alter_table('domain', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_domain_name')) + batch_op.drop_index(batch_op.f('ix_domain_banned')) + + op.drop_table('domain') + op.drop_table('banned_instances') + # ### end Alembic commands ###