diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 239b416a..309e1d3c 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -21,7 +21,8 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti update_post_from_activity, undo_vote, undo_downvote from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \ domain_from_url, markdown_to_html, community_membership, ap_datetime, markdown_to_text, ip_address, can_downvote, \ - can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest + can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest, \ + community_moderators import werkzeug.exceptions @@ -987,11 +988,11 @@ def community_outbox(actor): @bp.route('/c//moderators', methods=['GET']) -def community_moderators(actor): +def community_moderators_route(actor): actor = actor.strip() community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first() if community is not None: - moderator_ids = community.moderators() + moderator_ids = community_moderators(community.id) moderators = User.query.filter(User.id.in_([mod.user_id for mod in moderator_ids])).all() community_data = { "@context": default_context(), diff --git a/app/admin/forms.py b/app/admin/forms.py index 1a50d314..b0b8ff2c 100644 --- a/app/admin/forms.py +++ b/app/admin/forms.py @@ -52,6 +52,7 @@ class EditCommunityForm(FlaskForm): banner_file = FileField(_('Banner image')) rules = TextAreaField(_l('Rules')) nsfw = BooleanField(_l('Porn community')) + banned = BooleanField(_l('Banned - no new posts accepted')) local_only = BooleanField(_l('Only accept posts from current instance')) restricted_to_mods = BooleanField(_l('Only moderators can post')) new_mods_wanted = BooleanField(_l('New moderators wanted')) diff --git a/app/admin/routes.py b/app/admin/routes.py index 1c4ac642..a53d987d 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -199,7 +199,7 @@ def admin_communities(): page = request.args.get('page', 1, type=int) search = request.args.get('search', '') - communities = Community.query.filter_by(banned=False) + communities = Community.query if search: communities = communities.filter(Community.title.ilike(f"%{search}%")) communities = communities.order_by(Community.title).paginate(page=page, per_page=1000, error_out=False) @@ -228,6 +228,7 @@ def admin_community_edit(community_id): community.rules = form.rules.data community.rules_html = markdown_to_html(form.rules.data) community.nsfw = form.nsfw.data + community.banned = form.banned.data community.local_only = form.local_only.data community.restricted_to_mods = form.restricted_to_mods.data community.new_mods_wanted = form.new_mods_wanted.data @@ -255,7 +256,8 @@ def admin_community_edit(community_id): community.image = file db.session.commit() - community.topic.num_communities = community.topic.communities.count() + if community.topic_id: + community.topic.num_communities = community.topic.communities.count() db.session.commit() flash(_('Saved')) return redirect(url_for('admin.admin_communities')) @@ -267,6 +269,7 @@ def admin_community_edit(community_id): form.description.data = community.description form.rules.data = community.rules form.nsfw.data = community.nsfw + form.banned.data = community.banned form.local_only.data = community.local_only form.new_mods_wanted.data = community.new_mods_wanted form.restricted_to_mods.data = community.restricted_to_mods diff --git a/app/chat/routes.py b/app/chat/routes.py index 738483b9..f859c236 100644 --- a/app/chat/routes.py +++ b/app/chat/routes.py @@ -18,7 +18,7 @@ from app.chat import bp def chat_home(conversation_id=None): form = AddReply() if form.validate_on_submit(): - reply = send_message(form, conversation_id) + reply = send_message(form.message.data, conversation_id) return redirect(url_for('chat.chat_home', conversation_id=conversation_id, _anchor=f'message_{reply.id}')) else: conversations = Conversation.query.join(conversation_member, @@ -73,7 +73,7 @@ def new_message(to): conversation.members.append(current_user) db.session.add(conversation) db.session.commit() - reply = send_message(form, conversation.id) + reply = send_message(form.message.data, conversation.id) return redirect(url_for('chat.chat_home', conversation_id=conversation.id, _anchor=f'message_{reply.id}')) else: return render_template('chat/new_message.html', form=form, title=_('New message to "%(recipient_name)s"', recipient_name=recipient.link()), diff --git a/app/chat/util.py b/app/chat/util.py index e56b3eb5..ee78e4a6 100644 --- a/app/chat/util.py +++ b/app/chat/util.py @@ -9,10 +9,10 @@ from app.models import User, ChatMessage, Notification, utcnow, Conversation from app.utils import allowlist_html, shorten_string, gibberish, markdown_to_html -def send_message(form, conversation_id: int) -> ChatMessage: +def send_message(message: str, conversation_id: int) -> ChatMessage: conversation = Conversation.query.get(conversation_id) reply = ChatMessage(sender_id=current_user.id, conversation_id=conversation.id, - body=form.message.data, body_html=allowlist_html(markdown_to_html(form.message.data))) + body=message, body_html=allowlist_html(markdown_to_html(message))) for recipient in conversation.members: if recipient.id != current_user.id: if recipient.is_local(): diff --git a/app/community/forms.py b/app/community/forms.py index 4b287a2c..75507e03 100644 --- a/app/community/forms.py +++ b/app/community/forms.py @@ -13,7 +13,7 @@ from io import BytesIO import pytesseract -class AddLocalCommunity(FlaskForm): +class AddCommunityForm(FlaskForm): community_name = StringField(_l('Name'), validators=[DataRequired()]) url = StringField(_l('Url')) description = TextAreaField(_l('Description')) @@ -37,6 +37,29 @@ class AddLocalCommunity(FlaskForm): return True +class EditCommunityForm(FlaskForm): + title = StringField(_l('Title'), validators=[DataRequired()]) + description = TextAreaField(_l('Description')) + icon_file = FileField(_('Icon image')) + banner_file = FileField(_('Banner image')) + rules = TextAreaField(_l('Rules')) + nsfw = BooleanField(_l('Porn community')) + local_only = BooleanField(_l('Only accept posts from current instance')) + restricted_to_mods = BooleanField(_l('Only moderators can post')) + new_mods_wanted = BooleanField(_l('New moderators wanted')) + topic = SelectField(_l('Topic'), coerce=int, validators=[Optional()]) + layouts = [('', _l('List')), + ('masonry', _l('Masonry')), + ('masonry_wide', _l('Wide masonry'))] + default_layout = SelectField(_l('Layout'), coerce=str, choices=layouts, validators=[Optional()]) + submit = SubmitField(_l('Save')) + + +class AddModeratorForm(FlaskForm): + user_name = StringField(_l('User name'), validators=[DataRequired()]) + submit = SubmitField(_l('Add')) + + class SearchRemoteCommunity(FlaskForm): address = StringField(_l('Community address'), render_kw={'placeholder': 'e.g. !name@server', 'autofocus': True}, validators=[DataRequired()]) submit = SubmitField(_l('Search')) diff --git a/app/community/routes.py b/app/community/routes.py index f9b58408..984e01c2 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -9,21 +9,24 @@ from sqlalchemy import or_, desc from app import db, constants, cache from app.activitypub.signature import RsaKeys, post_request -from app.activitypub.util import default_context, notify_about_post -from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm, ReportCommunityForm, \ - DeleteCommunityForm +from app.activitypub.util import default_context, notify_about_post, find_actor_or_create +from app.chat.util import send_message +from app.community.forms import SearchRemoteCommunity, CreatePostForm, ReportCommunityForm, \ + DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm from app.community.util import search_for_community, community_url_exists, actor_to_community, \ opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file, send_to_remote_instance from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \ SUBSCRIPTION_PENDING, SUBSCRIPTION_MODERATOR from app.inoculation import inoculation from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \ - File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog, Topic + File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog, Topic, Conversation from app.community import bp +from app.user.utils import search_for_user from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \ 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 + joined_communities, moderating_communities, blocked_domains, mimetype_from_url, blocked_instances, \ + community_moderators from feedgen.feed import FeedGenerator from datetime import timezone, timedelta @@ -32,7 +35,7 @@ from datetime import timezone, timedelta @login_required def add_local(): flash('PieFed is still being tested so hosting communities on piefed.social is not advised except for testing purposes.', 'warning') - form = AddLocalCommunity() + form = AddCommunityForm() if g.site.enable_nsfw is False: form.nsfw.render_kw = {'disabled': True} @@ -124,7 +127,7 @@ def show_community(community: Community): if current_user.is_anonymous and request_etag_matches(current_etag): return return_304(current_etag) - mods = community.moderators() + mods = community_moderators(community.id) is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods) is_owner = current_user.is_authenticated and any( @@ -584,6 +587,65 @@ def community_report(community_id: int): return render_template('community/community_report.html', title=_('Report community'), form=form, community=community) +@bp.route('/community//edit', methods=['GET', 'POST']) +@login_required +def community_edit(community_id: int): + from app.admin.util import topics_for_form + community = Community.query.get_or_404(community_id) + if community.is_owner() or current_user.is_admin(): + form = EditCommunityForm() + form.topic.choices = topics_for_form(0) + if form.validate_on_submit(): + community.title = form.title.data + community.description = form.description.data + community.description_html = markdown_to_html(form.description.data) + community.rules = form.rules.data + community.rules_html = markdown_to_html(form.rules.data) + community.nsfw = form.nsfw.data + community.local_only = form.local_only.data + community.restricted_to_mods = form.restricted_to_mods.data + community.new_mods_wanted = form.new_mods_wanted.data + community.topic_id = form.topic.data if form.topic.data != 0 else None + community.default_layout = form.default_layout.data + + icon_file = request.files['icon_file'] + if icon_file and icon_file.filename != '': + if community.icon_id: + community.icon.delete_from_disk() + file = save_icon_file(icon_file) + if file: + community.icon = file + banner_file = request.files['banner_file'] + if banner_file and banner_file.filename != '': + if community.image_id: + community.image.delete_from_disk() + file = save_banner_file(banner_file) + if file: + community.image = file + + db.session.commit() + community.topic.num_communities = community.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)) + else: + form.title.data = community.title + form.description.data = community.description + form.rules.data = community.rules + form.nsfw.data = community.nsfw + form.local_only.data = community.local_only + form.new_mods_wanted.data = community.new_mods_wanted + form.restricted_to_mods.data = community.restricted_to_mods + form.topic.data = community.topic_id if community.topic_id else None + form.default_layout.data = community.default_layout + return render_template('community/community_edit.html', title=_('Edit community'), form=form, + current_app=current_app, + community=community, moderating_communities=moderating_communities(current_user.get_id()), + joined_communities=joined_communities(current_user.get_id())) + else: + abort(401) + + @bp.route('/community//delete', methods=['GET', 'POST']) @login_required def community_delete(community_id: int): @@ -608,6 +670,96 @@ def community_delete(community_id: int): abort(401) +@bp.route('/community//moderators', methods=['GET', 'POST']) +@login_required +def community_mod_list(community_id: int): + community = Community.query.get_or_404(community_id) + if community.is_owner() or current_user.is_admin(): + + moderators = User.query.filter(User.banned == False).join(CommunityMember, CommunityMember.user_id == User.id).\ + filter(CommunityMember.community_id == community_id, or_(CommunityMember.is_moderator == True, CommunityMember.is_owner == True)).all() + + return render_template('community/community_mod_list.html', title=_('Moderators for %(community)s', community=community.display_name()), + moderators=moderators, community=community, + moderating_communities=moderating_communities(current_user.get_id()), + joined_communities=joined_communities(current_user.get_id()) + ) + + +@bp.route('/community//moderators/add', methods=['GET', 'POST']) +@login_required +def community_add_moderator(community_id: int): + community = Community.query.get_or_404(community_id) + if community.is_owner() or current_user.is_admin(): + form = AddModeratorForm() + if form.validate_on_submit(): + new_moderator = search_for_user(form.user_name.data) + if new_moderator: + existing_member = CommunityMember.query.filter(CommunityMember.user_id == new_moderator.id, CommunityMember.community_id == community_id).first() + if existing_member: + existing_member.is_moderator = True + else: + new_member = CommunityMember(community_id=community_id, user_id=new_moderator.id, is_moderator=True) + db.session.add(new_member) + db.session.commit() + flash(_('Moderator added')) + + # Notify new mod + if new_moderator.is_local(): + notify = Notification(title=_('You are now a moderator of %(name)s', name=community.display_name()), + url='/c/' + community.name, user_id=new_moderator.id, + author_id=current_user.id) + new_moderator.unread_notifications += 1 + db.session.add(notify) + db.session.commit() + else: + # for remote users, send a chat message to let them know + existing_conversation = Conversation.find_existing_conversation(recipient=new_moderator, + sender=current_user) + if not existing_conversation: + existing_conversation = Conversation(user_id=current_user.id) + existing_conversation.members.append(new_moderator) + existing_conversation.members.append(current_user) + db.session.add(existing_conversation) + db.session.commit() + server = current_app.config['SERVER_NAME'] + send_message(f"Hi there. I've added you as a moderator to the community !{community.name}@{server}.", existing_conversation.id) + + # Flush cache + cache.delete_memoized(moderating_communities, new_moderator.id) + cache.delete_memoized(joined_communities, new_moderator.id) + cache.delete_memoized(community_moderators, community_id) + return redirect(url_for('community.community_mod_list', community_id=community.id)) + else: + flash(_('Account not found'), 'warning') + + return render_template('community/community_add_moderator.html', title=_('Add moderator to %(community)s', community=community.display_name()), + community=community, form=form, + moderating_communities=moderating_communities(current_user.get_id()), + joined_communities=joined_communities(current_user.get_id()) + ) + + +@bp.route('/community//moderators/remove/', methods=['GET', 'POST']) +@login_required +def community_remove_moderator(community_id: int, user_id: int): + community = Community.query.get_or_404(community_id) + if community.is_owner() or current_user.is_admin(): + + existing_member = CommunityMember.query.filter(CommunityMember.user_id == user_id, + CommunityMember.community_id == community_id).first() + if existing_member: + existing_member.is_moderator = False + db.session.commit() + flash(_('Moderator removed')) + # Flush cache + cache.delete_memoized(moderating_communities, user_id) + cache.delete_memoized(joined_communities, user_id) + cache.delete_memoized(community_moderators, community_id) + + return redirect(url_for('community.community_mod_list', community_id=community.id)) + + @bp.route('/community//block_instance', methods=['GET', 'POST']) @login_required def community_block_instance(community_id: int): diff --git a/app/models.py b/app/models.py index 69449bd5..2f8e0d4c 100644 --- a/app/models.py +++ b/app/models.py @@ -342,7 +342,7 @@ class Community(db.Model): else: return self.ap_id.lower() - @cache.memoize(timeout=30) + @cache.memoize(timeout=3) def moderators(self): return CommunityMember.query.filter((CommunityMember.community_id == self.id) & (or_( diff --git a/app/post/routes.py b/app/post/routes.py index eef451ea..f76b1ecf 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -23,7 +23,7 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_ shorten_string, markdown_to_text, gibberish, ap_datetime, return_304, \ request_etag_matches, ip_address, user_ip_banned, instance_banned, can_downvote, can_upvote, post_ranking, \ reply_already_exists, reply_is_just_link_to_gif_reaction, confidence, moderating_communities, joined_communities, \ - blocked_instances, blocked_domains + blocked_instances, blocked_domains, community_moderators def show_post(post_id: int): @@ -43,7 +43,7 @@ def show_post(post_id: int): if post.mea_culpa: flash(_('%(name)s has indicated they made a mistake in this post.', name=post.author.user_name), 'warning') - mods = community.moderators() + mods = community_moderators(community.id) is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods) # handle top-level comments/replies diff --git a/app/static/structure.css b/app/static/structure.css index ca808a19..55609e4a 100644 --- a/app/static/structure.css +++ b/app/static/structure.css @@ -1217,6 +1217,12 @@ fieldset legend { color: black; } +h1 .warning_badge { + position: relative; + left: 15px; + top: -6px; +} + [data-bs-theme=dark] .warning_badge.nsfl { border: 1px solid white; color: white; @@ -1288,15 +1294,4 @@ fieldset legend { max-width: 100%; } -@media (orientation: portrait) { - .flex-sm-fill.text-sm-center.nav-link.active{ - border-bottom: var(--bs-nav-tabs-border-width) solid var(--bs-border-color); - border-bottom-right-radius: var(--bs-nav-tabs-border-radius); - border-bottom-left-radius: var(--bs-nav-tabs-border-radius); - } - .card-header-tabs { - margin-bottom:8px; - } -} - /*# sourceMappingURL=structure.css.map */ diff --git a/app/static/structure.scss b/app/static/structure.scss index c022c605..4db49593 100644 --- a/app/static/structure.scss +++ b/app/static/structure.scss @@ -900,6 +900,13 @@ fieldset { color:black; } } + +h1 .warning_badge { + position: relative; + left: 15px; + top: -6px; +} + [data-bs-theme=dark] .warning_badge.nsfl { border:1px solid white; color:white; diff --git a/app/templates/admin/communities.html b/app/templates/admin/communities.html index 548ed408..18853a1c 100644 --- a/app/templates/admin/communities.html +++ b/app/templates/admin/communities.html @@ -32,7 +32,7 @@ {{ community.name }} - {{ community.display_name() }} + {{ community.display_name() }}{% if community.banned %} (banned){% endif %} {{ community.topic.name }} {{ community.post_count }} {{ '✓'|safe if community.show_home else '✗'|safe }} diff --git a/app/templates/admin/edit_community.html b/app/templates/admin/edit_community.html index 2c5ca385..9096fdcc 100644 --- a/app/templates/admin/edit_community.html +++ b/app/templates/admin/edit_community.html @@ -42,6 +42,7 @@
{{ _('Will not be overwritten by remote server') }} {% endif %} + {{ render_field(form.banned) }} {{ render_field(form.local_only) }} {{ render_field(form.new_mods_wanted) }} {{ render_field(form.show_home) }} diff --git a/app/templates/community/community.html b/app/templates/community/community.html index ef5b3b5c..5c283f5c 100644 --- a/app/templates/community/community.html +++ b/app/templates/community/community.html @@ -24,6 +24,8 @@ {% if current_user.is_authenticated %} {% include 'community/_notification_toggle.html' %} {% endif %} + {% if community.nsfw %}nsfw{% endif %} + {% if community.nsfl %}nsfl{% endif %} {% elif community.icon_id and not low_bandwidth %}
@@ -43,6 +45,8 @@ {% if current_user.is_authenticated %} {% include 'community/_notification_toggle.html' %} {% endif %} + {% if community.nsfw %}nsfw{% endif %} + {% if community.nsfl %}nsfl{% endif %}
@@ -59,6 +63,8 @@ {% if current_user.is_authenticated %} {% include 'community/_notification_toggle.html' %} {% endif %} + {% if community.nsfw %}nsfw{% endif %} + {% if community.nsfl %}nsfl{% endif %} {% endif %} {% include "community/_community_nav.html" %} @@ -170,8 +176,8 @@

