Merge pull request 'refactor-aproutes' (#357) from refactor-aproutes into main

Reviewed-on: https://codeberg.org/rimu/pyfedi/pulls/357
This commit is contained in:
rimu 2024-11-27 21:10:01 +00:00
commit 3553534c00
5 changed files with 1224 additions and 1275 deletions

File diff suppressed because it is too large Load diff

View file

@ -258,7 +258,7 @@ def instance_allowed(host: str) -> bool:
return instance is not None return instance is not None
def find_actor_or_create(actor: str, create_if_not_found=True, community_only=False, signed_get=False) -> Union[User, Community, None]: def find_actor_or_create(actor: str, create_if_not_found=True, community_only=False) -> Union[User, Community, None]:
if isinstance(actor, dict): # Discourse does this if isinstance(actor, dict): # Discourse does this
actor = actor['id'] actor = actor['id']
actor_url = actor.strip() actor_url = actor.strip()
@ -316,34 +316,37 @@ def find_actor_or_create(actor: str, create_if_not_found=True, community_only=Fa
else: # User does not exist in the DB, it's going to need to be created from it's remote home instance else: # User does not exist in the DB, it's going to need to be created from it's remote home instance
if create_if_not_found: if create_if_not_found:
if actor.startswith('https://'): if actor.startswith('https://'):
if not signed_get: try:
actor_data = get_request(actor_url, headers={'Accept': 'application/activity+json'})
except httpx.HTTPError:
time.sleep(randint(3, 10))
try: try:
actor_data = get_request(actor_url, headers={'Accept': 'application/activity+json'}) actor_data = get_request(actor_url, headers={'Accept': 'application/activity+json'})
except httpx.HTTPError: except httpx.HTTPError as e:
time.sleep(randint(3, 10)) raise e
try: return None
actor_data = get_request(actor_url, headers={'Accept': 'application/activity+json'}) if actor_data.status_code == 200:
except httpx.HTTPError as e: try:
raise e actor_json = actor_data.json()
return None except Exception as e:
if actor_data.status_code == 200:
try:
actor_json = actor_data.json()
except Exception as e:
actor_data.close()
return None
actor_data.close() actor_data.close()
actor_model = actor_json_to_model(actor_json, address, server) return None
if community_only and not isinstance(actor_model, Community): actor_data.close()
return None actor_model = actor_json_to_model(actor_json, address, server)
return actor_model if community_only and not isinstance(actor_model, Community):
else: return None
return actor_model
elif actor_data.status_code == 401:
try: try:
site = Site.query.get(1) site = Site.query.get(1)
actor_data = signed_get_request(actor_url, site.private_key, actor_data = signed_get_request(actor_url, site.private_key,
f"https://{current_app.config['SERVER_NAME']}/actor#main-key") f"https://{current_app.config['SERVER_NAME']}/actor#main-key")
if actor_data.status_code == 200: if actor_data.status_code == 200:
actor_json = actor_data.json() try:
actor_json = actor_data.json()
except Exception as e:
actor_data.close()
return None
actor_data.close() actor_data.close()
actor_model = actor_json_to_model(actor_json, address, server) actor_model = actor_json_to_model(actor_json, address, server)
if community_only and not isinstance(actor_model, Community): if community_only and not isinstance(actor_model, Community):
@ -1337,116 +1340,122 @@ def is_activitypub_request():
return 'application/ld+json' in request.headers.get('Accept', '') or 'application/activity+json' in request.headers.get('Accept', '') return 'application/ld+json' in request.headers.get('Accept', '') or 'application/activity+json' in request.headers.get('Accept', '')
def delete_post_or_comment(user_ap_id, to_be_deleted_ap_id, aplog_id): def delete_post_or_comment(deletor, to_delete, store_ap_json, request_json):
deletor = find_actor_or_create(user_ap_id) community = to_delete.community
to_delete = find_liked_object(to_be_deleted_ap_id) if to_delete.user_id == deletor.id or deletor.is_admin() or community.is_moderator(deletor) or community.is_instance_admin(deletor):
aplog = ActivityPubLog.query.get(aplog_id) if isinstance(to_delete, Post):
to_delete.deleted = True
if to_delete and to_delete.deleted: to_delete.deleted_by = deletor.id
if aplog: community.post_count -= 1
aplog.result = 'ignored' to_delete.author.post_count -= 1
aplog.exception_message = 'Activity about local content which is already deleted' if to_delete.url and to_delete.cross_posts is not None:
return old_cross_posts = Post.query.filter(Post.id.in_(to_delete.cross_posts)).all()
to_delete.cross_posts.clear()
if deletor and to_delete: for ocp in old_cross_posts:
community = to_delete.community if ocp.cross_posts is not None and to_delete.id in ocp.cross_posts:
if to_delete.author.id == deletor.id or deletor.is_admin() or community.is_moderator(deletor) or community.is_instance_admin(deletor): ocp.cross_posts.remove(to_delete.id)
if isinstance(to_delete, Post): db.session.commit()
to_delete.deleted = True if to_delete.author.id != deletor.id:
to_delete.deleted_by = deletor.id add_to_modlog_activitypub('delete_post', deletor, community_id=community.id,
community.post_count -= 1 link_text=shorten_string(to_delete.title), link=f'post/{to_delete.id}')
to_delete.author.post_count -= 1 elif isinstance(to_delete, PostReply):
if to_delete.url and to_delete.cross_posts is not None: to_delete.deleted = True
old_cross_posts = Post.query.filter(Post.id.in_(to_delete.cross_posts)).all() to_delete.deleted_by = deletor.id
to_delete.cross_posts.clear() to_delete.author.post_reply_count -= 1
for ocp in old_cross_posts: community.post_reply_count -= 1
if ocp.cross_posts is not None and to_delete.id in ocp.cross_posts: if not to_delete.author.bot:
ocp.cross_posts.remove(to_delete.id) to_delete.post.reply_count -= 1
db.session.commit() db.session.commit()
if to_delete.author.id != deletor.id: if to_delete.author.id != deletor.id:
add_to_modlog_activitypub('delete_post', deletor, community_id=community.id, add_to_modlog_activitypub('delete_post_reply', deletor, community_id=community.id,
link_text=shorten_string(to_delete.title), link=f'post/{to_delete.id}') link_text=f'comment on {shorten_string(to_delete.post.title)}',
elif isinstance(to_delete, PostReply): link=f'post/{to_delete.post.id}#comment_{to_delete.id}')
to_delete.deleted = True log_incoming_ap(request_json['id'], APLOG_DELETE, APLOG_SUCCESS, request_json if store_ap_json else None)
to_delete.deleted_by = deletor.id
to_delete.author.post_reply_count -= 1
if not to_delete.author.bot:
to_delete.post.reply_count -= 1
db.session.commit()
if to_delete.author.id != deletor.id:
add_to_modlog_activitypub('delete_post_reply', deletor, community_id=community.id,
link_text=f'comment on {shorten_string(to_delete.post.title)}',
link=f'post/{to_delete.post.id}#comment_{to_delete.id}')
if aplog:
aplog.result = 'success'
else:
if aplog:
aplog.result = 'failure'
aplog.exception_message = 'Deletor did not have permission'
else: else:
if aplog: log_incoming_ap(request_json['id'], APLOG_DELETE, APLOG_FAILURE, request_json if store_ap_json else None, 'Deletor did not have permisson')
aplog.result = 'failure'
aplog.exception_message = 'Unable to resolve deletor, or target'
def restore_post_or_comment(object_json, aplog_id): def restore_post_or_comment(restorer, to_restore, store_ap_json, request_json):
restorer = find_actor_or_create(object_json['actor']) if 'actor' in object_json else None community = to_restore.community
to_restore = find_liked_object(object_json['object']) if 'object' in object_json else None if to_restore.user_id == restorer.id or restorer.is_admin() or community.is_moderator(restorer) or community.is_instance_admin(restorer):
aplog = ActivityPubLog.query.get(aplog_id) if isinstance(to_restore, Post):
to_restore.deleted = False
to_restore.deleted_by = None
community.post_count += 1
to_restore.author.post_count += 1
if to_restore.url:
new_cross_posts = Post.query.filter(Post.id != to_restore.id, Post.url == to_restore.url, Post.deleted == False,
Post.posted_at > utcnow() - timedelta(days=6)).all()
for ncp in new_cross_posts:
if ncp.cross_posts is None:
ncp.cross_posts = [to_restore.id]
else:
ncp.cross_posts.append(to_restore.id)
if to_restore.cross_posts is None:
to_restore.cross_posts = [ncp.id]
else:
to_restore.cross_posts.append(ncp.id)
db.session.commit()
if to_restore.author.id != restorer.id:
add_to_modlog_activitypub('restore_post', restorer, community_id=community.id,
link_text=shorten_string(to_restore.title), link=f'post/{to_restore.id}')
if to_restore and not to_restore.deleted: elif isinstance(to_restore, PostReply):
if aplog: to_restore.deleted = False
aplog.result = 'ignored' to_restore.deleted_by = None
aplog.exception_message = 'Activity about local content which is already restored' if not to_restore.author.bot:
return to_restore.post.reply_count += 1
to_restore.author.post_reply_count += 1
if restorer and to_restore: db.session.commit()
community = to_restore.community if to_restore.author.id != restorer.id:
if to_restore.author.id == restorer.id or restorer.is_admin() or community.is_moderator(restorer) or community.is_instance_admin(restorer): add_to_modlog_activitypub('restore_post_reply', restorer, community_id=community.id,
if isinstance(to_restore, Post): link_text=f'comment on {shorten_string(to_restore.post.title)}',
to_restore.deleted = False link=f'post/{to_restore.post_id}#comment_{to_restore.id}')
to_restore.deleted_by = None log_incoming_ap(request_json['id'], APLOG_UNDO_DELETE, APLOG_SUCCESS, request_json if store_ap_json else None)
community.post_count += 1
to_restore.author.post_count += 1
if to_restore.url:
new_cross_posts = Post.query.filter(Post.id != to_restore.id, Post.url == to_restore.url, Post.deleted == False,
Post.posted_at > utcnow() - timedelta(days=6)).all()
for ncp in new_cross_posts:
if ncp.cross_posts is None:
ncp.cross_posts = [to_restore.id]
else:
ncp.cross_posts.append(to_restore.id)
if to_restore.cross_posts is None:
to_restore.cross_posts = [ncp.id]
else:
to_restore.cross_posts.append(ncp.id)
db.session.commit()
if to_restore.author.id != restorer.id:
add_to_modlog_activitypub('restore_post', restorer, community_id=community.id,
link_text=shorten_string(to_restore.title), link=f'post/{to_restore.id}')
elif isinstance(to_restore, PostReply):
to_restore.deleted = False
to_restore.deleted_by = None
if not to_restore.author.bot:
to_restore.post.reply_count += 1
to_restore.author.post_reply_count += 1
db.session.commit()
if to_restore.author.id != restorer.id:
add_to_modlog_activitypub('restore_post_reply', restorer, community_id=community.id,
link_text=f'comment on {shorten_string(to_restore.post.title)}',
link=f'post/{to_restore.post_id}#comment_{to_restore.id}')
if aplog:
aplog.result = 'success'
else:
if aplog:
aplog.result = 'failure'
aplog.exception_message = 'Restorer did not have permission'
else: else:
if aplog: log_incoming_ap(request_json['id'], APLOG_UNDO_DELETE, APLOG_FAILURE, request_json if store_ap_json else None, 'Restorer did not have permisson')
aplog.result = 'failure'
aplog.exception_message = 'Unable to resolve restorer or target'
def site_ban_remove_data(blocker_id, blocked):
replies = PostReply.query.filter_by(user_id=blocked.id, deleted=False)
for reply in replies:
reply.deleted = True
reply.deleted_by = blocker_id
if not blocked.bot:
reply.post.reply_count -= 1
reply.community.post_reply_count -= 1
blocked.reply_count = 0
db.session.commit()
posts = Post.query.filter_by(user_id=blocked.id, deleted=False)
for post in posts:
post.deleted = True
post.deleted_by = blocker_id
post.community.post_count -= 1
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 and post.id in ocp.cross_posts:
ocp.cross_posts.remove(post.id)
blocked.post_count = 0
db.session.commit()
# Delete all their images to save moderators from having to see disgusting stuff.
# Images attached to posts can't be restored, but site ban reversals don't have a 'removeData' field anyway.
files = File.query.join(Post).filter(Post.user_id == blocked.id).all()
for file in files:
file.delete_from_disk()
file.source_url = ''
if blocked.avatar_id:
blocked.avatar.delete_from_disk()
blocked.avatar.source_url = ''
if blocked.cover_id:
blocked.cover.delete_from_disk()
blocked.cover.source_url = ''
# blocked.banned = True # uncommented until there's a mechanism for processing ban expiry date
db.session.commit()
def remove_data_from_banned_user(deletor_ap_id, user_ap_id, target): def remove_data_from_banned_user(deletor_ap_id, user_ap_id, target):
@ -1496,132 +1505,98 @@ def remove_data_from_banned_user_task(deletor_ap_id, user_ap_id, target):
db.session.commit() db.session.commit()
def ban_local_user(deletor_ap_id, user_ap_id, target, request_json): def community_ban_remove_data(blocker_id, community_id, blocked):
if current_app.debug: replies = PostReply.query.filter_by(user_id=blocked.id, deleted=False, community_id=community_id)
ban_local_user_task(deletor_ap_id, user_ap_id, target, request_json) for reply in replies:
else: reply.deleted = True
ban_local_user_task.delay(deletor_ap_id, user_ap_id, target, request_json) reply.deleted_by = blocker_id
if not blocked.bot:
reply.post.reply_count -= 1
reply.community.post_reply_count -= 1
blocked.post_reply_count -= 1
db.session.commit()
posts = Post.query.filter_by(user_id=blocked.id, deleted=False, community_id=community_id)
for post in posts:
post.deleted = True
post.deleted_by = blocker_id
post.community.post_count -= 1
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 and post.id in ocp.cross_posts:
ocp.cross_posts.remove(post.id)
blocked.post_count -= 1
db.session.commit()
# Delete attached images to save moderators from having to see disgusting stuff.
files = File.query.join(Post).filter(Post.user_id == blocked.id, Post.community_id == community_id).all()
for file in files:
file.delete_from_disk()
file.source_url = ''
db.session.commit()
@celery.task def ban_local_user(blocker, blocked, community, request_json):
def ban_local_user_task(deletor_ap_id, user_ap_id, target, request_json): existing = CommunityBan.query.filter_by(community_id=community.id, user_id=blocked.id).first()
# same info in 'Block' and 'Announce/Block' can be sent at same time, and both call this function if not existing:
ban_in_progress = cache.get(f'{deletor_ap_id} is banning {user_ap_id} from {target}') new_ban = CommunityBan(community_id=community.id, user_id=blocked.id, banned_by=blocker.id)
if not ban_in_progress: if 'summary' in request_json:
cache.set(f'{deletor_ap_id} is banning {user_ap_id} from {target}', True, timeout=60) new_ban.reason=request_json['object']['summary']
else: if 'expires' in request_json and datetime.fromisoformat(request_json['object']['expires']) > datetime.now(timezone.utc):
return new_ban.ban_until = datetime.fromisoformat(request_json['object']['expires'])
elif 'endTime' in request_json and datetime.fromisoformat(request_json['object']['endTime']) > datetime.now(timezone.utc):
new_ban.ban_until = datetime.fromisoformat(request_json['object']['endTime'])
db.session.add(new_ban)
db.session.commit()
deletor = find_actor_or_create(deletor_ap_id, create_if_not_found=False) db.session.query(CommunityJoinRequest).filter(CommunityJoinRequest.community_id == community.id, CommunityJoinRequest.user_id == blocked.id).delete()
user = find_actor_or_create(user_ap_id, create_if_not_found=False) community_membership_record = CommunityMember.query.filter_by(community_id=community.id, user_id=blocked.id).first()
community = Community.query.filter_by(ap_profile_id=target).first()
if not deletor or not user:
return
# site bans by admins
if deletor.instance.user_is_admin(deletor.id) and target == f"https://{deletor.instance.domain}/":
# need instance_ban table?
...
# community bans by mods or admins
elif community and (community.is_moderator(deletor) or community.is_instance_admin(deletor)):
existing = CommunityBan.query.filter_by(community_id=community.id, user_id=user.id).first()
if not existing:
new_ban = CommunityBan(community_id=community.id, user_id=user.id, banned_by=deletor.id)
if 'summary' in request_json:
new_ban.reason=request_json['summary']
if 'expires' in request_json and datetime.fromisoformat(request_json['expires']) > datetime.now(timezone.utc):
new_ban.ban_until = datetime.fromisoformat(request_json['expires'])
elif 'endTime' in request_json and datetime.fromisoformat(request_json['endTime']) > datetime.now(timezone.utc):
new_ban.ban_until = datetime.fromisoformat(request_json['endTime'])
db.session.add(new_ban)
db.session.commit()
db.session.query(CommunityJoinRequest).filter(CommunityJoinRequest.community_id == community.id, CommunityJoinRequest.user_id == user.id).delete()
community_membership_record = CommunityMember.query.filter_by(community_id=community.id, user_id=user.id).first()
if community_membership_record: if community_membership_record:
community_membership_record.is_banned = True community_membership_record.is_banned = True
cache.delete_memoized(communities_banned_from, user.id)
cache.delete_memoized(joined_communities, user.id)
cache.delete_memoized(moderating_communities, user.id)
# Notify banned person # Notify banned person
notify = Notification(title=shorten_string('You have been banned from ' + community.title), notify = Notification(title=shorten_string('You have been banned from ' + community.title),
url=f'/notifications', user_id=user.id, url=f'/notifications', user_id=blocked.id,
author_id=deletor.id) author_id=blocker.id)
db.session.add(notify) db.session.add(notify)
if not current_app.debug: # user.unread_notifications += 1 hangs app if 'user' is the same person if not current_app.debug: # user.unread_notifications += 1 hangs app if 'user' is the same person
user.unread_notifications += 1 # who pressed 'Re-submit this activity'. blocked.unread_notifications += 1 # who pressed 'Re-submit this activity'.
db.session.commit()
# Remove their notification subscription, if any # Remove their notification subscription, if any
db.session.query(NotificationSubscription).filter(NotificationSubscription.entity_id == community.id, db.session.query(NotificationSubscription).filter(NotificationSubscription.entity_id == community.id,
NotificationSubscription.user_id == user.id, NotificationSubscription.user_id == blocked.id,
NotificationSubscription.type == NOTIF_COMMUNITY).delete() NotificationSubscription.type == NOTIF_COMMUNITY).delete()
add_to_modlog_activitypub('ban_user', deletor, community_id=community.id, link_text=user.display_name(), link=user.link())
def unban_local_user(deletor_ap_id, user_ap_id, target):
if current_app.debug:
unban_local_user_task(deletor_ap_id, user_ap_id, target)
else:
unban_local_user_task.delay(deletor_ap_id, user_ap_id, target)
@celery.task
def unban_local_user_task(deletor_ap_id, user_ap_id, target):
# same info in 'Block' and 'Announce/Block' can be sent at same time, and both call this function
unban_in_progress = cache.get(f'{deletor_ap_id} is undoing ban of {user_ap_id} from {target}')
if not unban_in_progress:
cache.set(f'{deletor_ap_id} is undoing ban of {user_ap_id} from {target}', True, timeout=60)
else:
return
deletor = find_actor_or_create(deletor_ap_id, create_if_not_found=False)
user = find_actor_or_create(user_ap_id, create_if_not_found=False)
community = Community.query.filter_by(ap_profile_id=target).first()
if not deletor or not user:
return
# site undo bans by admins
if deletor.instance.user_is_admin(deletor.id) and target == f"https://{deletor.instance.domain}/":
# need instance_ban table?
...
# community undo bans by mods or admins
elif community and (community.is_moderator(deletor) or community.is_instance_admin(deletor)):
existing_ban = CommunityBan.query.filter_by(community_id=community.id, user_id=user.id).first()
if existing_ban:
db.session.delete(existing_ban)
db.session.commit()
community_membership_record = CommunityMember.query.filter_by(community_id=community.id, user_id=user.id).first()
if community_membership_record:
community_membership_record.is_banned = False
db.session.commit()
cache.delete_memoized(communities_banned_from, user.id)
cache.delete_memoized(joined_communities, user.id)
cache.delete_memoized(moderating_communities, user.id)
# Notify previously banned person
notify = Notification(title=shorten_string('You have been un-banned from ' + community.title),
url=f'/notifications', user_id=user.id,
author_id=deletor.id)
db.session.add(notify)
if not current_app.debug: # user.unread_notifications += 1 hangs app if 'user' is the same person
user.unread_notifications += 1 # who pressed 'Re-submit this activity'.
db.session.commit() db.session.commit()
add_to_modlog_activitypub('unban_user', deletor, community_id=community.id, link_text=user.display_name(), link=user.link()) cache.delete_memoized(communities_banned_from, blocked.id)
cache.delete_memoized(joined_communities, blocked.id)
cache.delete_memoized(moderating_communities, blocked.id)
add_to_modlog_activitypub('ban_user', blocker, community_id=community.id, link_text=blocked.display_name(), link=blocked.link())
def unban_local_user(blocker, blocked, community, request_json):
db.session.query(CommunityBan).filter(CommunityBan.community_id == community.id, CommunityBan.user_id == blocked.id).delete()
community_membership_record = CommunityMember.query.filter_by(community_id=community.id, user_id=blocked.id).first()
if community_membership_record:
community_membership_record.is_banned = False
# Notify unbanned person
notify = Notification(title=shorten_string('You have been unbanned from ' + community.title),
url=f'/notifications', user_id=blocked.id, author_id=blocker.id)
db.session.add(notify)
if not current_app.debug: # user.unread_notifications += 1 hangs app if 'user' is the same person
blocked.unread_notifications += 1 # who pressed 'Re-submit this activity'.
db.session.commit()
cache.delete_memoized(communities_banned_from, blocked.id)
cache.delete_memoized(joined_communities, blocked.id)
cache.delete_memoized(moderating_communities, blocked.id)
add_to_modlog_activitypub('unban_user', blocker, community_id=community.id, link_text=blocked.display_name(), link=blocked.link())
def lock_post(mod_ap_id, post_id, comments_enabled): def lock_post(mod_ap_id, post_id, comments_enabled):
@ -1641,10 +1616,9 @@ def lock_post_task(mod_ap_id, post_id, comments_enabled):
db.session.commit() db.session.commit()
def create_post_reply(activity_log: ActivityPubLog, community: Community, in_reply_to, request_json: dict, user: User, announce_id=None) -> Union[PostReply, None]: def create_post_reply(store_ap_json, community: Community, in_reply_to, request_json: dict, user: User, announce_id=None) -> Union[PostReply, None]:
if community.local_only: if community.local_only:
activity_log.exception_message = 'Community is local only, reply discarded' log_incoming_ap(request_json['id'], APLOG_CREATE, APLOG_FAILURE, request_json if store_ap_json else None, 'Community is local only, reply discarded')
activity_log.result = 'ignored'
return None return None
post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to) post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to)
@ -1655,7 +1629,7 @@ def create_post_reply(activity_log: ActivityPubLog, community: Community, in_rep
else: else:
parent_comment = None parent_comment = None
if post_id is None: if post_id is None:
activity_log.exception_message = 'Could not find parent post' log_incoming_ap(request_json['id'], APLOG_CREATE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not find parent post')
return None return None
post = Post.query.get(post_id) post = Post.query.get(post_id)
@ -1681,31 +1655,29 @@ def create_post_reply(activity_log: ActivityPubLog, community: Community, in_rep
language = find_language(next(iter(request_json['object']['contentMap']))) # Combination of next and iter gets the first key in a dict language = find_language(next(iter(request_json['object']['contentMap']))) # Combination of next and iter gets the first key in a dict
language_id = language.id if language else None language_id = language.id if language else None
post_reply = None
try: try:
post_reply = PostReply.new(user, post, parent_comment, notify_author=True, body=body, body_html=body_html, post_reply = PostReply.new(user, post, parent_comment, notify_author=True, body=body, body_html=body_html,
language_id=language_id, request_json=request_json, announce_id=announce_id) language_id=language_id, request_json=request_json, announce_id=announce_id)
activity_log.result = 'success' return post_reply
except Exception as ex: except Exception as ex:
activity_log.exception_message = str(ex) log_incoming_ap(request_json['id'], APLOG_CREATE, APLOG_FAILURE, request_json if store_ap_json else None, str(ex))
activity_log.result = 'ignored' return None
db.session.commit() else:
return post_reply log_incoming_ap(request_json['id'], APLOG_CREATE, APLOG_FAILURE, request_json if store_ap_json else None, 'Unable to find parent post/comment')
return None
def create_post(activity_log: ActivityPubLog, community: Community, request_json: dict, user: User, announce_id=None) -> Union[Post, None]: def create_post(store_ap_json, community: Community, request_json: dict, user: User, announce_id=None) -> Union[Post, None]:
if community.local_only: if community.local_only:
activity_log.exception_message = 'Community is local only, post discarded' log_incoming_ap(request_json['id'], APLOG_CREATE, APLOG_FAILURE, request_json if store_ap_json else None, 'Community is local only, post discarded')
activity_log.result = 'ignored'
return None return None
try: try:
post = Post.new(user, community, request_json, announce_id) post = Post.new(user, community, request_json, announce_id)
return post
except Exception as ex: except Exception as ex:
activity_log.exception_message = str(ex) log_incoming_ap(request_json['id'], APLOG_CREATE, APLOG_FAILURE, request_json if store_ap_json else None, str(ex))
return None return None
return post
def notify_about_post(post: Post): def notify_about_post(post: Post):
# todo: eventually this function could trigger a lot of DB activity. This function will need to be a celery task. # todo: eventually this function could trigger a lot of DB activity. This function will need to be a celery task.
@ -1969,11 +1941,10 @@ def undo_downvote(activity_log, comment, post, target_ap_id, user):
return post return post
def undo_vote(activity_log, comment, post, target_ap_id, user): def undo_vote(comment, post, target_ap_id, user):
voted_on = find_liked_object(target_ap_id) voted_on = find_liked_object(target_ap_id)
if (user and not user.is_local()) and isinstance(voted_on, Post): if isinstance(voted_on, Post):
post = voted_on post = voted_on
user.last_seen = utcnow()
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first() existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first()
if existing_vote: if existing_vote:
post.author.reputation -= existing_vote.effect post.author.reputation -= existing_vote.effect
@ -1983,8 +1954,9 @@ def undo_vote(activity_log, comment, post, target_ap_id, user):
post.up_votes -= 1 post.up_votes -= 1
post.score -= existing_vote.effect post.score -= existing_vote.effect
db.session.delete(existing_vote) db.session.delete(existing_vote)
activity_log.result = 'success' db.session.commit()
if (user and not user.is_local()) and isinstance(voted_on, PostReply): return post
if isinstance(voted_on, PostReply):
comment = voted_on comment = voted_on
existing_vote = PostReplyVote.query.filter_by(user_id=user.id, post_reply_id=comment.id).first() existing_vote = PostReplyVote.query.filter_by(user_id=user.id, post_reply_id=comment.id).first()
if existing_vote: if existing_vote:
@ -1995,22 +1967,13 @@ def undo_vote(activity_log, comment, post, target_ap_id, user):
comment.up_votes -= 1 comment.up_votes -= 1
comment.score -= existing_vote.effect comment.score -= existing_vote.effect
db.session.delete(existing_vote) db.session.delete(existing_vote)
activity_log.result = 'success' db.session.commit()
if user is None or (post is None and comment is None):
activity_log.exception_message = 'Blocked or unfound user or comment'
if user and user.is_local():
activity_log.exception_message = 'Activity about local content which is already present'
activity_log.result = 'ignored'
if post:
return post
if comment:
return comment return comment
return None return None
def process_report(user, reported, request_json, activity_log): def process_report(user, reported, request_json):
if len(request_json['summary']) < 15: if len(request_json['summary']) < 15:
reasons = request_json['summary'] reasons = request_json['summary']
description = '' description = ''
@ -2300,7 +2263,7 @@ def can_delete(user_ap_id, post):
return can_edit(user_ap_id, post) return can_edit(user_ap_id, post)
def resolve_remote_post(uri: str, community_id: int, announce_actor=None) -> Union[Post, PostReply, None]: def resolve_remote_post(uri: str, community_id: int, announce_actor=None, store_ap_json=False) -> Union[Post, PostReply, None]:
post = Post.query.filter_by(ap_id=uri).first() post = Post.query.filter_by(ap_id=uri).first()
if post: if post:
return post return post
@ -2380,18 +2343,14 @@ def resolve_remote_post(uri: str, community_id: int, announce_actor=None) -> Uni
if not community_found: if not community_found:
return None return None
activity_log = ActivityPubLog(direction='in', activity_id=post_data['id'], activity_type='Resolve Post', result='failure')
if site.log_activitypub_json:
activity_log.activity_json = json.dumps(post_data)
db.session.add(activity_log)
user = find_actor_or_create(actor) user = find_actor_or_create(actor)
if user and community and post_data: if user and community and post_data:
request_json = { request_json = {
'id': f"https://{uri_domain}/activities/create/gibberish(15)", 'id': f"https://{uri_domain}/activities/create/{gibberish(15)}",
'object': post_data 'object': post_data
} }
if 'inReplyTo' in request_json['object'] and request_json['object']['inReplyTo']: if 'inReplyTo' in request_json['object'] and request_json['object']['inReplyTo']:
post_reply = create_post_reply(activity_log, community, request_json['object']['inReplyTo'], request_json, user) post_reply = create_post_reply(store_ap_json, community, request_json['object']['inReplyTo'], request_json, user)
if post_reply: if post_reply:
if 'published' in post_data: if 'published' in post_data:
post_reply.posted_at = post_data['published'] post_reply.posted_at = post_data['published']
@ -2400,7 +2359,7 @@ def resolve_remote_post(uri: str, community_id: int, announce_actor=None) -> Uni
db.session.commit() db.session.commit()
return post_reply return post_reply
else: else:
post = create_post(activity_log, community, request_json, user) post = create_post(store_ap_json, community, request_json, user)
if post: if post:
if 'published' in post_data: if 'published' in post_data:
post.posted_at=post_data['published'] post.posted_at=post_data['published']
@ -2568,3 +2527,59 @@ def inform_followers_of_post_update_task(post_id: int, sending_instance_id: int)
post_request(i.inbox, update_json, post.author.private_key, post.author.public_url() + '#main-key') post_request(i.inbox, update_json, post.author.private_key, post.author.public_url() + '#main-key')
except Exception: except Exception:
pass pass
def log_incoming_ap(id, aplog_type, aplog_result, request_json, message=None):
aplog_in = APLOG_IN
if aplog_in and aplog_type[0] and aplog_result[0]:
activity_log = ActivityPubLog(direction='in', activity_id=id, activity_type=aplog_type[1], result=aplog_result[1])
if message:
activity_log.exception_message = message
if request_json:
activity_log.activity_json = json.dumps(request_json)
db.session.add(activity_log)
db.session.commit()
def find_community_ap_id(request_json):
locations = ['audience', 'cc', 'to']
if 'object' in request_json and isinstance(request_json['object'], dict):
rjs = [request_json, request_json['object']]
else:
rjs = [request_json]
for rj in rjs:
for location in locations:
if location in rj:
potential_id = rj[location]
if isinstance(potential_id, str):
if not potential_id.startswith('https://www.w3.org') and not potential_id.endswith('/followers'):
potential_community = Community.query.filter_by(ap_profile_id=potential_id.lower()).first()
if potential_community:
return potential_id
if isinstance(potential_id, list):
for c in potential_id:
if not c.startswith('https://www.w3.org') and not c.endswith('/followers'):
potential_community = Community.query.filter_by(ap_profile_id=c.lower()).first()
if potential_community:
return c
if not 'object' in request_json:
return None
if 'inReplyTo' in request_json['object'] and request_json['object']['inReplyTo'] is not None:
post_being_replied_to = Post.query.filter_by(ap_id=request_json['object']['inReplyTo']).first()
if post_being_replied_to:
return 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:
return comment_being_replied_to.community.ap_profile_id
if request_json['object']['type'] == 'Video': # PeerTube
if 'attributedTo' in request_json['object'] and isinstance(request_json['object']['attributedTo'], list):
for a in request_json['object']['attributedTo']:
if a['type'] == 'Group':
return a['id']
return None

View file

@ -12,7 +12,7 @@ from sqlalchemy import text, desc, or_
from PIL import Image from PIL import Image
from app import db, celery, cache from app import db, celery, cache
from app.activitypub.routes import process_inbox_request, process_delete_request from app.activitypub.routes import process_inbox_request, process_delete_request, replay_inbox_request
from app.activitypub.signature import post_request, default_context from app.activitypub.signature import post_request, default_context
from app.activitypub.util import instance_allowed, instance_blocked, extract_domain_and_actor from app.activitypub.util import instance_allowed, instance_blocked, extract_domain_and_actor
from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm, EditCommunityForm, EditUserForm, \ from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm, EditCommunityForm, EditUserForm, \
@ -611,10 +611,8 @@ def activity_json(activity_id):
def activity_replay(activity_id): def activity_replay(activity_id):
activity = ActivityPubLog.query.get_or_404(activity_id) activity = ActivityPubLog.query.get_or_404(activity_id)
request_json = json.loads(activity.activity_json) request_json = json.loads(activity.activity_json)
if 'type' in request_json and request_json['type'] == 'Delete' and request_json['id'].endswith('#delete'): replay_inbox_request(request_json)
process_delete_request(request_json, activity.id, None)
else:
process_inbox_request(request_json, activity.id, None)
return 'Ok' return 'Ok'
@ -1379,4 +1377,4 @@ def admin_instance_edit(instance_id):
joined_communities=joined_communities(current_user.get_id()), joined_communities=joined_communities(current_user.get_id()),
menu_topics=menu_topics(), menu_topics=menu_topics(),
site=g.site site=g.site
) )

View file

@ -435,6 +435,10 @@ def do_subscribe(actor, user_id, admin_preload=False):
else: else:
pre_load_message['community_banned_by_local_instance'] = True pre_load_message['community_banned_by_local_instance'] = True
success = True success = True
# for local communities, joining is instant
member = CommunityMember(user_id=user.id, community_id=community.id)
db.session.add(member)
db.session.commit()
if remote: if remote:
# send ActivityPub message to remote community, asking to follow. Accept message will be sent to our shared inbox # send ActivityPub message to remote community, asking to follow. Accept message will be sent to our shared inbox
join_request = CommunityJoinRequest(user_id=user.id, community_id=community.id) join_request = CommunityJoinRequest(user_id=user.id, community_id=community.id)
@ -464,10 +468,6 @@ def do_subscribe(actor, user_id, admin_preload=False):
else: else:
pre_load_message['status'] = msg_to_user pre_load_message['status'] = msg_to_user
# for local communities, joining is instant
member = CommunityMember(user_id=user.id, community_id=community.id)
db.session.add(member)
db.session.commit()
if success is True: if success is True:
if not admin_preload: if not admin_preload:
flash('You joined ' + community.title) flash('You joined ' + community.title)

View file

@ -36,3 +36,37 @@ ROLE_STAFF = 3
ROLE_ADMIN = 4 ROLE_ADMIN = 4
MICROBLOG_APPS = ["mastodon", "misskey", "akkoma", "iceshrimp", "pleroma"] MICROBLOG_APPS = ["mastodon", "misskey", "akkoma", "iceshrimp", "pleroma"]
APLOG_IN = True
APLOG_MONITOR = (True, 'Debug this')
APLOG_SUCCESS = (True, 'success')
APLOG_FAILURE = (True, 'failure')
APLOG_IGNORED = (True, 'ignored')
APLOG_PROCESSING = (True, 'processing')
APLOG_NOTYPE = (True, 'Unknown')
APLOG_DUPLICATE = (True, 'Duplicate')
APLOG_FOLLOW = (True, 'Follow')
APLOG_ACCEPT = (True, 'Accept')
APLOG_DELETE = (True, 'Delete')
APLOG_CHATMESSAGE = (True, 'Create ChatMessage')
APLOG_CREATE = (True, 'Create')
APLOG_UPDATE = (True, 'Update')
APLOG_LIKE = (True, 'Like')
APLOG_DISLIKE = (True, 'Dislike')
APLOG_REPORT = (True, 'Report')
APLOG_USERBAN = (True, 'User Ban')
APLOG_LOCK = (True, 'Post Lock')
APLOG_UNDO_FOLLOW = (True, 'Undo Follow')
APLOG_UNDO_DELETE = (True, 'Undo Delete')
APLOG_UNDO_VOTE = (True, 'Undo Vote')
APLOG_UNDO_USERBAN = (True, 'Undo User Ban')
APLOG_ADD = (True, 'Add Mod/Sticky')
APLOG_REMOVE = (True, 'Remove Mod/Sticky')
APLOG_ANNOUNCE = (True, 'Announce')
APLOG_PT_VIEW = (True, 'PeerTube View')