diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 6de85220..e48019ca 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -549,7 +549,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address): user.last_seen = community.last_active = site.last_active = utcnow() object_type = request_json['object']['type'] - new_content_types = ['Page', 'Article', 'Link', 'Note'] + new_content_types = ['Page', 'Article', 'Link', 'Note', 'Question'] 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: diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 596e5a76..a1313736 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -13,7 +13,7 @@ from sqlalchemy import text, func from app import db, cache, constants, celery from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow, \ PostVote, PostReplyVote, ActivityPubLog, Notification, Site, CommunityMember, InstanceRole, Report, Conversation, \ - Language, Tag + Language, Tag, Poll, PollChoice from app.activitypub.signature import signed_get_request import time import base64 @@ -179,7 +179,25 @@ def post_to_activity(post: Post, community: Community): if post.image_id is not None: activity_data["object"]["object"]["image"] = {"url": post.image.view_url(), "type": "Image"} if post.image.alt_text: - activity_data["object"]["object"]["image"]['altText'] = post.image.alt_text + activity_data["object"]["object"]["image"]['name'] = post.image.alt_text + if post.type == POST_TYPE_POLL: + poll = Poll.query.filter_by(post_id=post.id).first() + activity_data["object"]["object"]['type'] = 'Question' + mode = 'oneOf' if poll.mode == 'single' else 'anyOf' + choices = [] + for choice in PollChoice.query.filter_by(post_id=post.id).order_by(PollChoice.sort_order).all(): + choices.append({ + "type": "Note", + "name": choice.choice_text, + "replies": { + "type": "Collection", + "totalItems": choice.num_votes + } + }) + activity_data["object"]["object"][mode] = choices + activity_data["object"]["object"]['endTime'] = ap_datetime(poll.end_poll) + activity_data["object"]["object"]['votersCount'] = poll.total_votes() + return activity_data @@ -1554,6 +1572,21 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json activity_log.result = 'success' db.session.commit() + # Polls need to be processed quite late because they need a post_id to refer to + if request_json['object']['type'] == 'Question': + post.type = POST_TYPE_POLL + mode = 'single' + if 'anyOf' in request_json['object']: + mode = 'multiple' + poll = Poll(post_id=post.id, end_poll=request_json['object']['endTime'], mode=mode, local_only=False) + db.session.add(poll) + i = 1 + for choice_ap in request_json['object']['oneOf' if mode == 'single' else 'anyOf']: + new_choice = PollChoice(post_id=post.id, choice_text=choice_ap['name'], sort_order=i) + db.session.add(new_choice) + i += 1 + db.session.commit() + if post.image_id: make_image_sizes(post.image_id, 150, 512, 'posts') # the 512 sized image is for masonry view diff --git a/app/community/routes.py b/app/community/routes.py index 8e212bd2..8e27d3d6 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -21,11 +21,11 @@ from app.community.util import search_for_community, actor_to_community, \ delete_post_from_community, delete_post_reply_from_community, community_in_list from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \ SUBSCRIPTION_PENDING, SUBSCRIPTION_MODERATOR, REPORT_STATE_NEW, REPORT_STATE_ESCALATED, REPORT_STATE_RESOLVED, \ - REPORT_STATE_DISCARDED, POST_TYPE_VIDEO, NOTIF_COMMUNITY + REPORT_STATE_DISCARDED, POST_TYPE_VIDEO, NOTIF_COMMUNITY, POST_TYPE_POLL from app.inoculation import inoculation from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \ File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog, Topic, Conversation, PostReply, \ - NotificationSubscription, UserFollower, Instance, Language + NotificationSubscription, UserFollower, Instance, Language, Poll, PollChoice from app.community import bp from app.user.utils import search_for_user from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \ @@ -805,6 +805,7 @@ def add_poll_post(actor): abort(401) post = Post(user_id=current_user.id, community_id=form.communities.data, instance_id=1) save_post(form, post, 'poll') + poll = Poll.query.filter_by(post_id=post.id).first() community.post_count += 1 community.last_active = g.site.last_active = utcnow() db.session.commit() @@ -815,9 +816,9 @@ def add_poll_post(actor): notify_about_post(post) - if not community.local_only: + if not community.local_only and not poll.local_only: federate_post(community, post) - federate_post_to_user_followers(post) + federate_post_to_user_followers(post) return redirect(f"/post/{post.id}") else: @@ -897,6 +898,23 @@ def federate_post(community, post): if post.type == POST_TYPE_IMAGE: page['attachment'] = [{'type': 'Link', 'href': post.image.source_url}] # source_url is always a https link, no need for .replace() as done above + + if post.type == POST_TYPE_POLL: + poll = Poll.query.filter_by(post_id=post.id).first() + page['type'] = 'Question' + page['endTime'] = ap_datetime(poll.end_poll) + page['votersCount'] = 0 + choices = [] + for choice in PollChoice.query.filter_by(post_id=post.id).all(): + choices.append({ + "type": "Note", + "name": choice.choice_text, + "replies": { + "type": "Collection", + "totalItems": 0 + } + }) + page['oneOf' if poll.mode == 'single' else 'anyOf'] = choices if not community.is_local(): # this is a remote community - send the post to the instance that hosts it success = post_request(community.ap_inbox_url, create, current_user.private_key, current_user.ap_profile_id + '#main-key') @@ -989,8 +1007,25 @@ def federate_post_to_user_followers(post): if post.body_html: note['content'] = note['content'] + '
' + post.body_html + '
' + if post.type == POST_TYPE_POLL: + poll = Poll.query.filter_by(post_id=post.id).first() + note['type'] = 'Question' + note['endTime'] = ap_datetime(poll.end_poll) + note['votersCount'] = 0 + choices = [] + for choice in PollChoice.query.filter_by(post_id=post.id).all(): + choices.append({ + "type": "Note", + "name": choice.choice_text, + "replies": { + "type": "Collection", + "totalItems": 0 + } + }) + note['oneOf' if poll.mode == 'single' else 'anyOf'] = choices + instances = Instance.query.join(User, User.instance_id == Instance.id).join(UserFollower, UserFollower.remote_user_id == User.id) - instances = instances.filter(UserFollower.local_user_id == post.user_id) + instances = instances.filter(UserFollower.local_user_id == post.user_id).filter(Instance.gone_forever == False) for i in instances: post_request(i.inbox, create, current_user.private_key, current_user.ap_profile_id + '#main-key') diff --git a/app/post/routes.py b/app/post/routes.py index c5e13fed..8f9c3fb1 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -1233,6 +1233,8 @@ def post_edit_poll_post(post_id: int): ) else: abort(401) + + def federate_post_update(post): page_json = { 'type': 'Page', @@ -1288,6 +1290,23 @@ def federate_post_update(post): if post.type == POST_TYPE_IMAGE: page_json['attachment'] = [{'type': 'Link', 'href': post.image.source_url}] # source_url is always a https link, no need for .replace() as done above + if post.type == POST_TYPE_POLL: + poll = Poll.query.filter_by(post_id=post.id).first() + page_json['type'] = 'Question' + page_json['endTime'] = ap_datetime(poll.end_poll) + page_json['votersCount'] = 0 + choices = [] + for choice in PollChoice.query.filter_by(post_id=post.id).all(): + choices.append({ + "type": "Note", + "name": choice.choice_text, + "replies": { + "type": "Collection", + "totalItems": 0 + } + }) + page_json['oneOf' if poll.mode == 'single' else 'anyOf'] = choices + if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it success = post_request(post.community.ap_inbox_url, update_json, current_user.private_key, current_user.ap_profile_id + '#main-key') @@ -1369,6 +1388,22 @@ def federate_post_edit_to_user_followers(post): note['attachment'] = [{'type': 'Document', 'url': post.image.source_url, 'name': post.image.alt_text}] else: note['attachment'] = [{'type': 'Document', 'url': post.image.source_url}] + elif post.type == POST_TYPE_POLL: + poll = Poll.query.filter_by(post_id=post.id).first() + note['type'] = 'Question' + note['endTime'] = ap_datetime(poll.end_poll) + note['votersCount'] = 0 + choices = [] + for choice in PollChoice.query.filter_by(post_id=post.id).all(): + choices.append({ + "type": "Note", + "name": choice.choice_text, + "replies": { + "type": "Collection", + "totalItems": 0 + } + }) + note['oneOf' if poll.mode == 'single' else 'anyOf'] = choices if post.body_html: note['content'] = note['content'] + '' + post.body_html + '
'