pyfedi/app/shared/tasks/pages.py

266 lines
9.9 KiB
Python

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 '<p>' + post.title + '</p>',
'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'] = '<p><a href=' + post.url + '>' + post.title + '</a></p>'
else:
note['content'] = '<p>' + post.title + '</p>'
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')