API: process /community/follow (for joining and leaving communities)

This commit is contained in:
freamon 2024-10-07 00:51:05 +00:00
parent ab6d66e7e2
commit 5f42de3893
5 changed files with 251 additions and 7 deletions

View file

@ -2,7 +2,7 @@ from app.api.alpha import bp
from app.api.alpha.utils import get_site, \
get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe, \
get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, \
get_community_list, get_community, \
get_community_list, get_community, post_community_follow, \
get_user, post_user_block
from app.shared.auth import log_user_in
@ -46,6 +46,18 @@ def get_alpha_community_list():
return jsonify({"error": str(ex)}), 400
@bp.route('/api/alpha/community/follow', methods=['POST'])
def post_alpha_community_follow():
if not current_app.debug:
return jsonify({'error': 'alpha api routes only available in debug mode'})
try:
auth = request.headers.get('Authorization')
data = request.get_json(force=True) or {}
return jsonify(post_community_follow(auth, data))
except Exception as ex:
return jsonify({"error": str(ex)}), 400
# Post
@bp.route('/api/alpha/post/list', methods=['GET'])
def get_alpha_post_list():
@ -214,7 +226,6 @@ def alpha_miscellaneous():
@bp.route('/api/alpha/community', methods=['POST'])
@bp.route('/api/alpha/community', methods=['PUT'])
@bp.route('/api/alpha/community/hide', methods=['PUT'])
@bp.route('/api/alpha/community/follow', methods=['POST'])
@bp.route('/api/alpha/community/block', methods=['POST'])
@bp.route('/api/alpha/community/delete', methods=['POST'])
@bp.route('/api/alpha/community/remove', methods=['POST'])

View file

@ -1,7 +1,7 @@
from app.api.alpha.utils.site import get_site
from app.api.alpha.utils.post import get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe
from app.api.alpha.utils.reply import get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe
from app.api.alpha.utils.community import get_community, get_community_list
from app.api.alpha.utils.community import get_community, get_community_list, post_community_follow
from app.api.alpha.utils.user import get_user, post_user_block

View file

@ -1,7 +1,9 @@
from app import cache
from app.api.alpha.views import community_view
from app.api.alpha.utils.validators import required, integer_expected, boolean_expected
from app.utils import authorise_api_user
from app.models import Community, CommunityMember
from app.shared.community import join_community, leave_community
from app.utils import communities_banned_from
@ -26,8 +28,8 @@ def get_community_list(auth, data):
if auth:
try:
user_id = authorise_api_user(auth)
except Exception as e:
raise e
except:
raise
else:
user_id = None
@ -58,8 +60,8 @@ def get_community(auth, data):
if auth:
try:
user_id = authorise_api_user(auth)
except Exception as e:
raise e
except:
raise
else:
user_id = None
@ -68,3 +70,36 @@ def get_community(auth, data):
return community_json
except:
raise
# would be in app/constants.py
SRC_API = 3
def post_community_follow(auth, data):
try:
required(['community_id', 'follow'], data)
integer_expected(['community_id'], data)
boolean_expected(['follow'], data)
except:
raise
community_id = data['community_id']
follow = data['follow']
if auth:
try:
user_id = authorise_api_user(auth)
except:
raise
else:
user_id = None
try:
if follow == True:
user_id = join_community(community_id, SRC_API, auth)
else:
user_id = leave_community(community_id, SRC_API, auth)
community_json = community_view(community=community_id, variant=4, stub=False, user_id=user_id)
return community_json
except:
raise

View file

@ -226,6 +226,11 @@ def community_view(community: Community | int | str, variant, stub=False, user_i
'discussion_languages': []}
return v3
# Variant 4 - models/community/community_response.dart - /community/follow api endpoint
if variant == 4:
v4 = {'community_view': community_view(community=community, variant=2, stub=False, user_id=user_id),
'discussion_languages': []}
return v4
# would be better to incrementally add to a post_reply.path field

193
app/shared/community.py Normal file
View file

