Merge remote-tracking branch 'origin/main'

This commit is contained in:
rimu 2024-10-08 07:02:05 +13:00
commit 88e7bdca57
17 changed files with 567 additions and 60 deletions

View file

@ -945,6 +945,7 @@ def post_json_to_model(activity_log, post_json, user, community) -> Post:
post.image = image
db.session.add(post)
community.post_count += 1
user.post_count += 1
activity_log.result = 'success'
db.session.commit()

View file

@ -662,6 +662,7 @@ def admin_communities_no_topic():
def admin_community_edit(community_id):
form = EditCommunityForm()
community = Community.query.get_or_404(community_id)
old_topic_id = community.topic_id if community.topic_id else None
form.topic.choices = topics_for_form(0)
form.languages.choices = languages_for_form()
if form.validate_on_submit():
@ -708,11 +709,17 @@ def admin_community_edit(community_id):
community.languages.append(Language.query.get(language_choice))
# Always include the undetermined language, so posts with no language will be accepted
community.languages.append(Language.query.filter(Language.code == 'und').first())
db.session.commit()
if community.topic_id != old_topic_id:
if community.topic_id:
community.topic.num_communities = community.topic.communities.count()
if old_topic_id:
topic = Topic.query.get(old_topic_id)
if topic:
topic.num_communities = topic.communities.count()
db.session.commit()
flash(_('Saved'))
return redirect(url_for('admin.admin_communities'))
else:

View file

