diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 25df9423..3901c636 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -7,10 +7,11 @@ from flask import request, Response, render_template, current_app, abort, jsonif from app.activitypub.signature import HttpSignature from app.community.routes import show_community -from app.models import User, Community, CommunityJoinRequest, CommunityMember, CommunityBan, ActivityPubLog +from app.models import User, Community, CommunityJoinRequest, CommunityMember, CommunityBan, ActivityPubLog, Post, \ + PostReply, Instance, PostVote, PostReplyVote from app.activitypub.util import public_key, users_total, active_half_year, active_month, local_posts, local_comments, \ post_to_activity, find_actor_or_create -from app.utils import gibberish +from app.utils import gibberish, get_setting INBOX = [] @@ -222,7 +223,48 @@ def shared_inbox(): if 'type' in request_json: activity_log.activity_type = request_json['type'] if request_json['type'] == 'Announce': - ... + if request_json['object']['type'] == 'Like' or request_json['object']['type'] == 'Dislike': + activity_log.activity_type = request_json['object']['type'] + vote_effect = 1.0 if request_json['object']['type'] == 'Like' else -1.0 + if vote_effect < 0 and get_setting('allow_dislike', True) is False: + activity_log.exception_message = 'Dislike ignored because of allow_dislike setting' + else: + user_ap_id = request_json['object']['actor'] + liked_ap_id = request_json['object']['object'] + user = find_actor_or_create(user_ap_id) + vote_weight = 1.0 + if user.ap_domain: + instance = Instance.query.filter_by(domain=user.ap_domain).fetch() + if instance: + vote_weight = instance.vote_weight + liked = find_liked_object(liked_ap_id) + # insert into voted table + if isinstance(liked, Post): + existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=liked.id).first() + if existing_vote: + existing_vote.effect = vote_effect * vote_weight + else: + vote = PostVote(user_id=user.id, author_id=liked.user_id, post_id=liked.id, + effect=vote_effect * vote_weight) + db.session.add(vote) + db.session.commit() + activity_log.result = 'success' + elif isinstance(liked, PostReply): + existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=liked.id).first() + if existing_vote: + existing_vote.effect = vote_effect * vote_weight + else: + vote = PostReplyVote(user_id=user.id, author_id=liked.user_id, post_reply_id=liked.id, + effect=vote_effect * vote_weight) + db.session.add(vote) + db.session.commit() + activity_log.result = 'success' + else: + activity_log.result='failure' + activity_log.exception_message = 'Could not detect type of like' + if activity_log.result == 'success': + ... # todo: recalculate 'hotness' of liked post/reply + # remote user wants to follow one of our communities elif request_json['type'] == 'Follow': user_ap_id = request_json['actor'] diff --git a/app/cli.py b/app/cli.py index ce40e6fc..8a495f4e 100644 --- a/app/cli.py +++ b/app/cli.py @@ -49,6 +49,7 @@ def register(app): db.configure_mappers() db.create_all() db.session.append(Settings(name='allow_nsfw', value=json.dumps(False))) + db.session.append(Settings(name='allow_dislike', value=json.dumps(True))) db.session.append(BannedInstances(domain='lemmygrad.ml')) db.session.append(BannedInstances(domain='gab.com')) db.session.append(BannedInstances(domain='exploding-heads.com')) diff --git a/app/models.py b/app/models.py index 3fb5f1b1..103436d4 100644 --- a/app/models.py +++ b/app/models.py @@ -117,6 +117,7 @@ class User(UserMixin, db.Model): newsletter = db.Column(db.Boolean, default=True) bounces = db.Column(db.SmallInteger, default=0) timezone = db.Column(db.String(20)) + reputation = db.Column(db.Float, default=0.0) stripe_customer_id = db.Column(db.String(50)) stripe_subscription_id = db.Column(db.String(50)) searchable = db.Column(db.Boolean, default=True) @@ -349,6 +350,7 @@ class Instance(db.Model): 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) class Settings(db.Model): @@ -374,11 +376,29 @@ class UserFollowRequest(db.Model): follow_id = db.Column(db.Integer, db.ForeignKey('user.id')) +class PostVote(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + author_id = db.Column(db.Integer, db.ForeignKey('user.id')) + post_id = db.Column(db.Integer, db.ForeignKey('post.id')) + effect = db.Column(db.Float) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + +class PostReplyVote(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + author_id = db.Column(db.Integer, db.ForeignKey('user.id')) + post_reply_id = db.Column(db.Integer, db.ForeignKey('post_reply.id')) + effect = db.Column(db.Float) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + # 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' - activity_id = db.Column(db.String(100), indexed=True) + activity_id = db.Column(db.String(100), 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' diff --git a/migrations/versions/6b84580a94cd_voting.py b/migrations/versions/6b84580a94cd_voting.py new file mode 100644 index 00000000..319e355e --- /dev/null +++ b/migrations/versions/6b84580a94cd_voting.py @@ -0,0 +1,72 @@ +"""voting + +Revision ID: 6b84580a94cd +Revises: f032dbdfbd1d +Create Date: 2023-09-10 19:59:15.735823 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6b84580a94cd' +down_revision = 'f032dbdfbd1d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('post_vote', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('author_id', sa.Integer(), nullable=True), + sa.Column('post_id', sa.Integer(), nullable=True), + sa.Column('effect', sa.Float(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['author_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['post_id'], ['post.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('post_reply_vote', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('author_id', sa.Integer(), nullable=True), + sa.Column('post_reply_id', sa.Integer(), nullable=True), + sa.Column('effect', sa.Float(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['author_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['post_reply_id'], ['post_reply.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('activity_pub_log', schema=None) as batch_op: + batch_op.add_column(sa.Column('activity_id', sa.String(length=100), nullable=True)) + batch_op.create_index(batch_op.f('ix_activity_pub_log_activity_id'), ['activity_id'], unique=False) + + with op.batch_alter_table('instance', schema=None) as batch_op: + batch_op.add_column(sa.Column('vote_weight', sa.Float(), nullable=True)) + + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.add_column(sa.Column('reputation', sa.Float(), nullable=True)) + + # ### 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_column('reputation') + + with op.batch_alter_table('instance', schema=None) as batch_op: + batch_op.drop_column('vote_weight') + + with op.batch_alter_table('activity_pub_log', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_activity_pub_log_activity_id')) + batch_op.drop_column('activity_id') + + op.drop_table('post_reply_vote') + op.drop_table('post_vote') + # ### end Alembic commands ###