diff --git a/app/community/forms.py b/app/community/forms.py index bb700ede..5f87147f 100644 --- a/app/community/forms.py +++ b/app/community/forms.py @@ -68,7 +68,7 @@ class SearchRemoteCommunity(FlaskForm): class BanUserCommunityForm(FlaskForm): reason = StringField(_l('Reason'), render_kw={'autofocus': True}, validators=[DataRequired()]) - ban_until = DateField(_l('Ban until')) + ban_until = DateField(_l('Ban until'), validators=[Optional()]) delete_posts = BooleanField(_l('Also delete all their posts')) delete_post_replies = BooleanField(_l('Also delete all their comments')) submit = SubmitField(_l('Ban')) diff --git a/app/community/routes.py b/app/community/routes.py index ed350e22..21f5a69a 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -27,7 +27,7 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_ shorten_string, gibberish, community_membership, ap_datetime, \ request_etag_matches, return_304, instance_banned, can_create_post, can_upvote, can_downvote, user_filters_posts, \ joined_communities, moderating_communities, blocked_domains, mimetype_from_url, blocked_instances, \ - community_moderators + community_moderators, communities_banned_from from feedgen.feed import FeedGenerator from datetime import timezone, timedelta @@ -793,6 +793,9 @@ def community_ban_user(community_id: int, user_id: int): form = BanUserCommunityForm() if form.validate_on_submit(): + # Both CommunityBan and CommunityMember need to be updated. CommunityBan is under the control of moderators while + # CommunityMember can be cleared by the user by leaving the group and rejoining. CommunityMember.is_banned stops + # posts from the community from showing up in the banned person's home feed. if not existing: new_ban = CommunityBan(community_id=community_id, user_id=user.id, banned_by=current_user.id, reason=form.reason.data) @@ -800,7 +803,13 @@ def community_ban_user(community_id: int, user_id: int): new_ban.ban_until = form.ban_until.data db.session.add(new_ban) db.session.commit() - flash(_('%(name)s has been banned.', name=user.display_name())) + + community_membership_record = CommunityMember.query.filter_by(community_id=community.id, user_id=user.id).first() + if community_membership_record: + community_membership_record.is_banned = True + db.session.commit() + + flash(_('%(name)s has been banned.', name=user.display_name())) if form.delete_posts.data: posts = Post.query.filter(Post.user_id == user.id, Post.community_id == community.id).all() @@ -819,8 +828,11 @@ def community_ban_user(community_id: int, user_id: int): # notify banned person if user.is_local(): + cache.delete_memoized(communities_banned_from, user.id) + cache.delete_memoized(joined_communities, user.id) + cache.delete_memoized(moderating_communities, user.id) notify = Notification(title=shorten_string('You have been banned from ' + community.title), - url=f'/', user_id=user.id, + url=f'/notifications', user_id=user.id, author_id=1) db.session.add(notify) user.unread_notifications += 1 @@ -839,6 +851,42 @@ def community_ban_user(community_id: int, user_id: int): ) +@bp.route('/community///unban_user_community', methods=['GET', 'POST']) +@login_required +def community_unban_user(community_id: int, user_id: int): + community = Community.query.get_or_404(community_id) + user = User.query.get_or_404(user_id) + existing_ban = CommunityBan.query.filter_by(community_id=community.id, user_id=user.id).first() + if existing_ban: + db.session.delete(existing_ban) + db.session.commit() + + community_membership_record = CommunityMember.query.filter_by(community_id=community.id, user_id=user.id).first() + if community_membership_record: + community_membership_record.is_banned = False + db.session.commit() + + flash(_('%(name)s has been unbanned.', name=user.display_name())) + + # todo: federate ban to post author instance + + # notify banned person + if user.is_local(): + cache.delete_memoized(communities_banned_from, user.id) + cache.delete_memoized(joined_communities, user.id) + cache.delete_memoized(moderating_communities, user.id) + notify = Notification(title=shorten_string('You have been un-banned from ' + community.title), + url=f'/notifications', user_id=user.id, + author_id=1) + db.session.add(notify) + user.unread_notifications += 1 + db.session.commit() + else: + ... + # todo: send chatmessage to remote user and federate it + + return redirect(url_for('community.community_moderate_banned', actor=community.link())) + @bp.route('//notification', methods=['GET', 'POST']) @login_required diff --git a/app/main/routes.py b/app/main/routes.py index b2d292a0..44cca30e 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -24,7 +24,7 @@ from sqlalchemy_searchable import search from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains, \ ap_datetime, ip_address, retrieve_block_list, shorten_string, markdown_to_text, user_filters_home, \ joined_communities, moderating_communities, parse_page, theme_list, get_request, markdown_to_html, allowlist_html, \ - blocked_instances + blocked_instances, communities_banned_from from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain, Topic, File, Instance, \ InstanceRole, Notification from PIL import Image @@ -131,7 +131,13 @@ def home_page(type, sort): next_url = url_for('main.all_posts', page=posts.next_num, sort=sort) if posts.has_next else None prev_url = url_for('main.all_posts', page=posts.prev_num, sort=sort) if posts.has_prev and page != 1 else None - active_communities = Community.query.filter_by(banned=False).order_by(desc(Community.last_active)).limit(5).all() + # Active Communities + active_communities = Community.query.filter_by(banned=False) + if current_user.is_authenticated: # do not show communities current user is banned from + banned_from = communities_banned_from(current_user.id) + if banned_from: + active_communities = active_communities.filter(Community.id.not_in(banned_from)) + active_communities = active_communities.order_by(desc(Community.last_active)).limit(5).all() return render_template('index.html', posts=posts, active_communities=active_communities, show_post_community=True, POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, @@ -178,6 +184,11 @@ def list_communities(): if topic_id != 0: communities = communities.filter_by(topic_id=topic_id) + if current_user.is_authenticated: + banned_from = communities_banned_from(current_user.id) + if banned_from: + communities = communities.filter(Community.id.not_in(banned_from)) + return render_template('list_communities.html', communities=communities.order_by(sort_by).all(), search=search_param, title=_('Communities'), SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR, diff --git a/app/templates/community/community_moderate_banned.html b/app/templates/community/community_moderate_banned.html index 3ce5a8f2..96505f46 100644 --- a/app/templates/community/community_moderate_banned.html +++ b/app/templates/community/community_moderate_banned.html @@ -48,11 +48,12 @@ {{ user.reports if user.reports > 0 }} {{ user.ip_address if user.ip_address }} {% if user.is_local() %} - View local + View {% else %} + View local | View remote {% endif %} - | {{ _('Un ban') }} + | {{ _('Un ban') }} {% endfor %} diff --git a/app/utils.py b/app/utils.py index bb775f67..afffa592 100644 --- a/app/utils.py +++ b/app/utils.py @@ -28,7 +28,7 @@ import re from app.email import send_welcome_email from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan, \ - Site, Post, PostReply, utcnow, Filter, CommunityMember, InstanceBlock + Site, Post, PostReply, utcnow, Filter, CommunityMember, InstanceBlock, CommunityBan # Flask's render_template function, with support for themes added @@ -312,6 +312,12 @@ def community_membership(user: User, community: Community) -> int: return user.subscribed(community.id) +@cache.memoize(timeout=86400) +def communities_banned_from(user_id) -> List[int]: + community_bans = CommunityBan.query.filter(CommunityBan.user_id == user_id).all() + return [cb.community_id for cb in community_bans] + + @cache.memoize(timeout=86400) def blocked_domains(user_id) -> List[int]: blocks = DomainBlock.query.filter_by(user_id=user_id) @@ -460,6 +466,9 @@ def can_downvote(user, community: Community, site=None) -> bool: if user.attitude < -0.40 or user.reputation < -10: # this should exclude about 3.7% of users. return False + if community.id in communities_banned_from(user.id): + return False + return True @@ -467,6 +476,9 @@ def can_upvote(user, community: Community) -> bool: if user is None or community is None or user.banned: return False + if community.id in communities_banned_from(user.id): + return False + return True @@ -483,6 +495,9 @@ def can_create_post(user, content: Community) -> bool: if content.local_only and not user.is_local(): return False + if content.id in communities_banned_from(user.id): + return False + return True @@ -496,6 +511,9 @@ def can_create_post_reply(user, content: Community) -> bool: if content.local_only and not user.is_local(): return False + if content.id in communities_banned_from(user.id): + return False + return True @@ -602,7 +620,8 @@ def moderating_communities(user_id): return [] return Community.query.join(CommunityMember, Community.id == CommunityMember.community_id).\ filter(Community.banned == False).\ - filter(or_(CommunityMember.is_moderator == True, CommunityMember.is_owner == True)).\ + filter(or_(CommunityMember.is_moderator == True, CommunityMember.is_owner == True)). \ + filter(CommunityMember.is_banned == False). \ filter(CommunityMember.user_id == user_id).order_by(Community.title).all() @@ -613,6 +632,7 @@ def joined_communities(user_id): return Community.query.join(CommunityMember, Community.id == CommunityMember.community_id).\ filter(Community.banned == False). \ filter(CommunityMember.is_moderator == False, CommunityMember.is_owner == False). \ + filter(CommunityMember.is_banned == False). \ filter(CommunityMember.user_id == user_id).order_by(Community.title).all()