@ -2,8 +2,8 @@ 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_user
get_community_list, get_community, post_community_follow, post_community_block, \
get_user, post_user_block
from app.shared.auth import log_user_in
from flask import current_app, jsonify, request
@ -27,7 +27,7 @@ def get_alpha_community():
if not current_app.debug:
return jsonify({'error': 'alpha api routes only available in debug mode'})
try:
auth = None
auth = request.headers.get('Authorization')
data = request.args.to_dict() or None
return jsonify(get_community(auth, data))
except Exception as ex:
@ -46,6 +46,30 @@ 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
@bp.route('/api/alpha/community/block', methods=['POST'])
def post_alpha_community_block():
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_block(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():
@ -170,7 +194,7 @@ def get_alpha_user():
@bp.route('/api/alpha/user/login', methods=['POST'])
def post_alpha_login():
def post_alpha_user_login():
if not current_app.debug:
return jsonify({'error': 'alpha api routes only available in debug mode'})
try:
@ -181,6 +205,18 @@ def post_alpha_login():
return jsonify({"error": str(ex)}), 400
@bp.route('/api/alpha/user/block', methods=['POST'])
def post_alpha_user_block():
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_user_block(auth, data))
except Exception as ex:
return jsonify({"error": str(ex)}), 400
# Not yet implemented. Copied from lemmy's V3 api, so some aren't needed, and some need changing
# Site - not yet implemented
@ -202,8 +238,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'])
@bp.route('/api/alpha/community/transfer', methods=['POST'])
@ -219,7 +253,6 @@ def alpha_community():
@bp.route('/api/alpha/post/remove', methods=['POST'])
@bp.route('/api/alpha/post/lock', methods=['POST'])
@bp.route('/api/alpha/post/feature', methods=['POST'])
@bp.route('/api/alpha/post/save', methods=['PUT'])
@bp.route('/api/alpha/post/report', methods=['POST'])
@bp.route('/api/alpha/post/report/resolve', methods=['PUT'])
@bp.route('/api/alpha/post/report/list', methods=['GET'])
@ -261,7 +294,6 @@ def alpha_chat():
@bp.route('/api/alpha/user/replies', methods=['GET'])
@bp.route('/api/alpha/user/ban', methods=['POST'])
@bp.route('/api/alpha/user/banned', methods=['GET'])
@bp.route('/api/alpha/user/block', methods=['POST'])
@bp.route('/api/alpha/user/delete_account', methods=['POST'])
@bp.route('/api/alpha/user/password_reset', methods=['POST'])
@bp.route('/api/alpha/user/password_change', 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.user import get_user
from app.api.alpha.utils.community import get_community, get_community_list, post_community_follow, post_community_block
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, block_community, unblock_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
@ -55,8 +57,71 @@ def get_community(auth, data):
elif 'name' in data:
community = data['name']
if auth:
try:
community_json = community_view(community=community, variant=3)
user_id = authorise_api_user(auth)
except:
raise
else:
user_id = None
try:
community_json = community_view(community=community, variant=3, stub=False, user_id=user_id)
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
def post_community_block(auth, data):
try:
required(['community_id', 'block'], data)
integer_expected(['community_id'], data)
boolean_expected(['block'], data)
except:
raise
community_id = data['community_id']
block = data['block']
try:
if block == True:
user_id = block_community(community_id, SRC_API, auth)
else:
user_id = unblock_community(community_id, SRC_API, auth)
community_json = community_view(community=community_id, variant=5, user_id=user_id)
return community_json
except:
raise

View file

@ -3,7 +3,7 @@ from app.api.alpha.views import post_view
from app.api.alpha.utils.validators import required, integer_expected, boolean_expected
from app.models import Post, Community, CommunityMember, utcnow
from app.shared.post import vote_for_post, bookmark_the_post, remove_the_bookmark_from_post, toggle_post_notification
from app.utils import authorise_api_user
from app.utils import authorise_api_user, blocked_users, blocked_communities
from datetime import timedelta
from sqlalchemy import desc
@ -28,6 +28,14 @@ def cached_post_list(type, sort, user_id, community_id, community_name, person_i
else:
posts = Post.query.filter_by(deleted=False)
if user_id is not None:
blocked_person_ids = blocked_users(user_id)
if blocked_person_ids:
posts = posts.filter(Post.user_id.not_in(blocked_person_ids))
blocked_community_ids = blocked_communities(user_id)
if blocked_community_ids:
posts = posts.filter(Post.community_id.not_in(blocked_community_ids))
if sort == "Hot":
posts = posts.order_by(desc(Post.ranking)).order_by(desc(Post.posted_at))
elif sort == "TopDay":
@ -49,8 +57,8 @@ def get_post_list(auth, data, user_id=None):
if auth:
try:
user_id = authorise_api_user(auth)
except Exception as e:
raise e
except:
raise
# user_id: the logged in user
# person_id: the author of the posts being requested

View file

@ -1,20 +1,25 @@
from app import cache
from app.utils import authorise_api_user
from app.api.alpha.utils.validators import required, integer_expected, boolean_expected
from app.api.alpha.views import reply_view
from app.models import PostReply
from app.shared.reply import vote_for_reply, bookmark_the_post_reply, remove_the_bookmark_from_post_reply, toggle_post_reply_notification
from app.utils import authorise_api_user, blocked_users
from sqlalchemy import desc
# person_id param: the author of the reply; user_id param: the current logged-in user
@cache.memoize(timeout=3)
def cached_reply_list(post_id, person_id, sort, max_depth):
def cached_reply_list(post_id, person_id, sort, max_depth, user_id):
if post_id:
replies = PostReply.query.filter(PostReply.deleted == False, PostReply.post_id == post_id, PostReply.depth <= max_depth)
if person_id:
replies = PostReply.query.filter_by(deleted=False, user_id=person_id)
if user_id is not None:
blocked_person_ids = blocked_users(user_id)
if blocked_person_ids:
replies = replies.filter(PostReply.user_id.not_in(blocked_person_ids))
if sort == "Hot":
replies = replies.order_by(desc(PostReply.ranking)).order_by(desc(PostReply.posted_at))
elif sort == "Top":
@ -36,13 +41,12 @@ def get_reply_list(auth, data, user_id=None):
if data and not post_id and not person_id:
raise Exception('missing_parameters')
else:
replies = cached_reply_list(post_id, person_id, sort, max_depth)
if auth:
try:
user_id = authorise_api_user(auth)
except Exception as e:
raise e
except:
raise
replies = cached_reply_list(post_id, person_id, sort, max_depth, user_id)
# user_id: the logged in user
# person_id: the author of the posts being requested

View file

@ -1,4 +1,5 @@
from app import db
from app.api.alpha.views import user_view, community_view
from app.utils import authorise_api_user
from app.models import Language
@ -69,9 +70,9 @@ def get_site(auth):
},
#"moderates": [],
#"follows": [],
"community_blocks": [], # TODO
"community_blocks": [],
"instance_blocks": [], # TODO
"person_blocks": [], # TODO
"person_blocks": [],
"discussion_languages": [] # TODO
}
"""
@ -83,7 +84,12 @@ def get_site(auth):
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)})
data = {
"version": "1.0.0",
"site": site

View file

@ -2,6 +2,8 @@ from app.api.alpha.views import user_view
from app.utils import authorise_api_user
from app.api.alpha.utils.post import get_post_list
from app.api.alpha.utils.reply import get_reply_list
from app.api.alpha.utils.validators import required, integer_expected, boolean_expected
from app.shared.user import block_another_user, unblock_another_user
def get_user(auth, data):
@ -39,4 +41,26 @@ def get_user(auth, data):
raise
# would be in app/constants.py
SRC_API = 3
def post_user_block(auth, data):
try:
required(['person_id', 'block'], data)
integer_expected(['post_id'], data)
boolean_expected(['block'], data)
except:
raise
person_id = data['person_id']
block = data['block']
try:
if block == True:
user_id = block_another_user(person_id, SRC_API, auth)
else:
user_id = unblock_another_user(person_id, SRC_API, auth)
user_json = user_view(user=person_id, variant=4, user_id=user_id)
return user_json
except:
raise

View file

@ -3,6 +3,7 @@ from __future__ import annotations
from app import cache, db
from app.constants import *
from app.models import Community, CommunityMember, Post, PostReply, PostVote, User
from app.utils import blocked_communities
from sqlalchemy import text
@ -50,8 +51,12 @@ def post_view(post: Post | int, variant, stub=False, user_id=None, my_vote=0):
# counts - models/post/post_aggregates.dart
counts = {'post_id': post.id, 'comments': post.reply_count, 'score': post.score, 'upvotes': post.up_votes, 'downvotes': post.down_votes,
'published': post.posted_at.isoformat() + 'Z', 'newest_comment_time': post.last_active.isoformat() + 'Z'}
if user_id:
bookmarked = db.session.execute(text('SELECT user_id FROM "post_bookmark" WHERE post_id = :post_id and user_id = :user_id'), {'post_id': post.id, 'user_id': user_id}).scalar()
post_sub = db.session.execute(text('SELECT user_id FROM "notification_subscription" WHERE type = :type and entity_id = :entity_id and user_id = :user_id'), {'type': NOTIF_POST, 'entity_id': post.id, 'user_id': user_id}).scalar()
followed = db.session.execute(text('SELECT user_id FROM "community_member" WHERE community_id = :community_id and user_id = :user_id'), {"community_id": post.community_id, "user_id": user_id}).scalar()
else:
bookmarked = post_sub = followed = False
if not stub:
banned = db.session.execute(text('SELECT user_id FROM "community_ban" WHERE user_id = :user_id and community_id = :community_id'), {'user_id': post.user_id, 'community_id': post.community_id}).scalar()
moderator = db.session.execute(text('SELECT is_moderator FROM "community_member" WHERE user_id = :user_id and community_id = :community_id'), {'user_id': post.user_id, 'community_id': post.community_id}).scalar()
@ -72,8 +77,9 @@ def post_view(post: Post | int, variant, stub=False, user_id=None, my_vote=0):
creator_banned_from_community = True if banned else False
creator_is_moderator = True if moderator else False
creator_is_admin = True if admin else False
v2 = {'post': post_view(post=post, variant=1, stub=stub), 'counts': counts, 'banned_from_community': False, 'subscribed': 'NotSubscribed',
'saved': saved, 'read': False, 'hidden': False, 'creator_blocked': False, 'unread_comments': post.reply_count, 'my_vote': my_vote, 'activity_alert': activity_alert,
subscribe_type = 'Subscribed' if followed else 'NotSubscribed'
v2 = {'post': post_view(post=post, variant=1, stub=stub), 'counts': counts, 'banned_from_community': False, 'subscribed': subscribe_type,
'saved': saved, 'read': False, 'hidden': False, 'unread_comments': post.reply_count, 'my_vote': my_vote, 'activity_alert': activity_alert,
'creator_banned_from_community': creator_banned_from_community, 'creator_is_moderator': creator_is_moderator, 'creator_is_admin': creator_is_admin}
try:
@ -127,7 +133,8 @@ def cached_user_view_variant_1(user: User, stub=False):
return v1
def user_view(user: User | int, variant, stub=False):
# 'user' param can be anyone (including the logged in user), 'user_id' param belongs to the user making the request
def user_view(user: User | int, variant, stub=False, user_id=None):
if isinstance(user, int):
user = User.query.get(user)
if not user:
@ -144,6 +151,7 @@ def user_view(user: User | int, variant, stub=False):
return v2
# Variant 3 - models/user/get_person_details.dart - /user?person_id api endpoint
if variant == 3:
modlist = cached_modlist_for_user(user)
v3 = {'person_view': user_view(user=user, variant=2),
@ -152,6 +160,14 @@ def user_view(user: User | int, variant, stub=False):
'comments': []}
return v3
# Variant 4 - models/user/block_person_response.dart - /user/block api endpoint
if variant == 4:
block = db.session.execute(text('SELECT blocker_id FROM "user_block" WHERE blocker_id = :blocker_id and blocked_id = :blocked_id'), {'blocker_id': user_id, 'blocked_id': user.id}).scalar()
blocked = True if block else False
v4 = {'person_view': user_view(user=user, variant=2),
'blocked': blocked}
return v4
@cache.memoize(timeout=600)
def cached_community_view_variant_1(community: Community, stub=False):
@ -194,18 +210,37 @@ def community_view(community: Community | int | str, variant, stub=False, user_i
include = ['id', 'subscriptions_count', 'post_count', 'post_reply_count']
counts = {column.name: getattr(community, column.name) for column in community.__table__.columns if column.name in include}
counts.update({'published': community.created_at.isoformat() + 'Z'})
v2 = {'community': community_view(community=community, variant=1, stub=stub), 'subscribed': 'NotSubscribed', 'blocked': False, 'counts': counts}
if user_id:
followed = db.session.execute(text('SELECT user_id FROM "community_member" WHERE community_id = :community_id and user_id = :user_id'), {"community_id": community.id, "user_id": user_id}).scalar()
blocked = True if community.id in blocked_communities(user_id) else False
else:
followed = blocked = False
subscribe_type = 'Subscribed' if followed else 'NotSubscribed'
v2 = {'community': community_view(community=community, variant=1, stub=stub), 'subscribed': subscribe_type, 'blocked': blocked, 'counts': counts}
return v2
# Variant 3 - models/community/get_community_response.dart - /community api endpoint
if variant == 3:
modlist = cached_modlist_for_community(community.id)
v3 = {'community_view': community_view(community=community, variant=2),
v3 = {'community_view': community_view(community=community, variant=2, stub=False, user_id=user_id),
'moderators': modlist,
'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
# Variant 5 - models/community/block_community_response.dart - /community/block api endpoint
if variant == 5:
block = db.session.execute(text('SELECT user_id FROM "community_block" WHERE user_id = :user_id and community_id = :community_id'), {'user_id': user_id, 'community_id': community.id}).scalar()
blocked = True if block else False
v5 = {'community_view': community_view(community=community, variant=2, stub=False, user_id=user_id),
'blocked': blocked}
return v5
# would be better to incrementally add to a post_reply.path field

View file

@ -929,6 +929,7 @@ def community_edit(community_id: int):
if current_user.banned:
return show_ban_message()
community = Community.query.get_or_404(community_id)
old_topic_id = community.topic_id if community.topic_id else None
if community.is_owner() or current_user.is_admin():
form = EditCommunityForm()
form.topic.choices = topics_for_form(0)
@ -970,10 +971,15 @@ def community_edit(community_id: int):
community.languages.append(Language.query.get(language_choice))
# Always include the undetermined language, so posts with no language will be accepted
community.languages.append(Language.query.filter(Language.code == 'und').first())
db.session.commit()
if community.topic:
if community.topic_id != old_topic_id:
if community.topic_id:
community.topic.num_communities = community.topic.communities.count()
if old_topic_id:
topic = Topic.query.get(old_topic_id)
if topic:
topic.num_communities = topic.communities.count()
db.session.commit()
flash(_('Saved'))
return redirect(url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name))

View file

@ -32,7 +32,7 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_
blocked_instances, blocked_domains, community_moderators, blocked_phrases, show_ban_message, recently_upvoted_posts, \
recently_downvoted_posts, recently_upvoted_post_replies, recently_downvoted_post_replies, reply_is_stupid, \
languages_for_form, menu_topics, add_to_modlog, blocked_communities, piefed_markdown_to_lemmy_markdown, \
permission_required
permission_required, blocked_users
def show_post(post_id: int):
@ -1258,6 +1258,7 @@ def post_block_user(post_id: int):
db.session.add(UserBlock(blocker_id=current_user.id, blocked_id=post.author.id))
db.session.commit()
flash(_('%(name)s has been blocked.', name=post.author.user_name))
cache.delete_memoized(blocked_users, current_user.id)
# todo: federate block to post author instance
@ -1428,6 +1429,7 @@ def post_reply_block_user(post_id: int, comment_id: int):
db.session.add(UserBlock(blocker_id=current_user.id, blocked_id=post_reply.author.id))
db.session.commit()
flash(_('%(name)s has been blocked.', name=post_reply.author.user_name))
cache.delete_memoized(blocked_users, current_user.id)
# todo: federate block to post_reply author instance

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

@ -0,0 +1,223 @@
from app import db, cache
from app.activitypub.signature import post_request
from app.constants import *
from app.models import Community, CommunityBan, CommunityBlock, CommunityJoinRequest, CommunityMember
from app.utils import authorise_api_user, blocked_communities, 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
def block_community(community_id, src, auth=None):
if src == SRC_API:
try:
user_id = authorise_api_user(auth)
except:
raise
else:
user_id = current_user.id
existing = CommunityBlock.query.filter_by(user_id=user_id, community_id=community_id).first()
if not existing:
db.session.add(CommunityBlock(user_id=user_id, community_id=community_id))
db.session.commit()
cache.delete_memoized(blocked_communities, user_id)
if src == SRC_API:
return user_id
else:
return # let calling function handle confirmation flash message and redirect
def unblock_community(community_id, src, auth=None):
if src == SRC_API:
try:
user_id = authorise_api_user(auth)
except:
raise
else:
user_id = current_user.id
existing_block = CommunityBlock.query.filter_by(user_id=user_id, community_id=community_id).first()
if existing_block:
db.session.delete(existing_block)
db.session.commit()
cache.delete_memoized(blocked_communities, user_id)
if src == SRC_API:
return user_id
else:
return # let calling function handle confirmation flash message and redirect

View file

@ -19,7 +19,7 @@ SRC_API = 3
# post_vote in app/post/routes would just need to do 'return vote_for_post(post_id, vote_direction, SRC_WEB)'
def vote_for_post(post_id: int, vote_direction, src, auth=None):
if src == SRC_API and auth is not None:
if src == SRC_API:
post = Post.query.get(post_id)
if not post:
raise Exception('post_not_found')
@ -99,7 +99,7 @@ def vote_for_post(post_id: int, vote_direction, src, auth=None):
# function can be shared between WEB and API (only API calls it for now)
# post_bookmark in app/post/routes would just need to do 'return bookmark_the_post(post_id, SRC_WEB)'
def bookmark_the_post(post_id: int, src, auth=None):
if src == SRC_API and auth is not None:
if src == SRC_API:
post = Post.query.get(post_id)
if not post or post.deleted:
raise Exception('post_not_found')
@ -132,7 +132,7 @@ def bookmark_the_post(post_id: int, src, auth=None):
# function can be shared between WEB and API (only API calls it for now)
# post_remove_bookmark in app/post/routes would just need to do 'return remove_the_bookmark_from_post(post_id, SRC_WEB)'
def remove_the_bookmark_from_post(post_id: int, src, auth=None):
if src == SRC_API and auth is not None:
if src == SRC_API:
post = Post.query.get(post_id)
if not post or post.deleted:
raise Exception('post_not_found')
@ -164,7 +164,7 @@ def remove_the_bookmark_from_post(post_id: int, src, auth=None):
# post_notification in app/post/routes would just need to do 'return toggle_post_notification(post_id, SRC_WEB)'
def toggle_post_notification(post_id: int, src, auth=None):
# Toggle whether the current user is subscribed to notifications about top-level replies to this post or not
if src == SRC_API and auth is not None:
if src == SRC_API:
post = Post.query.get(post_id)
if not post or post.deleted:
raise Exception('post_not_found')

View file

@ -19,7 +19,7 @@ SRC_API = 3
# comment_vote in app/post/routes would just need to do 'return vote_for_reply(reply_id, vote_direction, SRC_WEB)'
def vote_for_reply(reply_id: int, vote_direction, src, auth=None):
if src == SRC_API and auth is not None:
if src == SRC_API:
reply = PostReply.query.get(reply_id)
if not reply:
raise Exception('reply_not_found')
@ -99,7 +99,7 @@ def vote_for_reply(reply_id: int, vote_direction, src, auth=None):
# function can be shared between WEB and API (only API calls it for now)
# post_reply_bookmark in app/post/routes would just need to do 'return bookmark_the_post_reply(comment_id, SRC_WEB)'
def bookmark_the_post_reply(comment_id: int, src, auth=None):
if src == SRC_API and auth is not None:
if src == SRC_API:
post_reply = PostReply.query.get(comment_id)
if not post_reply or post_reply.deleted:
raise Exception('comment_not_found')
@ -133,7 +133,7 @@ def bookmark_the_post_reply(comment_id: int, src, auth=None):
# function can be shared between WEB and API (only API calls it for now)
# post_reply_remove_bookmark in app/post/routes would just need to do 'return remove_the_bookmark_from_post_reply(comment_id, SRC_WEB)'
def remove_the_bookmark_from_post_reply(comment_id: int, src, auth=None):
if src == SRC_API and auth is not None:
if src == SRC_API:
post_reply = PostReply.query.get(comment_id)
if not post_reply or post_reply.deleted:
raise Exception('comment_not_found')
@ -165,7 +165,7 @@ def remove_the_bookmark_from_post_reply(comment_id: int, src, auth=None):
# post_reply_notification in app/post/routes would just need to do 'return toggle_post_reply_notification(post_reply_id, SRC_WEB)'
def toggle_post_reply_notification(post_reply_id: int, src, auth=None):
# Toggle whether the current user is subscribed to notifications about replies to this reply or not
if src == SRC_API and auth is not None:
if src == SRC_API:
post_reply = PostReply.query.get(post_reply_id)
if not post_reply or post_reply.deleted:
raise Exception('comment_not_found')

92
app/shared/user.py Normal file
View file

@ -0,0 +1,92 @@
from app import db, cache
from app.constants import ROLE_STAFF, ROLE_ADMIN
from app.models import UserBlock
from app.utils import authorise_api_user, blocked_users
from flask import flash
from flask_babel import _
from flask_login import current_user
from sqlalchemy import text
# would be in app/constants.py
SRC_WEB = 1
SRC_PUB = 2
SRC_API = 3
# only called from API for now, but can be called from web using [un]block_another_user(user.id, SRC_WEB)
# user_id: the local, logged-in user
# person_id: the person they want to block
def block_another_user(person_id, src, auth=None):
if src == SRC_API:
try:
user_id = authorise_api_user(auth)
except:
raise
else:
user_id = current_user.id
if user_id == person_id:
if src == SRC_API:
raise Exception('cannot_block_self')
else:
flash(_('You cannot block yourself.'), 'error')
return
role = db.session.execute(text('SELECT role_id FROM "user_role" WHERE user_id = :person_id'), {'person_id': person_id}).scalar()
if role == ROLE_ADMIN or role == ROLE_STAFF:
if src == SRC_API:
raise Exception('cannot_block_admin_or_staff')
else:
flash(_('You cannot block admin or staff.'), 'error')
return
existing_block = UserBlock.query.filter_by(blocker_id=user_id, blocked_id=person_id).first()
if not existing_block:
block = UserBlock(blocker_id=user_id, blocked_id=person_id)
db.session.add(block)
db.session.execute(text('DELETE FROM "notification_subscription" WHERE entity_id = :current_user AND user_id = :user_id'),
{'current_user': user_id, 'user_id': person_id})
db.session.commit()
cache.delete_memoized(blocked_users, user_id)
# Nothing to fed? (Lemmy doesn't federate anything to the blocked person)
if src == SRC_API:
return user_id
else:
return # let calling function handle confirmation flash message and redirect
def unblock_another_user(person_id, src, auth=None):
if src == SRC_API:
try:
user_id = authorise_api_user(auth)
except:
raise
else:
user_id = current_user.id
if user_id == person_id:
if src == SRC_API:
raise Exception('cannot_unblock_self')
else:
flash(_('You cannot unblock yourself.'), 'error')
return
existing_block = UserBlock.query.filter_by(blocker_id=user_id, blocked_id=person_id).first()
if existing_block:
db.session.delete(existing_block)
db.session.commit()
cache.delete_memoized(blocked_users, user_id)
# Nothing to fed? (Lemmy doesn't federate anything to the unblocked person)
if src == SRC_API:
return user_id
else:
return # let calling function handle confirmation flash message and redirect

View file

@ -1282,6 +1282,8 @@ def add_to_modlog_activitypub(action: str, actor: User, community_id: int = None
def authorise_api_user(auth, return_type='id'):
if not auth:
raise Exception('incorrect_login')
token = auth[7:] # remove 'Bearer '
try:
@ -1294,7 +1296,7 @@ def authorise_api_user(auth, return_type='id'):
if return_type == 'model':
return user
else:
return user_id
return user.id
else:
raise Exception('incorrect_login')
except jwt.InvalidTokenError: