ignore mastodon delete spam and lemmy retry spam

This commit is contained in:
rimu 2023-12-23 11:32:22 +13:00
parent 46646590a9
commit 684e68c3fd
3 changed files with 69 additions and 31 deletions

View file

@ -1,5 +1,3 @@
from datetime import datetime
from app import db, constants, cache from app import db, constants, cache
from app.activitypub import bp from app.activitypub import bp
from flask import request, Response, current_app, abort, jsonify, json, g from flask import request, Response, current_app, abort, jsonify, json, g
@ -14,7 +12,7 @@ from app.models import User, Community, CommunityJoinRequest, CommunityMember, C
from app.activitypub.util import public_key, users_total, active_half_year, active_month, local_posts, local_comments, \ 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, \ 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, \ lemmy_site_data, instance_weight, is_activitypub_request, downvote_post_reply, downvote_post, upvote_post_reply, \
upvote_post upvote_post, activity_already_ingested
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \ from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \
domain_from_url, markdown_to_html, community_membership, ap_datetime domain_from_url, markdown_to_html, community_membership, ap_datetime
import werkzeug.exceptions import werkzeug.exceptions
@ -279,11 +277,28 @@ def shared_inbox():
activity_log.result = 'failure' activity_log.result = 'failure'
db.session.add(activity_log) db.session.add(activity_log)
db.session.commit() db.session.commit()
return return ''
else: else:
if 'id' in request_json: if 'id' in request_json:
activity_log.activity_id = request_json['id'] activity_log.activity_id = request_json['id']
if activity_already_ingested(request_json['id']): # Lemmy has an extremely short POST timeout and tends to retry unnecessarily. Ignore their retries.
activity_log.result = 'ignored'
db.session.add(activity_log)
db.session.commit()
return ''
activity_log.activity_id = request_json['id']
activity_log.activity_json = json.dumps(request_json)
# Mastodon spams the whole fediverse whenever any of their users are deleted. Ignore them, for now. The Activity includes the Actor signature so it should be possible to verify the POST and do the delete if valid, without a call to find_actor_or_create() and all the network activity that involves. One day.
if 'type' in request_json and request_json['type'] == 'Delete' and request_json['id'].endswith('#delete'):
activity_log.result = 'ignored'
activity_log.activity_type = 'Delete'
db.session.add(activity_log)
db.session.commit()
return ''
actor = find_actor_or_create(request_json['actor']) if 'actor' in request_json else None actor = find_actor_or_create(request_json['actor']) if 'actor' in request_json else None
if actor is not None: if actor is not None:
if HttpSignature.verify_request(request, actor.public_key, skip_date=True): if HttpSignature.verify_request(request, actor.public_key, skip_date=True):
@ -304,7 +319,7 @@ def shared_inbox():
community_ap_id = request_json['object']['cc'][0] community_ap_id = request_json['object']['cc'][0]
community = find_actor_or_create(community_ap_id) community = find_actor_or_create(community_ap_id)
user = find_actor_or_create(user_ap_id) user = find_actor_or_create(user_ap_id)
if user and community: if (user and not user.is_local()) and community:
user.last_seen = community.last_active = g.site.last_active = utcnow() user.last_seen = community.last_active = g.site.last_active = utcnow()
object_type = request_json['object']['type'] object_type = request_json['object']['type']
@ -367,8 +382,8 @@ def shared_inbox():
activity_log.result = 'success' activity_log.result = 'success'
db.session.commit() db.session.commit()
vote = PostVote(user_id=user.id, author_id=post.user_id, vote = PostVote(user_id=user.id, author_id=post.user_id,
post_id=post.id, post_id=post.id,
effect=instance_weight(user.ap_domain)) effect=instance_weight(user.ap_domain))
db.session.add(vote) db.session.add(vote)
else: else:
post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to) post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to)
@ -401,8 +416,9 @@ def shared_inbox():
community.last_active = post.last_active = utcnow() community.last_active = post.last_active = utcnow()
activity_log.result = 'success' activity_log.result = 'success'
db.session.commit() db.session.commit()
vote = PostReplyVote(user_id=user.id, author_id=post_reply.user_id, post_reply_id=post_reply.id, vote = PostReplyVote(user_id=user.id, author_id=post_reply.user_id,
effect=instance_weight(user.ap_domain)) post_reply_id=post_reply.id,
effect=instance_weight(user.ap_domain))
db.session.add(vote) db.session.add(vote)
else: else:
activity_log.exception_message = 'Comments disabled' activity_log.exception_message = 'Comments disabled'
@ -421,7 +437,7 @@ def shared_inbox():
user.last_seen = community.last_active = g.site.last_active = utcnow() user.last_seen = community.last_active = g.site.last_active = utcnow()
object_type = request_json['object']['object']['type'] object_type = request_json['object']['object']['type']
new_content_types = ['Page', 'Article', 'Link', 'Note'] new_content_types = ['Page', 'Article', 'Link', 'Note']
if object_type in new_content_types: # create a new post if object_type in new_content_types: # create a new post
in_reply_to = request_json['object']['object']['inReplyTo'] if 'inReplyTo' in \ in_reply_to = request_json['object']['object']['inReplyTo'] if 'inReplyTo' in \
request_json['object']['object'] else None request_json['object']['object'] else None
@ -526,8 +542,8 @@ def shared_inbox():
else: else:
activity_log.exception_message = 'Could not detect type of like' activity_log.exception_message = 'Could not detect type of like'
if activity_log.result == 'success': if activity_log.result == 'success':
... # todo: recalculate 'hotness' of liked post/reply ... # todo: recalculate 'hotness' of liked post/reply
# todo: if vote was on content in local community, federate the vote out to followers # todo: if vote was on content in local community, federate the vote out to followers
elif request_json['object']['type'] == 'Dislike': elif request_json['object']['type'] == 'Dislike':
activity_log.activity_type = request_json['object']['type'] activity_log.activity_type = request_json['object']['type']
if g.site.enable_downvotes is False: if g.site.enable_downvotes is False:
@ -554,7 +570,7 @@ def shared_inbox():
# todo: if vote was on content in local community, federate the vote out to followers # todo: if vote was on content in local community, federate the vote out to followers
# Follow: remote user wants to join/follow one of our communities # Follow: remote user wants to join/follow one of our communities
elif request_json['type'] == 'Follow': # Follow is when someone wants to join a community elif request_json['type'] == 'Follow': # Follow is when someone wants to join a community
user_ap_id = request_json['actor'] user_ap_id = request_json['actor']
community_ap_id = request_json['object'] community_ap_id = request_json['object']
follow_id = request_json['id'] follow_id = request_json['id']
@ -594,10 +610,10 @@ def shared_inbox():
except Exception as e: except Exception as e:
accept_log = ActivityPubLog(direction='out', activity_json=json.dumps(accept), accept_log = ActivityPubLog(direction='out', activity_json=json.dumps(accept),
result='failure', activity_id=accept['id'], result='failure', activity_id=accept['id'],
exception_message = 'could not send Accept' + str(e)) exception_message='could not send Accept' + str(e))
db.session.add(accept_log) db.session.add(accept_log)
db.session.commit() db.session.commit()
return return ''
activity_log.result = 'success' activity_log.result = 'success'
else: else:
activity_log.exception_message = 'user is banned from this community' activity_log.exception_message = 'user is banned from this community'
@ -652,7 +668,7 @@ def shared_inbox():
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first() existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first()
if existing_vote: if existing_vote:
post.author.reputation -= existing_vote.effect post.author.reputation -= existing_vote.effect
if existing_vote.effect < 0: # Lemmy sends 'like' for upvote and 'dislike' for down votes. Cool! When it undoes an upvote it sends an 'Undo Like'. Fine. When it undoes a downvote it sends an 'Undo Like' - not 'Undo Dislike'?! if existing_vote.effect < 0: # Lemmy sends 'like' for upvote and 'dislike' for down votes. Cool! When it undoes an upvote it sends an 'Undo Like'. Fine. When it undoes a downvote it sends an 'Undo Like' - not 'Undo Dislike'?!
post.down_votes -= 1 post.down_votes -= 1
else: else:
post.up_votes -= 1 post.up_votes -= 1
@ -701,7 +717,7 @@ def shared_inbox():
activity_log.result = 'success' activity_log.result = 'success'
elif request_json['type'] == 'Update': elif request_json['type'] == 'Update':
if request_json['object']['type'] == 'Page': # Editing a post if request_json['object']['type'] == 'Page': # Editing a post
post = Post.query.filter_by(ap_id=request_json['object']['id']).first() post = Post.query.filter_by(ap_id=request_json['object']['id']).first()
if post: if post:
if 'source' in request_json['object'] and \ if 'source' in request_json['object'] and \
@ -729,9 +745,9 @@ def shared_inbox():
activity_log.result = 'success' activity_log.result = 'success'
elif request_json['type'] == 'Delete': elif request_json['type'] == 'Delete':
if isinstance(request_json['object'], str): if isinstance(request_json['object'], str):
ap_id = request_json['object'] # lemmy ap_id = request_json['object'] # lemmy
else: else:
ap_id = request_json['object']['id'] # kbin ap_id = request_json['object']['id'] # kbin
post = Post.query.filter_by(ap_id=ap_id).first() post = Post.query.filter_by(ap_id=ap_id).first()
if post: if post:
post.delete_dependencies() post.delete_dependencies()
@ -743,7 +759,7 @@ def shared_inbox():
reply.body = 'deleted' reply.body = 'deleted'
db.session.commit() db.session.commit()
activity_log.result = 'success' activity_log.result = 'success'
elif request_json['type'] == 'Like': # Upvote elif request_json['type'] == 'Like': # Upvote
activity_log.activity_type = request_json['type'] activity_log.activity_type = request_json['type']
user_ap_id = request_json['actor'] user_ap_id = request_json['actor']
user = find_actor_or_create(user_ap_id) user = find_actor_or_create(user_ap_id)
@ -761,7 +777,7 @@ def shared_inbox():
upvote_post_reply(comment, user) upvote_post_reply(comment, user)
activity_log.result = 'success' activity_log.result = 'success'
elif request_json['type'] == 'Dislike': # Downvote elif request_json['type'] == 'Dislike': # Downvote
if get_setting('allow_dislike', True) is False: if get_setting('allow_dislike', True) is False:
activity_log.exception_message = 'Dislike ignored because of allow_dislike setting' activity_log.exception_message = 'Dislike ignored because of allow_dislike setting'
else: else:
@ -786,7 +802,7 @@ def shared_inbox():
# Flush the caches of any major object that was created. To be sure. # Flush the caches of any major object that was created. To be sure.
if 'user' in vars() and user is not None: if 'user' in vars() and user is not None:
user.flush_cache() user.flush_cache()
#if 'community' in vars() and community is not None: # if 'community' in vars() and community is not None:
# community.flush_cache() # community.flush_cache()
if 'post' in vars() and post is not None: if 'post' in vars() and post is not None:
post.flush_cache() post.flush_cache()
@ -801,9 +817,7 @@ def shared_inbox():
activity_log.result = 'failure' activity_log.result = 'failure'
db.session.add(activity_log) db.session.add(activity_log)
db.session.commit() db.session.commit()
return '' return ''
@bp.route('/c/<actor>/outbox', methods=['GET']) @bp.route('/c/<actor>/outbox', methods=['GET'])
@ -927,6 +941,7 @@ def post_ap(post_id):
@bp.route('/activities/<type>/<id>') @bp.route('/activities/<type>/<id>')
@cache.cached(timeout=600)
def activities_json(type, id): def activities_json(type, id):
activity = ActivityPubLog.query.filter_by(activity_id=f"https://{current_app.config['SERVER_NAME']}/activities/{type}/{id}").first() activity = ActivityPubLog.query.filter_by(activity_id=f"https://{current_app.config['SERVER_NAME']}/activities/{type}/{id}").first()
if activity: if activity:

