From f7cfd1f92bcd010369b1935729fe458ed57f5ab1 Mon Sep 17 00:00:00 2001 From: freamon Date: Mon, 29 Apr 2024 16:13:29 +0100 Subject: [PATCH] Accept Follow requests from remote actors to local actors --- app/activitypub/routes.py | 84 ++++++++++++++++++- app/models.py | 8 ++ .../versions/5487a1886c62_user_follower.py | 37 ++++++++ 3 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 migrations/versions/5487a1886c62_user_follower.py diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 5848453d..762bf341 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -6,7 +6,7 @@ from app import db, constants, cache, celery from app.activitypub import bp from flask import request, current_app, abort, jsonify, json, g, url_for, redirect, make_response -from app.activitypub.signature import HttpSignature, post_request +from app.activitypub.signature import HttpSignature, post_request, VerificationError from app.community.routes import show_community from app.community.util import send_to_remote_instance from app.post.routes import continue_discussion, show_post @@ -14,7 +14,7 @@ from app.user.routes import show_profile from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, SUBSCRIPTION_MEMBER from app.models import User, Community, CommunityJoinRequest, CommunityMember, CommunityBan, ActivityPubLog, Post, \ PostReply, Instance, PostVote, PostReplyVote, File, AllowedInstances, BannedInstances, utcnow, Site, Notification, \ - ChatMessage, Conversation + ChatMessage, Conversation, UserFollower 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, default_context, instance_blocked, find_reply_parent, find_liked_object, \ lemmy_site_data, instance_weight, is_activitypub_request, downvote_post_reply, downvote_post, upvote_post_reply, \ @@ -1186,13 +1186,91 @@ def community_moderators_route(actor): return jsonify(community_data) -@bp.route('/u//inbox', methods=['GET', 'POST']) +@bp.route('/u//inbox', methods=['POST']) def user_inbox(actor): + site = Site.query.get(1) + activity_log = ActivityPubLog(direction='in', result='failure') + activity_log.result = 'processing' + db.session.add(activity_log) + db.session.commit() + + try: + request_json = request.get_json(force=True) + except werkzeug.exceptions.BadRequest as e: + activity_log.exception_message = 'Unable to parse json body: ' + e.description + activity_log.result = 'failure' + db.session.commit() + return '', 400 + + if 'id' in request_json: + activity_log.activity_id = request_json['id'] + if site.log_activitypub_json: + activity_log.activity_json = json.dumps(request_json) + + actor = find_actor_or_create(request_json['actor']) if 'actor' in request_json else None + if actor is not None: + try: + HttpSignature.verify_request(request, actor.public_key, skip_date=True) + if 'type' in request_json and request_json['type'] == 'Follow': + if current_app.debug: + process_user_follow_request(request_json, activity_log.id, actor.id) + else: + process_user_follow_request.delay(request_json, activity_log.id, actor.id) + return '' + # todo: undo/follow + except VerificationError: + activity_log.result = 'failure' + activity_log.exception_message = 'Could not verify signature' + db.session.commit() + return '', 400 + else: + actor_name = request_json['actor'] if 'actor' in request_json else '' + activity_log.exception_message = f'Actor could not be found: {actor_name}' + + if activity_log.exception_message is not None: + activity_log.result = 'failure' + db.session.commit() resp = jsonify('ok') resp.content_type = 'application/activity+json' return resp +def process_user_follow_request(request_json, activitypublog_id, remote_user_id): + activity_log = ActivityPubLog.query.get(activitypublog_id) + local_user_ap_id = request_json['object'] + follow_id = request_json['id'] + local_user = find_actor_or_create(local_user_ap_id, create_if_not_found=False) + remote_user = User.query.get(remote_user_id) + if local_user and local_user.is_local() and not remote_user.is_local(): + existing_follower = UserFollower.query.filter_by(local_user_id=local_user.id, remote_user_id=remote_user.id).first() + if not existing_follower: + auto_accept = not local_user.ap_manually_approves_followers + new_follower = UserFollower(local_user_id=local_user.id, remote_user_id=remote_user.id, is_accepted=auto_accept) + db.session.add(new_follower) + accept = { + "@context": default_context(), + "actor": local_user.ap_profile_id, + "to": [ + remote_user.ap_profile_id + ], + "object": { + "actor": remote_user.ap_profile_id, + "to": None, + "object": local_user.ap_profile_id, + "type": "Follow", + "id": follow_id + }, + "type": "Accept", + "id": f"https://{current_app.config['SERVER_NAME']}/activities/accept/" + gibberish(32) + } + if post_request(remote_user.ap_inbox_url, accept, local_user.private_key, f"https://{current_app.config['SERVER_NAME']}/u/{local_user.user_name}#main-key"): + activity_log.result = 'success' + else: + activity_log.exception_message = 'Error sending Accept' + + db.session.commit() + + @bp.route('/c//inbox', methods=['GET', 'POST']) def community_inbox(actor): return shared_inbox() diff --git a/app/models.py b/app/models.py index c7ac0ee6..6b49fb8d 100644 --- a/app/models.py +++ b/app/models.py @@ -1144,6 +1144,14 @@ class CommunityMember(db.Model): created_at = db.Column(db.DateTime, default=utcnow) +class UserFollower(db.Model): + local_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) + remote_user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) + is_accepted = db.Column(db.Boolean, default=True) # flip to ban remote user / reject follow + is_inward = db.Column(db.Boolean, default=True) # true = remote user is following a local one + created_at = db.Column(db.DateTime, default=utcnow) + + # people banned from communities class CommunityBan(db.Model): user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) # person who is banned, not the banner diff --git a/migrations/versions/5487a1886c62_user_follower.py b/migrations/versions/5487a1886c62_user_follower.py new file mode 100644 index 00000000..fd60cf58 --- /dev/null +++ b/migrations/versions/5487a1886c62_user_follower.py @@ -0,0 +1,37 @@ +"""user_follower + +Revision ID: 5487a1886c62 +Revises: 46c170499e71 +Create Date: 2024-04-29 15:55:33.954059 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '5487a1886c62' +down_revision = '46c170499e71' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user_follower', + sa.Column('local_user_id', sa.Integer(), nullable=False), + sa.Column('remote_user_id', sa.Integer(), nullable=False), + sa.Column('is_accepted', sa.Boolean(), nullable=True), + sa.Column('is_inward', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['local_user_id'], ['user.id'], ), + sa.ForeignKeyConstraint(['remote_user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('local_user_id', 'remote_user_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('user_follower') + # ### end Alembic commands ###