From 19771a5ea4584e32ab149b37e5bfcc47808bb122 Mon Sep 17 00:00:00 2001 From: freamon Date: Fri, 17 Jan 2025 17:20:15 +0000 Subject: [PATCH] API: merge up make_post and edit_post --- app/api/alpha/utils/post.py | 20 +- app/shared/post.py | 487 +++++++++++++++--------------------- app/shared/tasks/pages.py | 116 +++++---- 3 files changed, 272 insertions(+), 351 deletions(-) diff --git a/app/api/alpha/utils/post.py b/app/api/alpha/utils/post.py index a69f368e..12510c8d 100644 --- a/app/api/alpha/utils/post.py +++ b/app/api/alpha/utils/post.py @@ -166,14 +166,8 @@ def post_post(auth, data): if language_id < 2: language_id = 2 - if not url: - type = 'discussion' - elif is_image_url(url): - type = 'image' - elif is_video_url(url): - type = 'video' - else: - type = 'link' + # change when Polls are supported + type = None input = {'title': title, 'body': body, 'url': url, 'nsfw': nsfw, 'language_id': language_id, 'notify_author': True} community = Community.query.filter_by(id=community_id).one() @@ -198,14 +192,8 @@ def put_post(auth, data): if language_id < 2: language_id = 2 - if not url: - type = POST_TYPE_ARTICLE - elif is_image_url(url): - type = POST_TYPE_IMAGE - elif is_video_url(url): - type = POST_TYPE_VIDEO - else: - type = POST_TYPE_LINK + # change when Polls are supported + type = None input = {'title': title, 'body': body, 'url': url, 'nsfw': nsfw, 'language_id': language_id, 'notify_author': True} post = Post.query.filter_by(id=post_id).one() diff --git a/app/shared/post.py b/app/shared/post.py index 73430a6a..7804fb12 100644 --- a/app/shared/post.py +++ b/app/shared/post.py @@ -1,11 +1,13 @@ -from app import db +from app import db, cache +from app.activitypub.util import make_image_sizes from app.constants import * -from app.community.util import tags_from_string, tags_from_string_old, end_poll_date -from app.models import File, Language, NotificationSubscription, Poll, PollChoice, Post, PostBookmark, utcnow +from app.community.util import tags_from_string_old, end_poll_date +from app.models import File, Language, NotificationSubscription, Poll, PollChoice, Post, PostBookmark, PostVote, utcnow from app.shared.tasks import task_selector from app.utils import render_template, authorise_api_user, shorten_string, gibberish, ensure_directory_exists, \ piefed_markdown_to_lemmy_markdown, markdown_to_html, remove_tracking_from_link, domain_from_url, \ - opengraph_parse, url_to_thumbnail_file, blocked_phrases + opengraph_parse, url_to_thumbnail_file, can_create_post, is_video_hosting_site, recently_upvoted_posts, \ + is_image_url, is_video_hosting_site from flask import abort, flash, redirect, request, url_for, current_app, g from flask_babel import _ @@ -144,6 +146,76 @@ def make_post(input, community, type, src, auth=None, uploaded_file=None): #if not basic_rate_limit_check(user): # raise Exception('rate_limited') title = input['title'] + url = input['url'] + language_id = input['language_id'] + else: + user = current_user + title = input.title.data.strip() + if type == POST_TYPE_LINK: + url = input.link_url.data.strip() + elif type == POST_TYPE_VIDEO: + url = input.video_url.data.strip() + else: + url = None + language_id = input.language_id.data + + # taking values from one JSON to put in another JSON to put in a DB to put in another JSON feels bad + # instead, make_post shares code with edit_post + # ideally, a similar change could be made for incoming activitypub (create_post() and update_post_from_activity() could share code) + # once this happens, and post.new() just does the minimum before being passed off to an update function, post.new() can be used here again. + + if not can_create_post(user, community): + raise Exception('You are not permitted to make posts in this community') + + if url: + domain = domain_from_url(url) + if domain: + if domain.banned or domain.name.endswith('.pages.dev'): + raise Exception(domain.name + ' is blocked by admin') + + if uploaded_file and uploaded_file.filename != '': + # check if this is an allowed type of file + allowed_extensions = ['.gif', '.jpg', '.jpeg', '.png', '.webp', '.heic', '.mpo', '.avif', '.svg'] + file_ext = os.path.splitext(uploaded_file.filename)[1] + if file_ext.lower() not in allowed_extensions: + raise Exception('filetype not allowed') + + post = Post(user_id=user.id, community_id=community.id, instance_id=user.instance_id, posted_at=utcnow(), + ap_id=gibberish(), title=title, language_id=language_id) + db.session.add(post) + db.session.commit() + + post.up_votes = 1 + if user.reputation > 100: + post.up_votes += 1 + effect = user.instance.vote_weight + post.score = post.up_votes * effect + post.ranking = post.post_ranking(post.score, post.posted_at) + cache.delete_memoized(recently_upvoted_posts, user.id) + + community.post_count += 1 + community.last_active = g.site.last_active = utcnow() + user.post_count += 1 + + post.ap_id = f"https://{current_app.config['SERVER_NAME']}/post/{post.id}" + vote = PostVote(user_id=user.id, post_id=post.id, author_id=user.id, effect=1) + db.session.add(vote) + db.session.commit() + + post = edit_post(input, post, type, src, user, auth, uploaded_file, from_scratch=True) + + if src == SRC_API: + return user.id, post + else: + return post + + +# 'from_scratch == True' means that it's not really a user edit, we're just re-using code for make_post() +def edit_post(input, post, type, src, user=None, auth=None, uploaded_file=None, from_scratch=False): + if src == SRC_API: + if not user: + user = authorise_api_user(auth, return_type='model') + title = input['title'] body = input['body'] url = input['url'] nsfw = input['nsfw'] @@ -151,46 +223,82 @@ def make_post(input, community, type, src, auth=None, uploaded_file=None): language_id = input['language_id'] tags = [] else: - user = current_user - title = input.title.data + if not user: + user = current_user + title = input.title.data.strip() body = input.body.data - url = input.link_url.data + if type == POST_TYPE_LINK: + url = input.link_url.data.strip() + elif type == POST_TYPE_VIDEO: + url = input.video_url.data.strip() + elif type == POST_TYPE_IMAGE and not from_scratch: + url = post.url + else: + url = None nsfw = input.nsfw.data notify_author = input.notify_author.data language_id = input.language_id.data - tags = tags_from_string(input.tags.data) + tags = tags_from_string_old(input.tags.data) - language = Language.query.filter_by(id=language_id).one() + post.indexable = user.indexable + post.sticky = False if src == SRC_API else input.sticky.data + post.nsfw = nsfw + post.nsfl = False if src == SRC_API else input.nsfl.data + post.notify_author = notify_author + post.language_id = language_id + user.language_id = language_id + post.title = title + post.body = piefed_markdown_to_lemmy_markdown(body) + post.body_html = markdown_to_html(post.body) + post.type = type - request_json = { - 'id': None, - 'object': { - 'name': title, - 'type': 'Page', - 'stickied': False if src == SRC_API else input.sticky.data, - 'sensitive': nsfw, - 'nsfl': False if src == SRC_API else input.nsfl.data, - 'id': gibberish(), # this will be updated once we have the post.id - 'mediaType': 'text/markdown', - 'content': body, - 'tag': tags, - 'language': {'identifier': language.code, 'name': language.name} - } - } + url_changed = False - if type == 'link' or (type == 'image' and src == SRC_API): - request_json['object']['attachment'] = [{'type': 'Link', 'href': url}] - elif type == 'image' and src == SRC_WEB and uploaded_file and uploaded_file.filename != '': + if not from_scratch: + # Remove any subscription that currently exists + existing_notification = NotificationSubscription.query.filter(NotificationSubscription.entity_id == post.id, + NotificationSubscription.user_id == user.id, + NotificationSubscription.type == NOTIF_POST).first() + if existing_notification: + db.session.delete(existing_notification) + + # Remove any poll votes that currently exists + # Partially because it's easier to code but also to stop malicious alterations to polls after people have already voted + db.session.execute(text('DELETE FROM "poll_choice_vote" WHERE post_id = :post_id'), {'post_id': post.id}) + db.session.execute(text('DELETE FROM "poll_choice" WHERE post_id = :post_id'), {'post_id': post.id}) + + # Remove any old images, and set url_changed + if url != post.url or uploaded_file: + url_changed = True + if post.image_id: + remove_file = File.query.get(post.image_id) + if remove_file: + remove_file.delete_from_disk() + post.image_id = None + domain = domain_from_url(post.url) + if domain: + domain.post_count -= 1 + + # remove any old tags + db.session.execute(text('DELETE FROM "post_tag" WHERE post_id = :post_id'), {'post_id': post.id}) + + post.edited_at = utcnow() + + db.session.commit() + + if uploaded_file and uploaded_file.filename != '': # check if this is an allowed type of file + allowed_extensions = ['.gif', '.jpg', '.jpeg', '.png', '.webp', '.heic', '.mpo', '.avif', '.svg'] file_ext = os.path.splitext(uploaded_file.filename)[1] if file_ext.lower() not in allowed_extensions: - abort(400, description="Invalid image type.") + raise Exception('filetype not allowed') new_filename = gibberish(15) # set up the storage directory directory = 'app/static/media/posts/' + new_filename[0:2] + '/' + new_filename[2:4] ensure_directory_exists(directory) + # save the file final_place = os.path.join(directory, new_filename + file_ext) uploaded_file.seek(0) uploaded_file.save(final_place) @@ -202,267 +310,74 @@ def make_post(input, community, type, src, auth=None, uploaded_file=None): Image.MAX_IMAGE_PIXELS = 89478485 - # resize if necessary + # limit full sized version to 2000px if not final_place.endswith('.svg'): img = Image.open(final_place) if '.' + img.format.lower() in allowed_extensions: img = ImageOps.exif_transpose(img) - # limit full sized version to 2000px img.thumbnail((2000, 2000)) img.save(final_place) - request_json['object']['attachment'] = [{ - 'type': 'Image', - 'url': f'https://{current_app.config["SERVER_NAME"]}/{final_place.replace("app/", "")}', - 'name': input.image_alt_text.data, - 'file_path': final_place - }] - elif type == 'video': - request_json['object']['attachment'] = [{'type': 'Document', 'url': url}] - elif type == 'poll': - request_json['object']['type'] = 'Question' - choices = [input.choice_1, input.choice_2, input.choice_3, input.choice_4, input.choice_5, - input.choice_6, input.choice_7, input.choice_8, input.choice_9, input.choice_10] - key = 'oneOf' if input.mode.data == 'single' else 'anyOf' - request_json['object'][key] = [] - for choice in choices: - choice_data = choice.data.strip() - if choice_data: - request_json['object'][key].append({'name': choice_data}) - request_json['object']['endTime'] = end_poll_date(input.finish_in.data) - - post = Post.new(user, community, request_json) - post.ap_id = f"https://{current_app.config['SERVER_NAME']}/post/{post.id}" - db.session.commit() - - task_selector('make_post', user_id=user.id, post_id=post.id) - - if src == SRC_API: - return user.id, post - else: - return post - - -def edit_post(input, post, type, src, auth=None, uploaded_file=None): - if src == SRC_API: - user = authorise_api_user(auth, return_type='model') - title = input['title'] - body = input['body'] - url = input['url'] - nsfw = input['nsfw'] - notify_author = input['notify_author'] - language_id = input['language_id'] - tags = [] - else: - user = current_user - title = input.title.data - body = input.body.data - url = input.link_url.data - nsfw = input.nsfw.data - notify_author = input.notify_author.data - language_id = input.language_id.data - tags = tags_from_string(input.tags.data) - - post.indexable = user.indexable - post.sticky = False if src == SRC_API else input.sticky.data - post.nsfw = nsfw - post.nsfl = False if src == SRC_API else input.nsfl.data - post.notify_author = notify_author - post.language_id = language_id - user.language_id = language_id - post.title = title.strip() - post.body = piefed_markdown_to_lemmy_markdown(body) - post.body_html = markdown_to_html(post.body) - - allowed_extensions = ['.gif', '.jpg', '.jpeg', '.png', '.webp', '.heic', '.mpo', '.avif', '.svg'] - - if not type or type == POST_TYPE_ARTICLE: - post.type = POST_TYPE_ARTICLE - elif type == POST_TYPE_LINK: - url_changed = post.id is None or url != post.url # post.id ? - post.url = remove_tracking_from_link(url.strip()) - post.type = POST_TYPE_LINK - domain = domain_from_url(url) - domain.post_count += 1 - post.domain = domain - - if url_changed: - if post.image_id: - remove_file = File.query.get(post.image_id) - remove_file.delete_from_disk() - post.image_id = None - - if post.url.endswith('.mp4') or post.url.endswith('.webm'): - post.type = POST_TYPE_VIDEO - file = File(source_url=url) # make_image_sizes() will take care of turning this into a still image - post.image = file - db.session.add(file) + url = f"https://{current_app.config['SERVER_NAME']}/{final_place.replace('app/', '')}" else: - unused, file_extension = os.path.splitext(url) - # this url is a link to an image - turn it into a image post - if file_extension.lower() in allowed_extensions: - file = File(source_url=url) - post.image = file - db.session.add(file) - post.type = POST_TYPE_IMAGE - else: - # check opengraph tags on the page and make a thumbnail if an image is available in the og:image meta tag - if not post.type == POST_TYPE_VIDEO: - tn_url = url - if tn_url[:32] == 'https://www.youtube.com/watch?v=': - tn_url = 'https://youtu.be/' + tn_url[32:43] # better chance of thumbnail from youtu.be than youtube.com - opengraph = opengraph_parse(tn_url) - if opengraph and (opengraph.get('og:image', '') != '' or opengraph.get('og:image:url', '') != ''): - filename = opengraph.get('og:image') or opengraph.get('og:image:url') - if not filename.startswith('/'): - file = url_to_thumbnail_file(filename) - if file: - file.alt_text = shorten_string(opengraph.get('og:title'), 295) - post.image = file - db.session.add(file) + raise Exception('filetype not allowed') - elif type == POST_TYPE_IMAGE: - post.type = POST_TYPE_IMAGE - # If we are uploading new file in the place of existing one just remove the old one - if post.image_id is not None and src == SRC_WEB: - post.image.delete_from_disk() - image_id = post.image_id - post.image_id = None - db.session.add(post) + if url and (from_scratch or url_changed): + domain = domain_from_url(url) + if domain: + if domain.banned or domain.name.endswith('.pages.dev'): + raise Exception(domain.name + ' is blocked by admin') + post.domain = domain + domain.post_count += 1 + already_notified = set() # often admins and mods are the same people - avoid notifying them twice + if domain.notify_mods: + for community_member in post.community.moderators(): + if community_member.is_local(): + notify = Notification(title='Suspicious content', url=post.ap_id, user_id=community_member.user_id, author_id=user.id) + db.session.add(notify) + already_notified.add(community_member.user_id) + if domain.notify_admins: + for admin in Site.admins(): + if admin.id not in already_notified: + notify = Notification(title='Suspicious content', url=post.ap_id, user_id=admin.id, author_id=user.id) + db.session.add(notify) + if is_image_url(url): + file = File(source_url=url) + if uploaded_file and type == POST_TYPE_IMAGE: + # change this line when uploaded_file is supported in API + file.alt_text = input.image_alt_text.data if input.image_alt_text.data else title + db.session.add(file) db.session.commit() - File.query.filter_by(id=image_id).delete() + post.image_id = file.id + make_image_sizes(post.image_id, 170, 512, 'posts', post.community.low_quality) + post.url = url + post.type = POST_TYPE_IMAGE + else: + opengraph = opengraph_parse(url) + if opengraph and (opengraph.get('og:image', '') != '' or opengraph.get('og:image:url', '') != ''): + filename = opengraph.get('og:image') or opengraph.get('og:image:url') + if not filename.startswith('/'): + file = url_to_thumbnail_file(filename) + if file: + file.alt_text = shorten_string(opengraph.get('og:title'), 295) + post.image = file + db.session.add(file) - if uploaded_file and uploaded_file.filename != '' and src == SRC_WEB: - if post.image_id: - remove_file = File.query.get(post.image_id) - remove_file.delete_from_disk() - post.image_id = None + post.url = remove_tracking_from_link(url) - # check if this is an allowed type of file - file_ext = os.path.splitext(uploaded_file.filename)[1] - if file_ext.lower() not in allowed_extensions: - abort(400) - new_filename = gibberish(15) - - # set up the storage directory - directory = 'app/static/media/posts/' + new_filename[0:2] + '/' + new_filename[2:4] - ensure_directory_exists(directory) - - # save the file - final_place = os.path.join(directory, new_filename + file_ext) - final_place_medium = os.path.join(directory, new_filename + '_medium.webp') - final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp') - uploaded_file.seek(0) - uploaded_file.save(final_place) - - if file_ext.lower() == '.heic': - register_heif_opener() - - Image.MAX_IMAGE_PIXELS = 89478485 - - # resize if necessary - img = Image.open(final_place) - if '.' + img.format.lower() in allowed_extensions: - img = ImageOps.exif_transpose(img) - - # limit full sized version to 2000px - img_width = img.width - img_height = img.height - img.thumbnail((2000, 2000)) - img.save(final_place) - - # medium sized version - img.thumbnail((512, 512)) - img.save(final_place_medium, format="WebP", quality=93) - - # save a third, smaller, version as a thumbnail - img.thumbnail((170, 170)) - img.save(final_place_thumbnail, format="WebP", quality=93) - thumbnail_width = img.width - thumbnail_height = img.height - - alt_text = input.image_alt_text.data if input.image_alt_text.data else title - - file = File(file_path=final_place_medium, file_name=new_filename + file_ext, alt_text=alt_text, - width=img_width, height=img_height, thumbnail_width=thumbnail_width, - thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail, - source_url=final_place.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/")) - db.session.add(file) - db.session.commit() - post.image_id = file.id - - elif type == POST_TYPE_VIDEO: - url = url.strip() - url_changed = post.id is None or url != post.url - post.url = remove_tracking_from_link(url) - post.type = POST_TYPE_VIDEO - domain = domain_from_url(url) - domain.post_count += 1 - post.domain = domain - - if url_changed: - if post.image_id: - remove_file = File.query.get(post.image_id) - remove_file.delete_from_disk() - post.image_id = None - if url.endswith('.mp4') or url('.webm'): - file = File(source_url=url) # make_image_sizes() will take care of turning this into a still image - post.image = file - db.session.add(file) + if url.endswith('.mp4') or url.endswith('.webm') or is_video_hosting_site(url): + post.type = POST_TYPE_VIDEO else: - # check opengraph tags on the page and make a thumbnail if an image is available in the og:image meta tag - tn_url = url - if tn_url[:32] == 'https://www.youtube.com/watch?v=': - tn_url = 'https://youtu.be/' + tn_url[32:43] # better chance of thumbnail from youtu.be than youtube.com - opengraph = opengraph_parse(tn_url) - if opengraph and (opengraph.get('og:image', '') != '' or opengraph.get('og:image:url', '') != ''): - filename = opengraph.get('og:image') or opengraph.get('og:image:url') - if not filename.startswith('/'): - file = url_to_thumbnail_file(filename) - if file: - file.alt_text = shorten_string(opengraph.get('og:title'), 295) - post.image = file - db.session.add(file) + post.type = POST_TYPE_LINK - elif type == POST_TYPE_POLL: - post.body = title + '\n' + body if title not in body else body - post.body_html = markdown_to_html(post.body) - post.type = POST_TYPE_POLL - else: - raise Exception('invalid post type') + post.calculate_cross_posts(url_changed=url_changed) - if post.id is None: - if user.reputation > 100: - post.up_votes = 1 - post.score = 1 - if user.reputation < -100: - post.score = -1 - post.ranking = post.post_ranking(post.score, utcnow()) - - # Filter by phrase - blocked_phrases_list = blocked_phrases() - for blocked_phrase in blocked_phrases_list: - if blocked_phrase in post.title: - abort(401) - return - if post.body: - for blocked_phrase in blocked_phrases_list: - if blocked_phrase in post.body: - abort(401) - return - - db.session.add(post) - else: - db.session.execute(text('DELETE FROM "post_tag" WHERE post_id = :post_id'), {'post_id': post.id}) - post.tags = tags - db.session.commit() - - # Save poll choices. NB this will delete all votes whenever a poll is edited. Partially because it's easier to code but also to stop malicious alterations to polls after people have already voted + federate = True if type == POST_TYPE_POLL: - db.session.execute(text('DELETE FROM "poll_choice_vote" WHERE post_id = :post_id'), {'post_id': post.id}) - db.session.execute(text('DELETE FROM "poll_choice" WHERE post_id = :post_id'), {'post_id': post.id}) + post.type = POST_TYPE_POLL for i in range(1, 10): + # change this line when polls are supported in API choice_data = getattr(input, f"choice_{i}").data.strip() if choice_data != '': db.session.add(PollChoice(post_id=post.id, choice_text=choice_data, sort_order=i)) @@ -478,24 +393,30 @@ def edit_post(input, post, type, src, auth=None, uploaded_file=None): poll.latest_vote = None db.session.commit() - # Notify author about replies - # Remove any subscription that currently exists - existing_notification = NotificationSubscription.query.filter(NotificationSubscription.entity_id == post.id, - NotificationSubscription.user_id == user.id, - NotificationSubscription.type == NOTIF_POST).first() - if existing_notification: - db.session.delete(existing_notification) + if poll.local_only: + federate = False + + # add tags + post.tags = tags # Add subscription if necessary if notify_author: - new_notification = NotificationSubscription(name=post.title, user_id=user.id, entity_id=post.id, - type=NOTIF_POST) + new_notification = NotificationSubscription(name=post.title, user_id=user.id, entity_id=post.id, type=NOTIF_POST) db.session.add(new_notification) - g.site.last_active = utcnow() db.session.commit() - task_selector('edit_post', user_id=user.id, post_id=post.id) + if from_scratch: + if federate: + task_selector('make_post', user_id=user.id, post_id=post.id) + elif federate: + task_selector('edit_post', user_id=user.id, post_id=post.id) if src == SRC_API: - return user.id, post + if from_scratch: + return post + else: + return user.id, post + elif from_scratch: + return post + diff --git a/app/shared/tasks/pages.py b/app/shared/tasks/pages.py index 0499d59e..e88701db 100644 --- a/app/shared/tasks/pages.py +++ b/app/shared/tasks/pages.py @@ -1,7 +1,7 @@ from app import celery, db from app.activitypub.signature import default_context, post_request from app.constants import POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, POST_TYPE_VIDEO, POST_TYPE_POLL, MICROBLOG_APPS -from app.models import CommunityBan, Instance, Notification, Post, User, UserFollower, utcnow +from app.models import CommunityBan, Instance, Notification, Poll, PollChoice, Post, User, UserFollower, utcnow from app.user.utils import search_for_user from app.utils import gibberish, instance_banned, ap_datetime @@ -113,7 +113,13 @@ def send_post(user_id, post_id, edit=False): db.session.add(notification) db.session.commit() - if community.local_only or not community.instance.online(): + if not community.instance.online(): + return + + # local_only communities can also be used to send activity to User Followers + # return now though, if there aren't any + followers = UserFollower.query.filter_by(local_user_id=post.user_id).first() + if not followers and community.local_only: return banned = CommunityBan.query.filter_by(user_id=user_id, community_id=community.id).first() @@ -146,7 +152,7 @@ def send_post(user_id, post_id, edit=False): 'cc': cc, 'tag': tag, 'audience': community.public_url(), - 'content': post.body_html if post.type != POST_TYPE_POLL else '

