community owners can change settings and appoint moderators #21

This commit is contained in:
rimu 2024-03-13 16:40:20 +13:00
parent 8b4b0c2e7f
commit 4fc715bb18
19 changed files with 401 additions and 38 deletions

View file

@ -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/<actor>/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(),

View file

@ -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'))

View file

@ -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

View file

@ -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()),

View file

@ -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():

View file

@ -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'))

View file

@ -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/<int:community_id>/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/<int:community_id>/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/<int:community_id>/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/<int:community_id>/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/<int:community_id>/moderators/remove/<int:user_id>', 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/<int:community_id>/block_instance', methods=['GET', 'POST'])
@login_required
def community_block_instance(community_id: int):

View file

@ -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_(

View file

@ -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

View file

@ -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 */

View file

@ -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;

View file

@ -32,7 +32,7 @@
<tr>
<td>{{ community.name }}</td>
<td><img src="{{ community.icon_image('tiny') }}" class="community_icon rounded-circle" loading="lazy" />
{{ community.display_name() }}</td>
{{ community.display_name() }}{% if community.banned %} (banned){% endif %}</td>
<td>{{ community.topic.name }}</td>
<td>{{ community.post_count }}</td>
<th>{{ '&check;'|safe if community.show_home else '&cross;'|safe }}</th>

View file

@ -42,6 +42,7 @@
<fieldset class="border pl-2 pt-2 mb-4">
<legend>{{ _('Will not be overwritten by remote server') }}</legend>
{% endif %}
{{ render_field(form.banned) }}
{{ render_field(form.local_only) }}
{{ render_field(form.new_mods_wanted) }}
{{ render_field(form.show_home) }}

View file

@ -24,6 +24,8 @@
{% if current_user.is_authenticated %}
{% include 'community/_notification_toggle.html' %}
{% endif %}
{% if community.nsfw %}<span class="warning_badge nsfw" title="{{ _('Not safe for work') }}">nsfw</span>{% endif %}
{% if community.nsfl %}<span class="warning_badge nsfl" title="{{ _('Not safe for life') }}">nsfl</span>{% endif %}
</h1>
{% elif community.icon_id and not low_bandwidth %}
<div class="row">
@ -43,6 +45,8 @@
{% if current_user.is_authenticated %}
{% include 'community/_notification_toggle.html' %}
{% endif %}
{% if community.nsfw %}<span class="warning_badge nsfw" title="{{ _('Not safe for work') }}">nsfw</span>{% endif %}
{% if community.nsfl %}<span class="warning_badge nsfl" title="{{ _('Not safe for life') }}">nsfl</span>{% endif %}
</h1>
</div>
</div>
@ -59,6 +63,8 @@
{% if current_user.is_authenticated %}
{% include 'community/_notification_toggle.html' %}
{% endif %}
{% if community.nsfw %}<span class="warning_badge nsfw" title="{{ _('Not safe for work') }}">nsfw</span>{% endif %}
{% if community.nsfl %}<span class="warning_badge nsfl" title="{{ _('Not safe for life') }}">nsfl</span>{% endif %}
</h1>
{% endif %}
{% include "community/_community_nav.html" %}
@ -170,8 +176,8 @@
</div>
<div class="card-body">
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</a></p>
{% if community.is_owner() or current_user.is_admin() %}
<p><a href="{{ url_for('community.community_edit', community_id=community.id) }}" class="btn btn-primary">{{ _('Settings') }}</a></p>
{% if community.is_local() and (community.is_owner() or current_user.is_admin()) %}
<p><a class="btn btn-primary btn-warning" href="{{ url_for('community.community_delete', community_id=community.id) }}" rel="nofollow">Delete community</a></p>
{% endif %}
</div>

View file

@ -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 %}
<div class="row">
<div class="col col-login mx-auto">
<div class="card mt-5">
<div class="card-body p-6">
<div class="card-title">{{ _('Add moderator to %(community)s', community=community.display_name()) }}</div>
{{ render_form(form) }}
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -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 %}
<div class="row">
<div class="col-12 col-md-8 position-relative main_pane">
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not none else community.name) }}">{{ (community.title + '@' + community.ap_domain)|shorten }}</a></li>
<li class="breadcrumb-item active">{{ _('Settings') }}</li>
</ol>
</nav>
<h1 class="mt-2">
{% if community %}
{{ _('Edit community') }}
{% else %}
{{ _('Create community') }}
{% endif %}
</h1>
<form method="post" enctype="multipart/form-data" id="add_local_community_form" role="form">
{{ form.csrf_token() }}
{{ render_field(form.title) }}
{{ render_field(form.description) }}
{% if community.icon_id %}
<img class="community_icon_big rounded-circle" src="{{ community.icon_image() }}" />
{% endif %}
{{ render_field(form.icon_file) }}
<small class="field_hint">Provide a square image that looks good when small.</small>
{% if community.image_id %}
<a href="{{ community.header_image() }}"><img class="community_icon_big" src="{{ community.header_image() }}" /></a>
{% endif %}
{{ render_field(form.banner_file) }}
<small class="field_hint">Provide a wide image - letterbox orientation.</small>
{{ 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) }}
<div class="row">
<div class="col-auto">
{{ render_field(form.submit) }}
</div>
<div class="col-auto">
<a class="btn btn-outline-secondary" href="{{ url_for('community.community_mod_list', community_id=community.id) }}">{{ _('Moderators') }}</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -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 %}
<div class="row">
<div class="col-12 col-md-8 position-relative main_pane">
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not none else community.name) }}">{{ (community.title + '@' + community.ap_domain)|shorten }}</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('community.community_edit', community_id=community.id) }}">{{ _('Settings') }}</a></li>
<li class="breadcrumb-item active">{{ _('Moderators') }}</li>
</ol>
</nav>
<div class="row">
<div class="col col-6">
<h1 class="mt-2">{{ _('Moderators for %(community)s', community=community.display_name()) }}</h1>
</div>
<div class="col col-6 text-right">
<a class="btn btn-primary" href="{{ url_for('community.community_add_moderator', community_id=community.id) }}">{{ _('Add moderator') }}</a>
</div>
</div>
<table class="table table-responsive">
<thead>
<tr>
<th>{{ _('Name') }}</th>
<th>{{ _('Action') }}</th>
</tr>
</thead>
<tbody>
{% for moderator in moderators %}
<tr>
<td>{{ moderator.display_name() }}</td>
<td>{% if not community.is_owner(moderator) %}
<a class="no-underline confirm_first"
href="{{ url_for('community.community_remove_moderator', community_id=community.id, user_id=moderator.id) }}"
rel="nofollow"><span class="fe fe-delete"> {{ _('Remove') }}</span></a>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View file

@ -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()
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

View file

@ -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