From 6182240ad39619c022a8539366829400f1af8008 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Sun, 24 Dec 2023 13:28:41 +1300 Subject: [PATCH] move long-running tasks to separate background process (celery + redis) --- .gitignore | 1 + app/__init__.py | 3 + app/activitypub/routes.py | 1017 ++++++++++++------------ app/activitypub/util.py | 12 +- app/email.py | 9 +- app/main/routes.py | 1 + app/static/structure.css | 10 + app/static/structure.scss | 10 + app/templates/base.html | 20 +- app/templates/community/community.html | 4 +- app/templates/post/post.html | 4 +- app/utils.py | 1 - celery_worker.default.py | 11 + config.py | 2 + dev_notes.txt | 12 + interests.txt | 1 - requirements.txt | 2 + 17 files changed, 595 insertions(+), 525 deletions(-) create mode 100644 celery_worker.default.py create mode 100644 dev_notes.txt diff --git a/.gitignore b/.gitignore index 32ce9f95..d8d64113 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,4 @@ cython_debug/ .idea/ app/static/*.css.map /app/static/media/ +celery_worker.py diff --git a/app/__init__.py b/app/__init__.py index 17d6b14a..d654229e 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -13,6 +13,7 @@ from flask_mail import Mail from flask_moment import Moment from flask_babel import Babel, lazy_gettext as _l from flask_caching import Cache +from celery import Celery from sqlalchemy_searchable import make_searchable from config import Config @@ -28,6 +29,7 @@ bootstrap = Bootstrap5() moment = Moment() babel = Babel() cache = Cache() +celery = Celery(__name__, broker=Config.CELERY_BROKER_URL) def create_app(config_class=Config): @@ -43,6 +45,7 @@ def create_app(config_class=Config): make_searchable(db.metadata) babel.init_app(app, locale_selector=get_locale) cache.init_app(app) + celery.conf.update(app.config) from app.main import bp as main_bp app.register_blueprint(main_bp) diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 69c8b866..18c00293 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -1,4 +1,4 @@ -from app import db, constants, cache +from app import db, constants, cache, celery from app.activitypub import bp from flask import request, Response, current_app, abort, jsonify, json, g @@ -8,7 +8,7 @@ from app.post.routes import continue_discussion, show_post 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 + PostReply, Instance, PostVote, PostReplyVote, File, AllowedInstances, BannedInstances, utcnow, Site 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, \ @@ -98,7 +98,7 @@ def nodeinfo2(): "localPosts": local_posts(), "localComments": local_comments() }, - "openRegistrations": True + "openRegistrations": g.site.registration_mode == 'Open' } return jsonify(nodeinfo_data) @@ -278,546 +278,551 @@ def shared_inbox(): db.session.add(activity_log) db.session.commit() return '' + + if 'id' in request_json: + 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) + activity_log.result = 'processing' + + # 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 '' + else: + db.session.add(activity_log) + db.session.commit() else: - if 'id' in request_json: - 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 '' + activity_log.activity_id = '' + activity_log.activity_json = json.dumps(request_json) + db.session.add(activity_log) + db.session.commit() actor = find_actor_or_create(request_json['actor']) if 'actor' in request_json else None if actor is not None: if HttpSignature.verify_request(request, actor.public_key, skip_date=True): - if 'type' in request_json: - activity_log.activity_type = request_json['type'] - if not instance_blocked(request_json['id']): - # Create is new content - if request_json['type'] == 'Create': - activity_log.activity_type = 'Create' - user_ap_id = request_json['object']['attributedTo'] - community_ap_id = request_json['to'][0] - if community_ap_id == 'https://www.w3.org/ns/activitystreams#Public': # kbin does this when posting a reply - if 'to' in request_json['object'] and request_json['object']['to']: - community_ap_id = request_json['object']['to'][0] - if community_ap_id == 'https://www.w3.org/ns/activitystreams#Public' and 'cc' in request_json['object'] and request_json['object']['cc']: - community_ap_id = request_json['object']['cc'][0] - elif 'cc' in request_json['object'] and request_json['object']['cc']: - community_ap_id = request_json['object']['cc'][0] - community = find_actor_or_create(community_ap_id) - user = find_actor_or_create(user_ap_id) - if (user and not user.is_local()) and community: - user.last_seen = community.last_active = g.site.last_active = utcnow() + if current_app.debug: + process_inbox_request(request_json, activity_log.id) + else: + process_inbox_request.delay(request_json, activity_log.id) + return '' + else: + activity_log.exception_message = 'Could not verify signature' + else: + actor_name = request_json['actor'] if 'actor' in request_json else '' + activity_log.exception_message = f'Actor could not be found: {actor_name}' - object_type = request_json['object']['type'] - new_content_types = ['Page', 'Article', 'Link', 'Note'] - if object_type in new_content_types: # create a new post - in_reply_to = request_json['object']['inReplyTo'] if 'inReplyTo' in \ - request_json[ - 'object'] else None - if not in_reply_to: - post = Post(user_id=user.id, community_id=community.id, - title=request_json['object']['name'], - comments_enabled=request_json['object'][ - 'commentsEnabled'], - sticky=request_json['object']['stickied'] if 'stickied' in - request_json[ - 'object'] else False, - nsfw=request_json['object']['sensitive'], - nsfl=request_json['object']['nsfl'] if 'nsfl' in request_json[ - 'object'] else False, - ap_id=request_json['object']['id'], - ap_create_id=request_json['id'], - ap_announce_id=None, - type=constants.POST_TYPE_ARTICLE, - up_votes=1, - score=instance_weight(user.ap_domain) - ) - if 'source' in request_json['object'] and \ - request_json['object']['source'][ - 'mediaType'] == 'text/markdown': - post.body = request_json['object']['source']['content'] - post.body_html = markdown_to_html(post.body) - elif 'content' in request_json['object'] and request_json['object']['content'] is not None: - post.body_html = allowlist_html(request_json['object']['content']) - post.body = html_to_markdown(post.body_html) - if 'attachment' in request_json['object'] and \ - len(request_json['object']['attachment']) > 0 and \ - 'type' in request_json['object']['attachment'][0]: - if request_json['object']['attachment'][0]['type'] == 'Link': - post.url = request_json['object']['attachment'][0]['href'] - if is_image_url(post.url): - post.type = POST_TYPE_IMAGE - else: - post.type = POST_TYPE_LINK - domain = domain_from_url(post.url) - if not domain.banned: - post.domain_id = domain.id - else: - post = None - activity_log.exception_message = domain.name + ' is blocked by admin' - activity_log.result = 'failure' - if 'image' in request_json['object']: - image = File(source_url=request_json['object']['image']['url']) - db.session.add(image) - post.image = image + if activity_log.exception_message is not None: + activity_log.result = 'failure' + db.session.commit() + return '' - if post is not None: - db.session.add(post) - community.post_count += 1 - community.last_active = utcnow() - activity_log.result = 'success' - db.session.commit() - vote = PostVote(user_id=user.id, author_id=post.user_id, - post_id=post.id, - effect=instance_weight(user.ap_domain)) - db.session.add(vote) + +@celery.task +def process_inbox_request(request_json, activitypublog_id): + with current_app.app_context(): + activity_log = ActivityPubLog.query.get(activitypublog_id) + site = Site.query.get(1) # can't use g.site because celery doesn't use Flask's g variable + if 'type' in request_json: + activity_log.activity_type = request_json['type'] + if not instance_blocked(request_json['id']): + # Create is new content + if request_json['type'] == 'Create': + activity_log.activity_type = 'Create' + user_ap_id = request_json['object']['attributedTo'] + community_ap_id = request_json['to'][0] + if community_ap_id == 'https://www.w3.org/ns/activitystreams#Public': # kbin does this when posting a reply + if 'to' in request_json['object'] and request_json['object']['to']: + community_ap_id = request_json['object']['to'][0] + if community_ap_id == 'https://www.w3.org/ns/activitystreams#Public' and 'cc' in \ + request_json['object'] and request_json['object']['cc']: + community_ap_id = request_json['object']['cc'][0] + elif 'cc' in request_json['object'] and request_json['object']['cc']: + community_ap_id = request_json['object']['cc'][0] + community = find_actor_or_create(community_ap_id) + user = find_actor_or_create(user_ap_id) + if (user and not user.is_local()) and community: + user.last_seen = community.last_active = site.last_active = utcnow() + + object_type = request_json['object']['type'] + new_content_types = ['Page', 'Article', 'Link', 'Note'] + if object_type in new_content_types: # create a new post + in_reply_to = request_json['object']['inReplyTo'] if 'inReplyTo' in request_json['object'] else None + if not in_reply_to: + post = Post(user_id=user.id, community_id=community.id, + title=request_json['object']['name'], + comments_enabled=request_json['object']['commentsEnabled'], + sticky=request_json['object']['stickied'] if 'stickied' in request_json['object'] else False, + nsfw=request_json['object']['sensitive'], + nsfl=request_json['object']['nsfl'] if 'nsfl' in request_json['object'] else False, + ap_id=request_json['object']['id'], + ap_create_id=request_json['id'], + ap_announce_id=None, + type=constants.POST_TYPE_ARTICLE, + up_votes=1, + score=instance_weight(user.ap_domain) + ) + if 'source' in request_json['object'] and request_json['object']['source']['mediaType'] == 'text/markdown': + post.body = request_json['object']['source']['content'] + post.body_html = markdown_to_html(post.body) + elif 'content' in request_json['object'] and request_json['object']['content'] is not None: + post.body_html = allowlist_html(request_json['object']['content']) + post.body = html_to_markdown(post.body_html) + if 'attachment' in request_json['object'] and len(request_json['object']['attachment']) > 0 and \ + 'type' in request_json['object']['attachment'][0]: + if request_json['object']['attachment'][0]['type'] == 'Link': + post.url = request_json['object']['attachment'][0]['href'] + if is_image_url(post.url): + post.type = POST_TYPE_IMAGE + else: + post.type = POST_TYPE_LINK + domain = domain_from_url(post.url) + if not domain.banned: + post.domain_id = domain.id + else: + post = None + activity_log.exception_message = domain.name + ' is blocked by admin' + if 'image' in request_json['object']: + image = File(source_url=request_json['object']['image']['url']) + db.session.add(image) + post.image = image + + if post is not None: + db.session.add(post) + community.post_count += 1 + community.last_active = utcnow() + activity_log.result = 'success' + db.session.commit() + vote = PostVote(user_id=user.id, author_id=post.user_id, + post_id=post.id, + effect=instance_weight(user.ap_domain)) + db.session.add(vote) + else: + post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to) + post_reply = PostReply(user_id=user.id, community_id=community.id, + post_id=post_id, parent_id=parent_comment_id, + root_id=root_id, + nsfw=community.nsfw, + nsfl=community.nsfl, + up_votes=1, + score=instance_weight(user.ap_domain), + ap_id=request_json['object']['id'], + ap_create_id=request_json['id'], + ap_announce_id=None) + if 'source' in request_json['object'] and \ + request_json['object']['source']['mediaType'] == 'text/markdown': + post_reply.body = request_json['object']['source']['content'] + post_reply.body_html = markdown_to_html(post_reply.body) + elif 'content' in request_json['object']: + post_reply.body_html = allowlist_html(request_json['object']['content']) + post_reply.body = html_to_markdown(post_reply.body_html) + + if post_reply is not None: + post = Post.query.get(post_id) + if post.comments_enabled: + db.session.add(post_reply) + post.reply_count += 1 + community.post_reply_count += 1 + community.last_active = post.last_active = utcnow() + activity_log.result = 'success' + db.session.commit() + vote = PostReplyVote(user_id=user.id, author_id=post_reply.user_id, + post_reply_id=post_reply.id, + effect=instance_weight(user.ap_domain)) + db.session.add(vote) else: - post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to) + activity_log.exception_message = 'Comments disabled' + else: + activity_log.exception_message = 'Unacceptable type (kbin): ' + object_type + + # Announce is new content and votes, mastodon style (?) + if request_json['type'] == 'Announce': + if request_json['object']['type'] == 'Create': + activity_log.activity_type = request_json['object']['type'] + user_ap_id = request_json['object']['object']['attributedTo'] + community_ap_id = request_json['object']['audience'] + community = find_actor_or_create(community_ap_id) + user = find_actor_or_create(user_ap_id) + if (user and not user.is_local()) and community: + user.last_seen = community.last_active = site.last_active = utcnow() + object_type = request_json['object']['object']['type'] + new_content_types = ['Page', 'Article', 'Link', 'Note'] + if object_type in new_content_types: # create a new post + in_reply_to = request_json['object']['object']['inReplyTo'] if 'inReplyTo' in \ + request_json['object']['object'] else None + + if not in_reply_to: + post = Post(user_id=user.id, community_id=community.id, + title=request_json['object']['object']['name'], + comments_enabled=request_json['object']['object']['commentsEnabled'], + sticky=request_json['object']['object']['stickied'] if 'stickied' in request_json['object']['object'] else False, + nsfw=request_json['object']['object']['sensitive'] if 'sensitive' in request_json['object']['object'] else False, + nsfl=request_json['object']['object']['nsfl'] if 'nsfl' in request_json['object']['object'] else False, + ap_id=request_json['object']['object']['id'], + ap_create_id=request_json['object']['id'], + ap_announce_id=request_json['id'], + type=constants.POST_TYPE_ARTICLE + ) + if 'source' in request_json['object']['object'] and \ + request_json['object']['object']['source']['mediaType'] == 'text/markdown': + post.body = request_json['object']['object']['source']['content'] + post.body_html = markdown_to_html(post.body) + elif 'content' in request_json['object']['object']: + post.body_html = allowlist_html(request_json['object']['object']['content']) + post.body = html_to_markdown(post.body_html) + if 'attachment' in request_json['object']['object'] and \ + len(request_json['object']['object']['attachment']) > 0 and \ + 'type' in request_json['object']['object']['attachment'][0]: + if request_json['object']['object']['attachment'][0]['type'] == 'Link': + post.url = request_json['object']['object']['attachment'][0]['href'] + if is_image_url(post.url): + post.type = POST_TYPE_IMAGE + else: + post.type = POST_TYPE_LINK + domain = domain_from_url(post.url) + if not domain.banned: + post.domain_id = domain.id + else: + post = None + activity_log.exception_message = domain.name + ' is blocked by admin' + if 'image' in request_json['object']['object']: + image = File(source_url=request_json['object']['object']['image']['url']) + db.session.add(image) + post.image = image + + if post is not None: + db.session.add(post) + community.post_count += 1 + db.session.commit() + else: + post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to) + if post_id or parent_comment_id or root_id: post_reply = PostReply(user_id=user.id, community_id=community.id, post_id=post_id, parent_id=parent_comment_id, root_id=root_id, nsfw=community.nsfw, nsfl=community.nsfl, - up_votes=1, - score=instance_weight(user.ap_domain), - ap_id=request_json['object']['id'], - ap_create_id=request_json['id'], - ap_announce_id=None) - if 'source' in request_json['object'] and \ - request_json['object']['source'][ - 'mediaType'] == 'text/markdown': - post_reply.body = request_json['object']['source']['content'] + ap_id=request_json['object']['object']['id'], + ap_create_id=request_json['object']['id'], + ap_announce_id=request_json['id']) + if 'source' in request_json['object']['object'] and \ + request_json['object']['object']['source']['mediaType'] == 'text/markdown': + post_reply.body = request_json['object']['object']['source']['content'] post_reply.body_html = markdown_to_html(post_reply.body) - elif 'content' in request_json['object']: + elif 'content' in request_json['object']['object']: post_reply.body_html = allowlist_html( - request_json['object']['content']) + request_json['object']['object']['content']) post_reply.body = html_to_markdown(post_reply.body_html) if post_reply is not None: post = Post.query.get(post_id) if post.comments_enabled: db.session.add(post_reply) - post.reply_count += 1 community.post_reply_count += 1 - community.last_active = post.last_active = utcnow() + community.last_active = utcnow() + post.last_active = utcnow() + post.reply_count += 1 activity_log.result = 'success' db.session.commit() - vote = PostReplyVote(user_id=user.id, author_id=post_reply.user_id, - post_reply_id=post_reply.id, - effect=instance_weight(user.ap_domain)) - db.session.add(vote) else: activity_log.exception_message = 'Comments disabled' - else: - activity_log.exception_message = 'Unacceptable type (kbin): ' + object_type - - # Announce is new content and votes, mastodon style (?) - if request_json['type'] == 'Announce': - if request_json['object']['type'] == 'Create': - activity_log.activity_type = request_json['object']['type'] - user_ap_id = request_json['object']['object']['attributedTo'] - community_ap_id = request_json['object']['audience'] - community = find_actor_or_create(community_ap_id) - user = find_actor_or_create(user_ap_id) - if (user and not user.is_local()) and community: - user.last_seen = community.last_active = g.site.last_active = utcnow() - object_type = request_json['object']['object']['type'] - new_content_types = ['Page', 'Article', 'Link', 'Note'] - if object_type in new_content_types: # create a new post - in_reply_to = request_json['object']['object']['inReplyTo'] if 'inReplyTo' in \ - request_json['object']['object'] else None - - if not in_reply_to: - post = Post(user_id=user.id, community_id=community.id, - title=request_json['object']['object']['name'], - comments_enabled=request_json['object']['object']['commentsEnabled'], - sticky=request_json['object']['object']['stickied'] if 'stickied' in request_json['object']['object'] else False, - nsfw=request_json['object']['object']['sensitive'], - nsfl=request_json['object']['object']['nsfl'] if 'nsfl' in request_json['object']['object'] else False, - ap_id=request_json['object']['object']['id'], - ap_create_id=request_json['object']['id'], - ap_announce_id=request_json['id'], - type=constants.POST_TYPE_ARTICLE - ) - if 'source' in request_json['object']['object'] and \ - request_json['object']['object']['source']['mediaType'] == 'text/markdown': - post.body = request_json['object']['object']['source']['content'] - post.body_html = markdown_to_html(post.body) - elif 'content' in request_json['object']['object']: - post.body_html = allowlist_html(request_json['object']['object']['content']) - post.body = html_to_markdown(post.body_html) - if 'attachment' in request_json['object']['object'] and \ - len(request_json['object']['object']['attachment']) > 0 and \ - 'type' in request_json['object']['object']['attachment'][0]: - if request_json['object']['object']['attachment'][0]['type'] == 'Link': - post.url = request_json['object']['object']['attachment'][0]['href'] - if is_image_url(post.url): - post.type = POST_TYPE_IMAGE - else: - post.type = POST_TYPE_LINK - domain = domain_from_url(post.url) - if not domain.banned: - post.domain_id = domain.id - else: - post = None - activity_log.exception_message = domain.name + ' is blocked by admin' - activity_log.result = 'failure' - if 'image' in request_json['object']['object']: - image = File(source_url=request_json['object']['object']['image']['url']) - db.session.add(image) - post.image = image - - if post is not None: - db.session.add(post) - community.post_count += 1 - db.session.commit() - else: - post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to) - if post_id or parent_comment_id or root_id: - post_reply = PostReply(user_id=user.id, community_id=community.id, - post_id=post_id, parent_id=parent_comment_id, - root_id=root_id, - nsfw=community.nsfw, - nsfl=community.nsfl, - ap_id=request_json['object']['object']['id'], - ap_create_id=request_json['object']['id'], - ap_announce_id=request_json['id']) - if 'source' in request_json['object']['object'] and \ - request_json['object']['object']['source'][ - 'mediaType'] == 'text/markdown': - post_reply.body = request_json['object']['object']['source']['content'] - post_reply.body_html = markdown_to_html(post_reply.body) - elif 'content' in request_json['object']['object']: - post_reply.body_html = allowlist_html( - request_json['object']['object']['content']) - post_reply.body = html_to_markdown(post_reply.body_html) - - if post_reply is not None: - post = Post.query.get(post_id) - if post.comments_enabled: - db.session.add(post_reply) - community.post_reply_count += 1 - community.last_active = utcnow() - post.last_active = utcnow() - post.reply_count += 1 - activity_log.result = 'success' - db.session.commit() - else: - activity_log.exception_message = 'Comments disabled' - else: - activity_log.exception_message = 'Parent not found' else: - activity_log.exception_message = 'Unacceptable type: ' + object_type + activity_log.exception_message = 'Parent not found' + else: + activity_log.exception_message = 'Unacceptable type: ' + object_type - elif request_json['object']['type'] == 'Like': - activity_log.activity_type = request_json['object']['type'] - user_ap_id = request_json['object']['actor'] - liked_ap_id = request_json['object']['object'] - user = find_actor_or_create(user_ap_id) - if user and not user.is_local(): - liked = find_liked_object(liked_ap_id) - # insert into voted table - if liked is None: - activity_log.exception_message = 'Liked object not found' - elif liked is not None and isinstance(liked, Post): - upvote_post(liked, user) - activity_log.result = 'success' - elif liked is not None and isinstance(liked, PostReply): - upvote_post_reply(liked, user) - activity_log.result = 'success' - else: - activity_log.exception_message = 'Could not detect type of like' - if activity_log.result == 'success': - ... # todo: recalculate 'hotness' of liked post/reply - # todo: if vote was on content in local community, federate the vote out to followers - elif request_json['object']['type'] == 'Dislike': - activity_log.activity_type = request_json['object']['type'] - if g.site.enable_downvotes 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) - if user and not user.is_local(): - disliked = find_liked_object(liked_ap_id) - # insert into voted table - if disliked is None: - activity_log.exception_message = 'Liked object not found' - elif disliked is not None and isinstance(disliked, Post): - downvote_post(disliked, user) - activity_log.result = 'success' - elif disliked is not None and isinstance(disliked, PostReply): - downvote_post_reply(disliked, user) - activity_log.result = 'success' - else: - activity_log.exception_message = 'Could not detect type of like' - if activity_log.result == 'success': - ... # todo: recalculate 'hotness' of liked post/reply - # 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 - elif request_json['type'] == 'Follow': # Follow is when someone wants to join a community - user_ap_id = request_json['actor'] - community_ap_id = request_json['object'] - follow_id = request_json['id'] + elif request_json['object']['type'] == 'Like': + activity_log.activity_type = request_json['object']['type'] + user_ap_id = request_json['object']['actor'] + liked_ap_id = request_json['object']['object'] + user = find_actor_or_create(user_ap_id) + if user and not user.is_local(): + liked = find_liked_object(liked_ap_id) + # insert into voted table + if liked is None: + activity_log.exception_message = 'Liked object not found' + elif liked is not None and isinstance(liked, Post): + upvote_post(liked, user) + activity_log.result = 'success' + elif liked is not None and isinstance(liked, PostReply): + upvote_post_reply(liked, user) + activity_log.result = 'success' + else: + activity_log.exception_message = 'Could not detect type of like' + if activity_log.result == 'success': + ... # todo: recalculate 'hotness' of liked post/reply + # todo: if vote was on content in local community, federate the vote out to followers + elif request_json['object']['type'] == 'Dislike': + activity_log.activity_type = request_json['object']['type'] + if site.enable_downvotes 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) - community = find_actor_or_create(community_ap_id) - if user is not None and community is not None: - # check if user is banned from this community - banned = CommunityBan.query.filter_by(user_id=user.id, - community_id=community.id).first() - if banned is None: - user.last_seen = utcnow() - if community_membership(user, community) != SUBSCRIPTION_MEMBER: - member = CommunityMember(user_id=user.id, community_id=community.id) - db.session.add(member) - db.session.commit() - cache.delete_memoized(community_membership, user, community) - # send accept message to acknowledge the follow - accept = { - "@context": default_context(), - "actor": community.ap_profile_id, - "to": [ - user.ap_profile_id - ], - "object": { - "actor": user.ap_profile_id, - "to": None, - "object": community.ap_profile_id, - "type": "Follow", - "id": follow_id - }, - "type": "Accept", - "id": f"https://{current_app.config['SERVER_NAME']}/activities/accept/" + gibberish(32) - } - try: - HttpSignature.signed_request(user.ap_inbox_url, accept, community.private_key, - f"https://{current_app.config['SERVER_NAME']}/c/{community.name}#main-key") - except Exception as e: - accept_log = ActivityPubLog(direction='out', activity_json=json.dumps(accept), - result='failure', activity_id=accept['id'], - exception_message='could not send Accept' + str(e)) - db.session.add(accept_log) - db.session.commit() - return '' + if user and not user.is_local(): + disliked = find_liked_object(liked_ap_id) + # insert into voted table + if disliked is None: + activity_log.exception_message = 'Liked object not found' + elif disliked is not None and isinstance(disliked, Post): + downvote_post(disliked, user) + activity_log.result = 'success' + elif disliked is not None and isinstance(disliked, PostReply): + downvote_post_reply(disliked, user) activity_log.result = 'success' else: - activity_log.exception_message = 'user is banned from this community' - # Accept: remote server is accepting our previous follow request - elif request_json['type'] == 'Accept': - if request_json['object']['type'] == 'Follow': - community_ap_id = request_json['actor'] - user_ap_id = request_json['object']['actor'] - user = find_actor_or_create(user_ap_id) - community = find_actor_or_create(community_ap_id) - if user and community: - join_request = CommunityJoinRequest.query.filter_by(user_id=user.id, - community_id=community.id).first() - if join_request: - member = CommunityMember(user_id=user.id, community_id=community.id) - db.session.add(member) - community.subscriptions_count += 1 - db.session.commit() - activity_log.result = 'success' - cache.delete_memoized(community_membership, user, community) + activity_log.exception_message = 'Could not detect type of like' + if activity_log.result == 'success': + ... # todo: recalculate 'hotness' of liked post/reply + # todo: if vote was on content in local community, federate the vote out to followers - elif request_json['type'] == 'Undo': - if request_json['object']['type'] == 'Follow': # Unsubscribe from a community - community_ap_id = request_json['object']['object'] - user_ap_id = request_json['object']['actor'] - user = find_actor_or_create(user_ap_id) - community = find_actor_or_create(community_ap_id) - if user and community: - user.last_seen = utcnow() - member = CommunityMember.query.filter_by(user_id=user.id, community_id=community.id).first() - join_request = CommunityJoinRequest.query.filter_by(user_id=user.id, - community_id=community.id).first() - if member: - db.session.delete(member) - if join_request: - db.session.delete(join_request) - db.session.commit() - activity_log.result = 'success' - elif request_json['object']['type'] == 'Like': # Undoing an upvote or downvote - activity_log.activity_type = request_json['object']['type'] - user_ap_id = request_json['actor'] - user = find_actor_or_create(user_ap_id) - post = None - comment = None - target_ap_id = request_json['object']['object'] - if '/comment/' in target_ap_id: - comment = PostReply.query.filter_by(ap_id=target_ap_id).first() - if '/post/' in target_ap_id: - post = Post.query.filter_by(ap_id=target_ap_id).first() - if (user and not user.is_local()) and post: - user.last_seen = utcnow() - existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first() - if existing_vote: - 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'?! - post.down_votes -= 1 - else: - post.up_votes -= 1 - post.score -= existing_vote.effect - db.session.delete(existing_vote) - activity_log.result = 'success' - if (user and not user.is_local()) and comment: - existing_vote = PostReplyVote.query.filter_by(user_id=user.id, post_reply_id=comment.id).first() - if existing_vote: - comment.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'?! - comment.down_votes -= 1 - else: - comment.up_votes -= 1 - comment.score -= existing_vote.effect - db.session.delete(existing_vote) - activity_log.result = 'success' + # 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 + user_ap_id = request_json['actor'] + community_ap_id = request_json['object'] + follow_id = request_json['id'] + user = find_actor_or_create(user_ap_id) + community = find_actor_or_create(community_ap_id) + if user is not None and community is not None: + # check if user is banned from this community + banned = CommunityBan.query.filter_by(user_id=user.id, community_id=community.id).first() + if banned is None: + user.last_seen = utcnow() + if community_membership(user, community) != SUBSCRIPTION_MEMBER: + member = CommunityMember(user_id=user.id, community_id=community.id) + db.session.add(member) + db.session.commit() + cache.delete_memoized(community_membership, user, community) + # send accept message to acknowledge the follow + accept = { + "@context": default_context(), + "actor": community.ap_profile_id, + "to": [ + user.ap_profile_id + ], + "object": { + "actor": user.ap_profile_id, + "to": None, + "object": community.ap_profile_id, + "type": "Follow", + "id": follow_id + }, + "type": "Accept", + "id": f"https://{current_app.config['SERVER_NAME']}/activities/accept/" + gibberish(32) + } + try: + HttpSignature.signed_request(user.ap_inbox_url, accept, community.private_key, + f"https://{current_app.config['SERVER_NAME']}/c/{community.name}#main-key") + except Exception as e: + accept_log = ActivityPubLog(direction='out', activity_json=json.dumps(accept), + result='failure', activity_id=accept['id'], + exception_message='could not send Accept' + str(e)) + db.session.add(accept_log) + db.session.commit() + return '' + activity_log.result = 'success' + else: + activity_log.exception_message = 'user is banned from this community' + # Accept: remote server is accepting our previous follow request + elif request_json['type'] == 'Accept': + if request_json['object']['type'] == 'Follow': + community_ap_id = request_json['actor'] + user_ap_id = request_json['object']['actor'] + user = find_actor_or_create(user_ap_id) + community = find_actor_or_create(community_ap_id) + if user and community: + join_request = CommunityJoinRequest.query.filter_by(user_id=user.id, community_id=community.id).first() + if join_request: + member = CommunityMember(user_id=user.id, community_id=community.id) + db.session.add(member) + community.subscriptions_count += 1 + db.session.commit() + activity_log.result = 'success' + cache.delete_memoized(community_membership, user, community) - elif request_json['object']['type'] == 'Dislike': # Undoing a downvote - probably unused - activity_log.activity_type = request_json['object']['type'] - user_ap_id = request_json['actor'] - user = find_actor_or_create(user_ap_id) - post = None - comment = None - target_ap_id = request_json['object']['object'] - if '/comment/' in target_ap_id: - comment = PostReply.query.filter_by(ap_id=target_ap_id).first() - if '/post/' in target_ap_id: - post = Post.query.filter_by(ap_id=target_ap_id).first() - if (user and not user.is_local()) and post: - existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first() - if existing_vote: - post.author.reputation -= existing_vote.effect - post.down_votes -= 1 - post.score -= existing_vote.effect - db.session.delete(existing_vote) - activity_log.result = 'success' - if (user and not user.is_local()) and comment: - existing_vote = PostReplyVote.query.filter_by(user_id=user.id, - post_reply_id=comment.id).first() - if existing_vote: - comment.author.reputation -= existing_vote.effect - comment.down_votes -= 1 - comment.score -= existing_vote.effect - db.session.delete(existing_vote) - activity_log.result = 'success' - - elif request_json['type'] == 'Update': - if request_json['object']['type'] == 'Page': # Editing a post - post = Post.query.filter_by(ap_id=request_json['object']['id']).first() - if post: - if 'source' in request_json['object'] and \ - request_json['object']['source']['mediaType'] == 'text/markdown': - post.body = request_json['object']['source']['content'] - post.body_html = markdown_to_html(post.body) - elif 'content' in request_json['object']: - post.body_html = allowlist_html(request_json['object']['content']) - post.body = html_to_markdown(post.body_html) - post.edited_at = utcnow() - db.session.commit() - activity_log.result = 'success' - elif request_json['object']['type'] == 'Note': # Editing a reply - reply = PostReply.query.filter_by(ap_id=request_json['object']['id']).first() - if reply: - if 'source' in request_json['object'] and \ - request_json['object']['source']['mediaType'] == 'text/markdown': - reply.body = request_json['object']['source']['content'] - reply.body_html = markdown_to_html(reply.body) - elif 'content' in request_json['object']: - reply.body_html = allowlist_html(request_json['object']['content']) - reply.body = html_to_markdown(reply.body_html) - reply.edited_at = utcnow() - db.session.commit() - activity_log.result = 'success' - elif request_json['type'] == 'Delete': - if isinstance(request_json['object'], str): - ap_id = request_json['object'] # lemmy - else: - ap_id = request_json['object']['id'] # kbin - post = Post.query.filter_by(ap_id=ap_id).first() - if post: - post.delete_dependencies() - db.session.delete(post) - else: - reply = PostReply.query.filter_by(ap_id=ap_id).first() - if reply: - reply.body_html = '
deleted
' - reply.body = 'deleted' + elif request_json['type'] == 'Undo': + if request_json['object']['type'] == 'Follow': # Unsubscribe from a community + community_ap_id = request_json['object']['object'] + user_ap_id = request_json['object']['actor'] + user = find_actor_or_create(user_ap_id) + community = find_actor_or_create(community_ap_id) + if user and community: + user.last_seen = utcnow() + member = CommunityMember.query.filter_by(user_id=user.id, community_id=community.id).first() + join_request = CommunityJoinRequest.query.filter_by(user_id=user.id, community_id=community.id).first() + if member: + db.session.delete(member) + if join_request: + db.session.delete(join_request) db.session.commit() activity_log.result = 'success' - elif request_json['type'] == 'Like': # Upvote - activity_log.activity_type = request_json['type'] - user_ap_id = request_json['actor'] - user = find_actor_or_create(user_ap_id) - target_ap_id = request_json['object'] - post = None - comment = None - if '/comment/' in target_ap_id: - comment = PostReply.query.filter_by(ap_id=target_ap_id).first() - if '/post/' in target_ap_id: - post = Post.query.filter_by(ap_id=target_ap_id).first() - if (user and not user.is_local()) and post: - upvote_post(post, user) - activity_log.result = 'success' - elif (user and not user.is_local()) and comment: - upvote_post_reply(comment, user) - activity_log.result = 'success' - - elif request_json['type'] == 'Dislike': # Downvote - if get_setting('allow_dislike', True) is False: - activity_log.exception_message = 'Dislike ignored because of allow_dislike setting' - else: - activity_log.activity_type = request_json['type'] - user_ap_id = request_json['actor'] - user = find_actor_or_create(user_ap_id) - target_ap_id = request_json['object'] - post = None - comment = None - if '/comment/' in target_ap_id: - comment = PostReply.query.filter_by(ap_id=target_ap_id).first() - if '/post/' in target_ap_id: - post = Post.query.filter_by(ap_id=target_ap_id).first() - if (user and not user.is_local()) and comment: - downvote_post_reply(comment, user) - activity_log.result = 'success' - elif (user and not user.is_local()) and post: - downvote_post(post, user) - activity_log.result = 'success' + elif request_json['object']['type'] == 'Like': # Undoing an upvote or downvote + activity_log.activity_type = request_json['object']['type'] + user_ap_id = request_json['actor'] + user = find_actor_or_create(user_ap_id) + post = None + comment = None + target_ap_id = request_json['object']['object'] + if '/comment/' in target_ap_id: + comment = PostReply.query.filter_by(ap_id=target_ap_id).first() + if '/post/' in target_ap_id: + post = Post.query.filter_by(ap_id=target_ap_id).first() + if (user and not user.is_local()) and post: + user.last_seen = utcnow() + existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first() + if existing_vote: + 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'?! + post.down_votes -= 1 else: - activity_log.exception_message = 'Could not find user or content for vote' - # Flush the caches of any major object that was created. To be sure. - if 'user' in vars() and user is not None: - user.flush_cache() - # if 'community' in vars() and community is not None: - # community.flush_cache() - if 'post' in vars() and post is not None: - post.flush_cache() - else: - activity_log.exception_message = 'Instance banned' - else: - activity_log.exception_message = 'Could not verify signature' - else: - activity_log.exception_message = 'Actor could not be found: ' + request_json['actor'] + post.up_votes -= 1 + post.score -= existing_vote.effect + db.session.delete(existing_vote) + activity_log.result = 'success' + if (user and not user.is_local()) and comment: + existing_vote = PostReplyVote.query.filter_by(user_id=user.id, post_reply_id=comment.id).first() + if existing_vote: + comment.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'?! + comment.down_votes -= 1 + else: + comment.up_votes -= 1 + comment.score -= existing_vote.effect + db.session.delete(existing_vote) + activity_log.result = 'success' - if activity_log.exception_message is not None: - activity_log.result = 'failure' - db.session.add(activity_log) - db.session.commit() - return '' + elif request_json['object']['type'] == 'Dislike': # Undoing a downvote - probably unused + activity_log.activity_type = request_json['object']['type'] + user_ap_id = request_json['actor'] + user = find_actor_or_create(user_ap_id) + post = None + comment = None + target_ap_id = request_json['object']['object'] + if '/comment/' in target_ap_id: + comment = PostReply.query.filter_by(ap_id=target_ap_id).first() + if '/post/' in target_ap_id: + post = Post.query.filter_by(ap_id=target_ap_id).first() + if (user and not user.is_local()) and post: + existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first() + if existing_vote: + post.author.reputation -= existing_vote.effect + post.down_votes -= 1 + post.score -= existing_vote.effect + db.session.delete(existing_vote) + activity_log.result = 'success' + if (user and not user.is_local()) and comment: + existing_vote = PostReplyVote.query.filter_by(user_id=user.id, + post_reply_id=comment.id).first() + if existing_vote: + comment.author.reputation -= existing_vote.effect + comment.down_votes -= 1 + comment.score -= existing_vote.effect + db.session.delete(existing_vote) + activity_log.result = 'success' + + elif request_json['type'] == 'Update': + if request_json['object']['type'] == 'Page': # Editing a post + post = Post.query.filter_by(ap_id=request_json['object']['id']).first() + if post: + if 'source' in request_json['object'] and request_json['object']['source']['mediaType'] == 'text/markdown': + post.body = request_json['object']['source']['content'] + post.body_html = markdown_to_html(post.body) + elif 'content' in request_json['object']: + post.body_html = allowlist_html(request_json['object']['content']) + post.body = html_to_markdown(post.body_html) + post.edited_at = utcnow() + db.session.commit() + activity_log.result = 'success' + elif request_json['object']['type'] == 'Note': # Editing a reply + reply = PostReply.query.filter_by(ap_id=request_json['object']['id']).first() + if reply: + if 'source' in request_json['object'] and request_json['object']['source']['mediaType'] == 'text/markdown': + reply.body = request_json['object']['source']['content'] + reply.body_html = markdown_to_html(reply.body) + elif 'content' in request_json['object']: + reply.body_html = allowlist_html(request_json['object']['content']) + reply.body = html_to_markdown(reply.body_html) + reply.edited_at = utcnow() + db.session.commit() + activity_log.result = 'success' + elif request_json['type'] == 'Delete': + if isinstance(request_json['object'], str): + ap_id = request_json['object'] # lemmy + else: + ap_id = request_json['object']['id'] # kbin + post = Post.query.filter_by(ap_id=ap_id).first() + if post: + post.delete_dependencies() + db.session.delete(post) + else: + reply = PostReply.query.filter_by(ap_id=ap_id).first() + if reply: + reply.body_html = 'deleted
' + reply.body = 'deleted' + db.session.commit() + activity_log.result = 'success' + elif request_json['type'] == 'Like': # Upvote + activity_log.activity_type = request_json['type'] + user_ap_id = request_json['actor'] + user = find_actor_or_create(user_ap_id) + target_ap_id = request_json['object'] + post = None + comment = None + if '/comment/' in target_ap_id: + comment = PostReply.query.filter_by(ap_id=target_ap_id).first() + if '/post/' in target_ap_id: + post = Post.query.filter_by(ap_id=target_ap_id).first() + if (user and not user.is_local()) and post: + upvote_post(post, user) + activity_log.result = 'success' + elif (user and not user.is_local()) and comment: + upvote_post_reply(comment, user) + activity_log.result = 'success' + + elif request_json['type'] == 'Dislike': # Downvote + if get_setting('allow_dislike', True) is False: + activity_log.exception_message = 'Dislike ignored because of allow_dislike setting' + else: + activity_log.activity_type = request_json['type'] + user_ap_id = request_json['actor'] + user = find_actor_or_create(user_ap_id) + target_ap_id = request_json['object'] + post = None + comment = None + if '/comment/' in target_ap_id: + comment = PostReply.query.filter_by(ap_id=target_ap_id).first() + if '/post/' in target_ap_id: + post = Post.query.filter_by(ap_id=target_ap_id).first() + if (user and not user.is_local()) and comment: + downvote_post_reply(comment, user) + activity_log.result = 'success' + elif (user and not user.is_local()) and post: + downvote_post(post, user) + activity_log.result = 'success' + else: + activity_log.exception_message = 'Could not find user or content for vote' + # Flush the caches of any major object that was created. To be sure. + if 'user' in vars() and user is not None: + user.flush_cache() + # if 'community' in vars() and community is not None: + # community.flush_cache() + if 'post' in vars() and post is not None: + post.flush_cache() + else: + activity_log.exception_message = 'Instance banned' + + if activity_log.exception_message is not None: + activity_log.result = 'failure' + db.session.commit() @bp.route('/c/{{ community.description|safe }}
-{{ community.rules|safe }}
+{{ community.description_html|safe }}
+{{ community.rules_html|safe }}
{% if len(mods) > 0 and not community.private_mods %}{{ post.community.description|safe }}
-{{ post.community.rules|safe }}
+{{ post.community.description_html|safe }}
+{{ post.community.rules_html|safe }}
{% if len(mods) > 0 and not post.community.private_mods %}