' + post.title + '

', + 'content': post.body_html if post.type != POST_TYPE_POLL else '

' + post.title + '

' + post.body_html, 'mediaType': 'text/html', 'source': source, 'published': ap_datetime(post.posted_at), @@ -161,7 +167,7 @@ def send_post(user_id, post_id, edit=False): if post.type != POST_TYPE_POLL: page['name'] = post.title if edit: - page['updated']: ap_datetime(utcnow()) + page['updated'] = ap_datetime(utcnow()) if post.image_id: image_url = '' if post.image.source_url: @@ -174,10 +180,10 @@ def send_post(user_id, post_id, edit=False): if post.type == POST_TYPE_POLL: poll = Poll.query.filter_by(post_id=post.id).first() page['endTime'] = ap_datetime(poll.end_poll) - page['votersCount'] = 0 + page['votersCount'] = poll.total_votes() if edit else 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}}) + choices.append({'type': 'Note', 'name': choice.choice_text, 'replies': {'type': 'Collection', 'totalItems': choice.num_votes if edit else 0}}) page['oneOf' if poll.mode == 'single' else 'anyOf'] = choices activity = 'create' if not edit else 'update' @@ -191,64 +197,70 @@ def send_post(user_id, post_id, edit=False): 'to': to, 'cc': cc, '@context': default_context(), + 'audience': community.public_url(), 'tag': tag } domains_sent_to = [current_app.config['SERVER_NAME']] - # send the activity as an Announce if the community is local, or as a Create if not - if community.is_local(): - del create['@context'] + # if the community is local, and remote instance is something like Lemmy, Announce the activity + # if the community is local, and remote instance is something like Mastodon, Announce creates (so the community Boosts it), but send updates directly and from the user + # Announce of Poll doesn't work for Mastodon, so don't add domain to domains_sent_to, so they receive it if they're also following the User or they get Mentioned + # if the community is remote, send activity directly + if not community.local_only: + if community.is_local(): + del create['@context'] - announce_id = f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}" - actor = community.public_url() - cc = [community.ap_followers_url] - group_announce = { - 'id': announce_id, - 'type': 'Announce', - 'actor': community.public_url(), - 'object': create, - 'to': to, - 'cc': cc, - '@context': default_context() - } - microblog_announce = { - 'id': announce_id, - 'type': 'Announce', - 'actor': community.public_url(), - 'object': post.ap_id, - 'to': to, - 'cc': cc, - '@context': default_context() - } - for instance in community.following_instances(): - if instance.inbox and instance.online() and not user.has_blocked_instance(instance.id) and not instance_banned(instance.domain): - if instance.software in MICROBLOG_APPS: - post_request(instance.inbox, microblog_announce, community.private_key, community.public_url() + '#main-key') - else: - post_request(instance.inbox, group_announce, community.private_key, community.public_url() + '#main-key') - domains_sent_to.append(instance.domain) - else: - post_request(community.ap_inbox_url, create, user.private_key, user.public_url() + '#main-key') - domains_sent_to.append(community.instance.domain) + announce_id = f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}" + actor = community.public_url() + cc = [community.ap_followers_url] + group_announce = { + 'id': announce_id, + 'type': 'Announce', + 'actor': community.public_url(), + 'object': create, + 'to': to, + 'cc': cc, + '@context': default_context() + } + microblog_announce = { + 'id': announce_id, + 'type': 'Announce', + 'actor': community.public_url(), + 'object': post.ap_id, + 'to': to, + 'cc': cc, + '@context': default_context() + } + for instance in community.following_instances(): + if instance.inbox and instance.online() and not user.has_blocked_instance(instance.id) and not instance_banned(instance.domain): + if instance.software in MICROBLOG_APPS: + if activity == 'create': + post_request(instance.inbox, microblog_announce, community.private_key, community.public_url() + '#main-key') + else: + post_request(instance.inbox, create, user.private_key, user.public_url() + '#main-key') + else: + post_request(instance.inbox, group_announce, community.private_key, community.public_url() + '#main-key') + if post.type < POST_TYPE_POLL: + domains_sent_to.append(instance.domain) + else: + post_request(community.ap_inbox_url, create, user.private_key, user.public_url() + '#main-key') + domains_sent_to.append(community.instance.domain) - # send copy of the Create to anyone else Mentioned in post, but not on an instance that's already sent to. - if '@context' not in create: - create['@context'] = default_context() - for recipient in recipients: - if recipient.instance.domain not in domains_sent_to: - post_request(recipient.instance.inbox, create, user.private_key, user.public_url() + '#main-key') - domains_sent_to.append(recipient.instance.domain) + # send copy of the Create to anyone else Mentioned in post, but not on an instance that's already sent to. + if '@context' not in create: + create['@context'] = default_context() + for recipient in recipients: + if recipient.instance.domain not in domains_sent_to: + post_request(recipient.instance.inbox, create, user.private_key, user.public_url() + '#main-key') + domains_sent_to.append(recipient.instance.domain) # send amended copy of the Create to anyone who is following the User, but hasn't already received something - followers = UserFollower.query.filter_by(local_user_id=post.user_id) - if not followers: - return - if 'name' in page: del page['name'] note = page - note['type'] = 'Note' + if note['type'] == 'Page': + note['type'] = 'Note' if post.type == POST_TYPE_LINK or post.type == POST_TYPE_VIDEO: note['content'] = '

' + post.title + '

' else: