Merge remote-tracking branch 'origin/main'

This commit is contained in:
rimu 2025-01-08 13:15:33 +13:00
commit b90f4feb3c
16 changed files with 558 additions and 233 deletions

View file

@ -25,7 +25,7 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti
update_post_from_activity, undo_vote, undo_downvote, post_to_page, get_redis_connection, find_reported_object, \
process_report, ensure_domains_match, can_edit, can_delete, remove_data_from_banned_user, resolve_remote_post, \
inform_followers_of_post_update, comment_model_to_json, restore_post_or_comment, ban_user, unban_user, \
log_incoming_ap, find_community_ap_id, site_ban_remove_data, community_ban_remove_data
log_incoming_ap, find_community, site_ban_remove_data, community_ban_remove_data, verify_object_from_source
from app.utils import gibberish, get_setting, render_template, \
community_membership, ap_datetime, ip_address, can_downvote, \
can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest, \
@ -465,14 +465,19 @@ def shared_inbox():
HttpSignature.verify_request(request, actor.public_key, skip_date=True)
except VerificationError as e:
bounced = True
if not 'signature' in request_json:
log_incoming_ap(id, APLOG_NOTYPE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify HTTP signature: ' + str(e))
return '', 400
# HTTP sig will fail if a.gup.pe or PeerTube have bounced a request, so check LD sig instead
try:
LDSignature.verify_signature(request_json, actor.public_key)
except VerificationError as e:
log_incoming_ap(id, APLOG_NOTYPE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify LD signature: ' + str(e))
if 'signature' in request_json:
try:
LDSignature.verify_signature(request_json, actor.public_key)
except VerificationError as e:
log_incoming_ap(id, APLOG_NOTYPE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify LD signature: ' + str(e))
return '', 400
# not HTTP sig, and no LD sig, so reduce the inner object to just its remote ID, and then fetch it and check it in process_inbox_request()
elif ((request_json['type'] == 'Create' or request_json['type'] == 'Update') and
isinstance(request_json['object'], dict) and 'id' in request_json['object'] and isinstance(request_json['object']['id'], str)):
request_json['object'] = request_json['object']['id']
else:
log_incoming_ap(id, APLOG_NOTYPE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify HTTP signature: ' + str(e))
return '', 400
actor.instance.last_seen = utcnow()
@ -599,6 +604,40 @@ def process_inbox_request(request_json, store_ap_json):
db.session.commit()
community = None # found as needed
# Announce: take care of inner objects that are just a URL (PeerTube, a.gup.pe), or find the user if the inner object is a dict
if request_json['type'] == 'Announce':
if isinstance(request_json['object'], str):
if request_json['object'].startswith('https://' + current_app.config['SERVER_NAME']):
log_incoming_ap(id, APLOG_DUPLICATE, APLOG_IGNORED, request_json if store_ap_json else None, 'Activity about local content which is already present')
return
post = resolve_remote_post(request_json['object'], community.id, announce_actor=community.ap_profile_id, store_ap_json=store_ap_json)
if post:
log_incoming_ap(id, APLOG_ANNOUNCE, APLOG_SUCCESS, request_json)
else:
log_incoming_ap(id, APLOG_ANNOUNCE, APLOG_FAILURE, request_json, 'Could not resolve post')
return
user_ap_id = request_json['object']['actor']
user = find_actor_or_create(user_ap_id)
if not user or not isinstance(user, User):
log_incoming_ap(id, APLOG_ANNOUNCE, APLOG_FAILURE, request_json if store_ap_json else None, 'Blocked or unfound user for Announce object actor ' + user_ap_id)
return
user.last_seen = site.last_active = utcnow()
user.instance.last_seen = utcnow()
user.instance.dormant = False
user.instance.gone_forever = False
user.instance.failures = 0
db.session.commit()
# Now that we have the community and the user from an Announce, we can save repeating code by removing it
# core_activity is checked for its Type, but request_json is passed to any other functions
announced = True
core_activity = request_json['object']
else:
announced = False
core_activity = request_json
# Follow: remote user wants to join/follow one of our users or communities
if request_json['type'] == 'Follow':
target_ap_id = request_json['object']
@ -706,6 +745,12 @@ def process_inbox_request(request_json, store_ap_json):
# Create is new content. Update is often an edit, but Updates from Lemmy can also be new content
if request_json['type'] == 'Create' or request_json['type'] == 'Update':
if isinstance(request_json['object'], str):
request_json = verify_object_from_source(request_json) # change request_json['object'] from str to dict, then process normally
if not request_json:
log_incoming_ap(id, APLOG_CREATE, APLOG_FAILURE, request_json if store_ap_json else None, 'Could not verify unsigned request from source')
return
if request_json['object']['type'] == 'ChatMessage':
sender = user
recipient_ap_id = request_json['object']['to'][0]
@ -761,11 +806,10 @@ def process_inbox_request(request_json, store_ap_json):
if post_being_replied_to.author.is_local():
inform_followers_of_post_update(post_being_replied_to.id, user.instance_id)
return
community_ap_id = find_community_ap_id(request_json)
community = find_community(request_json)
if not ensure_domains_match(request_json['object']):
log_incoming_ap(id, APLOG_CREATE, APLOG_FAILURE, request_json if store_ap_json else None, 'Domains do not match')
return
community = find_actor_or_create(community_ap_id, community_only=True, create_if_not_found=False) if community_ap_id else None
if community and community.local_only:
log_incoming_ap(id, APLOG_CREATE, APLOG_FAILURE, request_json if store_ap_json else None, 'Remote Create in local_only community')
return
@ -812,40 +856,69 @@ def process_inbox_request(request_json, store_ap_json):
log_incoming_ap(id, APLOG_DELETE, APLOG_FAILURE, request_json if store_ap_json else None, 'Delete: cannot find ' + ap_id)
return
if request_json['type'] == 'Like' or request_json['type'] == 'EmojiReact': # Upvote
process_upvote(user, store_ap_json, request_json, announced=False)
if core_activity['type'] == 'Like' or core_activity['type'] == 'EmojiReact': # Upvote
process_upvote(user, store_ap_json, request_json, announced)
return
if request_json['type'] == 'Dislike': # Downvote
if core_activity['type'] == 'Dislike': # Downvote
if site.enable_downvotes is False:
log_incoming_ap(id, APLOG_DISLIKE, APLOG_IGNORED, request_json if store_ap_json else None, 'Dislike ignored because of allow_dislike setting')
return
process_downvote(user, store_ap_json, request_json, announced=False)
process_downvote(user, store_ap_json, request_json, announced)
return
if request_json['type'] == 'Flag': # Reported content
reported = find_reported_object(request_json['object'])
if core_activity['type'] == 'Flag': # Reported content
reported = find_reported_object(core_activity['object'])
if reported:
process_report(user, reported, request_json)
process_report(user, reported, core_activity)
log_incoming_ap(id, APLOG_REPORT, APLOG_SUCCESS, request_json if store_ap_json else None)
announce_activity_to_followers(reported.community, user, request_json)
else:
log_incoming_ap(id, APLOG_REPORT, APLOG_IGNORED, request_json if store_ap_json else None, 'Report ignored due to missing content')
return
if request_json['type'] == 'Add': # remote site is adding a local user as a moderator, and is sending directly rather than announcing (happens if not subscribed)
if core_activity['type'] == 'Lock': # Post lock
mod = user
community_ap_id = find_community_ap_id(request_json)
community = find_actor_or_create(community_ap_id, community_only=True, create_if_not_found=False) if community_ap_id else None
post_id = core_activity['object']
post = Post.query.filter_by(ap_id=post_id).first()
reason = core_activity['summary'] if 'summary' in core_activity else ''
if post:
if post.community.is_moderator(mod) or post.community.is_instance_admin(mod):
post.comments_enabled = False
db.session.commit()
add_to_modlog_activitypub('lock_post', mod, community_id=post.community.id,
link_text=shorten_string(post.title), link=f'post/{post.id}',
reason=reason)
log_incoming_ap(id, APLOG_LOCK, APLOG_SUCCESS, request_json if store_ap_json else None)
else:
log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: Does not have permission')
else:
log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: post not found')
return
if core_activity['type'] == 'Add': # Add mods, or sticky a post
mod = user
if not announced:
community = find_community(core_activity)
if community:
if not community.is_moderator(mod) and not community.is_instance_admin(mod):
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Does not have permission')
return
target = request_json['target']
target = core_activity['target']
featured_url = community.ap_featured_url
moderators_url = community.ap_moderators_url
if target == featured_url:
post = Post.query.filter_by(ap_id=core_activity['object']).first()
if post:
post.sticky = True
db.session.commit()
log_incoming_ap(id, APLOG_ADD, APLOG_SUCCESS, request_json if store_ap_json else None)
else:
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + core_activity['object'])
return
if target == moderators_url:
new_mod = find_actor_or_create(request_json['object'], create_if_not_found=False)
if new_mod and new_mod.is_local():
new_mod = find_actor_or_create(core_activity['object'])
if new_mod:
existing_membership = CommunityMember.query.filter_by(community_id=community.id, user_id=new_mod.id).first()
if existing_membership:
existing_membership.is_moderator = True
@ -855,39 +928,45 @@ def process_inbox_request(request_json, store_ap_json):
db.session.commit()
log_incoming_ap(id, APLOG_ADD, APLOG_SUCCESS, request_json if store_ap_json else None)
else:
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object'])
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + core_activity['object'])
return
else:
# Lemmy might not send anything directly to sticky a post if no-one is subscribed (could not get it to generate the activity)
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Add')
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Add')
else:
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Add: cannot find community')
return
if request_json['type'] == 'Remove': # remote site is removing a local user as a moderator, and is sending directly rather than announcing (happens if not subscribed)
if core_activity['type'] == 'Remove': # Remove mods, or unsticky a post
mod = user
community_ap_id = find_community_ap_id(request_json)
community = find_actor_or_create(community_ap_id, community_only=True, create_if_not_found=False) if community_ap_id else None
if not announced:
community = find_community(core_activity)
if community:
if not community.is_moderator(mod) and not community.is_instance_admin(mod):
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Does not have permission')
return
target = request_json['target']
target = core_activity['target']
featured_url = community.ap_featured_url
moderators_url = community.ap_moderators_url
if target == featured_url:
post = Post.query.filter_by(ap_id=core_activity['object']).first()
if post:
post.sticky = False
db.session.commit()
log_incoming_ap(id, APLOG_REMOVE, APLOG_SUCCESS, request_json if store_ap_json else None)
else:
log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + core_activity['object'])
return
if target == moderators_url:
old_mod = find_actor_or_create(request_json['object'], create_if_not_found=False)
if old_mod and old_mod.is_local():
old_mod = find_actor_or_create(core_activity['object'])
if old_mod:
existing_membership = CommunityMember.query.filter_by(community_id=community.id, user_id=old_mod.id).first()
if existing_membership:
existing_membership.is_moderator = False
db.session.commit()
log_incoming_ap(id, APLOG_REMOVE, APLOG_SUCCESS, request_json if store_ap_json else None)
else:
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object'])
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + core_activity['object'])
return
else:
# Lemmy might not send anything directly to unsticky a post if no-one is subscribed (could not get it to generate the activity)
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Remove')
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Remove')
else:
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Remove: cannot find community')
return
@ -1024,29 +1103,8 @@ def process_inbox_request(request_json, store_ap_json):
# Announce is new content and votes that happened on a remote server.
if request_json['type'] == 'Announce':
if isinstance(request_json['object'], str): # Mastodon, PeerTube, A.gup.pe
if request_json['object'].startswith('https://' + current_app.config['SERVER_NAME']):
log_incoming_ap(id, APLOG_DUPLICATE, APLOG_IGNORED, request_json if store_ap_json else None, 'Activity about local content which is already present')
return
post = resolve_remote_post(request_json['object'], community.id, announce_actor=community.ap_profile_id, store_ap_json=store_ap_json)
if post:
log_incoming_ap(id, APLOG_ANNOUNCE, APLOG_SUCCESS, request_json)
else:
log_incoming_ap(id, APLOG_ANNOUNCE, APLOG_FAILURE, request_json, 'Could not resolve post')
return
user_ap_id = request_json['object']['actor']
user = find_actor_or_create(user_ap_id)
if not user or not isinstance(user, User):
log_incoming_ap(id, APLOG_ANNOUNCE, APLOG_FAILURE, request_json if store_ap_json else None, 'Blocked or unfound user for Announce object actor ' + user_ap_id)
return
user.last_seen = site.last_active = utcnow()
user.instance.last_seen = utcnow()
user.instance.dormant = False
user.instance.gone_forever = False
user.instance.failures = 0
db.session.commit()
# should be able to remove the rest of this, and process the activities above.
# Then for any activities that are both sent direct and Announced, one can be dropped when shared_inbox() checks for dupes
if request_json['object']['type'] == 'Create' or request_json['object']['type'] == 'Update':
object_type = request_json['object']['object']['type']
@ -1078,101 +1136,101 @@ def process_inbox_request(request_json, store_ap_json):
log_incoming_ap(id, APLOG_DELETE, APLOG_FAILURE, request_json if store_ap_json else None, 'Delete: cannot find ' + ap_id)
return
if request_json['object']['type'] == 'Like' or request_json['object']['type'] == 'EmojiReact': # Announced Upvote
process_upvote(user, store_ap_json, request_json)
return
#if request_json['object']['type'] == 'Like' or request_json['object']['type'] == 'EmojiReact': # Announced Upvote
# process_upvote(user, store_ap_json, request_json)
# return
if request_json['object']['type'] == 'Dislike': # Announced Downvote
if site.enable_downvotes is False:
log_incoming_ap(id, APLOG_DISLIKE, APLOG_IGNORED, request_json if store_ap_json else None, 'Dislike ignored because of allow_dislike setting')
return
process_downvote(user, store_ap_json, request_json)
return
#if request_json['object']['type'] == 'Dislike': # Announced Downvote
# if site.enable_downvotes is False:
# log_incoming_ap(id, APLOG_DISLIKE, APLOG_IGNORED, request_json if store_ap_json else None, 'Dislike ignored because of allow_dislike setting')
# return
# process_downvote(user, store_ap_json, request_json)
# return
if request_json['object']['type'] == 'Flag': # Announce of reported content
reported = find_reported_object(request_json['object']['object'])
if reported:
process_report(user, reported, request_json['object'])
log_incoming_ap(id, APLOG_REPORT, APLOG_SUCCESS, request_json if store_ap_json else None)
else:
log_incoming_ap(id, APLOG_REPORT, APLOG_IGNORED, request_json if store_ap_json else None, 'Report ignored due to missing content')
return
#if request_json['object']['type'] == 'Flag': # Announce of reported content
# reported = find_reported_object(request_json['object']['object'])
# if reported:
# process_report(user, reported, request_json['object'])
# log_incoming_ap(id, APLOG_REPORT, APLOG_SUCCESS, request_json if store_ap_json else None)
# else:
# log_incoming_ap(id, APLOG_REPORT, APLOG_IGNORED, request_json if store_ap_json else None, 'Report ignored due to missing content')
# return
if request_json['object']['type'] == 'Lock': # Announce of post lock
mod = user
post_id = request_json['object']['object']
post = Post.query.filter_by(ap_id=post_id).first()
reason = request_json['object']['summary'] if 'summary' in request_json['object'] else ''
if post:
if post.community.is_moderator(mod) or post.community.is_instance_admin(mod):
post.comments_enabled = False
db.session.commit()
add_to_modlog_activitypub('lock_post', mod, community_id=post.community.id,
link_text=shorten_string(post.title), link=f'post/{post.id}',
reason=reason)
log_incoming_ap(id, APLOG_LOCK, APLOG_SUCCESS, request_json if store_ap_json else None)
else:
log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: Does not have permission')
else:
log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: post not found')
return
#if request_json['object']['type'] == 'Lock': # Announce of post lock
# mod = user
# post_id = request_json['object']['object']
# post = Post.query.filter_by(ap_id=post_id).first()
# reason = request_json['object']['summary'] if 'summary' in request_json['object'] else ''
# if post:
# if post.community.is_moderator(mod) or post.community.is_instance_admin(mod):
# post.comments_enabled = False
# db.session.commit()
# add_to_modlog_activitypub('lock_post', mod, community_id=post.community.id,
# link_text=shorten_string(post.title), link=f'post/{post.id}',
# reason=reason)
# log_incoming_ap(id, APLOG_LOCK, APLOG_SUCCESS, request_json if store_ap_json else None)
# else:
# log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: Does not have permission')
# else:
# log_incoming_ap(id, APLOG_LOCK, APLOG_FAILURE, request_json if store_ap_json else None, 'Lock: post not found')
# return
if request_json['object']['type'] == 'Add': # Announce of adding mods or stickying a post
target = request_json['object']['target']
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()
if post:
post.sticky = True
db.session.commit()
log_incoming_ap(id, APLOG_ADD, APLOG_SUCCESS, request_json if store_ap_json else None)
else:
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']['object'])
return
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()
log_incoming_ap(id, APLOG_ADD, APLOG_SUCCESS, request_json if store_ap_json else None)
else:
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']['object'])
return
log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Add')
return
#if request_json['object']['type'] == 'Add': # Announce of adding mods or stickying a post
# target = request_json['object']['target']
# 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()
# if post:
# post.sticky = True
# db.session.commit()
# log_incoming_ap(id, APLOG_ADD, APLOG_SUCCESS, request_json if store_ap_json else None)
# else:
# log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']['object'])
# return
# 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()
# log_incoming_ap(id, APLOG_ADD, APLOG_SUCCESS, request_json if store_ap_json else None)
# else:
# log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']['object'])
# return
# log_incoming_ap(id, APLOG_ADD, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Add')
# return
if request_json['object']['type'] == 'Remove': # Announce of removing mods or unstickying a post
target = request_json['object']['target']
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()
if post:
post.sticky = False
db.session.commit()
log_incoming_ap(id, APLOG_REMOVE, APLOG_SUCCESS, request_json if store_ap_json else None)
else:
log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + target)
return
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
db.session.commit()
log_incoming_ap(id, APLOG_REMOVE, APLOG_SUCCESS, request_json if store_ap_json else None)
else:
log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']['object'])
return
log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Remove')
return
#if request_json['object']['type'] == 'Remove': # Announce of removing mods or unstickying a post
# target = request_json['object']['target']
# 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()
# if post:
# post.sticky = False
# db.session.commit()
# log_incoming_ap(id, APLOG_REMOVE, APLOG_SUCCESS, request_json if store_ap_json else None)
# else:
# log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + target)
# return
# 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
# db.session.commit()
# log_incoming_ap(id, APLOG_REMOVE, APLOG_SUCCESS, request_json if store_ap_json else None)
# else:
# log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Cannot find: ' + request_json['object']['object'])
# return
# log_incoming_ap(id, APLOG_REMOVE, APLOG_FAILURE, request_json if store_ap_json else None, 'Unknown target for Remove')
# return
if request_json['object']['type'] == 'Block': # Announce of user ban. Mod is banning a user from a community,
blocker = user # or an admin is banning a user from all the site's communities as part of a site ban
@ -1615,7 +1673,7 @@ def process_new_content(user, community, store_ap_json, request_json, announced=
return
def process_upvote(user, store_ap_json, request_json, announced=True):
def process_upvote(user, store_ap_json, request_json, announced):
id = request_json['id']
ap_id = request_json['object'] if not announced else request_json['object']['object']
if isinstance(ap_id, dict) and 'id' in ap_id:
@ -1634,7 +1692,7 @@ def process_upvote(user, store_ap_json, request_json, announced=True):
log_incoming_ap(id, APLOG_LIKE, APLOG_IGNORED, request_json if store_ap_json else None, 'Cannot upvote this')
def process_downvote(user, store_ap_json, request_json, announced=True):
def process_downvote(user, store_ap_json, request_json, announced):
id = request_json['id']
ap_id = request_json['object'] if not announced else request_json['object']['object']
if isinstance(ap_id, dict) and 'id' in ap_id:

View file

@ -1782,8 +1782,38 @@ def update_post_from_activity(post: Post, request_json: dict):
post.edited_at = utcnow()
if request_json['object']['type'] == 'Video':
# fetching individual user details to attach to votes is probably too convoluted, so take the instance's word for it
upvotes = 1 # from OP
downvotes = 0
endpoints = ['likes', 'dislikes']
for endpoint in endpoints:
if endpoint in request_json['object']:
try:
object_request = get_request(request_json['object'][endpoint], headers={'Accept': 'application/activity+json'})
except httpx.HTTPError:
time.sleep(3)
try:
object_request = get_request(request_json['object'][endpoint], headers={'Accept': 'application/activity+json'})
except httpx.HTTPError:
object_request = None
if object_request and object_request.status_code == 200:
try:
object = object_request.json()
except:
object_request.close()
object = None
object_request.close()
if object and 'totalItems' in object:
if endpoint == 'likes':
upvotes += object['totalItems']
if endpoint == 'dislikes':
downvotes += object['totalItems']
post.up_votes = upvotes
post.down_votes = downvotes
post.score = upvotes - downvotes
post.ranking = post.post_ranking(post.score, post.posted_at)
# return now for PeerTube, otherwise rest of this function breaks the post
# consider querying the Likes endpoint (that mostly seems to be what Updates are about)
db.session.commit()
return
# Links
@ -2479,6 +2509,68 @@ def resolve_remote_post_from_search(uri: str) -> Union[Post, None]:
return None
# called from activitypub/routes if something is posted to us without any kind of signature (typically from PeerTube)
def verify_object_from_source(request_json):
uri = request_json['object']
uri_domain = urlparse(uri).netloc
if not uri_domain:
return None
create_domain = urlparse(request_json['actor']).netloc
if create_domain != uri_domain:
return None
try:
object_request = get_request(uri, headers={'Accept': 'application/activity+json'})
except httpx.HTTPError:
time.sleep(3)
try:
object_request = get_request(uri, headers={'Accept': 'application/activity+json'})
except httpx.HTTPError:
return None
if object_request.status_code == 200:
try:
object = object_request.json()
except:
object_request.close()
return None
object_request.close()
elif object_request.status_code == 401:
try:
site = Site.query.get(1)
object_request = signed_get_request(uri, site.private_key, f"https://{current_app.config['SERVER_NAME']}/actor#main-key")
except httpx.HTTPError:
time.sleep(3)
try:
object_request = signed_get_request(uri, site.private_key, f"https://{current_app.config['SERVER_NAME']}/actor#main-key")
except httpx.HTTPError:
return None
try:
object = object_request.json()
except:
object_request.close()
return None
object_request.close()
else:
return None
if not 'id' in object or not 'type' in object or not 'attributedTo' in object:
return None
if isinstance(object['attributedTo'], str):
actor_domain = urlparse(object['attributedTo']).netloc
elif isinstance(object['attributedTo'], dict) and 'id' in object['attributedTo']:
actor_domain = urlparse(object['attributedTo']['id']).netloc
else:
return None
if uri_domain != actor_domain:
return None
request_json['object'] = object
return request_json
# This is for followers on microblog apps
# Used to let them know a Poll has been updated with a new vote
# The plan is to also use it for activities on local user's posts that aren't understood by being Announced (anything beyond the initial Create)
@ -2547,7 +2639,7 @@ def log_incoming_ap(id, aplog_type, aplog_result, request_json, message=None):
db.session.commit()
def find_community_ap_id(request_json):
def find_community(request_json):
locations = ['audience', 'cc', 'to']
if 'object' in request_json and isinstance(request_json['object'], dict):
rjs = [request_json, request_json['object']]
@ -2561,30 +2653,32 @@ def find_community_ap_id(request_json):
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
return potential_community
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
return potential_community
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()
post_being_replied_to = Post.query.filter_by(ap_id=request_json['object']['inReplyTo'].lower()).first()
if post_being_replied_to:
return post_being_replied_to.community.ap_profile_id
return post_being_replied_to.community
else:
comment_being_replied_to = PostReply.query.filter_by(ap_id=request_json['object']['inReplyTo']).first()
comment_being_replied_to = PostReply.query.filter_by(ap_id=request_json['object']['inReplyTo'].lower()).first()
if comment_being_replied_to:
return comment_being_replied_to.community.ap_profile_id
return comment_being_replied_to.community
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']
potential_community = Community.query.filter_by(ap_profile_id=a['id'].lower()).first()
if potential_community:
return potential_community
return None

View file

@ -406,6 +406,7 @@ def alpha_emoji():
# HTML routes
from flask import abort, render_template
from app.models import Community
from app.utils import current_theme
import os
@ -463,3 +464,19 @@ def get_alpha_communities():
return render_template(f'themes/{theme}/{template_name}')
else:
return render_template(template_name)
@bp.route('/api/alpha/c/<actor>', methods=['GET'])
def community_profile(actor):
if '@' in actor:
community = Community.query.filter_by(ap_id=actor.lower(), banned=False).first()
else:
community = Community.query.filter_by(name=actor, ap_id=None).first()
template_name = "community.html"
theme = current_theme()
if theme != '' and os.path.exists(f'app/templates/themes/{theme}/{template_name}'):
return render_template(f'themes/{theme}/{template_name}', community_id=community.id)
else:
return render_template(template_name, community_id=community.id)

View file

@ -1,18 +1,65 @@
from app import db
from app import cache, db
from app.api.alpha.views import user_view, community_view, instance_view
from app.api.alpha.utils.validators import required, integer_expected, boolean_expected
from app.utils import authorise_api_user
from app.models import InstanceBlock, Language
from app.models import CommunityMember, InstanceBlock, Language
from app.shared.site import block_remote_instance, unblock_remote_instance
from flask import current_app, g
from sqlalchemy import text
@cache.memoize(timeout=86400)
def users_total():
return db.session.execute(text(
'SELECT COUNT(id) as c FROM "user" WHERE ap_id is null AND verified is true AND banned is false AND deleted is false')).scalar()
@cache.memoize(timeout=86400)
def moderating_communities(user):
cms = CommunityMember.query.filter_by(user_id=user.id, is_moderator=True)
moderates = []
for cm in cms:
moderates.append({'community': community_view(cm.community_id, variant=1, stub=True), 'moderator': user_view(user, variant=1, stub=True)})
return moderates
@cache.memoize(timeout=86400)
def joined_communities(user):
cms = CommunityMember.query.filter_by(user_id=user.id, is_banned=False)
follows = []
for cm in cms:
follows.append({'community': community_view(cm.community_id, variant=1, stub=True), 'follower': user_view(user, variant=1, stub=True)})
return follows
@cache.memoize(timeout=86400)
def blocked_people(user):
blocked = []
blocked_ids = db.session.execute(text('SELECT blocked_id FROM "user_block" WHERE blocker_id = :blocker_id'), {"blocker_id": user.id}).scalars()
for blocked_id in blocked_ids:
blocked.append({'person': user_view(user, variant=1, stub=True), 'target': user_view(blocked_id, variant=1, stub=True)})
return blocked
@cache.memoize(timeout=86400)
def blocked_communities(user):
blocked = []
blocked_ids = db.session.execute(text('SELECT community_id FROM "community_block" WHERE user_id = :user_id'), {"user_id": user.id}).scalars()
for blocked_id in blocked_ids:
blocked.append({'person': user_view(user, variant=1, stub=True), 'community': community_view(blocked_id, variant=1, stub=True)})
return blocked
@cache.memoize(timeout=86400)
def blocked_instances(user):
blocked = []
blocked_ids = db.session.execute(text('SELECT instance_id FROM "instance_block" WHERE user_id = :user_id'), {"user_id": user.id}).scalars()
for blocked_id in blocked_ids:
blocked.append({'person': user_view(user, variant=1, stub=True), 'instance': instance_view(blocked_id, variant=1)})
return blocked
def get_site(auth):
if auth:
user = authorise_api_user(auth, return_type='model')
@ -74,31 +121,13 @@ def get_site(auth):
"comment_count": user.post_reply_count
}
},
#"moderates": [],
#"follows": [],
"community_blocks": [],
"instance_blocks": [],
"person_blocks": [],
"moderates": moderating_communities(user),
"follows": joined_communities(user),
"community_blocks": blocked_communities(user),
"instance_blocks": blocked_instances(user),
"person_blocks": blocked_people(user),
"discussion_languages": [] # TODO
}
"""
Note: Thunder doesn't use moderates[] and follows[] from here, but it would be more efficient if it did (rather than getting them from /user and /community)
cms = CommunityMember.query.filter_by(user_id=user_id, is_moderator=True)
for cm in cms:
my_user['moderates'].append({'community': Community.api_json(variant=1, id=cm.community_id, stub=True), 'moderator': User.api_json(variant=1, id=user_id, stub=True)})
cms = CommunityMember.query.filter_by(user_id=user_id, is_banned=False)
for cm in cms:
my_user['follows'].append({'community': Community.api_json(variant=1, id=cm.community_id, stub=True), 'follower': User.api_json(variant=1, id=user_id, stub=True)})
"""
blocked_ids = db.session.execute(text('SELECT blocked_id FROM "user_block" WHERE blocker_id = :blocker_id'), {"blocker_id": user.id}).scalars()
for blocked_id in blocked_ids:
my_user['person_blocks'].append({'person': user_view(user, variant=1, stub=True), 'target': user_view(blocked_id, variant=1, stub=True)})
blocked_ids = db.session.execute(text('SELECT community_id FROM "community_block" WHERE user_id = :user_id'), {"user_id": user.id}).scalars()
for blocked_id in blocked_ids:
my_user['community_blocks'].append({'person': user_view(user, variant=1, stub=True), 'community': community_view(blocked_id, variant=1, stub=True)})
blocked_ids = db.session.execute(text('SELECT instance_id FROM "instance_block" WHERE user_id = :user_id'), {"user_id": user.id}).scalars()
for blocked_id in blocked_ids:
my_user['instance_blocks'].append({'person': user_view(user, variant=1, stub=True), 'instance': instance_view(blocked_id, variant=1)})
data = {
"version": "1.0.0",
"site": site

View file

@ -173,7 +173,8 @@ def cached_community_view_variant_1(community: Community, stub=False):
'actor_id': community.public_url(),
'local': community.is_local(),
'hidden': not community.show_all,
'instance_id': community.instance_id if community.instance_id else 1})
'instance_id': community.instance_id if community.instance_id else 1,
'ap_domain': community.ap_domain})
if community.description and not stub:
v1['description'] = community.description
if community.icon_id:

View file

@ -722,8 +722,6 @@ def add_post(actor, type):
post.ap_id = f"https://{current_app.config['SERVER_NAME']}/post/{post.id}"
db.session.commit()
upvote_own_post(post)
if post.type == POST_TYPE_POLL:
poll = Poll.query.filter_by(post_id=post.id).first()
if not poll.local_only:
@ -2007,11 +2005,3 @@ def check_url_already_posted():
abort(404)
def upvote_own_post(post):
post.score = 1
post.up_votes = 1
post.ranking = post.post_ranking(post.score, utcnow())
vote = PostVote(user_id=current_user.id, post_id=post.id, author_id=current_user.id, effect=1)
db.session.add(vote)
db.session.commit()
cache.delete_memoized(recently_upvoted_posts, current_user.id)

View file

@ -290,10 +290,13 @@ def list_subscribed_communities():
all_communities = Community.query.filter_by(banned=False)
# get the user's joined communities
user_joined_communities = joined_communities(current_user.id)
user_moderating_communities = moderating_communities(current_user.id)
# get the joined community ids list
joined_ids = []
for jc in user_joined_communities:
joined_ids.append(jc.id)
for mc in user_moderating_communities:
joined_ids.append(mc.id)
# filter down to just the joined communities
communities = all_communities.filter(Community.id.in_(joined_ids))

View file

@ -1171,7 +1171,7 @@ class Post(db.Model):
find_licence_or_create, make_image_sizes, notify_about_post
from app.utils import allowlist_html, markdown_to_html, html_to_text, microblog_content_to_title, blocked_phrases, \
is_image_url, is_video_url, domain_from_url, opengraph_parse, shorten_string, remove_tracking_from_link, \
is_video_hosting_site, communities_banned_from
is_video_hosting_site, communities_banned_from, recently_upvoted_posts
microblog = False
if 'name' not in request_json['object']: # Microblog posts
@ -1399,11 +1399,16 @@ class Post(db.Model):
if post.community_id not in communities_banned_from(user.id):
notify_about_post(post)
# attach initial upvote to author
vote = PostVote(user_id=user.id, post_id=post.id, author_id=user.id, effect=1)
db.session.add(vote)
if user.is_local():
cache.delete_memoized(recently_upvoted_posts, user.id)
if user.reputation > 100:
post.up_votes += 1
post.score += 1
post.ranking = post.post_ranking(post.score, post.posted_at)
db.session.commit()
db.session.commit()
return post

View file

@ -80,8 +80,8 @@
<td><span title="{{ _('Banned') }}">{{ '<span class="red">Banned</span>'|safe if user.banned }}
{{ '<span class="red">Banned posts</span>'|safe if user.ban_posts }}
{{ '<span class="red">Banned comments</span>'|safe if user.ban_comments }}</span></td>
<td><span title="{{ user.last_seen }}">{{ arrow.get(user.last_seen).humanize(locale=locale) }}</span></td>
<td><span title="{{ _('Reports') }}">{{ user.reports if user.reports > 0 }}</span></td>
<td><span title="{{ _('Attitude') }}">{% if user.attitude %}{{ (user.attitude * 100) | round | int }}%{% endif %}</span></td>
<td><span title="{{ _('Reputation') }}">{% if user.reputation %}R {{ user.reputation | round | int }}{% endif %}</span></td>
<td><span title="{{ _('Last Seen') }}: {{ user.last_seen }}">{{ arrow.get(user.last_seen).humanize(locale=locale) }}</span></td>
<td><a href="{{ url_for('admin.admin_user_edit', user_id=user.id) }}">Edit</a>,

View file

@ -1,7 +1,7 @@
{% extends 'themes/' + theme() + '/base.html' %}
{% block app_content %}
<p class="mb-0">GET <code>/api/alpha/site</code></p>
<p class="mb-0" id="site_request"></p>
<details><summary>JSON</summary><pre id="site_json"></pre></details>
<p class="mb-0">POST <code>/api/alpha/user/login</code></p>

View file

@ -40,7 +40,7 @@
<link id="icon_32" rel="icon" type="image/png" sizes="32x32" href="">
<link id="icon_16" rel="icon" type="image/png" sizes="16x16" href="">
<link id="icon_shortcut" rel="shortcut icon" type="image/png" href="">
<link id="favicon" rel='icon' type="image/x-icon" href="">
<link rel='icon' type="image/x-icon" href="/static/images/favicon.ico">
{{ bootstrap.load_css() }}
<link href="{{ '/static/themes/' + theme() + '/css/navbars.css' }}" rel="stylesheet">

View file

@ -0,0 +1,12 @@
{% extends 'themes/' + theme() + '/base.html' %}
{% block app_content %}
<p class="mb-0" id="site_request"></p>
<details><summary>JSON</summary><pre id="site_json"></pre></details>
<p class="mb-0" id="community_request" data-value="{{ community_id }}">GET <code>/api/alpha/community?id={{ community_id }}</code></p>
<details><summary>JSON</summary><pre id="community_json"></pre></details>
<p class="mb-0" id="community_post_list_request">GET <code>/api/alpha/post/list?sort=Hot&page=1&community_id={{ community_id }}</code></p>
<details><summary>JSON</summary><pre id="community_post_list_json"></pre></details>
<script src="{{ '/static/themes/' + theme() + '/js/community.js' }}" type="module" data-param1="{{ community_id }}"></script>
{% endblock %}

View file

@ -1,6 +1,12 @@
{% extends 'themes/' + theme() + '/base.html' %}
{% block app_content %}
<p class="mb-0">GET <code>/api/alpha/site</code></p>
<p class="mb-0" id="site_request"></p>
<details><summary>JSON</summary><pre id="site_json"></pre></details>
<hr />
<p class="mb-0" id="post_list_request"></p>
<details><summary>JSON</summary><pre id="post_list_json"></pre></details>
{% endblock%}

View file

@ -0,0 +1,26 @@
const element = document.getElementById('community_request');
const community_id = element.getAttribute('data-value');
import { baseUrl } from './site.js';
const community_api = baseUrl + '/api/alpha/community?id=' + community_id;
const community_post_list_api = baseUrl + '/api/alpha/post/list?sort=Hot&page=1&community_id=' + community_id;
import { jwt } from './site.js';
if (jwt != null) {
var request = {method: "GET", headers: {Authorization: `Bearer ${jwt}`}};
} else {
var request = {method: "GET"};
}
fetch(community_api, request)
.then(response => response.json())
.then(data => {
document.querySelector('#community_json').textContent = JSON.stringify(data, null, 2);
})
fetch(community_post_list_api, request)
.then(response => response.json())
.then(data => {
document.querySelector('#community_post_list_json').textContent = JSON.stringify(data, null, 2);
})

