mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
community owners can change settings and appoint moderators #21
This commit is contained in:
parent
8b4b0c2e7f
commit
4fc715bb18
19 changed files with 401 additions and 38 deletions
|
@ -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(),
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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_(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>{{ '✓'|safe if community.show_home else '✗'|safe }}</th>
|
||||
|
|
|
@ -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) }}
|
||||
|
|
|
@ -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>
|
||||
|
|
19
app/templates/community/community_add_moderator.html
Normal file
19
app/templates/community/community_add_moderator.html
Normal 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 %}
|
58
app/templates/community/community_edit.html
Normal file
58
app/templates/community/community_edit.html
Normal 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 %}
|
48
app/templates/community/community_mod_list.html
Normal file
48
app/templates/community/community_mod_list.html
Normal 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 %}
|
|
@ -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
|
||||
|
|
10
app/utils.py
10
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
|
||||
|
|
Loading…
Add table
Reference in a new issue