2025-01-14 13:54:17 +00:00
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
2025-01-17 17:20:15 +00:00
from app . models import CommunityBan , Instance , Notification , Poll , PollChoice , Post , User , UserFollower , utcnow
2025-01-14 13:54:17 +00:00
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 ' : { }
2025-01-17 22:23:08 +00:00
' inReplyTo ' : ( only added for Mentions and user followers , as Note with inReplyTo : None )
2025-01-14 13:54:17 +00:00
' 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 )
}
"""
@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 ( )
2025-01-17 17:20:15 +00:00
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
2025-01-17 22:23:08 +00:00
followers = UserFollower . query . filter_by ( local_user_id = post . user_id ) . all ( )
2025-01-17 17:20:15 +00:00
if not followers and community . local_only :
2025-01-14 13:54:17 +00:00
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 ( ) ,
2025-01-17 17:20:15 +00:00
' content ' : post . body_html if post . type != POST_TYPE_POLL else ' <p> ' + post . title + ' </p> ' + post . body_html ,
2025-01-14 13:54:17 +00:00
' 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 :
2025-01-17 17:20:15 +00:00
page [ ' updated ' ] = ap_datetime ( utcnow ( ) )
2025-01-14 13:54:17 +00:00
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 )
2025-01-17 17:20:15 +00:00
page [ ' votersCount ' ] = poll . total_votes ( ) if edit else 0
2025-01-14 13:54:17 +00:00
choices = [ ]
for choice in PollChoice . query . filter_by ( post_id = post . id ) . all ( ) :
2025-01-17 17:20:15 +00:00
choices . append ( { ' type ' : ' Note ' , ' name ' : choice . choice_text , ' replies ' : { ' type ' : ' Collection ' , ' totalItems ' : choice . num_votes if edit else 0 } } )
2025-01-14 13:54:17 +00:00
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 ( ) ,
2025-01-17 22:23:08 +00:00
' audience ' : community . public_url ( )
2025-01-14 13:54:17 +00:00
}
domains_sent_to = [ current_app . config [ ' SERVER_NAME ' ] ]
2025-01-17 17:20:15 +00:00
# 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 ' ]
2025-01-14 13:54:17 +00:00
2025-01-17 17:20:15 +00:00
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 )
2025-01-14 13:54:17 +00:00
2025-01-17 22:23:08 +00:00
# amend copy of the Create, for anyone Mentioned in post body or who is following the user, to a format more likely to be understood
if ' @context ' not in create :
create [ ' @context ' ] = default_context ( )
2025-01-14 13:54:17 +00:00
if ' name ' in page :
del page [ ' name ' ]
note = page
2025-01-17 17:20:15 +00:00
if note [ ' type ' ] == ' Page ' :
note [ ' type ' ] = ' Note '
2025-01-14 13:54:17 +00:00
if post . type == POST_TYPE_LINK or post . type == POST_TYPE_VIDEO :
note [ ' content ' ] = ' <p><a href= ' + post . url + ' > ' + post . title + ' </a></p> '
2025-01-17 22:23:08 +00:00
elif post . type != POST_TYPE_POLL :
2025-01-14 13:54:17 +00:00
note [ ' content ' ] = ' <p> ' + post . title + ' </p> '
if post . body_html :
note [ ' content ' ] = note [ ' content ' ] + post . body_html
note [ ' inReplyTo ' ] = None
create [ ' object ' ] = note
2025-01-17 22:23:08 +00:00
if not community . local_only :
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 )
if not followers :
return
2025-01-14 13:54:17 +00:00
2025-01-17 22:23:08 +00:00
# send the amended copy of the Create to anyone who is following the User, but hasn't already received something
for follower in followers :
user_details = User . query . get ( follower . remote_user_id )
if user_details :
create [ ' cc ' ] . append ( user_details . public_url ( ) )
2025-01-14 13:54:17 +00:00
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 ' )