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//outbox', methods=['GET']) diff --git a/app/activitypub/util.py b/app/activitypub/util.py index a83f2794..beb37e92 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -215,7 +215,6 @@ def find_actor_or_create(actor: str) -> Union[User, Community, None]: if 'rel' in links and links['rel'] == 'self': # this contains the URL of the activitypub profile type = links['type'] if 'type' in links else 'application/activity+json' # retrieve the activitypub profile - print('****', links['href']) actor_data = get_request(links['href'], headers={'Accept': type}) # to see the structure of the json contained in actor_data, do a GET to https://lemmy.world/c/technology with header Accept: application/activity+json if actor_data.status_code == 200: @@ -462,7 +461,10 @@ def find_instance_id(server): try: instance_data = get_request(f"https://{server}", headers={'Accept': 'application/activity+json'}) except: - return None + new_instance = Instance(domain=server, software='unknown', created_at=utcnow()) + db.session.add(new_instance) + db.session.commit() + return new_instance.id if instance_data.status_code == 200: try: instance_json = instance_data.json() @@ -487,7 +489,11 @@ def find_instance_id(server): db.session.add(new_instance) db.session.commit() return new_instance.id - return None + else: + new_instance = Instance(domain=server, software='unknown', created_at=utcnow()) + db.session.add(new_instance) + db.session.commit() + return new_instance.id # alter the effect of upvotes based on their instance. Default to 1.0 diff --git a/app/email.py b/app/email.py index 83e256ad..d43bb935 100644 --- a/app/email.py +++ b/app/email.py @@ -1,5 +1,5 @@ from flask import current_app, render_template, escape -from app import db +from app import db, celery from flask_babel import _, lazy_gettext as _l # todo: set the locale based on account_id so that _() works import boto3 from botocore.exceptions import ClientError @@ -9,6 +9,7 @@ AWS_REGION = "ap-southeast-2" CHARSET = "UTF-8" +@celery.task def send_async_email(subject, sender, recipients, text_body, html_body, reply_to): if type(recipients) == str: recipients = [recipients] @@ -62,5 +63,7 @@ def send_async_email(subject, sender, recipients, text_body, html_body, reply_to def send_email(subject, sender, recipients: List[str], text_body, html_body, reply_to=None): - # todo: make async or threaded - send_async_email(subject, sender, recipients, text_body, html_body, reply_to) + if current_app.debug: + send_async_email(subject, sender, recipients, text_body, html_body, reply_to) + else: + send_async_email.delay(subject, sender, recipients, text_body, html_body, reply_to) diff --git a/app/main/routes.py b/app/main/routes.py index 0f402c7e..b75b49c7 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -107,6 +107,7 @@ def verification_warning(): flash(_('Please click the link in your email inbox to verify your account.'), 'warning') +@cache.cached(timeout=6) def activitypub_application(): application_data = { '@context': default_context(), diff --git a/app/static/structure.css b/app/static/structure.css index 572fe847..8f95894d 100644 --- a/app/static/structure.css +++ b/app/static/structure.css @@ -488,6 +488,16 @@ fieldset legend { max-width: 100%; } +.render_username { + display: inline; +} +.render_username a img { + width: 20px; + height: 20px; + border-radius: 50%; + vertical-align: bottom; +} + .comments > .comment { margin-left: 0; border-top: solid 1px #bbb; diff --git a/app/static/structure.scss b/app/static/structure.scss index bdf7957d..103458f5 100644 --- a/app/static/structure.scss +++ b/app/static/structure.scss @@ -198,6 +198,16 @@ nav, etc which are used site-wide */ } } +.render_username { + display: inline; + a img { + width: 20px; + height: 20px; + border-radius: 50%; + vertical-align: bottom; + } +} + .comments > .comment { margin-left: 0; border-top: solid 1px $grey; diff --git a/app/templates/base.html b/app/templates/base.html index 6f1a3084..4c13c381 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,12 +1,18 @@ {% macro render_username(user) %} -{% if user.deleted %} - [deleted] -{% else %} - {{ user.user_name }} - {% if user.created_recently() %} - + + {% if user.deleted %} + [deleted] + {% else %} + {% if user.avatar_id %} + + Avatar + {% endif %} + {{ user.user_name }} + {% if user.created_recently() %} + + {% endif %} {% endif %} -{% endif %} + {% endmacro %} diff --git a/app/templates/community/community.html b/app/templates/community/community.html index 289239a3..18d83eae 100644 --- a/app/templates/community/community.html +++ b/app/templates/community/community.html @@ -84,8 +84,8 @@

{{ _('About community') }}

-

{{ community.description|safe }}

-

{{ community.rules|safe }}

+

{{ community.description_html|safe }}

+

{{ community.rules_html|safe }}

{% if len(mods) > 0 and not community.private_mods %}

Moderators

-

{{ 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 %}

Moderators

    diff --git a/app/utils.py b/app/utils.py index 3cd63112..5f25102e 100644 --- a/app/utils.py +++ b/app/utils.py @@ -42,7 +42,6 @@ def render_template(template_name: str, **context) -> Response: def request_etag_matches(etag): - print(str(request.headers)) if 'If-None-Match' in request.headers: old_etag = request.headers['If-None-Match'] return old_etag == etag diff --git a/celery_worker.default.py b/celery_worker.default.py new file mode 100644 index 00000000..7642911e --- /dev/null +++ b/celery_worker.default.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +import os +from app import celery, create_app + + +app = create_app() +if not app.debug: + os.environ['DATABASE_URL'] = 'postgresql+psycopg2://pyfedi:pyfedi@127.0.0.1/pyfedi' + os.environ['SERVER_NAME'] = 'piefed.ngrok.app' + +app.app_context().push() diff --git a/config.py b/config.py index f6a2d3dc..b3765d8d 100644 --- a/config.py +++ b/config.py @@ -27,4 +27,6 @@ class Config(object): CACHE_DEFAULT_TIMEOUT = 300 CACHE_THRESHOLD = 1000 CACHE_KEY_PREFIX = 'pyfedi' + CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL') or 'redis://localhost:6379/0' + RESULT_BACKEND = os.environ.get('RESULT_BACKEND') or 'redis://localhost:6379/0' SQLALCHEMY_ECHO = False # set to true to see SQL in console diff --git a/dev_notes.txt b/dev_notes.txt new file mode 100644 index 00000000..abe4553a --- /dev/null +++ b/dev_notes.txt @@ -0,0 +1,12 @@ +for celery, run this: + +celery -A celery_worker.celery worker --loglevel=INFO + + +on prod web server, celery is managed by systemd: /etc/default/celeryd and /etc/systemd/system/celeryd.service + +sudo systemctl stop celeryd +sudo systemctl restart celeryd or sudo service celeryd restart + +*** check for celery-related problems by looking in /var/log/celery *** + diff --git a/interests.txt b/interests.txt index 93785953..78e16f47 100644 --- a/interests.txt +++ b/interests.txt @@ -144,7 +144,6 @@ environment https://sh.itjust.works/c/sewingrepairing https://lemmy.world/c/fuckcars https://lemmy.world/c/evs - https://feddit.uk/c/evs https://slrpnk.net/c/solarpunk https://slrpnk.net/c/climate https://slrpnk.net/c/energy diff --git a/requirements.txt b/requirements.txt index 473ad2e2..8606ccd6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,3 +26,5 @@ Pillow pillow-heif opengraph-parse=0.0.6 feedgen==0.9.0 +celery==5.3.6 +redis==5.0.1