2024-04-04 21:36:03 +13:00
from urllib . parse import urlparse , parse_qs
2024-01-09 20:44:08 +13:00
from flask_login import current_user
2023-12-30 13:23:12 +13:00
2023-12-24 13:28:41 +13:00
from app import db , constants , cache , celery
2023-08-05 21:24:10 +12:00
from app . activitypub import bp
2024-02-14 10:16:49 +13:00
from flask import request , current_app , abort , jsonify , json , g , url_for , redirect , make_response
2023-08-29 22:01:06 +12:00
2024-05-04 21:16:55 +01:00
from app . activitypub . signature import HttpSignature , post_request , VerificationError , default_context
2023-08-29 22:01:06 +12:00
from app . community . routes import show_community
2024-01-03 16:29:58 +13:00
from app . community . util import send_to_remote_instance
2023-12-09 22:14:16 +13:00
from app . post . routes import continue_discussion , show_post
2023-10-07 21:32:19 +13:00
from app . user . routes import show_profile
2023-12-03 22:41:15 +13:00
from app . constants import POST_TYPE_LINK , POST_TYPE_IMAGE , SUBSCRIPTION_MEMBER
2023-09-10 20:20:53 +12:00
from app . models import User , Community , CommunityJoinRequest , CommunityMember , CommunityBan , ActivityPubLog , Post , \
2024-02-17 20:05:57 +13:00
PostReply , Instance , PostVote , PostReplyVote , File , AllowedInstances , BannedInstances , utcnow , Site , Notification , \
2024-05-08 19:28:49 +12:00
ChatMessage , Conversation , UserFollower , UserBlock
2023-08-10 21:13:37 +12:00
from app . activitypub . util import public_key , users_total , active_half_year , active_month , local_posts , local_comments , \
2024-05-04 21:16:55 +01:00
post_to_activity , find_actor_or_create , instance_blocked , find_reply_parent , find_liked_object , \
2023-12-22 14:05:39 +13:00
lemmy_site_data , instance_weight , is_activitypub_request , downvote_post_reply , downvote_post , upvote_post_reply , \
2024-04-04 21:36:03 +13:00
upvote_post , delete_post_or_comment , community_members , \
2023-12-30 13:23:12 +13:00
user_removed_from_remote_server , create_post , create_post_reply , update_post_reply_from_activity , \
2024-04-06 16:29:47 +13:00
update_post_from_activity , undo_vote , undo_downvote , post_to_page , get_redis_connection , find_reported_object , \
2024-05-09 17:54:30 +12:00
process_report , ensure_domains_match , can_edit , can_delete
2024-03-27 16:15:29 +13:00
from app . utils import gibberish , get_setting , is_image_url , allowlist_html , render_template , \
domain_from_url , markdown_to_html , community_membership , ap_datetime , ip_address , can_downvote , \
2024-03-13 16:40:20 +13:00
can_upvote , can_create_post , awaken_dormant_instance , shorten_string , can_create_post_reply , sha256_digest , \
2024-05-14 19:33:08 +01:00
community_moderators , lemmy_markdown_to_html
2023-10-10 22:25:37 +13:00
import werkzeug . exceptions
2023-08-05 21:24:10 +12:00
2024-04-04 21:36:03 +13:00
@bp.route ( ' /testredis ' )
def testredis_get ( ) :
redis_client = get_redis_connection ( )
redis_client . set ( " cowbell " , " 1 " , ex = 600 )
x = redis_client . get ( ' cowbell ' )
2024-04-04 21:38:26 +13:00
if x is not None :
2024-04-04 21:36:03 +13:00
return " Redis: OK "
else :
return " Redis: FAIL "
2023-08-05 21:24:10 +12:00
@bp.route ( ' /.well-known/webfinger ' )
def webfinger ( ) :
if request . args . get ( ' resource ' ) :
query = request . args . get ( ' resource ' ) # acct:alice@tada.club
if ' acct: ' in query :
actor = query . split ( ' : ' ) [ 1 ] . split ( ' @ ' ) [ 0 ] # alice
elif ' https: ' in query or ' http: ' in query :
actor = query . split ( ' / ' ) [ - 1 ]
else :
return ' Webfinger regex failed to match '
2024-05-04 21:26:39 +01:00
# special case: instance actor
if actor == current_app . config [ ' SERVER_NAME ' ] :
webfinger_data = {
" subject " : f " acct: { actor } @ { current_app . config [ ' SERVER_NAME ' ] } " ,
" aliases " : [ f " https:// { current_app . config [ ' SERVER_NAME ' ] } /actor " ] ,
" links " : [
{
" rel " : " http://webfinger.net/rel/profile-page " ,
" type " : " text/html " ,
" href " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } /about "
} ,
{
" rel " : " self " ,
" type " : " application/activity+json " ,
" href " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } /actor " ,
}
]
}
resp = jsonify ( webfinger_data )
resp . headers . add_header ( ' Access-Control-Allow-Origin ' , ' * ' )
return resp
2023-08-05 21:24:10 +12:00
seperator = ' u '
type = ' Person '
user = User . query . filter_by ( user_name = actor . strip ( ) , deleted = False , banned = False , ap_id = None ) . first ( )
if user is None :
community = Community . query . filter_by ( name = actor . strip ( ) , ap_id = None ) . first ( )
if community is None :
return ' '
seperator = ' c '
type = ' Group '
webfinger_data = {
" subject " : f " acct: { actor } @ { current_app . config [ ' SERVER_NAME ' ] } " ,
" aliases " : [ f " https:// { current_app . config [ ' SERVER_NAME ' ] } / { seperator } / { actor } " ] ,
" links " : [
{
" rel " : " http://webfinger.net/rel/profile-page " ,
" type " : " text/html " ,
" href " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } / { seperator } / { actor } "
} ,
{
" rel " : " self " ,
" type " : " application/activity+json " ,
" href " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } / { seperator } / { actor } " ,
" properties " : {
" https://www.w3.org/ns/activitystreams#type " : type
}
}
]
}
resp = jsonify ( webfinger_data )
resp . headers . add_header ( ' Access-Control-Allow-Origin ' , ' * ' )
return resp
else :
abort ( 404 )
@bp.route ( ' /.well-known/nodeinfo ' )
2023-12-10 15:10:09 +13:00
@cache.cached ( timeout = 600 )
2023-08-05 21:24:10 +12:00
def nodeinfo ( ) :
nodeinfo_data = { " links " : [ { " rel " : " http://nodeinfo.diaspora.software/ns/schema/2.0 " ,
" href " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } /nodeinfo/2.0 " } ] }
return jsonify ( nodeinfo_data )
2024-02-14 10:16:49 +13:00
@bp.route ( ' /.well-known/host-meta ' )
@cache.cached ( timeout = 600 )
def host_meta ( ) :
2024-02-19 15:01:53 +13:00
resp = make_response ( ' <?xml version= " 1.0 " encoding= " UTF-8 " ?> \n <XRD xmlns= " http://docs.oasis-open.org/ns/xri/xrd-1.0 " > \n <Link rel= " lrdd " template= " https:// ' + current_app . config [ " SERVER_NAME " ] + ' /.well-known/webfinger?resource= {uri} " /> \n </XRD> ' )
2024-02-14 10:16:49 +13:00
resp . content_type = ' application/xrd+xml; charset=utf-8 '
return resp
2023-08-05 21:24:10 +12:00
@bp.route ( ' /nodeinfo/2.0 ' )
2023-11-23 15:10:44 +13:00
@bp.route ( ' /nodeinfo/2.0.json ' )
2023-12-10 15:10:09 +13:00
@cache.cached ( timeout = 600 )
2023-08-05 21:24:10 +12:00
def nodeinfo2 ( ) :
nodeinfo_data = {
" version " : " 2.0 " ,
" software " : {
2023-11-16 22:31:14 +13:00
" name " : " PieFed " ,
2023-08-05 21:24:10 +12:00
" version " : " 0.1 "
} ,
" protocols " : [
" activitypub "
] ,
" usage " : {
" users " : {
2023-08-10 21:13:37 +12:00
" total " : users_total ( ) ,
" activeHalfyear " : active_half_year ( ) ,
" activeMonth " : active_month ( )
2023-08-05 21:24:10 +12:00
} ,
2023-08-10 21:13:37 +12:00
" localPosts " : local_posts ( ) ,
" localComments " : local_comments ( )
2023-08-05 21:24:10 +12:00
} ,
2023-12-24 13:28:41 +13:00
" openRegistrations " : g . site . registration_mode == ' Open '
2023-08-05 21:24:10 +12:00
}
return jsonify ( nodeinfo_data )
2024-04-03 16:35:26 +13:00
@bp.route ( ' /api/v1/instance ' )
@cache.cached ( timeout = 600 )
def api_v1_instance ( ) :
retval = {
' title ' : g . site . name ,
' uri ' : current_app . config [ ' SERVER_NAME ' ] ,
' stats ' : {
" user_count " : users_total ( ) ,
" status_count " : local_posts ( ) + local_comments ( ) ,
" domain_count " : 1
} ,
' registrations ' : g . site . registration_mode != ' Closed ' ,
' approval_required ' : g . site . registration_mode == ' RequireApplication '
}
return jsonify ( retval )
2024-02-25 16:24:50 +13:00
@bp.route ( ' /api/v1/instance/domain_blocks ' )
@cache.cached ( timeout = 600 )
def domain_blocks ( ) :
use_allowlist = get_setting ( ' use_allowlist ' , False )
if use_allowlist :
return jsonify ( [ ] )
else :
retval = [ ]
for domain in BannedInstances . query . all ( ) :
retval . append ( {
' domain ' : domain . domain ,
' digest ' : sha256_digest ( domain . domain ) ,
' severity ' : ' suspend ' ,
' comment ' : domain . reason if domain . reason else ' '
} )
return jsonify ( retval )
2023-11-23 15:10:44 +13:00
@bp.route ( ' /api/v3/site ' )
2024-04-22 20:53:03 +12:00
@cache.cached ( timeout = 600 )
2023-11-23 15:10:44 +13:00
def lemmy_site ( ) :
return jsonify ( lemmy_site_data ( ) )
@bp.route ( ' /api/v3/federated_instances ' )
2023-12-10 15:10:09 +13:00
@cache.cached ( timeout = 600 )
2023-11-23 15:10:44 +13:00
def lemmy_federated_instances ( ) :
2024-02-14 12:31:44 +13:00
instances = Instance . query . filter ( Instance . id != 1 ) . all ( )
2023-11-23 15:10:44 +13:00
linked = [ ]
allowed = [ ]
blocked = [ ]
for instance in instances :
instance_data = { " id " : instance . id , " domain " : instance . domain , " published " : instance . created_at . isoformat ( ) , " updated " : instance . updated_at . isoformat ( ) }
if instance . software :
instance_data [ ' software ' ] = instance . software
if instance . version :
instance_data [ ' version ' ] = instance . version
linked . append ( instance_data )
for instance in AllowedInstances . query . all ( ) :
2023-12-12 08:53:35 +13:00
allowed . append ( { " id " : instance . id , " domain " : instance . domain , " published " : utcnow ( ) , " updated " : utcnow ( ) } )
2023-11-23 15:10:44 +13:00
for instance in BannedInstances . query . all ( ) :
2023-12-12 08:53:35 +13:00
blocked . append ( { " id " : instance . id , " domain " : instance . domain , " published " : utcnow ( ) , " updated " : utcnow ( ) } )
2023-11-23 15:10:44 +13:00
return jsonify ( {
" federated_instances " : {
" linked " : linked ,
" allowed " : allowed ,
" blocked " : blocked
}
} )
2023-12-29 17:32:35 +13:00
@bp.route ( ' /u/<actor> ' , methods = [ ' GET ' , ' HEAD ' ] )
2023-08-10 21:13:37 +12:00
def user_profile ( actor ) :
""" Requests to this endpoint can be for a JSON representation of the user, or a HTML rendering of their profile.
The two types of requests are differentiated by the header """
2024-03-27 22:55:31 +13:00
actor = actor . strip ( )
2024-04-20 17:16:17 +12:00
# admins can view deleted accounts
2024-01-09 20:44:08 +13:00
if current_user . is_authenticated and current_user . is_admin ( ) :
if ' @ ' in actor :
2024-04-23 21:28:58 +12:00
user : User = User . query . filter_by ( ap_id = actor . lower ( ) ) . first ( )
2024-01-09 20:44:08 +13:00
else :
user : User = User . query . filter_by ( user_name = actor , ap_id = None ) . first ( )
2024-04-20 17:16:17 +12:00
if user is None :
2024-04-28 16:49:49 +12:00
user = User . query . filter_by ( ap_profile_id = f ' https:// { current_app . config [ " SERVER_NAME " ] } /u/ { actor . lower ( ) } ' , deleted = False , ap_id = None ) . first ( )
2023-11-24 20:22:58 +13:00
else :
2024-01-09 20:44:08 +13:00
if ' @ ' in actor :
2024-04-23 21:28:58 +12:00
user : User = User . query . filter_by ( ap_id = actor . lower ( ) , deleted = False , banned = False ) . first ( )
2024-01-09 20:44:08 +13:00
else :
user : User = User . query . filter_by ( user_name = actor , deleted = False , ap_id = None ) . first ( )
2024-04-20 17:16:17 +12:00
if user is None :
2024-04-23 21:28:58 +12:00
user = User . query . filter_by ( ap_profile_id = f ' https:// { current_app . config [ " SERVER_NAME " ] } /u/ { actor . lower ( ) } ' , deleted = False , ap_id = None ) . first ( )
2023-11-24 20:22:58 +13:00
2023-08-10 21:13:37 +12:00
if user is not None :
2023-12-29 17:32:35 +13:00
if request . method == ' HEAD ' :
if is_activitypub_request ( ) :
resp = jsonify ( ' ' )
resp . content_type = ' application/activity+json '
return resp
else :
return ' '
2023-12-09 22:14:16 +13:00
if is_activitypub_request ( ) :
2023-08-10 21:13:37 +12:00
server = current_app . config [ ' SERVER_NAME ' ]
2023-09-16 19:09:04 +12:00
actor_data = { " @context " : default_context ( ) ,
2024-03-20 11:34:25 +00:00
" type " : " Person " if not user . bot else " Service " ,
2023-08-10 21:13:37 +12:00
" id " : f " https:// { server } /u/ { actor } " ,
" preferredUsername " : actor ,
2024-01-01 14:49:15 +13:00
" name " : user . title if user . title else user . user_name ,
2023-08-10 21:13:37 +12:00
" inbox " : f " https:// { server } /u/ { actor } /inbox " ,
" outbox " : f " https:// { server } /u/ { actor } /outbox " ,
2023-12-29 17:32:35 +13:00
" discoverable " : user . searchable ,
" indexable " : user . indexable ,
2024-03-21 23:26:03 +00:00
" manuallyApprovesFollowers " : False if not user . ap_manually_approves_followers else user . ap_manually_approves_followers ,
2023-08-10 21:13:37 +12:00
" publicKey " : {
" id " : f " https:// { server } /u/ { actor } #main-key " ,
" owner " : f " https:// { server } /u/ { actor } " ,
2023-12-03 22:41:15 +13:00
" publicKeyPem " : user . public_key # .replace("\n", "\\n") #LOOKSWRONG
2023-08-10 21:13:37 +12:00
} ,
" endpoints " : {
" sharedInbox " : f " https:// { server } /inbox "
} ,
2023-12-08 17:13:38 +13:00
" published " : ap_datetime ( user . created ) ,
2023-08-10 21:13:37 +12:00
}
if user . avatar_id is not None :
actor_data [ " icon " ] = {
" type " : " Image " ,
2023-12-08 17:13:38 +13:00
" url " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } { user . avatar_image ( ) } "
}
if user . cover_id is not None :
actor_data [ " image " ] = {
" type " : " Image " ,
" url " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } { user . cover_image ( ) } "
2023-08-10 21:13:37 +12:00
}
2024-05-14 16:24:05 +01:00
if user . about_html :
actor_data [ ' summary ' ] = user . about_html
2023-12-28 21:00:26 +13:00
if user . matrix_user_id :
actor_data [ ' matrixUserId ' ] = user . matrix_user_id
2023-08-10 21:13:37 +12:00
resp = jsonify ( actor_data )
resp . content_type = ' application/activity+json '
return resp
else :
2023-10-07 21:32:19 +13:00
return show_profile ( user )
2023-10-21 15:49:01 +13:00
else :
abort ( 404 )
2023-08-10 21:13:37 +12:00
2023-12-22 15:34:45 +13:00
@bp.route ( ' /u/<actor>/outbox ' , methods = [ ' GET ' ] )
def user_outbox ( actor ) :
outbox = {
" @context " : default_context ( ) ,
' type ' : ' OrderedCollection ' ,
' id ' : f " https:// { current_app . config [ ' SERVER_NAME ' ] } /u/ { actor } /outbox " ,
' orderedItems ' : [ ] ,
' totalItems ' : 0
}
resp = jsonify ( outbox )
resp . content_type = ' application/activity+json '
return resp
2023-08-10 21:13:37 +12:00
@bp.route ( ' /c/<actor> ' , methods = [ ' GET ' ] )
def community_profile ( actor ) :
""" Requests to this endpoint can be for a JSON representation of the community, or a HTML rendering of it.
The two types of requests are differentiated by the header """
actor = actor . strip ( )
2023-08-29 22:01:06 +12:00
if ' @ ' in actor :
# don't provide activitypub info for remote communities
2023-12-03 22:41:15 +13:00
if ' application/ld+json ' in request . headers . get ( ' Accept ' , ' ' ) or ' application/activity+json ' in request . headers . get ( ' Accept ' , ' ' ) :
2023-12-12 18:28:49 +13:00
abort ( 400 )
2024-04-30 21:11:57 +12:00
community : Community = Community . query . filter_by ( ap_id = actor . lower ( ) , banned = False ) . first ( )
2023-08-29 22:01:06 +12:00
else :
2024-01-01 11:38:24 +13:00
community : Community = Community . query . filter_by ( name = actor , ap_id = None ) . first ( )
2023-08-10 21:13:37 +12:00
if community is not None :
2023-12-09 22:14:16 +13:00
if is_activitypub_request ( ) :
2023-08-10 21:13:37 +12:00
server = current_app . config [ ' SERVER_NAME ' ]
2023-09-16 19:09:04 +12:00
actor_data = { " @context " : default_context ( ) ,
2023-08-10 21:13:37 +12:00
" type " : " Group " ,
" id " : f " https:// { server } /c/ { actor } " ,
2023-11-16 22:31:14 +13:00
" name " : community . title ,
" sensitive " : True if community . nsfw or community . nsfl else False ,
2023-08-10 21:13:37 +12:00
" preferredUsername " : actor ,
" inbox " : f " https:// { server } /c/ { actor } /inbox " ,
" outbox " : f " https:// { server } /c/ { actor } /outbox " ,
" followers " : f " https:// { server } /c/ { actor } /followers " ,
" moderators " : f " https:// { server } /c/ { actor } /moderators " ,
" featured " : f " https:// { server } /c/ { actor } /featured " ,
" attributedTo " : f " https:// { server } /c/ { actor } /moderators " ,
2024-01-02 19:41:00 +13:00
" postingRestrictedToMods " : community . restricted_to_mods or community . local_only ,
" newModsWanted " : community . new_mods_wanted ,
" privateMods " : community . private_mods ,
2023-08-10 21:13:37 +12:00
" url " : f " https:// { server } /c/ { actor } " ,
" publicKey " : {
" id " : f " https:// { server } /c/ { actor } #main-key " ,
" owner " : f " https:// { server } /c/ { actor } " ,
2023-11-22 22:12:58 +13:00
" publicKeyPem " : community . public_key
2023-08-10 21:13:37 +12:00
} ,
" endpoints " : {
" sharedInbox " : f " https:// { server } /inbox "
} ,
2023-12-08 17:13:38 +13:00
" published " : ap_datetime ( community . created_at ) ,
" updated " : ap_datetime ( community . last_active ) ,
2023-08-10 21:13:37 +12:00
}
2024-05-14 16:24:05 +01:00
if community . description_html :
actor_data [ " summary " ] = community . description_html
2023-11-16 22:31:14 +13:00
if community . icon_id is not None :
2023-08-10 21:13:37 +12:00
actor_data [ " icon " ] = {
" type " : " Image " ,
2023-12-08 17:13:38 +13:00
" url " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } { community . icon_image ( ) } "
}
if community . image_id is not None :
actor_data [ " image " ] = {
" type " : " Image " ,
" url " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } { community . header_image ( ) } "
2023-08-10 21:13:37 +12:00
}
resp = jsonify ( actor_data )
resp . content_type = ' application/activity+json '
return resp
2023-08-29 22:01:06 +12:00
else : # browser request - return html
return show_community ( community )
else :
abort ( 404 )
2023-08-10 21:13:37 +12:00
2023-09-08 20:04:01 +12:00
@bp.route ( ' /inbox ' , methods = [ ' GET ' , ' POST ' ] )
def shared_inbox ( ) :
if request . method == ' POST ' :
2023-12-22 14:05:39 +13:00
# save all incoming data to aid in debugging and development. Set result to 'success' if things go well
2024-01-13 11:12:31 +13:00
activity_log = ActivityPubLog ( direction = ' in ' , result = ' failure ' )
2023-09-09 20:46:40 +12:00
try :
request_json = request . get_json ( force = True )
except werkzeug . exceptions . BadRequest as e :
activity_log . exception_message = ' Unable to parse json body: ' + e . description
2023-09-16 19:09:04 +12:00
activity_log . result = ' failure '
2023-09-09 20:46:40 +12:00
db . session . add ( activity_log )
db . session . commit ( )
2023-12-23 11:32:22 +13:00
return ' '
2023-09-09 20:46:40 +12:00
2023-12-24 13:28:41 +13:00
if ' id ' in request_json :
2024-04-04 21:36:03 +13:00
redis_client = get_redis_connection ( )
if redis_client . get ( request_json [ ' id ' ] ) is not None : # Lemmy has an extremely short POST timeout and tends to retry unnecessarily. Ignore their retries.
2023-12-24 13:28:41 +13:00
activity_log . result = ' ignored '
2024-01-06 14:54:10 +13:00
activity_log . exception_message = ' Unnecessary retry attempt '
2023-12-24 13:28:41 +13:00
db . session . add ( activity_log )
db . session . commit ( )
return ' '
2023-12-23 11:32:22 +13:00
2024-04-04 21:36:03 +13:00
redis_client . set ( request_json [ ' id ' ] , 1 , ex = 90 ) # Save the activity ID into redis, to avoid duplicate activities that Lemmy sometimes sends
2023-12-24 13:28:41 +13:00
activity_log . activity_id = request_json [ ' id ' ]
2024-04-09 08:14:25 +12:00
g . site = Site . query . get ( 1 ) # g.site is not initialized by @app.before_request when request.path == '/inbox'
2024-01-13 11:12:31 +13:00
if g . site . log_activitypub_json :
activity_log . activity_json = json . dumps ( request_json )
2023-12-24 13:28:41 +13:00
activity_log . result = ' processing '
2023-12-29 17:32:35 +13:00
db . session . add ( activity_log )
db . session . commit ( )
2023-12-23 11:32:22 +13:00
2023-12-29 17:32:35 +13:00
# When a user is deleted, the only way to be fairly sure they get deleted everywhere is to tell the whole fediverse.
2023-12-24 13:28:41 +13:00
if ' type ' in request_json and request_json [ ' type ' ] == ' Delete ' and request_json [ ' id ' ] . endswith ( ' #delete ' ) :
2023-12-29 17:32:35 +13:00
if current_app . debug :
2024-01-01 14:49:15 +13:00
process_delete_request ( request_json , activity_log . id , ip_address ( ) )
2023-12-29 17:32:35 +13:00
else :
2024-01-01 14:49:15 +13:00
process_delete_request . delay ( request_json , activity_log . id , ip_address ( ) )
2023-12-24 13:28:41 +13:00
return ' '
else :
activity_log . activity_id = ' '
2024-01-13 11:12:31 +13:00
if g . site . log_activitypub_json :
activity_log . activity_json = json . dumps ( request_json )
2023-12-24 13:28:41 +13:00
db . session . add ( activity_log )
db . session . commit ( )
2023-12-23 11:32:22 +13:00
2023-09-16 19:09:04 +12:00
actor = find_actor_or_create ( request_json [ ' actor ' ] ) if ' actor ' in request_json else None
2023-09-08 20:04:01 +12:00
if actor is not None :
if HttpSignature . verify_request ( request , actor . public_key , skip_date = True ) :
2023-12-24 13:28:41 +13:00
if current_app . debug :
2024-01-01 14:49:15 +13:00
process_inbox_request ( request_json , activity_log . id , ip_address ( ) )
2023-12-24 13:28:41 +13:00
else :
2024-01-01 14:49:15 +13:00
process_inbox_request . delay ( request_json , activity_log . id , ip_address ( ) )
2023-12-24 13:28:41 +13:00
return ' '
else :
activity_log . exception_message = ' Could not verify signature '
else :
actor_name = request_json [ ' actor ' ] if ' actor ' in request_json else ' '
activity_log . exception_message = f ' Actor could not be found: { actor_name } '
if activity_log . exception_message is not None :
activity_log . result = ' failure '
db . session . commit ( )
return ' '
@celery.task
2024-01-01 14:49:15 +13:00
def process_inbox_request ( request_json , activitypublog_id , ip_address ) :
2023-12-24 13:28:41 +13:00
with current_app . app_context ( ) :
activity_log = ActivityPubLog . query . get ( activitypublog_id )
site = Site . query . get ( 1 ) # can't use g.site because celery doesn't use Flask's g variable
if ' type ' in request_json :
activity_log . activity_type = request_json [ ' type ' ]
if not instance_blocked ( request_json [ ' id ' ] ) :
# Create is new content
if request_json [ ' type ' ] == ' Create ' :
activity_log . activity_type = ' Create '
user_ap_id = request_json [ ' object ' ] [ ' attributedTo ' ]
2024-02-17 20:05:57 +13:00
if request_json [ ' object ' ] [ ' type ' ] == ' ChatMessage ' :
activity_log . activity_type = ' Create ChatMessage '
sender = find_actor_or_create ( user_ap_id )
recipient_ap_id = request_json [ ' object ' ] [ ' to ' ] [ 0 ]
recipient = find_actor_or_create ( recipient_ap_id )
if sender and recipient and recipient . is_local ( ) :
2024-02-19 15:56:56 +13:00
if sender . created_recently ( ) or sender . reputation < = - 10 :
2024-02-19 15:01:53 +13:00
activity_log . exception_message = " Sender not eligible to send "
elif recipient . has_blocked_user ( sender . id ) or recipient . has_blocked_instance ( sender . instance_id ) :
2024-02-17 20:05:57 +13:00
activity_log . exception_message = " Sender blocked by recipient "
else :
2024-02-19 15:56:56 +13:00
# Find existing conversation to add to
existing_conversation = Conversation . find_existing_conversation ( recipient = recipient , sender = sender )
if not existing_conversation :
existing_conversation = Conversation ( user_id = sender . id )
existing_conversation . members . append ( recipient )
existing_conversation . members . append ( sender )
db . session . add ( existing_conversation )
db . session . commit ( )
2024-02-17 20:05:57 +13:00
# Save ChatMessage to DB
encrypted = request_json [ ' object ' ] [ ' encrypted ' ] if ' encrypted ' in request_json [ ' object ' ] else None
2024-02-19 15:56:56 +13:00
new_message = ChatMessage ( sender_id = sender . id , recipient_id = recipient . id , conversation_id = existing_conversation . id ,
2024-02-17 20:05:57 +13:00
body = request_json [ ' object ' ] [ ' source ' ] [ ' content ' ] ,
2024-05-14 20:38:16 +01:00
body_html = lemmy_markdown_to_html ( request_json [ ' object ' ] [ ' source ' ] [ ' content ' ] ) ,
2024-02-17 20:05:57 +13:00
encrypted = encrypted )
db . session . add ( new_message )
2024-03-28 09:24:13 +13:00
existing_conversation . updated_at = utcnow ( )
2024-02-17 20:05:57 +13:00
db . session . commit ( )
# Notify recipient
notify = Notification ( title = shorten_string ( ' New message from ' + sender . display_name ( ) ) ,
2024-04-15 19:03:59 +12:00
url = f ' /chat/ { existing_conversation . id } #message_ { new_message } ' , user_id = recipient . id ,
2024-02-17 20:05:57 +13:00
author_id = sender . id )
db . session . add ( notify )
recipient . unread_notifications + = 1
2024-02-19 15:56:56 +13:00
existing_conversation . read = False
2024-02-17 20:05:57 +13:00
db . session . commit ( )
activity_log . result = ' success '
else :
try :
2024-03-26 15:03:45 +00:00
community_ap_id = ' '
locations = [ ' audience ' , ' cc ' , ' to ' ]
if ' object ' in request_json :
rjs = [ request_json , request_json [ ' object ' ] ]
else :
rjs = [ request_json ]
local_community_prefix = f " https:// { current_app . config [ ' SERVER_NAME ' ] } /c/ "
followers_suffix = ' /followers '
for rj in rjs :
for loc in locations :
if loc in rj :
id = rj [ loc ]
if isinstance ( id , str ) :
if id . startswith ( local_community_prefix ) and not id . endswith ( followers_suffix ) :
community_ap_id = id
if isinstance ( id , list ) :
for c in id :
if c . startswith ( local_community_prefix ) and not c . endswith ( followers_suffix ) :
community_ap_id = c
break
if community_ap_id :
break
if community_ap_id :
break
if not community_ap_id and ' object ' in request_json and ' inReplyTo ' in request_json [ ' object ' ] :
post_being_replied_to = Post . query . filter_by ( ap_id = request_json [ ' object ' ] [ ' inReplyTo ' ] ) . first ( )
if post_being_replied_to :
community_ap_id = post_being_replied_to . community . ap_profile_id
else :
comment_being_replied_to = PostReply . query . filter_by ( ap_id = request_json [ ' object ' ] [ ' inReplyTo ' ] ) . first ( )
if comment_being_replied_to :
community_ap_id = comment_being_replied_to . community . ap_profile_id
if not community_ap_id :
activity_log . result = ' failure '
activity_log . exception_message = ' Unable to extract community '
db . session . commit ( )
return
2024-02-17 20:05:57 +13:00
except :
activity_log . activity_type = ' exception '
db . session . commit ( )
return
2024-04-24 13:44:25 +01:00
if ' object ' in request_json :
if not ensure_domains_match ( request_json [ ' object ' ] ) :
activity_log . result = ' failure '
activity_log . exception_message = ' Domains do not match '
db . session . commit ( )
return
2024-03-02 10:20:15 +13:00
community = find_actor_or_create ( community_ap_id , community_only = True )
2024-04-20 13:03:39 +01:00
if community and community . local_only :
activity_log . exception_message = ' Remote Create in local_only community '
activity_log . result = ' ignored '
db . session . commit ( )
return
2024-02-17 20:05:57 +13:00
user = find_actor_or_create ( user_ap_id )
if ( user and not user . is_local ( ) ) and community :
user . last_seen = community . last_active = site . last_active = utcnow ( )
object_type = request_json [ ' object ' ] [ ' type ' ]
new_content_types = [ ' Page ' , ' Article ' , ' Link ' , ' Note ' ]
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 :
2024-02-24 11:07:06 +13:00
if can_create_post ( user , community ) :
2024-02-29 11:01:52 +13:00
try :
post = create_post ( activity_log , community , request_json , user )
2024-04-17 15:10:04 +01:00
if post :
announce_activity_to_followers ( community , user , request_json )
2024-02-29 11:01:52 +13:00
except TypeError as e :
activity_log . exception_message = ' TypeError. See log file. '
current_app . logger . error ( ' TypeError: ' + str ( request_json ) )
post = None
2024-02-24 11:07:06 +13:00
else :
post = None
2024-02-17 20:05:57 +13:00
else :
2024-02-24 11:07:06 +13:00
if can_create_post_reply ( user , community ) :
2024-02-29 11:01:52 +13:00
try :
post = create_post_reply ( activity_log , community , in_reply_to , request_json , user )
2024-04-17 15:10:04 +01:00
if post and community . is_local ( ) :
announce_activity_to_followers ( community , user , request_json )
2024-02-29 11:01:52 +13:00
except TypeError as e :
activity_log . exception_message = ' TypeError. See log file. '
current_app . logger . error ( ' TypeError: ' + str ( request_json ) )
post = None
2024-02-24 11:07:06 +13:00
else :
post = None
2023-12-24 13:28:41 +13:00
else :
2024-02-17 20:05:57 +13:00
activity_log . exception_message = ' Unacceptable type (create): ' + object_type
2023-12-24 13:28:41 +13:00
else :
2024-02-17 20:05:57 +13:00
if user is None or community is None :
activity_log . exception_message = ' Blocked or unfound user or community '
if user and user . is_local ( ) :
activity_log . exception_message = ' Activity about local content which is already present '
activity_log . result = ' ignored '
2023-12-24 13:28:41 +13:00
2023-12-26 21:39:52 +13:00
# Announce is new content and votes that happened on a remote server.
2023-12-24 13:28:41 +13:00
if request_json [ ' type ' ] == ' Announce ' :
2024-01-25 20:16:08 +13:00
if isinstance ( request_json [ ' object ' ] , str ) :
activity_log . activity_json = json . dumps ( request_json )
activity_log . exception_message = ' invalid json? '
elif request_json [ ' object ' ] [ ' type ' ] == ' Create ' :
2023-12-24 13:28:41 +13:00
activity_log . activity_type = request_json [ ' object ' ] [ ' type ' ]
2024-04-24 13:44:25 +01:00
if ' object ' in request_json and ' object ' in request_json [ ' object ' ] :
if not ensure_domains_match ( request_json [ ' object ' ] [ ' object ' ] ) :
activity_log . exception_message = ' Domains do not match '
activity_log . result = ' failure '
db . session . commit ( )
return
2023-12-24 13:28:41 +13:00
user_ap_id = request_json [ ' object ' ] [ ' object ' ] [ ' attributedTo ' ]
2024-01-05 08:45:33 +13:00
try :
community_ap_id = request_json [ ' object ' ] [ ' audience ' ] if ' audience ' in request_json [ ' object ' ] else request_json [ ' actor ' ]
except KeyError :
activity_log . activity_type = ' exception '
db . session . commit ( )
return
2024-03-02 10:20:15 +13:00
community = find_actor_or_create ( community_ap_id , community_only = True )
2023-12-24 13:28:41 +13:00
user = find_actor_or_create ( user_ap_id )
if ( user and not user . is_local ( ) ) and community :
user . last_seen = community . last_active = site . last_active = utcnow ( )
object_type = request_json [ ' object ' ] [ ' object ' ] [ ' type ' ]
new_content_types = [ ' Page ' , ' Article ' , ' Link ' , ' Note ' ]
if object_type in new_content_types : # create a new post
in_reply_to = request_json [ ' object ' ] [ ' object ' ] [ ' inReplyTo ' ] if ' inReplyTo ' in \
request_json [ ' object ' ] [ ' object ' ] else None
2024-02-24 11:07:06 +13:00
if not in_reply_to :
if can_create_post ( user , community ) :
2024-01-02 19:41:00 +13:00
post = create_post ( activity_log , community , request_json [ ' object ' ] , user , announce_id = request_json [ ' id ' ] )
else :
2024-02-24 11:07:06 +13:00
post = None
else :
if can_create_post_reply ( user , community ) :
2024-01-02 19:41:00 +13:00
post = create_post_reply ( activity_log , community , in_reply_to , request_json [ ' object ' ] , user , announce_id = request_json [ ' id ' ] )
2024-02-24 11:07:06 +13:00
else :
post = None
2023-12-24 13:28:41 +13:00
else :
activity_log . exception_message = ' Unacceptable type: ' + object_type
2023-12-24 16:20:18 +13:00
else :
if user is None or community is None :
activity_log . exception_message = ' Blocked or unfound user or community '
if user and user . is_local ( ) :
activity_log . exception_message = ' Activity about local content which is already present '
activity_log . result = ' ignored '
2023-12-24 13:28:41 +13:00
elif request_json [ ' object ' ] [ ' type ' ] == ' Like ' :
activity_log . activity_type = request_json [ ' object ' ] [ ' type ' ]
user_ap_id = request_json [ ' object ' ] [ ' actor ' ]
liked_ap_id = request_json [ ' object ' ] [ ' object ' ]
user = find_actor_or_create ( user_ap_id )
2024-01-02 19:41:00 +13:00
liked = find_liked_object ( liked_ap_id )
if user is None :
activity_log . exception_message = ' Blocked or unfound user '
2024-01-04 16:56:37 +13:00
elif liked is None :
2024-01-05 09:43:08 +13:00
activity_log . exception_message = ' Unfound object ' + liked_ap_id
2024-01-02 19:41:00 +13:00
elif user . is_local ( ) :
activity_log . exception_message = ' Activity about local content which is already present '
activity_log . result = ' ignored '
2024-01-03 16:29:58 +13:00
elif can_upvote ( user , liked . community ) :
2023-12-24 13:28:41 +13:00
# insert into voted table
if liked is None :
activity_log . exception_message = ' Liked object not found '
elif liked is not None and isinstance ( liked , Post ) :
upvote_post ( liked , user )
activity_log . result = ' success '
elif liked is not None and isinstance ( liked , PostReply ) :
upvote_post_reply ( liked , user )
activity_log . result = ' success '
else :
activity_log . exception_message = ' Could not detect type of like '
2023-12-24 16:20:18 +13:00
else :
2024-01-02 19:41:00 +13:00
activity_log . exception_message = ' Cannot upvote this '
activity_log . result = ' ignored '
2023-12-24 16:20:18 +13:00
2023-12-24 13:28:41 +13:00
elif request_json [ ' object ' ] [ ' type ' ] == ' Dislike ' :
activity_log . activity_type = request_json [ ' object ' ] [ ' type ' ]
if site . enable_downvotes is False :
activity_log . exception_message = ' Dislike ignored because of allow_dislike setting '
else :
user_ap_id = request_json [ ' object ' ] [ ' actor ' ]
liked_ap_id = request_json [ ' object ' ] [ ' object ' ]
2023-09-16 19:09:04 +12:00
user = find_actor_or_create ( user_ap_id )
2024-01-02 19:41:00 +13:00
disliked = find_liked_object ( liked_ap_id )
if user is None :
activity_log . exception_message = ' Blocked or unfound user '
2024-01-04 16:56:37 +13:00
elif disliked is None :
2024-01-05 09:43:08 +13:00
activity_log . exception_message = ' Unfound object ' + liked_ap_id
2024-01-02 19:41:00 +13:00
elif user . is_local ( ) :
activity_log . exception_message = ' Activity about local content which is already present '
activity_log . result = ' ignored '
2024-01-03 16:29:58 +13:00
elif can_downvote ( user , disliked . community , site ) :
2023-12-24 13:28:41 +13:00
# insert into voted table
if disliked is None :
activity_log . exception_message = ' Liked object not found '
2024-01-02 19:41:00 +13:00
elif isinstance ( disliked , ( Post , PostReply ) ) :
if isinstance ( disliked , Post ) :
downvote_post ( disliked , user )
elif isinstance ( disliked , PostReply ) :
downvote_post_reply ( disliked , user )
2023-11-22 20:48:27 +13:00
activity_log . result = ' success '
2024-01-02 19:41:00 +13:00
# todo: recalculate 'hotness' of liked post/reply
2023-12-24 13:28:41 +13:00
else :
activity_log . exception_message = ' Could not detect type of like '
2023-12-24 16:20:18 +13:00
else :
2024-01-02 19:41:00 +13:00
activity_log . exception_message = ' Cannot downvote this '
activity_log . result = ' ignored '
2023-12-26 12:36:02 +13:00
elif request_json [ ' object ' ] [ ' type ' ] == ' Delete ' :
activity_log . activity_type = request_json [ ' object ' ] [ ' type ' ]
user_ap_id = request_json [ ' object ' ] [ ' actor ' ]
2024-01-05 08:45:33 +13:00
community_ap_id = request_json [ ' object ' ] [ ' audience ' ] if ' audience ' in request_json [ ' object ' ] else request_json [ ' actor ' ]
2023-12-26 12:36:02 +13:00
to_be_deleted_ap_id = request_json [ ' object ' ] [ ' object ' ]
2024-01-05 10:36:55 +13:00
if isinstance ( to_be_deleted_ap_id , dict ) :
activity_log . result = ' failure '
activity_log . exception_message = ' dict instead of string ' + str ( to_be_deleted_ap_id )
else :
2024-04-02 17:44:26 +01:00
post = Post . query . filter_by ( ap_id = to_be_deleted_ap_id ) . first ( )
if post and post . url and post . cross_posts is not None :
old_cross_posts = Post . query . filter ( Post . id . in_ ( post . cross_posts ) ) . all ( )
post . cross_posts . clear ( )
for ocp in old_cross_posts :
if ocp . cross_posts is not None :
ocp . cross_posts . remove ( post . id )
2024-01-05 10:36:55 +13:00
delete_post_or_comment ( user_ap_id , community_ap_id , to_be_deleted_ap_id )
activity_log . result = ' success '
2024-04-15 22:06:50 +01:00
elif request_json [ ' object ' ] [ ' type ' ] == ' Page ' : # Sent for Mastodon's benefit
activity_log . result = ' ignored '
activity_log . exception_message = ' Intended for Mastodon '
db . session . add ( activity_log )
db . session . commit ( )
elif request_json [ ' object ' ] [ ' type ' ] == ' Note ' : # Never sent?
activity_log . result = ' ignored '
activity_log . exception_message = ' Intended for Mastodon '
db . session . add ( activity_log )
db . session . commit ( )
2024-01-05 09:13:00 +13:00
elif request_json [ ' object ' ] [ ' type ' ] == ' Update ' : # Editing a post or comment
2024-01-05 09:02:54 +13:00
if request_json [ ' object ' ] [ ' object ' ] [ ' type ' ] == ' Page ' :
post = Post . query . filter_by ( ap_id = request_json [ ' object ' ] [ ' object ' ] [ ' id ' ] ) . first ( )
if post :
try :
update_post_from_activity ( post , request_json [ ' object ' ] )
except KeyError :
activity_log . result = ' exception '
db . session . commit ( )
return
activity_log . result = ' success '
else :
activity_log . exception_message = ' Post not found '
elif request_json [ ' object ' ] [ ' object ' ] [ ' type ' ] == ' Note ' :
reply = PostReply . query . filter_by ( ap_id = request_json [ ' object ' ] [ ' object ' ] [ ' id ' ] ) . first ( )
if reply :
try :
update_post_reply_from_activity ( reply , request_json [ ' object ' ] )
except KeyError :
activity_log . result = ' exception '
db . session . commit ( )
return
activity_log . result = ' success '
else :
activity_log . exception_message = ' PostReply not found '
2024-01-05 09:39:20 +13:00
elif request_json [ ' object ' ] [ ' type ' ] == ' Undo ' :
2024-04-16 18:40:48 +01:00
if request_json [ ' object ' ] [ ' object ' ] [ ' type ' ] == ' Like ' or request_json [ ' object ' ] [ ' object ' ] [ ' type ' ] == ' Dislike ' :
2024-01-05 09:39:20 +13:00
activity_log . activity_type = request_json [ ' object ' ] [ ' object ' ] [ ' type ' ]
user_ap_id = request_json [ ' object ' ] [ ' actor ' ]
user = find_actor_or_create ( user_ap_id )
post = None
comment = None
target_ap_id = request_json [ ' object ' ] [ ' object ' ] [ ' object ' ] # object object object!
post = undo_vote ( activity_log , comment , post , target_ap_id , user )
activity_log . result = ' success '
2024-03-21 23:21:28 +00:00
elif request_json [ ' object ' ] [ ' type ' ] == ' Add ' and ' target ' in request_json [ ' object ' ] :
2024-03-19 15:38:35 +00:00
activity_log . activity_type = request_json [ ' object ' ] [ ' type ' ]
2024-03-21 23:21:28 +00:00
target = request_json [ ' object ' ] [ ' target ' ]
community = Community . query . filter_by ( ap_public_url = request_json [ ' actor ' ] ) . first ( )
if community :
featured_url = community . ap_featured_url
moderators_url = community . ap_moderators_url
if target == featured_url :
post = Post . query . filter_by ( ap_id = request_json [ ' object ' ] [ ' object ' ] ) . first ( )
2024-03-19 15:38:35 +00:00
if post :
post . sticky = True
activity_log . result = ' success '
2024-03-21 23:21:28 +00:00
if target == moderators_url :
user = find_actor_or_create ( request_json [ ' object ' ] [ ' object ' ] )
if user :
existing_membership = CommunityMember . query . filter_by ( community_id = community . id , user_id = user . id ) . first ( )
if existing_membership :
existing_membership . is_moderator = True
else :
new_membership = CommunityMember ( community_id = community . id , user_id = user . id , is_moderator = True )
db . session . add ( new_membership )
db . session . commit ( )
activity_log . result = ' success '
elif request_json [ ' object ' ] [ ' type ' ] == ' Remove ' and ' target ' in request_json [ ' object ' ] :
2024-03-19 15:38:35 +00:00
activity_log . activity_type = request_json [ ' object ' ] [ ' type ' ]
2024-03-21 23:21:28 +00:00
target = request_json [ ' object ' ] [ ' target ' ]
community = Community . query . filter_by ( ap_public_url = request_json [ ' actor ' ] ) . first ( )
if community :
featured_url = community . ap_featured_url
moderators_url = community . ap_moderators_url
if target == featured_url :
post = Post . query . filter_by ( ap_id = request_json [ ' object ' ] [ ' object ' ] ) . first ( )
2024-03-19 15:38:35 +00:00
if post :
post . sticky = False
activity_log . result = ' success '
2024-03-21 23:21:28 +00:00
if target == moderators_url :
user = find_actor_or_create ( request_json [ ' object ' ] [ ' object ' ] , create_if_not_found = False )
if user :
existing_membership = CommunityMember . query . filter_by ( community_id = community . id , user_id = user . id ) . first ( )
if existing_membership :
existing_membership . is_moderator = False
activity_log . result = ' success '
2023-12-26 21:39:52 +13:00
else :
activity_log . exception_message = ' Invalid type for Announce '
2023-12-24 13:28:41 +13:00
2023-12-26 12:36:02 +13:00
# Follow: remote user wants to join/follow one of our communities
2023-12-24 13:28:41 +13:00
elif request_json [ ' type ' ] == ' Follow ' : # Follow is when someone wants to join a community
user_ap_id = request_json [ ' actor ' ]
community_ap_id = request_json [ ' object ' ]
follow_id = request_json [ ' id ' ]
user = find_actor_or_create ( user_ap_id )
2024-03-02 10:20:15 +13:00
community = find_actor_or_create ( community_ap_id , community_only = True )
2024-05-17 21:03:38 +12:00
if isinstance ( community , Community ) :
if community and community . local_only and user :
activity_log . exception_message = ' Local only cannot be followed by remote users '
# send reject message to deny the follow
reject = {
" @context " : default_context ( ) ,
" actor " : community . ap_profile_id ,
" to " : [
user . ap_profile_id
] ,
" object " : {
" actor " : user . ap_profile_id ,
" to " : None ,
" object " : community . ap_profile_id ,
" type " : " Follow " ,
" id " : follow_id
} ,
" type " : " Reject " ,
" id " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } /activities/reject/ " + gibberish ( 32 )
}
# Lemmy doesn't yet understand Reject/Follow, so send without worrying about response for now.
post_request ( user . ap_inbox_url , reject , community . private_key , f " https:// { current_app . config [ ' SERVER_NAME ' ] } /c/ { community . name } #main-key " )
else :
if user is not None and community is not None :
# check if user is banned from this community
banned = CommunityBan . query . filter_by ( user_id = user . id , community_id = community . id ) . first ( )
if banned is None :
user . last_seen = utcnow ( )
if community_membership ( user , community ) != SUBSCRIPTION_MEMBER :
member = CommunityMember ( user_id = user . id , community_id = community . id )
db . session . add ( member )
db . session . commit ( )
cache . delete_memoized ( community_membership , user , community )
# send accept message to acknowledge the follow
accept = {
" @context " : default_context ( ) ,
" actor " : community . ap_profile_id ,
" to " : [
user . ap_profile_id
] ,
" object " : {
" actor " : user . ap_profile_id ,
" to " : None ,
" object " : community . ap_profile_id ,
" type " : " Follow " ,
" id " : follow_id
} ,
" type " : " Accept " ,
" id " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } /activities/accept/ " + gibberish ( 32 )
}
if post_request ( user . ap_inbox_url , accept , community . private_key , f " https:// { current_app . config [ ' SERVER_NAME ' ] } /c/ { community . name } #main-key " ) :
activity_log . result = ' success '
else :
activity_log . exception_message = ' Error sending Accept '
2024-01-27 12:22:35 +13:00
else :
2024-05-17 21:03:38 +12:00
activity_log . exception_message = ' user is banned from this community '
elif isinstance ( community , User ) : # Pixelfed sends follow requests to the shared inbox, not the user inbox...
if current_app . debug :
process_user_follow_request ( request_json , activity_log . id , user . id )
else :
process_user_follow_request . delay ( request_json , activity_log . id , user . id )
2023-12-24 13:28:41 +13:00
# Accept: remote server is accepting our previous follow request
elif request_json [ ' type ' ] == ' Accept ' :
if request_json [ ' object ' ] [ ' type ' ] == ' Follow ' :
community_ap_id = request_json [ ' actor ' ]
user_ap_id = request_json [ ' object ' ] [ ' actor ' ]
user = find_actor_or_create ( user_ap_id )
2024-03-02 10:20:15 +13:00
community = find_actor_or_create ( community_ap_id , community_only = True )
2023-12-24 13:28:41 +13:00
if user and community :
join_request = CommunityJoinRequest . query . filter_by ( user_id = user . id , community_id = community . id ) . first ( )
if join_request :
2024-03-08 21:40:47 +13:00
existing_membership = CommunityMember . query . filter_by ( user_id = user . id , community_id = community . id ) . first ( )
if not existing_membership :
member = CommunityMember ( user_id = user . id , community_id = community . id )
db . session . add ( member )
community . subscriptions_count + = 1
db . session . commit ( )
2023-12-24 13:28:41 +13:00
activity_log . result = ' success '
2024-01-04 17:07:02 +13:00
cache . delete_memoized ( community_membership , user , community )
2023-12-24 13:28:41 +13:00
elif request_json [ ' type ' ] == ' Undo ' :
if request_json [ ' object ' ] [ ' type ' ] == ' Follow ' : # Unsubscribe from a community
community_ap_id = request_json [ ' object ' ] [ ' object ' ]
user_ap_id = request_json [ ' object ' ] [ ' actor ' ]
user = find_actor_or_create ( user_ap_id )
2024-03-02 10:20:15 +13:00
community = find_actor_or_create ( community_ap_id , community_only = True )
2023-12-24 13:28:41 +13:00
if user and community :
user . last_seen = utcnow ( )
member = CommunityMember . query . filter_by ( user_id = user . id , community_id = community . id ) . first ( )
join_request = CommunityJoinRequest . query . filter_by ( user_id = user . id , community_id = community . id ) . first ( )
if member :
db . session . delete ( member )
2023-12-30 13:23:12 +13:00
community . subscriptions_count - = 1
2023-12-24 13:28:41 +13:00
if join_request :
db . session . delete ( join_request )
2023-11-21 23:05:07 +13:00
db . session . commit ( )
2024-01-04 17:07:02 +13:00
cache . delete_memoized ( community_membership , user , community )
2023-11-21 23:05:07 +13:00
activity_log . result = ' success '
2023-12-24 13:28:41 +13:00
elif request_json [ ' object ' ] [ ' type ' ] == ' Like ' : # Undoing an upvote or downvote
activity_log . activity_type = request_json [ ' object ' ] [ ' type ' ]
user_ap_id = request_json [ ' actor ' ]
user = find_actor_or_create ( user_ap_id )
post = None
comment = None
target_ap_id = request_json [ ' object ' ] [ ' object ' ]
2024-04-17 15:10:04 +01:00
post_or_comment = undo_vote ( activity_log , comment , post , target_ap_id , user )
if post_or_comment :
announce_activity_to_followers ( post_or_comment . community , user , request_json )
2024-01-05 09:39:20 +13:00
activity_log . result = ' success '
2023-12-24 13:28:41 +13:00
elif request_json [ ' object ' ] [ ' type ' ] == ' Dislike ' : # Undoing a downvote - probably unused
activity_log . activity_type = request_json [ ' object ' ] [ ' type ' ]
user_ap_id = request_json [ ' actor ' ]
user = find_actor_or_create ( user_ap_id )
post = None
comment = None
target_ap_id = request_json [ ' object ' ] [ ' object ' ]
2024-04-17 15:10:04 +01:00
post_or_comment = undo_downvote ( activity_log , comment , post , target_ap_id , user )
if post_or_comment :
announce_activity_to_followers ( post_or_comment . community , user , request_json )
2024-01-05 09:39:20 +13:00
activity_log . result = ' success '
2023-12-24 13:28:41 +13:00
elif request_json [ ' type ' ] == ' Update ' :
2023-12-26 21:39:52 +13:00
activity_log . activity_type = ' Update '
2023-12-24 13:28:41 +13:00
if request_json [ ' object ' ] [ ' type ' ] == ' Page ' : # Editing a post
post = Post . query . filter_by ( ap_id = request_json [ ' object ' ] [ ' id ' ] ) . first ( )
if post :
2024-05-09 17:54:30 +12:00
if can_edit ( request_json [ ' actor ' ] , post ) :
update_post_from_activity ( post , request_json )
announce_activity_to_followers ( post . community , post . author , request_json )
activity_log . result = ' success '
else :
activity_log . exception_message = ' Edit attempt denied '
2023-12-26 21:39:52 +13:00
else :
activity_log . exception_message = ' Post not found '
2023-12-24 13:28:41 +13:00
elif request_json [ ' object ' ] [ ' type ' ] == ' Note ' : # Editing a reply
reply = PostReply . query . filter_by ( ap_id = request_json [ ' object ' ] [ ' id ' ] ) . first ( )
if reply :
2024-05-09 17:54:30 +12:00
if can_edit ( request_json [ ' actor ' ] , reply ) :
update_post_reply_from_activity ( reply , request_json )
announce_activity_to_followers ( reply . community , reply . author , request_json )
activity_log . result = ' success '
else :
activity_log . exception_message = ' Edit attempt denied '
2023-12-26 21:39:52 +13:00
else :
activity_log . exception_message = ' PostReply not found '
2023-12-24 13:28:41 +13:00
elif request_json [ ' type ' ] == ' Delete ' :
if isinstance ( request_json [ ' object ' ] , str ) :
ap_id = request_json [ ' object ' ] # lemmy
else :
ap_id = request_json [ ' object ' ] [ ' id ' ] # kbin
post = Post . query . filter_by ( ap_id = ap_id ) . first ( )
2024-02-25 15:31:16 +13:00
# Delete post
2023-12-24 13:28:41 +13:00
if post :
2024-05-09 17:54:30 +12:00
if can_delete ( request_json [ ' actor ' ] , post ) :
if post . url and post . cross_posts is not None :
old_cross_posts = Post . query . filter ( Post . id . in_ ( post . cross_posts ) ) . all ( )
post . cross_posts . clear ( )
for ocp in old_cross_posts :
if ocp . cross_posts is not None :
ocp . cross_posts . remove ( post . id )
post . delete_dependencies ( )
post . community . post_count - = 1
announce_activity_to_followers ( post . community , post . author , request_json )
db . session . delete ( post )
db . session . commit ( )
activity_log . result = ' success '
else :
activity_log . exception_message = ' Delete attempt denied '
2023-09-16 19:09:04 +12:00
else :
2024-02-25 15:31:16 +13:00
# Delete PostReply
2023-12-24 13:28:41 +13:00
reply = PostReply . query . filter_by ( ap_id = ap_id ) . first ( )
if reply :
2024-05-09 17:54:30 +12:00
if can_delete ( request_json [ ' actor ' ] , reply ) :
reply . body_html = ' <p><em>deleted</em></p> '
reply . body = ' deleted '
reply . post . reply_count - = 1
announce_activity_to_followers ( reply . community , reply . author , request_json )
db . session . commit ( )
activity_log . result = ' success '
else :
activity_log . exception_message = ' Delete attempt denied '
2024-02-25 15:31:16 +13:00
else :
# Delete User
user = find_actor_or_create ( ap_id , create_if_not_found = False )
if user :
user . deleted = True
user . delete_dependencies ( )
db . session . commit ( )
activity_log . result = ' success '
else :
activity_log . exception_message = ' Delete: cannot find ' + ap_id
2023-12-24 13:28:41 +13:00
elif request_json [ ' type ' ] == ' Like ' : # Upvote
activity_log . activity_type = request_json [ ' type ' ]
user_ap_id = request_json [ ' actor ' ]
user = find_actor_or_create ( user_ap_id )
2024-01-03 16:29:58 +13:00
liked = find_liked_object ( request_json [ ' object ' ] )
if user is None :
activity_log . exception_message = ' Blocked or unfound user '
2024-01-04 16:56:37 +13:00
elif liked is None :
2024-01-05 09:43:08 +13:00
activity_log . exception_message = ' Unfound object ' + request_json [ ' object ' ]
2024-01-03 16:29:58 +13:00
elif user . is_local ( ) :
activity_log . exception_message = ' Activity about local content which is already present '
activity_log . result = ' ignored '
elif can_upvote ( user , liked . community ) :
# insert into voted table
if liked is None :
activity_log . exception_message = ' Liked object not found '
elif liked is not None and isinstance ( liked , Post ) :
upvote_post ( liked , user )
activity_log . result = ' success '
elif liked is not None and isinstance ( liked , PostReply ) :
upvote_post_reply ( liked , user )
activity_log . result = ' success '
else :
activity_log . exception_message = ' Could not detect type of like '
if activity_log . result == ' success ' :
2024-04-17 15:10:04 +01:00
announce_activity_to_followers ( liked . community , user , request_json )
2024-01-02 19:41:00 +13:00
else :
2024-01-03 16:29:58 +13:00
activity_log . exception_message = ' Cannot upvote this '
activity_log . result = ' ignored '
2023-12-24 13:28:41 +13:00
elif request_json [ ' type ' ] == ' Dislike ' : # Downvote
if get_setting ( ' allow_dislike ' , True ) is False :
activity_log . exception_message = ' Dislike ignored because of allow_dislike setting '
else :
activity_log . activity_type = request_json [ ' type ' ]
user_ap_id = request_json [ ' actor ' ]
user = find_actor_or_create ( user_ap_id )
target_ap_id = request_json [ ' object ' ]
2024-01-03 16:29:58 +13:00
disliked = find_liked_object ( target_ap_id )
if user is None :
activity_log . exception_message = ' Blocked or unfound user '
2024-01-04 16:56:37 +13:00
elif disliked is None :
2024-01-05 09:43:08 +13:00
activity_log . exception_message = ' Unfound object ' + target_ap_id
2024-01-03 16:29:58 +13:00
elif user . is_local ( ) :
activity_log . exception_message = ' Activity about local content which is already present '
activity_log . result = ' ignored '
elif can_downvote ( user , disliked . community , site ) :
# insert into voted table
if disliked is None :
activity_log . exception_message = ' Liked object not found '
elif isinstance ( disliked , ( Post , PostReply ) ) :
if isinstance ( disliked , Post ) :
downvote_post ( disliked , user )
elif isinstance ( disliked , PostReply ) :
downvote_post_reply ( disliked , user )
activity_log . result = ' success '
else :
activity_log . exception_message = ' Could not detect type of like '
2024-04-17 15:10:04 +01:00
if activity_log . result == ' success ' :
announce_activity_to_followers ( disliked . community , user , request_json )
2023-12-24 13:28:41 +13:00
else :
2024-01-03 16:29:58 +13:00
activity_log . exception_message = ' Cannot downvote this '
activity_log . result = ' ignored '
2024-04-06 16:29:47 +13:00
elif request_json [ ' type ' ] == ' Flag ' : # Reported content
activity_log . activity_type = ' Report '
user_ap_id = request_json [ ' actor ' ]
user = find_actor_or_create ( user_ap_id )
target_ap_id = request_json [ ' object ' ]
reported = find_reported_object ( target_ap_id )
if user and reported :
process_report ( user , reported , request_json , activity_log )
2024-04-17 15:10:04 +01:00
announce_activity_to_followers ( reported . community , user , request_json )
2024-04-06 16:29:47 +13:00
activity_log . result = ' success '
else :
activity_log . exception_message = ' Report ignored due to missing user or content '
# Flush the caches of any major object that was created. To be sure.
2023-12-24 13:28:41 +13:00
if ' user ' in vars ( ) and user is not None :
2024-01-03 16:29:58 +13:00
if user . instance_id and user . instance_id != 1 :
2023-12-30 11:36:24 +13:00
user . instance . last_seen = utcnow ( )
2024-01-04 22:08:32 +13:00
# user.instance.ip_address = ip_address
2024-01-03 16:29:58 +13:00
user . instance . dormant = False
2024-05-12 16:08:20 +12:00
user . instance . gone_forever = False
user . instance . failures = 0
2023-09-09 20:46:40 +12:00
else :
2024-01-03 16:29:58 +13:00
activity_log . exception_message = ' Instance blocked '
2023-09-08 20:04:01 +12:00
2023-12-24 16:20:18 +13:00
if activity_log . exception_message is not None and activity_log . result == ' processing ' :
2023-12-24 13:28:41 +13:00
activity_log . result = ' failure '
db . session . commit ( )
2023-12-22 14:05:39 +13:00
2023-12-29 17:32:35 +13:00
@celery.task
2024-01-01 14:49:15 +13:00
def process_delete_request ( request_json , activitypublog_id , ip_address ) :
2023-12-29 17:32:35 +13:00
with current_app . app_context ( ) :
activity_log = ActivityPubLog . query . get ( activitypublog_id )
if ' type ' in request_json and request_json [ ' type ' ] == ' Delete ' :
2024-02-29 17:10:38 +13:00
if isinstance ( request_json [ ' object ' ] , dict ) :
2024-03-02 09:22:58 +13:00
# wafrn sends invalid delete requests
return
2024-02-29 17:10:38 +13:00
else :
actor_to_delete = request_json [ ' object ' ] . lower ( )
user = User . query . filter_by ( ap_profile_id = actor_to_delete ) . first ( )
if user :
# check that the user really has been deleted, to avoid spoofing attacks
if not user . is_local ( ) :
if user_removed_from_remote_server ( actor_to_delete , is_piefed = user . instance . software == ' PieFed ' ) :
# Delete all their images to save moderators from having to see disgusting stuff.
files = File . query . join ( Post ) . filter ( Post . user_id == user . id ) . all ( )
for file in files :
file . delete_from_disk ( )
file . source_url = ' '
if user . avatar_id :
user . avatar . delete_from_disk ( )
user . avatar . source_url = ' '
if user . cover_id :
user . cover . delete_from_disk ( )
user . cover . source_url = ' '
user . banned = True
user . deleted = True
activity_log . result = ' success '
else :
activity_log . result = ' ignored '
activity_log . exception_message = ' User not actually deleted. '
2024-01-01 14:49:15 +13:00
else :
activity_log . result = ' ignored '
2024-02-29 17:10:38 +13:00
activity_log . exception_message = ' Only remote users can be deleted remotely '
2023-12-29 17:32:35 +13:00
else :
activity_log . result = ' ignored '
2024-02-29 17:10:38 +13:00
activity_log . exception_message = ' Does not exist here '
db . session . commit ( )
2023-12-29 17:32:35 +13:00
2024-01-03 16:29:58 +13:00
def announce_activity_to_followers ( community , creator , activity ) :
2024-04-06 22:42:25 +01:00
# remove context from what will be inner object
del activity [ " @context " ]
2024-01-03 16:29:58 +13:00
announce_activity = {
' @context ' : default_context ( ) ,
" actor " : community . profile_id ( ) ,
" to " : [
" https://www.w3.org/ns/activitystreams#Public "
] ,
" object " : activity ,
" cc " : [
f " { community . profile_id ( ) } /followers "
] ,
" type " : " Announce " ,
2024-04-17 15:10:04 +01:00
" id " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } /activities/announce/ { gibberish ( 15 ) } "
2024-01-03 16:29:58 +13:00
}
for instance in community . following_instances ( include_dormant = True ) :
# awaken dormant instances if they've been sleeping for long enough to be worth trying again
awaken_dormant_instance ( instance )
# All good? Send!
if instance and instance . online ( ) and not instance_blocked ( instance . inbox ) :
if creator . instance_id != instance . id : # don't send it to the instance that hosts the creator as presumably they already have the content
send_to_remote_instance ( instance . id , community . id , announce_activity )
2023-08-10 21:13:37 +12:00
@bp.route ( ' /c/<actor>/outbox ' , methods = [ ' GET ' ] )
def community_outbox ( actor ) :
actor = actor . strip ( )
community = Community . query . filter_by ( name = actor , banned = False , ap_id = None ) . first ( )
if community is not None :
posts = community . posts . limit ( 50 ) . all ( )
community_data = {
2023-09-16 19:09:04 +12:00
" @context " : default_context ( ) ,
2023-08-10 21:13:37 +12:00
" type " : " OrderedCollection " ,
" id " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } /c/ { actor } /outbox " ,
" totalItems " : len ( posts ) ,
" orderedItems " : [ ]
}
for post in posts :
community_data [ ' orderedItems ' ] . append ( post_to_activity ( post , community ) )
2023-08-05 21:24:10 +12:00
2023-08-10 21:13:37 +12:00
return jsonify ( community_data )
2023-08-05 21:24:10 +12:00
2024-03-24 22:10:41 +00:00
@bp.route ( ' /c/<actor>/featured ' , methods = [ ' GET ' ] )
def community_featured ( actor ) :
actor = actor . strip ( )
community = Community . query . filter_by ( name = actor , banned = False , ap_id = None ) . first ( )
if community is not None :
posts = Post . query . filter_by ( community_id = community . id , sticky = True ) . all ( )
community_data = {
" @context " : default_context ( ) ,
" type " : " OrderedCollection " ,
" id " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } /c/ { actor } /featured " ,
" totalItems " : len ( posts ) ,
" orderedItems " : [ ]
}
for post in posts :
community_data [ ' orderedItems ' ] . append ( post_to_page ( post , community ) )
return jsonify ( community_data )
2023-11-26 23:20:51 +13:00
@bp.route ( ' /c/<actor>/moderators ' , methods = [ ' GET ' ] )
2024-03-13 16:40:20 +13:00
def community_moderators_route ( actor ) :
2023-11-26 23:20:51 +13:00
actor = actor . strip ( )
community = Community . query . filter_by ( name = actor , banned = False , ap_id = None ) . first ( )
if community is not None :
2024-03-13 16:40:20 +13:00
moderator_ids = community_moderators ( community . id )
2023-11-26 23:20:51 +13:00
moderators = User . query . filter ( User . id . in_ ( [ mod . user_id for mod in moderator_ids ] ) ) . all ( )
community_data = {
" @context " : default_context ( ) ,
" type " : " OrderedCollection " ,
" id " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } /c/ { actor } /moderators " ,
" totalItems " : len ( moderators ) ,
" orderedItems " : [ ]
}
for moderator in moderators :
community_data [ ' orderedItems ' ] . append ( moderator . ap_profile_id )
return jsonify ( community_data )
2024-04-29 16:13:29 +01:00
@bp.route ( ' /u/<actor>/inbox ' , methods = [ ' POST ' ] )
2023-12-27 14:38:41 +13:00
def user_inbox ( actor ) :
2024-04-29 16:13:29 +01:00
site = Site . query . get ( 1 )
activity_log = ActivityPubLog ( direction = ' in ' , result = ' failure ' )
activity_log . result = ' processing '
db . session . add ( activity_log )
db . session . commit ( )
try :
request_json = request . get_json ( force = True )
except werkzeug . exceptions . BadRequest as e :
activity_log . exception_message = ' Unable to parse json body: ' + e . description
activity_log . result = ' failure '
db . session . commit ( )
return ' ' , 400
if ' id ' in request_json :
activity_log . activity_id = request_json [ ' id ' ]
if site . log_activitypub_json :
activity_log . activity_json = json . dumps ( request_json )
2024-05-04 22:32:17 +01:00
actor = find_actor_or_create ( request_json [ ' actor ' ] , signed_get = True ) if ' actor ' in request_json else None
2024-04-29 16:13:29 +01:00
if actor is not None :
2024-05-04 23:22:47 +01:00
if ( ( ' type ' in request_json and request_json [ ' type ' ] == ' Like ' ) or
( ' type ' in request_json and request_json [ ' type ' ] == ' Undo ' and
' object ' in request_json and request_json [ ' object ' ] [ ' type ' ] == ' Like ' ) ) :
return shared_inbox ( )
2024-04-29 16:13:29 +01:00
try :
HttpSignature . verify_request ( request , actor . public_key , skip_date = True )
if ' type ' in request_json and request_json [ ' type ' ] == ' Follow ' :
if current_app . debug :
process_user_follow_request ( request_json , activity_log . id , actor . id )
else :
process_user_follow_request . delay ( request_json , activity_log . id , actor . id )
return ' '
2024-04-29 17:13:15 +01:00
if ( ' type ' in request_json and request_json [ ' type ' ] == ' Undo ' and
' object ' in request_json and request_json [ ' object ' ] [ ' type ' ] == ' Follow ' ) :
if current_app . debug :
process_user_undo_follow_request ( request_json , activity_log . id , actor . id )
else :
process_user_undo_follow_request . delay ( request_json , activity_log . id , actor . id )
return ' '
2024-04-29 16:13:29 +01:00
except VerificationError :
activity_log . result = ' failure '
activity_log . exception_message = ' Could not verify signature '
db . session . commit ( )
return ' ' , 400
else :
actor_name = request_json [ ' actor ' ] if ' actor ' in request_json else ' '
activity_log . exception_message = f ' Actor could not be found: { actor_name } '
if activity_log . exception_message is not None :
activity_log . result = ' failure '
db . session . commit ( )
2023-12-27 14:38:41 +13:00
resp = jsonify ( ' ok ' )
resp . content_type = ' application/activity+json '
return resp
2023-08-05 21:24:10 +12:00
2024-05-03 06:27:25 +12:00
@celery.task
2024-04-29 16:13:29 +01:00
def process_user_follow_request ( request_json , activitypublog_id , remote_user_id ) :
activity_log = ActivityPubLog . query . get ( activitypublog_id )
local_user_ap_id = request_json [ ' object ' ]
follow_id = request_json [ ' id ' ]
2024-04-29 17:13:15 +01:00
local_user = find_actor_or_create ( local_user_ap_id , create_if_not_found = False )
2024-04-29 16:13:29 +01:00
remote_user = User . query . get ( remote_user_id )
if local_user and local_user . is_local ( ) and not remote_user . is_local ( ) :
existing_follower = UserFollower . query . filter_by ( local_user_id = local_user . id , remote_user_id = remote_user . id ) . first ( )
if not existing_follower :
auto_accept = not local_user . ap_manually_approves_followers
new_follower = UserFollower ( local_user_id = local_user . id , remote_user_id = remote_user . id , is_accepted = auto_accept )
2024-05-04 22:44:51 +01:00
if not local_user . ap_followers_url :
local_user . ap_followers_url = local_user . ap_public_url + ' /followers '
2024-04-29 16:13:29 +01:00
db . session . add ( new_follower )
accept = {
" @context " : default_context ( ) ,
" actor " : local_user . ap_profile_id ,
" to " : [
remote_user . ap_profile_id
] ,
" object " : {
" actor " : remote_user . ap_profile_id ,
" to " : None ,
" object " : local_user . ap_profile_id ,
" type " : " Follow " ,
" id " : follow_id
} ,
" type " : " Accept " ,
" id " : f " https:// { current_app . config [ ' SERVER_NAME ' ] } /activities/accept/ " + gibberish ( 32 )
}
if post_request ( remote_user . ap_inbox_url , accept , local_user . private_key , f " https:// { current_app . config [ ' SERVER_NAME ' ] } /u/ { local_user . user_name } #main-key " ) :
activity_log . result = ' success '
else :
activity_log . exception_message = ' Error sending Accept '
2024-04-30 12:41:37 +01:00
else :
activity_log . exception_message = ' Could not find local user '
activity_log . result = ' failure '
2024-04-29 16:13:29 +01:00
db . session . commit ( )
2024-04-29 17:13:15 +01:00
2024-05-03 06:27:25 +12:00
@celery.task
2024-04-29 17:13:15 +01:00
def process_user_undo_follow_request ( request_json , activitypublog_id , remote_user_id ) :
activity_log = ActivityPubLog . query . get ( activitypublog_id )
local_user_ap_id = request_json [ ' object ' ] [ ' object ' ]
local_user = find_actor_or_create ( local_user_ap_id , create_if_not_found = False )
remote_user = User . query . get ( remote_user_id )
if local_user :
2024-04-30 12:50:43 +01:00
db . session . query ( UserFollower ) . filter_by ( local_user_id = local_user . id , remote_user_id = remote_user . id , is_accepted = True ) . delete ( )
2024-04-29 17:13:15 +01:00
activity_log . result = ' success '
2024-04-30 12:41:37 +01:00
else :
activity_log . exception_message = ' Could not find local user '
activity_log . result = ' failure '
db . session . commit ( )
2024-04-29 16:13:29 +01:00
2023-12-27 14:38:41 +13:00
@bp.route ( ' /c/<actor>/inbox ' , methods = [ ' GET ' , ' POST ' ] )
def community_inbox ( actor ) :
return shared_inbox ( )
2023-08-05 21:24:10 +12:00
2023-12-27 14:38:41 +13:00
@bp.route ( ' /c/<actor>/followers ' , methods = [ ' GET ' ] )
def community_followers ( actor ) :
actor = actor . strip ( )
community = Community . query . filter_by ( name = actor , banned = False , ap_id = None ) . first ( )
if community is not None :
result = {
" @context " : default_context ( ) ,
2024-03-24 22:10:41 +00:00
" id " : f ' https:// { current_app . config [ " SERVER_NAME " ] } /c/ { actor } /followers ' ,
2023-12-27 14:38:41 +13:00
" type " : " Collection " ,
" totalItems " : community_members ( community . id ) ,
" items " : [ ]
}
resp = jsonify ( result )
resp . content_type = ' application/activity+json '
return resp
else :
abort ( 404 )
2023-12-09 22:14:16 +13:00
2024-04-29 19:47:06 +01:00
@bp.route ( ' /u/<actor>/followers ' , methods = [ ' GET ' ] )
def user_followers ( actor ) :
actor = actor . strip ( )
user = User . query . filter_by ( user_name = actor , banned = False , ap_id = None ) . first ( )
if user is not None and user . ap_followers_url :
2024-05-08 19:28:49 +12:00
# Get all followers, except those that are blocked by user by doing an outer join
followers = User . query . join ( UserFollower , User . id == UserFollower . remote_user_id ) \
. outerjoin ( UserBlock , ( User . id == UserBlock . blocker_id ) & ( UserFollower . local_user_id == UserBlock . blocked_id ) ) \
. filter ( ( UserFollower . local_user_id == user . id ) & ( UserBlock . id == None ) ) \
. all ( )
2024-04-29 19:47:06 +01:00
items = [ ]
for f in followers :
items . append ( f . ap_public_url )
result = {
" @context " : default_context ( ) ,
" id " : user . ap_followers_url ,
" type " : " Collection " ,
" totalItems " : len ( items ) ,
" items " : items
}
resp = jsonify ( result )
resp . content_type = ' application/activity+json '
return resp
else :
abort ( 404 )
2023-12-09 22:14:16 +13:00
@bp.route ( ' /comment/<int:comment_id> ' , methods = [ ' GET ' ] )
def comment_ap ( comment_id ) :
if is_activitypub_request ( ) :
reply = PostReply . query . get_or_404 ( comment_id )
reply_data = {
" @context " : default_context ( ) ,
" type " : " Note " ,
" id " : reply . ap_id ,
" attributedTo " : reply . author . profile_id ( ) ,
" inReplyTo " : reply . in_reply_to ( ) ,
" to " : [
" https://www.w3.org/ns/activitystreams#Public " ,
reply . to ( )
] ,
" cc " : [
reply . community . profile_id ( ) ,
reply . author . followers_url ( )
] ,
' content ' : reply . body_html ,
' mediaType ' : ' text/html ' ,
' published ' : ap_datetime ( reply . created_at ) ,
' distinguished ' : False ,
2024-05-09 17:54:30 +12:00
' audience ' : reply . community . profile_id ( ) ,
' language ' : {
' identifier ' : reply . language_code ( ) ,
' name ' : reply . language_name ( )
}
2023-12-09 22:14:16 +13:00
}
if reply . edited_at :
reply_data [ ' updated ' ] = ap_datetime ( reply . edited_at )
if reply . body . strip ( ) :
reply_data [ ' source ' ] = {
' content ' : reply . body ,
' mediaType ' : ' text/markdown '
}
resp = jsonify ( reply_data )
resp . content_type = ' application/activity+json '
2024-03-02 13:56:47 +13:00
resp . headers . set ( ' Vary ' , ' Accept ' )
2023-12-09 22:14:16 +13:00
return resp
else :
2024-02-23 20:23:59 +13:00
reply = PostReply . query . get_or_404 ( comment_id )
2023-12-30 11:36:24 +13:00
return continue_discussion ( reply . post . id , comment_id )
2023-12-09 22:14:16 +13:00
2024-01-12 20:21:41 +13:00
@bp.route ( ' /post/<int:post_id>/ ' , methods = [ ' GET ' ] )
def post_ap2 ( post_id ) :
return redirect ( url_for ( ' activitypub.post_ap ' , post_id = post_id ) )
2023-12-09 22:14:16 +13:00
@bp.route ( ' /post/<int:post_id> ' , methods = [ ' GET ' , ' POST ' ] )
def post_ap ( post_id ) :
if request . method == ' GET ' and is_activitypub_request ( ) :
post = Post . query . get_or_404 ( post_id )
post_data = post_to_activity ( post , post . community )
post_data = post_data [ ' object ' ] [ ' object ' ]
post_data [ ' @context ' ] = default_context ( )
resp = jsonify ( post_data )
resp . content_type = ' application/activity+json '
2024-03-02 13:56:47 +13:00
resp . headers . set ( ' Vary ' , ' Accept ' )
2023-12-09 22:14:16 +13:00
return resp
else :
return show_post ( post_id )
2023-12-22 14:05:39 +13:00
@bp.route ( ' /activities/<type>/<id> ' )
2023-12-23 11:32:22 +13:00
@cache.cached ( timeout = 600 )
2023-12-22 14:05:39 +13:00
def activities_json ( type , id ) :
activity = ActivityPubLog . query . filter_by ( activity_id = f " https:// { current_app . config [ ' SERVER_NAME ' ] } /activities/ { type } / { id } " ) . first ( )
if activity :
2024-03-17 02:02:32 +13:00
if activity . activity_json is not None :
activity_json = json . loads ( activity . activity_json )
else :
activity_json = { }
2023-12-22 15:34:45 +13:00
resp = jsonify ( activity_json )
resp . content_type = ' application/activity+json '
return resp
2023-12-22 14:05:39 +13:00
else :
abort ( 404 )
2024-04-20 20:46:51 +12:00
# Other instances can query the result of their POST to the inbox by using this endpoint. The ID of the activity they
# sent (minus the https:// on the front) is the id parameter. e.g. https://piefed.ngrok.app/activity_result/piefed.ngrok.app/activities/announce/EfjyZ3BE5SzQK0C
@bp.route ( ' /activity_result/<path:id> ' )
def activity_result ( id ) :
activity = ActivityPubLog . query . filter_by ( activity_id = f ' https:// { id } ' ) . first ( )
if activity :
if activity . result == ' success ' :
return jsonify ( ' Ok ' )
else :
return jsonify ( { ' error ' : activity . result , ' message ' : activity . exception_message } )
else :
abort ( 404 )