View file

@ -1,6 +1,6 @@
const url = new URL(window.location.href);
export const baseUrl = `${url.protocol}//${url.host}`;
const api = baseUrl + '/api/alpha/site';
const api_site = baseUrl + '/api/alpha/site';
let jwt = null;
let session_jwt = sessionStorage.getItem('jwt');
@ -14,39 +14,25 @@ if (session_jwt != null) {
}
export { jwt };
const ul = document.getElementById('navbar_items');
const navbar = document.getElementById('navbar_items');
if (jwt != null) {
var request = {method: "GET", headers: {Authorization: `Bearer ${jwt}`}};
ul.innerHTML = '<li class="nav-item dropdown">' +
'<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">' +
'Communities' +
'</a>' +
'<ul class="dropdown-menu">' +
'<li><a class="dropdown-item" href="/api/alpha/communities">All communities</a></li>' +
'</ul>' +
'</li>' +
'<li class="nav-item"><a class="nav-link" href="/user/settings">User settings</a></li>' +
'<li class="nav-item"><a class="nav-link" href="/donate">Donate</a></li>' +
'<li class="nav-item"><a class="nav-link" href="/api/alpha/auth/logout">Logout (via API)</a></li>';
} else {
var request = {method: "GET"};
ul.innerHTML = '<li class="nav-item"><a class="nav-link" href="/api/alpha/auth/login">Log in (via API)</a></li>' +
'<li class="nav-item dropdown">' +
'<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">' +
'Communities' +
'</a>' +
'<ul class="dropdown-menu">' +
'<li><a class="dropdown-item" href="/api/alpha/communities">All communities</a></li>' +
'</ul>' +
'</li>' +
'<li class="nav-item"><a class="nav-link" href="/user/settings">User settings</a></li>' +
'<li class="nav-item"><a class="nav-link" href="/donate">Donate</a></li>';
navbar.innerHTML = '<li class="nav-item"><a class="nav-link" href="/api/alpha/auth/login">Log in (via API)</a></li>' +
'<li class="nav-item dropdown">' +
'<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">' +
'Communities' +
'</a>' +
'<ul class="dropdown-menu">' +
'<li><a class="dropdown-item" href="/api/alpha/communities">All communities</a></li>' +
'</ul>' +
'</li>' +
'<li class="nav-item"><a class="nav-link" href="/user/settings">User settings</a></li>' +
'<li class="nav-item"><a class="nav-link" href="/donate">Donate</a></li>';
}
fetch(api, request)
fetch(api_site, request)
.then(response => response.json())
.then(data => {
// head
@ -55,12 +41,110 @@ fetch(api, request)
document.querySelector('#icon_32').href = data.site.icon_32;
document.querySelector('#icon_16').href = data.site.icon_16;
document.querySelector('#icon_shortcut').href = data.site.icon_32;
document.querySelector('#favicon').href = baseUrl + '/static/images/favicon.ico';
// navbar
document.querySelector('#navbar_title').innerHTML = '<img src="' + data.site.icon + '" alt="Logo" width="36" height="36" />' + ' ' + data.site.name;
if (jwt != null) {
const all_communities_item = document.createElement('li');
all_communities_item.innerHTML = '<a class="dropdown-item" href="/api/alpha/communities">All communities</a>'
const communities_menu = document.createElement('ul');
communities_menu.className = 'dropdown-menu'
communities_menu.appendChild(all_communities_item)
if (data.my_user.moderates.length > 0) {
const dropdown_divider = document.createElement('li');
dropdown_divider.innerHTML = '<hr class="dropdown-divider">'
communities_menu.appendChild(dropdown_divider)
const dropdown_header = document.createElement('li');
dropdown_header.innerHTML = '<h6 class="dropdown-header">Moderating</h6>'
communities_menu.appendChild(dropdown_header)
for (let mods of data.my_user.moderates) {
let moderated_community_item = document.createElement('li');
if (mods.community.local) {
moderated_community_item.innerHTML = '<a class="dropdown-item" href="' + baseUrl + '/api/alpha/c/' + mods.community.name + '">' +
mods.community.title + '<span class="text-body-secondary">' + ' (' + mods.community.ap_domain + ')</span>' +
'</a>'
} else {
moderated_community_item.innerHTML = '<a class="dropdown-item" href="' + baseUrl + '/api/alpha/c/' + mods.community.name + '@' + mods.community.ap_domain + '">' +
mods.community.title + '<span class="text-body-secondary">' + ' (' + mods.community.ap_domain + ')</span>' +
'</a>'
}
communities_menu.appendChild(moderated_community_item)
}
}
if (data.my_user.follows.length > 0) {
const dropdown_divider = document.createElement('li');
dropdown_divider.innerHTML = '<hr class="dropdown-divider">'
communities_menu.appendChild(dropdown_divider)
const dropdown_header = document.createElement('li');
dropdown_header.innerHTML = '<h6 class="dropdown-header">Joined Communities</h6>'
communities_menu.appendChild(dropdown_header)
for (let follows of data.my_user.follows) {
let followed_community_item = document.createElement('li');
if (follows.community.local) {
followed_community_item.innerHTML = '<a class="dropdown-item" href="' + baseUrl + '/api/alpha/c/' + follows.community.name + '">' +
follows.community.title + '<span class="text-body-secondary">' + ' (' + follows.community.ap_domain + ')</span>' +
'</a>'
} else {
followed_community_item.innerHTML = '<a class="dropdown-item" href="' + baseUrl + '/api/alpha/c/' + follows.community.name + '@' + follows.community.ap_domain + '">' +
follows.community.title + '<span class="text-body-secondary">' + ' (' + follows.community.ap_domain + ')</span>' +
'</a>'
}
communities_menu.appendChild(followed_community_item)
}
}
const communities_item = document.createElement('li')
communities_item.className = 'nav-item dropdown'
communities_item.innerHTML = '<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Communities</a>'
communities_item.appendChild(communities_menu)
navbar.appendChild(communities_item)
const user_settings_item = document.createElement('li')
user_settings_item.className = 'nav-item'
user_settings_item.innerHTML = '<a class="nav-link" href="/user/settings">User settings</a>';
navbar.appendChild(user_settings_item)
const logout_item = document.createElement('li')
logout_item.className = 'nav-item'
logout_item.innerHTML = '<a class="nav-link" href="/api/alpha/auth/logout">Log out (via API)</a>';
navbar.appendChild(logout_item)
}
// site info
let postlist = document.querySelector('#post_list_request')
if (jwt != null) {
document.querySelector('#site_request').innerHTML = 'GET <code>/api/alpha/site</code> [LOGGED IN]'
if (postlist) {
postlist.innerHTML = 'GET <code>/api/alpha/post/list?type_=Subscribed&sort=New&page=1</code></p>'
}
} else {
document.querySelector('#site_request').innerHTML = 'GET <code>/api/alpha/site</code> [LOGGED OUT]'
if (postlist) {
postlist.innerHTML = 'GET <code>/api/alpha/post/list?type_=Popular&sort=Hot&page=1</code></p>'
}
}
document.querySelector('#site_json').textContent = JSON.stringify(data, null, 2);
})
let postlist = document.querySelector('#post_list_request');
if (postlist) {
if (jwt != null) {
var api_postlist = baseUrl + '/api/alpha/post/list?type_=Subscribed&sort=New&page=1';
} else {
var api_postlist = baseUrl + '/api/alpha/post/list?type_=Popular&sort=Hot&page=1';
}
fetch(api_postlist, request)
.then(response => response.json())
.then(data => {
document.querySelector('#post_list_json').textContent = JSON.stringify(data, null, 2);
})
}

View file

@ -1,7 +1,7 @@
{% extends 'themes/' + theme() + '/base.html' %}
{% block app_content %}
<p class="mb-0">GET <code>/api/alpha/site</code></p>
<p class="mb-0" id="site_request"></p>
<details><summary>JSON</summary><pre id="site_json"></pre></details>
<p class="mb-0">GET <code>/api/alpha/community/list</code></p>
<details><summary>JSON</summary><pre id="community_list_json"></pre></details>