@ -0,0 +1,193 @@
from app import db, cache
from app.activitypub.signature import post_request
from app.constants import *
from app.models import Community, CommunityBan, CommunityJoinRequest, CommunityMember
from app.utils import authorise_api_user, community_membership, joined_communities, gibberish
from flask import abort, current_app, flash
from flask_babel import _
from flask_login import current_user
# would be in app/constants.py
SRC_WEB = 1
SRC_PUB = 2
SRC_API = 3
# function can be shared between WEB and API (only API calls it for now)
# call from admin.federation not tested
def join_community(community_id: int, src, auth=None, user_id=None, main_user_name=True):
if src == SRC_API:
community = Community.query.get(community_id)
if not community:
raise Exception('community_not_found')
try:
user = authorise_api_user(auth, return_type='model')
except:
raise
else:
community = Community.query.get_or_404(community_id)
if not user_id:
user = current_user
else:
user = User.query.get(user_id)
pre_load_message = {}
if community_membership(user, community) != SUBSCRIPTION_MEMBER and community_membership(user, community) != SUBSCRIPTION_PENDING:
banned = CommunityBan.query.filter_by(user_id=user.id, community_id=community.id).first()
if banned:
if src == SRC_API:
raise Exception('banned_from_community')
else:
if main_user_name:
flash(_('You cannot join this community'))
return
else:
pre_load_message['user_banned'] = True
return pre_load_message
else:
if src == SRC_API:
return user.id
else:
if not main_user_name:
pre_load_message['status'] = 'already subscribed, or subsciption pending'
return pre_load_message
success = True
remote = not community.is_local()
if remote:
# 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)
db.session.add(join_request)
db.session.commit()
if community.instance.online():
follow = {
"actor": user.public_url(main_user_name=main_user_name),
"to": [community.public_url()],
"object": community.public_url(),
"type": "Follow",
"id": f"https://{current_app.config['SERVER_NAME']}/activities/follow/{join_request.id}"
}
success = post_request(community.ap_inbox_url, follow, user.private_key,
user.public_url(main_user_name=main_user_name) + '#main-key', timeout=10)
if success is False or isinstance(success, str):
if 'is not in allowlist' in success:
if src == SRC_API:
raise Exception('not_in_remote_instance_allowlist')
else:
msg_to_user = f'{community.instance.domain} does not allow us to join their communities.'
if main_user_name:
flash(_(msg_to_user), 'error')
return
else:
pre_load_message['status'] = msg_to_user
return pre_load_message
else:
if src != SRC_API:
msg_to_user = "There was a problem while trying to communicate with remote server. If other people have already joined this community it won't matter."
if main_user_name:
flash(_(msg_to_user), 'error')
return
else:
pre_load_message['status'] = msg_to_user
return pre_load_message
# 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:
cache.delete_memoized(community_membership, user, community)
cache.delete_memoized(joined_communities, user.id)
if src == SRC_API:
return user.id
else:
if main_user_name:
flash('You joined ' + community.title)
else:
pre_load_message['status'] = 'joined'
if not main_user_name:
return pre_load_message
# for SRC_WEB, calling function should handle if the community isn't found
# function can be shared between WEB and API (only API calls it for now)
def leave_community(community_id: int, src, auth=None):
if src == SRC_API:
community = Community.query.get(community_id)
if not community:
raise Exception('community_not_found')
try:
user = authorise_api_user(auth, return_type='model')
except:
raise
else:
community = Community.query.get_or_404(community_id)
user = current_user
subscription = community_membership(user, community)
if subscription:
if subscription != SUBSCRIPTION_OWNER:
proceed = True
# Undo the Follow
if not community.is_local():
success = True
if not community.instance.gone_forever:
undo_id = f"https://{current_app.config['SERVER_NAME']}/activities/undo/" + gibberish(15)
follow = {
"actor": user.public_url(),
"to": [community.public_url()],
"object": community.public_url(),
"type": "Follow",
"id": f"https://{current_app.config['SERVER_NAME']}/activities/follow/{gibberish(15)}"
}
undo = {
'actor': user.public_url(),
'to': [community.public_url()],
'type': 'Undo',
'id': undo_id,
'object': follow
}
success = post_request(community.ap_inbox_url, undo, user.private_key,
user.public_url() + '#main-key', timeout=10)
if success is False or isinstance(success, str):
if src != SRC_API:
flash('There was a problem while trying to unsubscribe', 'error')
return
if proceed:
db.session.query(CommunityMember).filter_by(user_id=user.id, community_id=community.id).delete()
db.session.query(CommunityJoinRequest).filter_by(user_id=user.id, community_id=community.id).delete()
db.session.commit()
if src != SRC_API:
flash('You have left ' + community.title)
cache.delete_memoized(community_membership, user, community)
cache.delete_memoized(joined_communities, user.id)
else:
# todo: community deletion
if src == SRC_API:
raise Exception('need_to_make_someone_else_owner')
else:
flash('You need to make someone else the owner before unsubscribing.', 'warning')
return
if src == SRC_API:
return user.id
else:
# let calling function handle redirect
return