{{ _('Moderate') }}

-

{{ _('Settings') }}

- {% if community.is_owner() or current_user.is_admin() %} +

{{ _('Settings') }}

+ {% if community.is_local() and (community.is_owner() or current_user.is_admin()) %}

Delete community

{% endif %}
diff --git a/app/templates/community/community_add_moderator.html b/app/templates/community/community_add_moderator.html new file mode 100644 index 00000000..016efe74 --- /dev/null +++ b/app/templates/community/community_add_moderator.html @@ -0,0 +1,19 @@ +{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %} + {% extends 'themes/' + theme() + '/base.html' %} +{% else %} + {% extends "base.html" %} +{% endif %} %} +{% from 'bootstrap/form.html' import render_form %} + +{% block app_content %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/community/community_edit.html b/app/templates/community/community_edit.html new file mode 100644 index 00000000..6bf3744e --- /dev/null +++ b/app/templates/community/community_edit.html @@ -0,0 +1,58 @@ +{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %} + {% extends 'themes/' + theme() + '/base.html' %} +{% else %} + {% extends "base.html" %} +{% endif %} %} +{% from 'bootstrap/form.html' import render_field %} + +{% block app_content %} +
+
+ +

+ {% if community %} + {{ _('Edit community') }} + {% else %} + {{ _('Create community') }} + {% endif %} +

+
+ {{ form.csrf_token() }} + {{ render_field(form.title) }} + {{ render_field(form.description) }} + {% if community.icon_id %} + + {% endif %} + {{ render_field(form.icon_file) }} + Provide a square image that looks good when small. + {% if community.image_id %} + + {% endif %} + {{ render_field(form.banner_file) }} + Provide a wide image - letterbox orientation. + {{ render_field(form.rules) }} + {{ render_field(form.nsfw) }} + {{ render_field(form.restricted_to_mods) }} + {{ render_field(form.local_only) }} + {{ render_field(form.new_mods_wanted) }} + {{ render_field(form.topic) }} + {{ render_field(form.default_layout) }} +
+
+ {{ render_field(form.submit) }} +
+ +
+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/community/community_mod_list.html b/app/templates/community/community_mod_list.html new file mode 100644 index 00000000..faaa9f7d --- /dev/null +++ b/app/templates/community/community_mod_list.html @@ -0,0 +1,48 @@ +{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %} + {% extends 'themes/' + theme() + '/base.html' %} +{% else %} + {% extends "base.html" %} +{% endif %} %} +{% from 'bootstrap/form.html' import render_field %} + +{% block app_content %} +
+
+ +
+
+

{{ _('Moderators for %(community)s', community=community.display_name()) }}

+
+ +
+ + + + + + + + + {% for moderator in moderators %} + + + + + {% endfor %} + +
{{ _('Name') }}{{ _('Action') }}
{{ moderator.display_name() }}{% if not community.is_owner(moderator) %} + {{ _('Remove') }}{% endif %}
+
+
+{% endblock %} \ No newline at end of file diff --git a/app/user/utils.py b/app/user/utils.py index 4b8dda41..87648d51 100644 --- a/app/user/utils.py +++ b/app/user/utils.py @@ -4,10 +4,10 @@ from flask import current_app, json from app import celery, db from app.activitypub.signature import post_request -from app.activitypub.util import default_context +from app.activitypub.util import default_context, actor_json_to_model from app.community.util import send_to_remote_instance -from app.models import User, CommunityMember, Community, Instance, Site, utcnow, ActivityPubLog -from app.utils import gibberish, ap_datetime, instance_banned +from app.models import User, CommunityMember, Community, Instance, Site, utcnow, ActivityPubLog, BannedInstances +from app.utils import gibberish, ap_datetime, instance_banned, get_request def purge_user_then_delete(user_id): @@ -113,4 +113,43 @@ def unsubscribe_from_community(community, user): db.session.commit() post_request(community.ap_inbox_url, undo, user.private_key, user.profile_id() + '#main-key') activity.result = 'success' - db.session.commit() \ No newline at end of file + db.session.commit() + + +def search_for_user(address: str): + if '@' in address: + name, server = address.lower().split('@') + else: + name = address + server = '' + + if server: + banned = BannedInstances.query.filter_by(domain=server).first() + if banned: + reason = f" Reason: {banned.reason}" if banned.reason is not None else '' + raise Exception(f"{server} is blocked.{reason}") + already_exists = User.query.filter_by(ap_id=address).first() + else: + already_exists = User.query.filter_by(user_name=name).first() + if already_exists: + return already_exists + + # Look up the profile address of the user using WebFinger + # todo: try, except block around every get_request + webfinger_data = get_request(f"https://{server}/.well-known/webfinger", + params={'resource': f"acct:{address[1:]}"}) + if webfinger_data.status_code == 200: + webfinger_json = webfinger_data.json() + for links in webfinger_json['links']: + if 'rel' in links and links['rel'] == 'self': # this contains the URL of the activitypub profile + type = links['type'] if 'type' in links else 'application/activity+json' + # retrieve the activitypub profile + user_data = get_request(links['href'], headers={'Accept': type}) + # to see the structure of the json contained in community_data, do a GET to https://lemmy.world/c/technology with header Accept: application/activity+json + if user_data.status_code == 200: + user_json = user_data.json() + user_data.close() + if user_json['type'] == 'Person': + user = actor_json_to_model(user_json, name, server) + return user + return None diff --git a/app/utils.py b/app/utils.py index 88b2ad6f..e6ca7c59 100644 --- a/app/utils.py +++ b/app/utils.py @@ -611,6 +611,16 @@ def joined_communities(user_id): filter(CommunityMember.user_id == user_id).order_by(Community.title).all() +@cache.memoize(timeout=300) +def community_moderators(community_id): + return CommunityMember.query.filter((CommunityMember.community_id == community_id) & + (or_( + CommunityMember.is_owner, + CommunityMember.is_moderator + )) + ).all() + + def finalize_user_setup(user, application_required=False): from app.activitypub.signature import RsaKeys user.verified = True