diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index 5d274d63..22eadfcf 100644 --- a/app/api/alpha/routes.py +++ b/app/api/alpha/routes.py @@ -1,7 +1,7 @@ from app.api.alpha import bp from app.api.alpha.utils import get_site, post_site_block, \ get_search, \ - get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe, \ + get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe, post_post, \ get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, post_reply, put_reply, post_reply_delete, post_reply_report, \ get_community_list, get_community, post_community_follow, post_community_block, \ get_user, post_user_block @@ -9,12 +9,14 @@ from app.shared.auth import log_user_in from flask import current_app, jsonify, request +def enable_api(): + return True if current_app.debug else False # Site @bp.route('/api/alpha/site', methods=['GET']) def get_alpha_site(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') return jsonify(get_site(auth)) @@ -24,8 +26,8 @@ def get_alpha_site(): @bp.route('/api/alpha/site/block', methods=['POST']) def get_alpha_site_block(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -37,8 +39,8 @@ def get_alpha_site_block(): # Misc @bp.route('/api/alpha/search', methods=['GET']) def get_alpha_search(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.args.to_dict() or None @@ -50,8 +52,8 @@ def get_alpha_search(): # Community @bp.route('/api/alpha/community', methods=['GET']) def get_alpha_community(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.args.to_dict() or None @@ -62,8 +64,8 @@ def get_alpha_community(): @bp.route('/api/alpha/community/list', methods=['GET']) def get_alpha_community_list(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.args.to_dict() or None @@ -74,8 +76,8 @@ def get_alpha_community_list(): @bp.route('/api/alpha/community/follow', methods=['POST']) def post_alpha_community_follow(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -86,8 +88,8 @@ def post_alpha_community_follow(): @bp.route('/api/alpha/community/block', methods=['POST']) def post_alpha_community_block(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -99,8 +101,8 @@ def post_alpha_community_block(): # Post @bp.route('/api/alpha/post/list', methods=['GET']) def get_alpha_post_list(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.args.to_dict() or None @@ -111,8 +113,8 @@ def get_alpha_post_list(): @bp.route('/api/alpha/post', methods=['GET']) def get_alpha_post(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.args.to_dict() or None @@ -123,8 +125,8 @@ def get_alpha_post(): @bp.route('/api/alpha/post/like', methods=['POST']) def post_alpha_post_like(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -135,8 +137,8 @@ def post_alpha_post_like(): @bp.route('/api/alpha/post/save', methods=['PUT']) def put_alpha_post_save(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -147,8 +149,8 @@ def put_alpha_post_save(): @bp.route('/api/alpha/post/subscribe', methods=['PUT']) def put_alpha_post_subscribe(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -157,11 +159,23 @@ def put_alpha_post_subscribe(): return jsonify({"error": str(ex)}), 400 +@bp.route('/api/alpha/post', methods=['POST']) +def post_alpha_post(): + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) + try: + auth = request.headers.get('Authorization') + data = request.get_json(force=True) or {} + return jsonify(post_post(auth, data)) + except Exception as ex: + return jsonify({"error": str(ex)}), 400 + + # Reply @bp.route('/api/alpha/comment/list', methods=['GET']) def get_alpha_comment_list(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.args.to_dict() or None @@ -172,8 +186,8 @@ def get_alpha_comment_list(): @bp.route('/api/alpha/comment/like', methods=['POST']) def post_alpha_comment_like(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -184,8 +198,8 @@ def post_alpha_comment_like(): @bp.route('/api/alpha/comment/save', methods=['PUT']) def put_alpha_comment_save(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -196,8 +210,8 @@ def put_alpha_comment_save(): @bp.route('/api/alpha/comment/subscribe', methods=['PUT']) def put_alpha_comment_subscribe(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -208,8 +222,8 @@ def put_alpha_comment_subscribe(): @bp.route('/api/alpha/comment', methods=['POST']) def post_alpha_comment(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -220,8 +234,8 @@ def post_alpha_comment(): @bp.route('/api/alpha/comment', methods=['PUT']) def put_alpha_comment(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -232,8 +246,8 @@ def put_alpha_comment(): @bp.route('/api/alpha/comment/delete', methods=['POST']) def post_alpha_comment_delete(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -244,8 +258,8 @@ def post_alpha_comment_delete(): @bp.route('/api/alpha/comment/report', methods=['POST']) def post_alpha_comment_report(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -257,8 +271,8 @@ def post_alpha_comment_report(): # User @bp.route('/api/alpha/user', methods=['GET']) def get_alpha_user(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.args.to_dict() or None @@ -269,8 +283,8 @@ def get_alpha_user(): @bp.route('/api/alpha/user/login', methods=['POST']) def post_alpha_user_login(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: SRC_API = 3 # would be in app.constants data = request.get_json(force=True) or {} @@ -281,8 +295,8 @@ def post_alpha_user_login(): @bp.route('/api/alpha/user/block', methods=['POST']) def post_alpha_user_block(): - if not current_app.debug: - return jsonify({'error': 'alpha api routes only available in debug mode'}) + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) try: auth = request.headers.get('Authorization') data = request.get_json(force=True) or {} @@ -320,7 +334,6 @@ def alpha_community(): # Post - not yet implemented @bp.route('/api/alpha/post', methods=['PUT']) -@bp.route('/api/alpha/post', methods=['POST']) @bp.route('/api/alpha/post/delete', methods=['POST']) @bp.route('/api/alpha/post/remove', methods=['POST']) @bp.route('/api/alpha/post/lock', methods=['POST']) diff --git a/app/api/alpha/utils/__init__.py b/app/api/alpha/utils/__init__.py index 824050da..e43dd700 100644 --- a/app/api/alpha/utils/__init__.py +++ b/app/api/alpha/utils/__init__.py @@ -1,6 +1,6 @@ from app.api.alpha.utils.site import get_site, post_site_block from app.api.alpha.utils.misc import get_search -from app.api.alpha.utils.post import get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe +from app.api.alpha.utils.post import get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe, post_post from app.api.alpha.utils.reply import get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, post_reply, put_reply, post_reply_delete, post_reply_report from app.api.alpha.utils.community import get_community, get_community_list, post_community_follow, post_community_block from app.api.alpha.utils.user import get_user, post_user_block diff --git a/app/api/alpha/utils/post.py b/app/api/alpha/utils/post.py index 0d9fcf32..fd141057 100644 --- a/app/api/alpha/utils/post.py +++ b/app/api/alpha/utils/post.py @@ -1,9 +1,9 @@ from app import cache from app.api.alpha.views import post_view -from app.api.alpha.utils.validators import required, integer_expected, boolean_expected +from app.api.alpha.utils.validators import required, integer_expected, boolean_expected, string_expected from app.models import Post, Community, CommunityMember, utcnow -from app.shared.post import vote_for_post, bookmark_the_post, remove_the_bookmark_from_post, toggle_post_notification -from app.utils import authorise_api_user, blocked_users, blocked_communities, blocked_instances, community_ids_from_instances +from app.shared.post import vote_for_post, bookmark_the_post, remove_the_bookmark_from_post, toggle_post_notification, make_post +from app.utils import authorise_api_user, blocked_users, blocked_communities, blocked_instances, community_ids_from_instances, is_image_url from datetime import timedelta from sqlalchemy import desc @@ -149,3 +149,34 @@ def put_post_subscribe(auth, data): post_json = post_view(post=post_id, variant=4, user_id=user_id) return post_json + +def post_post(auth, data): + required(['title', 'community_id'], data) + integer_expected(['language_id'], data) + boolean_expected(['nsfw'], data) + string_expected(['string', 'body'], data) + + title = data['title'] + community_id = data['community_id'] + body = data['body'] if 'body' in data else '' + url = data['url'] if 'url' in data else None + nsfw = data['nsfw'] if 'nsfw' in data else False + language_id = data['language_id'] if 'language_id' in data else 2 # FIXME: use site language + if language_id < 2: + language_id = 2 + + if not url: + type = 'discussion' + elif is_image_url(url): + type = 'image' + else: + type = 'link' + + 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() + user_id, post = make_post(input, community, type, SRC_API, auth) + + post_json = post_view(post=post, variant=4, user_id=user_id) + return post_json + + diff --git a/app/shared/post.py b/app/shared/post.py index 3df03a70..31e39d53 100644 --- a/app/shared/post.py +++ b/app/shared/post.py @@ -1,13 +1,18 @@ from app import db from app.constants import * -from app.models import NotificationSubscription, Post, PostBookmark +from app.community.util import tags_from_string +from app.models import Language, NotificationSubscription, Post, PostBookmark from app.shared.tasks import task_selector -from app.utils import render_template, authorise_api_user, shorten_string +from app.utils import render_template, authorise_api_user, shorten_string, gibberish, ensure_directory_exists -from flask import abort, flash, redirect, request, url_for +from flask import abort, flash, redirect, request, url_for, current_app from flask_babel import _ from flask_login import current_user +from pillow_heif import register_heif_opener +from PIL import Image, ImageOps + +import os # would be in app/constants.py SRC_WEB = 1 @@ -127,3 +132,111 @@ def toggle_post_notification(post_id: int, src, auth=None): return user_id else: return render_template('post/_post_notification_toggle.html', post=post) + + +def make_post(input, community, type, src, auth=None, uploaded_file=None): + if src == SRC_API: + user = authorise_api_user(auth, return_type='model') + #if not basic_rate_limit_check(user): + # raise Exception('rate_limited') + 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) + + language = Language.query.filter_by(id=language_id).one() + + 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} + } + } + + 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 != '': + # 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, description="Invalid image type.") + + 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) + + final_place = os.path.join(directory, new_filename + file_ext) + uploaded_file.seek(0) + uploaded_file.save(final_place) + + if file_ext.lower() == '.heic': + register_heif_opener() + if file_ext.lower() == '.avif': + import pillow_avif + + Image.MAX_IMAGE_PIXELS = 89478485 + + # resize if necessary + 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 + + diff --git a/app/shared/tasks/__init__.py b/app/shared/tasks/__init__.py index 3c0a980a..8605a5b0 100644 --- a/app/shared/tasks/__init__.py +++ b/app/shared/tasks/__init__.py @@ -3,6 +3,7 @@ from app.shared.tasks.likes import vote_for_post, vote_for_reply from app.shared.tasks.notes import make_reply, edit_reply from app.shared.tasks.deletes import delete_reply, restore_reply from app.shared.tasks.flags import report_reply +from app.shared.tasks.pages import make_post, edit_post from flask import current_app @@ -17,7 +18,9 @@ def task_selector(task_key, send_async=True, **kwargs): 'edit_reply': edit_reply, 'delete_reply': delete_reply, 'restore_reply': restore_reply, - 'report_reply': report_reply + 'report_reply': report_reply, + 'make_post': make_post, + 'edit_post': edit_post } if current_app.debug: diff --git a/app/shared/tasks/pages.py b/app/shared/tasks/pages.py new file mode 100644 index 00000000..0499d59e --- /dev/null +++ b/app/shared/tasks/pages.py @@ -0,0 +1,266 @@ +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.user.utils import search_for_user +from app.utils import gibberish, instance_banned, ap_datetime + +from flask import current_app +from flask_babel import _ + +import re + + +""" Post JSON format +{ + 'id': + 'type': + 'attributedTo': + 'to': [] + 'cc': [] + 'tag': [] + 'audience': + 'content': + 'mediaType': + 'source': {} + 'published': + 'updated': (inner oject of Update only) + 'language': {} + 'name': (not included in Polls, which are sent out as microblogs) + 'attachment': [] + 'commentsEnabled': + 'sensitive': + 'nsfl': + 'stickied': + 'image': (for posts with thumbnails only) + 'endTime': (last 3 are for polls) + 'votersCount': + 'oneOf' / 'anyOf': +} +""" +""" Create / Update / Announce JSON format +{ + 'id': + 'type': + 'actor': + 'object': + 'to': [] + 'cc': [] + '@context': (outer object only) + 'audience': (not in Announce) + 'tag': [] (not in Announce) +} +""" + + + +@celery.task +def make_post(send_async, user_id, post_id): + send_post(user_id, post_id) + + +@celery.task +def edit_post(send_async, user_id, post_id): + send_post(user_id, post_id, edit=True) + + +def send_post(user_id, post_id, edit=False): + user = User.query.filter_by(id=user_id).one() + post = Post.query.filter_by(id=post_id).one() + community = post.community + + # Find any users Mentioned in post body with @user@instance syntax + recipients = [] + pattern = r"@([a-zA-Z0-9_.-]*)@([a-zA-Z0-9_.-]*)\b" + matches = re.finditer(pattern, post.body) + for match in matches: + recipient = None + if match.group(2) == current_app.config['SERVER_NAME']: + user_name = match.group(1) + if user_name != user.user_name: + try: + recipient = search_for_user(user_name) + except: + pass + else: + ap_id = f"{match.group(1)}@{match.group(2)}" + try: + recipient = search_for_user(ap_id) + except: + pass + if recipient: + add_recipient = True + for existing_recipient in recipients: + if ((not recipient.ap_id and recipient.user_name == existing_recipient.user_name) or + (recipient.ap_id and recipient.ap_id == existing_recipient.ap_id)): + add_recipient = False + break + if add_recipient: + recipients.append(recipient) + + # Notify any local users that have been Mentioned + for recipient in recipients: + if recipient.is_local(): + if edit: + existing_notification = Notification.query.filter(Notification.user_id == recipient.id, Notification.url == f"https://{current_app.config['SERVER_NAME']}/post/{post.id}").first() + else: + existing_notification = None + if not existing_notification: + notification = Notification(user_id=recipient.id, title=_(f"You have been mentioned in post {post.id}"), + url=f"https://{current_app.config['SERVER_NAME']}/post/{post.id}", + author_id=user.id) + recipient.unread_notifications += 1 + db.session.add(notification) + db.session.commit() + + if community.local_only or not community.instance.online(): + return + + banned = CommunityBan.query.filter_by(user_id=user_id, community_id=community.id).first() + if banned: + return + if not community.is_local(): + if user.has_blocked_instance(community.instance.id) or instance_banned(community.instance.domain): + return + + type = 'Question' if post.type == POST_TYPE_POLL else 'Page' + to = [community.public_url(), "https://www.w3.org/ns/activitystreams#Public"] + cc = [] + tag = post.tags_for_activitypub() + for recipient in recipients: + tag.append({'href': recipient.public_url(), 'name': recipient.mention_tag(), 'type': 'Mention'}) + cc.append(recipient.public_url()) + language = {'identifier': post.language_code(), 'name': post.language_name()} + source = {'content': post.body, 'mediaType': 'text/markdown'} + attachment = [] + if post.type == POST_TYPE_LINK or post.type == POST_TYPE_VIDEO: + attachment.append({'href': post.url, 'type': 'Link'}) + elif post.type == POST_TYPE_IMAGE: + attachment.append({'type': 'Image', 'url': post.image.source_url, 'name': post.image.alt_text}) + + page = { + 'id': post.public_url(), + 'type': type, + 'attributedTo': user.public_url(), + 'to': to, + 'cc': cc, + 'tag': tag, + 'audience': community.public_url(), + 'content': post.body_html if post.type != POST_TYPE_POLL else '

' + post.title + '

', + 'mediaType': 'text/html', + 'source': source, + 'published': ap_datetime(post.posted_at), + 'language': language, + 'name': post.title, + 'attachment': attachment, + 'commentsEnabled': post.comments_enabled, + 'sensitive': post.nsfw or post.nsfl, + 'nsfl': post.nsfl, + 'stickied': post.sticky + } + if post.type != POST_TYPE_POLL: + page['name'] = post.title + if edit: + page['updated']: ap_datetime(utcnow()) + if post.image_id: + image_url = '' + if post.image.source_url: + image_url = post.image.source_url + elif post.image.file_path: + image_url = post.image.file_path.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/") + elif post.image.thumbnail_path: + image_url = post.image.thumbnail_path.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/") + page['image'] = {'type': 'Image', 'url': image_url} + 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 + 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 + + activity = 'create' if not edit else 'update' + create_id = f"https://{current_app.config['SERVER_NAME']}/activities/{activity}/{gibberish(15)}" + type = 'Create' if not edit else 'Update' + create = { + 'id': create_id, + 'type': type, + 'actor': user.public_url(), + 'object': page, + 'to': to, + 'cc': cc, + '@context': default_context(), + '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'] + + 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) + + # 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 post.type == POST_TYPE_LINK or post.type == POST_TYPE_VIDEO: + note['content'] = '

' + post.title + '

' + else: + note['content'] = '

' + post.title + '

' + if post.body_html: + note['content'] = note['content'] + post.body_html + note['inReplyTo'] = None + create['object'] = note + + 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).filter(Instance.gone_forever == False) + for instance in instances: + if instance.domain not in domains_sent_to: + post_request(instance.inbox, create, user.private_key, user.public_url() + '#main-key') +