View file

@ -3,11 +3,11 @@ from __future__ import annotations
import json import json
import os import os
from typing import Union, Tuple from typing import Union, Tuple
from flask import current_app, request from flask import current_app, request, g
from sqlalchemy import text from sqlalchemy import text
from app import db, cache, constants from app import db, cache, constants
from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow, \ from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow, \
Site, PostVote, PostReplyVote Site, PostVote, PostReplyVote, ActivityPubLog
import time import time
import base64 import base64
import requests import requests
@ -504,6 +504,11 @@ def is_activitypub_request():
return 'application/ld+json' in request.headers.get('Accept', '') or 'application/activity+json' in request.headers.get('Accept', '') return 'application/ld+json' in request.headers.get('Accept', '') or 'application/activity+json' in request.headers.get('Accept', '')
def activity_already_ingested(ap_id):
return db.session.execute(text('SELECT id FROM "activity_pub_log" WHERE activity_id = :activity_id'),
{'activity_id': ap_id}).scalar()
def downvote_post(post, user): def downvote_post(post, user):
user.last_seen = utcnow() user.last_seen = utcnow()
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first() existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first()
@ -627,7 +632,7 @@ def upvote_post(post, user):
def lemmy_site_data(): def lemmy_site_data():
site = Site.query.get(1) site = g.site
data = { data = {
"site_view": { "site_view": {
"site": { "site": {

View file

@ -1,6 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from flask import request, flash from flask import request, flash, json, url_for
from flask_login import login_required, current_user from flask_login import login_required, current_user
from flask_babel import _ from flask_babel import _
from sqlalchemy import text, desc from sqlalchemy import text, desc
@ -126,5 +126,23 @@ def admin_activities():
ActivityPubLog.created_at < utcnow() - timedelta(days=3)).delete() ActivityPubLog.created_at < utcnow() - timedelta(days=3)).delete()
db.session.commit() db.session.commit()
return render_template('admin/activities.html', title=_('ActivityPub Log'), page = request.args.get('page', 1, type=int)
activities=ActivityPubLog.query.order_by(desc(ActivityPubLog.created_at)).all())
activities = ActivityPubLog.query.order_by(desc(ActivityPubLog.created_at)).paginate(page=page, per_page=1000, error_out=False)
next_url = url_for('admin.admin_activities',
page=activities.next_num) if activities.has_next else None
prev_url = url_for('admin.admin_activities',
page=activities.prev_num) if activities.has_prev and page != 1 else None
return render_template('admin/activities.html', title=_('ActivityPub Log'), next_url=next_url, prev_url=prev_url,
activities=activities)
@bp.route('/activity_json/<int:activity_id>')
@login_required
@permission_required('change instance settings')
def activity_json(activity_id):
activity = ActivityPubLog.query.get_or_404(activity_id)
return render_template('admin/activity_json.html', title=_('Activity JSON'),
activity_json_data=json.loads(activity.activity_json))