mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
Merge branch 'main' of https://codeberg.org/saint/pyfedi
This commit is contained in:
commit
7cff8ceeac
26 changed files with 489 additions and 73 deletions
|
@ -1055,6 +1055,7 @@ def comment_ap(comment_id):
|
||||||
}
|
}
|
||||||
resp = jsonify(reply_data)
|
resp = jsonify(reply_data)
|
||||||
resp.content_type = 'application/activity+json'
|
resp.content_type = 'application/activity+json'
|
||||||
|
resp.headers.set('Vary', 'Accept, Accept-Encoding, Cookie')
|
||||||
return resp
|
return resp
|
||||||
else:
|
else:
|
||||||
reply = PostReply.query.get_or_404(comment_id)
|
reply = PostReply.query.get_or_404(comment_id)
|
||||||
|
@ -1075,6 +1076,7 @@ def post_ap(post_id):
|
||||||
post_data['@context'] = default_context()
|
post_data['@context'] = default_context()
|
||||||
resp = jsonify(post_data)
|
resp = jsonify(post_data)
|
||||||
resp.content_type = 'application/activity+json'
|
resp.content_type = 'application/activity+json'
|
||||||
|
resp.headers.set('Vary', 'Accept, Accept-Encoding, Cookie')
|
||||||
return resp
|
return resp
|
||||||
else:
|
else:
|
||||||
return show_post(post_id)
|
return show_post(post_id)
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask_wtf.file import FileRequired, FileAllowed
|
from flask_wtf.file import FileRequired, FileAllowed
|
||||||
|
from sqlalchemy import func
|
||||||
from wtforms import StringField, PasswordField, SubmitField, HiddenField, BooleanField, TextAreaField, SelectField, \
|
from wtforms import StringField, PasswordField, SubmitField, HiddenField, BooleanField, TextAreaField, SelectField, \
|
||||||
FileField, IntegerField
|
FileField, IntegerField
|
||||||
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Optional
|
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Optional
|
||||||
from flask_babel import _, lazy_gettext as _l
|
from flask_babel import _, lazy_gettext as _l
|
||||||
|
|
||||||
|
from app.models import Community, User
|
||||||
|
|
||||||
|
|
||||||
class SiteProfileForm(FlaskForm):
|
class SiteProfileForm(FlaskForm):
|
||||||
name = StringField(_l('Name'))
|
name = StringField(_l('Name'))
|
||||||
|
@ -96,6 +99,70 @@ class EditTopicForm(FlaskForm):
|
||||||
submit = SubmitField(_l('Save'))
|
submit = SubmitField(_l('Save'))
|
||||||
|
|
||||||
|
|
||||||
|
class AddUserForm(FlaskForm):
|
||||||
|
user_name = StringField(_l('User name'), validators=[DataRequired()],
|
||||||
|
render_kw={'autofocus': True, 'autocomplete': 'off'})
|
||||||
|
email = StringField(_l('Email address'), validators=[Optional(), Length(max=255)])
|
||||||
|
password = PasswordField(_l('Password'), validators=[DataRequired(), Length(min=8, max=50)],
|
||||||
|
render_kw={'autocomplete': 'new-password'})
|
||||||
|
password2 = PasswordField(_l('Repeat password'), validators=[DataRequired(), EqualTo('password')])
|
||||||
|
about = TextAreaField(_l('Bio'), validators=[Optional(), Length(min=3, max=5000)])
|
||||||
|
matrix_user_id = StringField(_l('Matrix User ID'), validators=[Optional(), Length(max=255)])
|
||||||
|
profile_file = FileField(_l('Avatar image'))
|
||||||
|
banner_file = FileField(_l('Top banner image'))
|
||||||
|
bot = BooleanField(_l('This profile is a bot'))
|
||||||
|
verified = BooleanField(_l('Email address is verified'))
|
||||||
|
banned = BooleanField(_l('Banned'))
|
||||||
|
newsletter = BooleanField(_l('Subscribe to email newsletter'))
|
||||||
|
ignore_bots = BooleanField(_l('Hide posts by bots'))
|
||||||
|
nsfw = BooleanField(_l('Show NSFW posts'))
|
||||||
|
nsfl = BooleanField(_l('Show NSFL posts'))
|
||||||
|
role_options = [(2, _l('User')),
|
||||||
|
(3, _l('Staff')),
|
||||||
|
(4, _l('Admin')),
|
||||||
|
]
|
||||||
|
role = SelectField(_l('Role'), choices=role_options, default=2, coerce=int)
|
||||||
|
submit = SubmitField(_l('Save'))
|
||||||
|
|
||||||
|
def validate_email(self, email):
|
||||||
|
user = User.query.filter(func.lower(User.email) == func.lower(email.data.strip())).first()
|
||||||
|
if user is not None:
|
||||||
|
raise ValidationError(_l('An account with this email address already exists.'))
|
||||||
|
|
||||||
|
def validate_user_name(self, user_name):
|
||||||
|
if '@' in user_name.data:
|
||||||
|
raise ValidationError(_l('User names cannot contain @.'))
|
||||||
|
user = User.query.filter(func.lower(User.user_name) == func.lower(user_name.data.strip())).filter_by(ap_id=None).first()
|
||||||
|
if user is not None:
|
||||||
|
if user.deleted:
|
||||||
|
raise ValidationError(_l('This username was used in the past and cannot be reused.'))
|
||||||
|
else:
|
||||||
|
raise ValidationError(_l('An account with this user name already exists.'))
|
||||||
|
community = Community.query.filter(func.lower(Community.name) == func.lower(user_name.data.strip())).first()
|
||||||
|
if community is not None:
|
||||||
|
raise ValidationError(_l('A community with this name exists so it cannot be used for a user.'))
|
||||||
|
|
||||||
|
def validate_password(self, password):
|
||||||
|
if not password.data:
|
||||||
|
return
|
||||||
|
password.data = password.data.strip()
|
||||||
|
if password.data == 'password' or password.data == '12345678' or password.data == '1234567890':
|
||||||
|
raise ValidationError(_l('This password is too common.'))
|
||||||
|
|
||||||
|
first_char = password.data[0] # the first character in the string
|
||||||
|
|
||||||
|
all_the_same = True
|
||||||
|
# Compare all characters to the first character
|
||||||
|
for char in password.data:
|
||||||
|
if char != first_char:
|
||||||
|
all_the_same = False
|
||||||
|
if all_the_same:
|
||||||
|
raise ValidationError(_l('This password is not secure.'))
|
||||||
|
|
||||||
|
if password.data == 'password' or password.data == '12345678' or password.data == '1234567890':
|
||||||
|
raise ValidationError(_l('This password is too common.'))
|
||||||
|
|
||||||
|
|
||||||
class EditUserForm(FlaskForm):
|
class EditUserForm(FlaskForm):
|
||||||
about = TextAreaField(_l('Bio'), validators=[Optional(), Length(min=3, max=5000)])
|
about = TextAreaField(_l('Bio'), validators=[Optional(), Length(min=3, max=5000)])
|
||||||
email = StringField(_l('Email address'), validators=[Optional(), Length(max=255)])
|
email = StringField(_l('Email address'), validators=[Optional(), Length(max=255)])
|
||||||
|
@ -112,6 +179,11 @@ class EditUserForm(FlaskForm):
|
||||||
searchable = BooleanField(_l('Show profile in user list'))
|
searchable = BooleanField(_l('Show profile in user list'))
|
||||||
indexable = BooleanField(_l('Allow search engines to index this profile'))
|
indexable = BooleanField(_l('Allow search engines to index this profile'))
|
||||||
manually_approves_followers = BooleanField(_l('Manually approve followers'))
|
manually_approves_followers = BooleanField(_l('Manually approve followers'))
|
||||||
|
role_options = [(2, _l('User')),
|
||||||
|
(3, _l('Staff')),
|
||||||
|
(4, _l('Admin')),
|
||||||
|
]
|
||||||
|
role = SelectField(_l('Role'), choices=role_options, default=2, coerce=int)
|
||||||
submit = SubmitField(_l('Save'))
|
submit = SubmitField(_l('Save'))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,11 +11,11 @@ from app.activitypub.routes import process_inbox_request, process_delete_request
|
||||||
from app.activitypub.signature import post_request
|
from app.activitypub.signature import post_request
|
||||||
from app.activitypub.util import default_context
|
from app.activitypub.util import default_context
|
||||||
from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm, EditCommunityForm, EditUserForm, \
|
from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm, EditCommunityForm, EditUserForm, \
|
||||||
EditTopicForm, SendNewsletterForm
|
EditTopicForm, SendNewsletterForm, AddUserForm
|
||||||
from app.admin.util import unsubscribe_from_everything_then_delete, unsubscribe_from_community, send_newsletter
|
from app.admin.util import unsubscribe_from_everything_then_delete, unsubscribe_from_community, send_newsletter
|
||||||
from app.community.util import save_icon_file, save_banner_file
|
from app.community.util import save_icon_file, save_banner_file
|
||||||
from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site, Community, CommunityMember, \
|
from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site, Community, CommunityMember, \
|
||||||
User, Instance, File, Report, Topic, UserRegistration
|
User, Instance, File, Report, Topic, UserRegistration, Role
|
||||||
from app.utils import render_template, permission_required, set_setting, get_setting, gibberish, markdown_to_html, \
|
from app.utils import render_template, permission_required, set_setting, get_setting, gibberish, markdown_to_html, \
|
||||||
moderating_communities, joined_communities, finalize_user_setup, theme_list
|
moderating_communities, joined_communities, finalize_user_setup, theme_list
|
||||||
from app.admin import bp
|
from app.admin import bp
|
||||||
|
@ -553,6 +553,15 @@ def admin_user_edit(user_id):
|
||||||
user.searchable = form.searchable.data
|
user.searchable = form.searchable.data
|
||||||
user.indexable = form.indexable.data
|
user.indexable = form.indexable.data
|
||||||
user.ap_manually_approves_followers = form.manually_approves_followers.data
|
user.ap_manually_approves_followers = form.manually_approves_followers.data
|
||||||
|
|
||||||
|
# Update user roles. The UI only lets the user choose 1 role but the DB structure allows for multiple roles per user.
|
||||||
|
for role in user.roles:
|
||||||
|
if role.id != form.role.data:
|
||||||
|
user.roles.remove(role)
|
||||||
|
user.roles.append(Role.query.get(form.role.data))
|
||||||
|
if form.role.data == 4:
|
||||||
|
flash(_("Permissions are cached for 50 seconds so new admin roles won't take effect immediately."))
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
user.flush_cache()
|
user.flush_cache()
|
||||||
flash(_('Saved'))
|
flash(_('Saved'))
|
||||||
|
@ -573,6 +582,8 @@ def admin_user_edit(user_id):
|
||||||
form.searchable.data = user.searchable
|
form.searchable.data = user.searchable
|
||||||
form.indexable.data = user.indexable
|
form.indexable.data = user.indexable
|
||||||
form.manually_approves_followers.data = user.ap_manually_approves_followers
|
form.manually_approves_followers.data = user.ap_manually_approves_followers
|
||||||
|
if user.roles:
|
||||||
|
form.role.data = user.roles[0].id
|
||||||
|
|
||||||
return render_template('admin/edit_user.html', title=_('Edit user'), form=form, user=user,
|
return render_template('admin/edit_user.html', title=_('Edit user'), form=form, user=user,
|
||||||
moderating_communities=moderating_communities(current_user.get_id()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
@ -581,6 +592,74 @@ def admin_user_edit(user_id):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/users/add', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@permission_required('administer all users')
|
||||||
|
def admin_users_add():
|
||||||
|
form = AddUserForm()
|
||||||
|
user = User()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user.user_name = form.user_name.data
|
||||||
|
user.set_password(form.password.data)
|
||||||
|
user.about = form.about.data
|
||||||
|
user.email = form.email.data
|
||||||
|
user.about_html = markdown_to_html(form.about.data)
|
||||||
|
user.matrix_user_id = form.matrix_user_id.data
|
||||||
|
user.bot = form.bot.data
|
||||||
|
profile_file = request.files['profile_file']
|
||||||
|
if profile_file and profile_file.filename != '':
|
||||||
|
# remove old avatar
|
||||||
|
if user.avatar_id:
|
||||||
|
file = File.query.get(user.avatar_id)
|
||||||
|
file.delete_from_disk()
|
||||||
|
user.avatar_id = None
|
||||||
|
db.session.delete(file)
|
||||||
|
|
||||||
|
# add new avatar
|
||||||
|
file = save_icon_file(profile_file, 'users')
|
||||||
|
if file:
|
||||||
|
user.avatar = file
|
||||||
|
banner_file = request.files['banner_file']
|
||||||
|
if banner_file and banner_file.filename != '':
|
||||||
|
# remove old cover
|
||||||
|
if user.cover_id:
|
||||||
|
file = File.query.get(user.cover_id)
|
||||||
|
file.delete_from_disk()
|
||||||
|
user.cover_id = None
|
||||||
|
db.session.delete(file)
|
||||||
|
|
||||||
|
# add new cover
|
||||||
|
file = save_banner_file(banner_file, 'users')
|
||||||
|
if file:
|
||||||
|
user.cover = file
|
||||||
|
user.newsletter = form.newsletter.data
|
||||||
|
user.ignore_bots = form.ignore_bots.data
|
||||||
|
user.show_nsfw = form.nsfw.data
|
||||||
|
user.show_nsfl = form.nsfl.data
|
||||||
|
|
||||||
|
from app.activitypub.signature import RsaKeys
|
||||||
|
user.verified = True
|
||||||
|
user.last_seen = utcnow()
|
||||||
|
private_key, public_key = RsaKeys.generate_keypair()
|
||||||
|
user.private_key = private_key
|
||||||
|
user.public_key = public_key
|
||||||
|
user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}"
|
||||||
|
user.ap_public_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}"
|
||||||
|
user.ap_inbox_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}/inbox"
|
||||||
|
user.roles.append(Role.query.get(form.role.data))
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(_('User added'))
|
||||||
|
return redirect(url_for('admin.admin_users', local_remote='local'))
|
||||||
|
|
||||||
|
return render_template('admin/add_user.html', title=_('Add user'), form=form, user=user,
|
||||||
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
|
site=g.site
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/user/<int:user_id>/delete', methods=['GET'])
|
@bp.route('/user/<int:user_id>/delete', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required('administer all users')
|
@permission_required('administer all users')
|
||||||
|
|
|
@ -59,6 +59,15 @@ def register(app):
|
||||||
print(private_key)
|
print(private_key)
|
||||||
print(public_key)
|
print(public_key)
|
||||||
|
|
||||||
|
@app.cli.command("admin-keys")
|
||||||
|
def keys():
|
||||||
|
private_key, public_key = RsaKeys.generate_keypair()
|
||||||
|
u: User = User.query.get(1)
|
||||||
|
u.private_key = private_key
|
||||||
|
u.public_key = public_key
|
||||||
|
db.session.commit()
|
||||||
|
print('Admin keys have been reset')
|
||||||
|
|
||||||
@app.cli.command("init-db")
|
@app.cli.command("init-db")
|
||||||
def init_db():
|
def init_db():
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
|
|
|
@ -22,7 +22,7 @@ from app.community import bp
|
||||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||||
shorten_string, gibberish, community_membership, ap_datetime, \
|
shorten_string, gibberish, community_membership, ap_datetime, \
|
||||||
request_etag_matches, return_304, instance_banned, can_create_post, can_upvote, can_downvote, user_filters_posts, \
|
request_etag_matches, return_304, instance_banned, can_create_post, can_upvote, can_downvote, user_filters_posts, \
|
||||||
joined_communities, moderating_communities, blocked_domains
|
joined_communities, moderating_communities, blocked_domains, mimetype_from_url
|
||||||
from feedgen.feed import FeedGenerator
|
from feedgen.feed import FeedGenerator
|
||||||
from datetime import timezone, timedelta
|
from datetime import timezone, timedelta
|
||||||
|
|
||||||
|
@ -190,7 +190,7 @@ def show_community(community: Community):
|
||||||
SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
|
SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
|
||||||
etag=f"{community.id}{sort}{post_layout}_{hash(community.last_active)}", related_communities=related_communities,
|
etag=f"{community.id}{sort}{post_layout}_{hash(community.last_active)}", related_communities=related_communities,
|
||||||
next_url=next_url, prev_url=prev_url, low_bandwidth=low_bandwidth,
|
next_url=next_url, prev_url=prev_url, low_bandwidth=low_bandwidth,
|
||||||
rss_feed=f"https://{current_app.config['SERVER_NAME']}/community/{community.link()}/feed", rss_feed_name=f"{community.title} posts on PieFed",
|
rss_feed=f"https://{current_app.config['SERVER_NAME']}/community/{community.link()}/feed", rss_feed_name=f"{community.title} on PieFed",
|
||||||
content_filters=content_filters, moderating_communities=moderating_communities(current_user.get_id()),
|
content_filters=content_filters, moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_communities(current_user.get_id()), sort=sort,
|
joined_communities=joined_communities(current_user.get_id()), sort=sort,
|
||||||
inoculation=inoculation[randint(0, len(inoculation) - 1)], post_layout=post_layout, current_app=current_app)
|
inoculation=inoculation[randint(0, len(inoculation) - 1)], post_layout=post_layout, current_app=current_app)
|
||||||
|
@ -216,7 +216,7 @@ def show_community_rss(actor):
|
||||||
og_image = community.image.source_url if community.image_id else None
|
og_image = community.image.source_url if community.image_id else None
|
||||||
fg = FeedGenerator()
|
fg = FeedGenerator()
|
||||||
fg.id(f"https://{current_app.config['SERVER_NAME']}/c/{actor}")
|
fg.id(f"https://{current_app.config['SERVER_NAME']}/c/{actor}")
|
||||||
fg.title(community.title)
|
fg.title(f'{community.title} on {g.site.name}')
|
||||||
fg.link(href=f"https://{current_app.config['SERVER_NAME']}/c/{actor}", rel='alternate')
|
fg.link(href=f"https://{current_app.config['SERVER_NAME']}/c/{actor}", rel='alternate')
|
||||||
if og_image:
|
if og_image:
|
||||||
fg.logo(og_image)
|
fg.logo(og_image)
|
||||||
|
@ -233,6 +233,10 @@ def show_community_rss(actor):
|
||||||
fe = fg.add_entry()
|
fe = fg.add_entry()
|
||||||
fe.title(post.title)
|
fe.title(post.title)
|
||||||
fe.link(href=f"https://{current_app.config['SERVER_NAME']}/post/{post.id}")
|
fe.link(href=f"https://{current_app.config['SERVER_NAME']}/post/{post.id}")
|
||||||
|
if post.url:
|
||||||
|
type = mimetype_from_url(post.url)
|
||||||
|
if type and not type.startswith('text/'):
|
||||||
|
fe.enclosure(post.url, type=type)
|
||||||
fe.description(post.body_html)
|
fe.description(post.body_html)
|
||||||
fe.guid(post.profile_id(), permalink=True)
|
fe.guid(post.profile_id(), permalink=True)
|
||||||
fe.author(name=post.author.user_name)
|
fe.author(name=post.author.user_name)
|
||||||
|
|
|
@ -172,7 +172,7 @@ def show_post(post_id: int):
|
||||||
og_image = post.image.source_url if post.image_id else None
|
og_image = post.image.source_url if post.image_id else None
|
||||||
description = shorten_string(markdown_to_text(post.body), 150) if post.body else None
|
description = shorten_string(markdown_to_text(post.body), 150) if post.body else None
|
||||||
|
|
||||||
return render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, community=post.community,
|
response = render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, community=post.community,
|
||||||
canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH,
|
canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH,
|
||||||
description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE,
|
description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE,
|
||||||
POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE,
|
POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE,
|
||||||
|
@ -182,6 +182,8 @@ def show_post(post_id: int):
|
||||||
joined_communities=joined_communities(current_user.get_id()),
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||||
)
|
)
|
||||||
|
response.headers.set('Vary', 'Accept, Accept-Encoding, Cookie')
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/post/<int:post_id>/<vote_direction>', methods=['GET', 'POST'])
|
@bp.route('/post/<int:post_id>/<vote_direction>', methods=['GET', 'POST'])
|
||||||
|
@ -367,11 +369,13 @@ def continue_discussion(post_id, comment_id):
|
||||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
||||||
replies = get_comment_branch(post.id, comment.id, 'top')
|
replies = get_comment_branch(post.id, comment.id, 'top')
|
||||||
|
|
||||||
return render_template('post/continue_discussion.html', title=_('Discussing %(title)s', title=post.title), post=post,
|
response = render_template('post/continue_discussion.html', title=_('Discussing %(title)s', title=post.title), post=post,
|
||||||
is_moderator=is_moderator, comment=comment, replies=replies, markdown_editor=current_user.is_authenticated and current_user.markdown_editor,
|
is_moderator=is_moderator, comment=comment, replies=replies, markdown_editor=current_user.is_authenticated and current_user.markdown_editor,
|
||||||
moderating_communities=moderating_communities(current_user.get_id()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_communities(current_user.get_id()), community=post.community,
|
joined_communities=joined_communities(current_user.get_id()), community=post.community,
|
||||||
inoculation=inoculation[randint(0, len(inoculation) - 1)])
|
inoculation=inoculation[randint(0, len(inoculation) - 1)])
|
||||||
|
response.headers.set('Vary', 'Accept, Accept-Encoding, Cookie')
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/post/<int:post_id>/comment/<int:comment_id>/reply', methods=['GET', 'POST'])
|
@bp.route('/post/<int:post_id>/comment/<int:comment_id>/reply', methods=['GET', 'POST'])
|
||||||
|
|
|
@ -11,6 +11,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
setupKeyboardShortcuts();
|
setupKeyboardShortcuts();
|
||||||
setupTopicChooser();
|
setupTopicChooser();
|
||||||
setupConversationChooser();
|
setupConversationChooser();
|
||||||
|
setupMarkdownEditorEnabler();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -549,4 +550,36 @@ function displayTimeTracked() {
|
||||||
if(timeSpentElement && timeSpent) {
|
if(timeSpentElement && timeSpent) {
|
||||||
timeSpentElement.textContent = formatTime(timeSpent)
|
timeSpentElement.textContent = formatTime(timeSpent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupMarkdownEditorEnabler() {
|
||||||
|
const markdownEnablerLinks = document.querySelectorAll('.markdown_editor_enabler');
|
||||||
|
markdownEnablerLinks.forEach(function(link) {
|
||||||
|
link.addEventListener('click', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const dataId = link.dataset.id;
|
||||||
|
if(dataId) {
|
||||||
|
var downarea = new DownArea({
|
||||||
|
elem: document.querySelector('#' + dataId),
|
||||||
|
resize: DownArea.RESIZE_VERTICAL,
|
||||||
|
hide: ['heading', 'bold-italic'],
|
||||||
|
});
|
||||||
|
setupAutoResize(dataId);
|
||||||
|
link.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* register a service worker */
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
navigator.serviceWorker.register('/static/service_worker.js', {scope: '/static/'}).then(function(registration) {
|
||||||
|
// Registration was successful
|
||||||
|
// console.log('ServiceWorker2 registration successful with scope: ', registration.scope);
|
||||||
|
}, function(err) {
|
||||||
|
// registration failed :(
|
||||||
|
console.log('ServiceWorker registration failed: ', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
],
|
],
|
||||||
"theme_color": "#007BBF",
|
"theme_color": "#007BBF",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
"display": "browser",
|
"display": "minimal-ui",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"scope": "/"
|
"scope": "/"
|
||||||
}
|
}
|
31
app/static/service_worker.js
Normal file
31
app/static/service_worker.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
//https://developers.google.com/web/fundamentals/primers/service-workers
|
||||||
|
|
||||||
|
// Font files
|
||||||
|
var fontFiles = [
|
||||||
|
'/static/fonts/feather/feather.ttf',
|
||||||
|
];
|
||||||
|
|
||||||
|
//this is just an empty service worker so that the 'Install CB as an app' prompt appears in web browsers
|
||||||
|
self.addEventListener('install', function(event) {
|
||||||
|
event.waitUntil(caches.open('core').then(function (cache) {
|
||||||
|
fontFiles.forEach(function (file) {
|
||||||
|
cache.add(new Request(file));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', function(event) {
|
||||||
|
// Fonts
|
||||||
|
// Offline-first
|
||||||
|
if (request.url.includes('feather.ttf')) {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(request).then(function (response) {
|
||||||
|
return response || fetch(request).then(function (response) {
|
||||||
|
// Return the requested file
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -1255,4 +1255,22 @@ fieldset legend {
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#post_reply_markdown_editor_enabler {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 3px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
#post_reply_markdown_editor_enabler {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.create_post_markdown_editor_enabler {
|
||||||
|
text-align: right;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
/*# sourceMappingURL=structure.css.map */
|
/*# sourceMappingURL=structure.css.map */
|
||||||
|
|
|
@ -934,4 +934,20 @@ fieldset {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#post_reply_markdown_editor_enabler {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 3px;
|
||||||
|
right: 0;
|
||||||
|
@include breakpoint(phablet) {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.create_post_markdown_editor_enabler {
|
||||||
|
text-align: right;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
<nav class="mb-4">
|
<nav class="mb-4">
|
||||||
<a href="{{ url_for('admin.admin_home') }}">{{ _('Home') }}</a> |
|
<h2 class="visually-hidden">{{ _('Admin navigation') }}</h2>
|
||||||
|
<a href="{{ url_for('admin.admin_home') }}">{{ _('Admin home') }}</a> |
|
||||||
<a href="{{ url_for('admin.admin_site') }}">{{ _('Site profile') }}</a> |
|
<a href="{{ url_for('admin.admin_site') }}">{{ _('Site profile') }}</a> |
|
||||||
<a href="{{ url_for('admin.admin_misc') }}">{{ _('Misc settings') }}</a> |
|
<a href="{{ url_for('admin.admin_misc') }}">{{ _('Misc settings') }}</a> |
|
||||||
<a href="{{ url_for('admin.admin_communities') }}">{{ _('Communities') }}</a> |
|
<a href="{{ url_for('admin.admin_communities') }}">{{ _('Communities') }}</a> |
|
||||||
|
|
47
app/templates/admin/add_user.html
Normal file
47
app/templates/admin/add_user.html
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{% 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">
|
||||||
|
{% include 'admin/_nav.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-login mx-auto">
|
||||||
|
<h3>{{ _('Add new user') }}</h3>
|
||||||
|
<form method="post" enctype="multipart/form-data" id="add_local_user_form">
|
||||||
|
{{ form.csrf_token() }}
|
||||||
|
{{ render_field(form.user_name) }}
|
||||||
|
{{ render_field(form.email) }}
|
||||||
|
{{ render_field(form.password) }}
|
||||||
|
{{ render_field(form.password2) }}
|
||||||
|
{{ render_field(form.about) }}
|
||||||
|
{{ render_field(form.matrix_user_id) }}
|
||||||
|
{% if user.avatar_id %}
|
||||||
|
<img class="user_icon_big rounded-circle" src="{{ user.avatar_image() }}" width="120" height="120" />
|
||||||
|
{% endif %}
|
||||||
|
{{ render_field(form.profile_file) }}
|
||||||
|
<small class="field_hint">Provide a square image that looks good when small.</small>
|
||||||
|
{% if user.cover_id %}
|
||||||
|
<a href="{{ user.cover_image() }}"><img class="user_icon_big" src="{{ user.cover_image() }}" style="width: 300px; height: auto;" /></a>
|
||||||
|
{% endif %}
|
||||||
|
{{ render_field(form.banner_file) }}
|
||||||
|
<small class="field_hint">Provide a wide image - letterbox orientation.</small>
|
||||||
|
{{ render_field(form.bot) }}
|
||||||
|
{{ render_field(form.verified) }}
|
||||||
|
{{ render_field(form.banned) }}
|
||||||
|
{{ render_field(form.newsletter) }}
|
||||||
|
{{ render_field(form.nsfw) }}
|
||||||
|
{{ render_field(form.nsfl) }}
|
||||||
|
{{ render_field(form.role) }}
|
||||||
|
{{ render_field(form.submit) }}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -39,6 +39,7 @@
|
||||||
{{ render_field(form.searchable) }}
|
{{ render_field(form.searchable) }}
|
||||||
{{ render_field(form.indexable) }}
|
{{ render_field(form.indexable) }}
|
||||||
{{ render_field(form.manually_approves_followers) }}
|
{{ render_field(form.manually_approves_followers) }}
|
||||||
|
{{ render_field(form.role) }}
|
||||||
{{ render_field(form.submit) }}
|
{{ render_field(form.submit) }}
|
||||||
</form>
|
</form>
|
||||||
<p class="mt-4">
|
<p class="mt-4">
|
||||||
|
|
|
@ -14,13 +14,14 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
<a class="btn btn-primary" href="{{ url_for('admin.admin_users_add') }}" style="float: right;">{{ _('Add local user') }}</a>
|
||||||
<form method="get">
|
<form method="get">
|
||||||
<input type="search" name="search" value="{{ search }}">
|
<input type="search" name="search" value="{{ search }}">
|
||||||
<input type="radio" name="local_remote" value="local" id="local_remote_local" {{ 'checked' if local_remote == 'local' }}><label for="local_remote_local"> Local</label>
|
<input type="radio" name="local_remote" value="local" id="local_remote_local" {{ 'checked' if local_remote == 'local' }}><label for="local_remote_local"> Local</label>
|
||||||
<input type="radio" name="local_remote" value="remote" id="local_remote_remote" {{ 'checked' if local_remote == 'remote' }}><label for="local_remote_remote"> Remote</label>
|
<input type="radio" name="local_remote" value="remote" id="local_remote_remote" {{ 'checked' if local_remote == 'remote' }}><label for="local_remote_remote"> Remote</label>
|
||||||
<input type="submit" name="submit" value="Search" class="btn btn-primary">
|
<input type="submit" name="submit" value="Search" class="btn btn-primary">
|
||||||
</form>
|
</form>
|
||||||
<table class="table table-striped">
|
<table class="table table-striped mt-1">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Local/Remote</th>
|
<th>Local/Remote</th>
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<link href="{{ url_for('static', filename='structure.css', changed=getmtime('structure.css')) }}" type="text/css" rel="stylesheet" />
|
<link href="{{ url_for('static', filename='structure.css', changed=getmtime('structure.css')) }}" type="text/css" rel="stylesheet" />
|
||||||
<link href="{{ url_for('static', filename='styles.css', changed=getmtime('styles.css')) }}" type="text/css" rel="stylesheet" />
|
<link href="{{ url_for('static', filename='styles.css', changed=getmtime('styles.css')) }}" type="text/css" rel="stylesheet" />
|
||||||
<link href="{{ url_for('static', filename='themes/high_contrast/styles.css') }}" type="text/css" rel="alternate stylesheet" title="High contrast" />
|
<link href="{{ url_for('static', filename='themes/high_contrast/styles.css') }}" type="text/css" rel="alternate stylesheet" title="High contrast" />
|
||||||
{% if markdown_editor %}
|
{% if not low_bandwidth %}
|
||||||
<link href="{{ url_for('static', filename='js/markdown/downarea.css') }}" type="text/css" rel="stylesheet" />
|
<link href="{{ url_for('static', filename='js/markdown/downarea.css') }}" type="text/css" rel="stylesheet" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/styles.css') %}
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/styles.css') %}
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
<link rel="apple-touch-icon" sizes="152x152" href="/static/images/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="152x152" href="/static/images/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
|
||||||
<link rel="manifest" href="/static/site.webmanifest">
|
<link rel="manifest" href="/static/manifest.json">
|
||||||
<link rel="shortcut icon" type="image/png" href="/static/images/favicon-32x32.png">
|
<link rel="shortcut icon" type="image/png" href="/static/images/favicon-32x32.png">
|
||||||
<link href='/static/images/favicon.ico' rel='icon' type='image/x-icon'>
|
<link href='/static/images/favicon.ico' rel='icon' type='image/x-icon'>
|
||||||
<meta name="msapplication-TileColor" content="#da532c">
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
@ -235,7 +235,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/htmx.min.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename='js/htmx.min.js') }}"></script>
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/scripts.js', changed=getmtime('js/scripts.js')) }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename='js/scripts.js', changed=getmtime('js/scripts.js')) }}"></script>
|
||||||
{% if markdown_editor and not low_bandwidth %}
|
{% if not low_bandwidth %}
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/markdown/downarea.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename='js/markdown/downarea.js') }}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/scripts.js') %}
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/scripts.js') %}
|
||||||
|
|
|
@ -28,34 +28,42 @@
|
||||||
<div class="tab-pane fade show active" id="discussion-tab-pane" role="tabpanel" aria-labelledby="home-tab" tabindex="0">
|
<div class="tab-pane fade show active" id="discussion-tab-pane" role="tabpanel" aria-labelledby="home-tab" tabindex="0">
|
||||||
{{ render_field(form.discussion_title) }}
|
{{ render_field(form.discussion_title) }}
|
||||||
{{ render_field(form.discussion_body) }}
|
{{ render_field(form.discussion_body) }}
|
||||||
{% if markdown_editor %}
|
{% if not low_bandwidth %}
|
||||||
<script nonce="{{ session['nonce'] }}">
|
{% if markdown_editor %}
|
||||||
window.addEventListener("load", function () {
|
<script nonce="{{ session['nonce'] }}">
|
||||||
var downarea = new DownArea({
|
window.addEventListener("load", function () {
|
||||||
elem: document.querySelector('#discussion_body'),
|
var downarea = new DownArea({
|
||||||
resize: DownArea.RESIZE_VERTICAL,
|
elem: document.querySelector('#discussion_body'),
|
||||||
hide: ['heading', 'bold-italic'],
|
resize: DownArea.RESIZE_VERTICAL,
|
||||||
});
|
hide: ['heading', 'bold-italic'],
|
||||||
setupAutoResize('discussion_body');
|
});
|
||||||
});
|
setupAutoResize('discussion_body');
|
||||||
</script>
|
});
|
||||||
|
</script>
|
||||||
|
{% else %}
|
||||||
|
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="discussion_body">{{ _('Enable markdown editor') }}</a>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="link-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">
|
<div class="tab-pane fade" id="link-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">
|
||||||
{{ render_field(form.link_title) }}
|
{{ render_field(form.link_title) }}
|
||||||
{{ render_field(form.link_url) }}
|
{{ render_field(form.link_url) }}
|
||||||
{{ render_field(form.link_body) }}
|
{{ render_field(form.link_body) }}
|
||||||
{% if markdown_editor %}
|
{% if not low_bandwidth %}
|
||||||
<script nonce="{{ session['nonce'] }}">
|
{% if markdown_editor %}
|
||||||
window.addEventListener("load", function () {
|
<script nonce="{{ session['nonce'] }}">
|
||||||
var downarea = new DownArea({
|
window.addEventListener("load", function () {
|
||||||
elem: document.querySelector('#link_body'),
|
var downarea = new DownArea({
|
||||||
resize: DownArea.RESIZE_VERTICAL,
|
elem: document.querySelector('#link_body'),
|
||||||
hide: ['heading', 'bold-italic'],
|
resize: DownArea.RESIZE_VERTICAL,
|
||||||
});
|
hide: ['heading', 'bold-italic'],
|
||||||
setupAutoResize('link_body');
|
});
|
||||||
});
|
setupAutoResize('link_body');
|
||||||
</script>
|
});
|
||||||
|
</script>
|
||||||
|
{% else %}
|
||||||
|
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="link_body">{{ _('Enable markdown editor') }}</a>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="image-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">
|
<div class="tab-pane fade" id="image-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">
|
||||||
|
@ -64,17 +72,21 @@
|
||||||
{{ render_field(form.image_alt_text) }}
|
{{ render_field(form.image_alt_text) }}
|
||||||
<small class="field_hint">{{ _('Describe the image, to help visually impaired people.') }}</small>
|
<small class="field_hint">{{ _('Describe the image, to help visually impaired people.') }}</small>
|
||||||
{{ render_field(form.image_body) }}
|
{{ render_field(form.image_body) }}
|
||||||
{% if markdown_editor %}
|
{% if not low_bandwidth %}
|
||||||
<script nonce="{{ session['nonce'] }}">
|
{% if markdown_editor %}
|
||||||
window.addEventListener("load", function () {
|
<script nonce="{{ session['nonce'] }}">
|
||||||
var downarea = new DownArea({
|
window.addEventListener("load", function () {
|
||||||
elem: document.querySelector('#image_body'),
|
var downarea = new DownArea({
|
||||||
resize: DownArea.RESIZE_VERTICAL,
|
elem: document.querySelector('#image_body'),
|
||||||
hide: ['heading', 'bold-italic'],
|
resize: DownArea.RESIZE_VERTICAL,
|
||||||
});
|
hide: ['heading', 'bold-italic'],
|
||||||
setupAutoResize('image_body');
|
});
|
||||||
});
|
setupAutoResize('image_body');
|
||||||
</script>
|
});
|
||||||
|
</script>
|
||||||
|
{% else %}
|
||||||
|
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="image_body">{{ _('Enable markdown editor') }}</a>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="poll-tab-pane" role="tabpanel" aria-labelledby="disabled-tab" tabindex="0">
|
<div class="tab-pane fade" id="poll-tab-pane" role="tabpanel" aria-labelledby="disabled-tab" tabindex="0">
|
||||||
|
|
|
@ -21,17 +21,21 @@
|
||||||
<p>{{ _('This post is hosted on beehaw.org which has <a href="https://docs.beehaw.org/docs/core-principles/what-is-beehaw/" target="_blank" rel="nofollow">higher standards of behaviour than most places. Be nice</a>.') }}</p>
|
<p>{{ _('This post is hosted on beehaw.org which has <a href="https://docs.beehaw.org/docs/core-principles/what-is-beehaw/" target="_blank" rel="nofollow">higher standards of behaviour than most places. Be nice</a>.') }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ render_form(form) }}
|
{{ render_form(form) }}
|
||||||
{% if markdown_editor %}
|
{% if not low_bandwidth %}
|
||||||
<script nonce="{{ session['nonce'] }}">
|
{% if markdown_editor %}
|
||||||
window.addEventListener("load", function () {
|
<script nonce="{{ session['nonce'] }}">
|
||||||
var downarea = new DownArea({
|
window.addEventListener("load", function () {
|
||||||
elem: document.querySelector('#body'),
|
var downarea = new DownArea({
|
||||||
resize: DownArea.RESIZE_VERTICAL,
|
elem: document.querySelector('#body'),
|
||||||
hide: ['heading', 'bold-italic'],
|
resize: DownArea.RESIZE_VERTICAL,
|
||||||
|
hide: ['heading', 'bold-italic'],
|
||||||
|
});
|
||||||
|
setupAutoResize('body');
|
||||||
});
|
});
|
||||||
setupAutoResize('body');
|
</script>
|
||||||
});
|
{% else %}
|
||||||
</script>
|
<a href="#" aria-hidden="true" id="post_reply_markdown_editor_enabler" class="markdown_editor_enabler" data-id="body">{{ _('Enable markdown editor') }}</a>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,20 +20,24 @@
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="reply_form_inner position-relative">
|
<div class="reply_form_inner position-relative">
|
||||||
{% if post.community.ap_id and '@beehaw.org' in post.community.ap_id %}
|
{% if post.community.ap_id and '@beehaw.org' in post.community.ap_id %}
|
||||||
<p>{{ _('This post is hosted on beehaw.org which has <a href="https://docs.beehaw.org/docs/core-principles/what-is-beehaw/" target="_blank" rel="nofollow">high er standards of behaviour than most places. Be nice</a>.') }}</p>
|
<p>{{ _('This post is hosted on beehaw.org which has <a href="https://docs.beehaw.org/docs/core-principles/what-is-beehaw/" target="_blank" rel="nofollow">higher standards of behaviour than most places. Be nice</a>.') }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ render_form(form) }}
|
{{ render_form(form) }}
|
||||||
{% if markdown_editor %}
|
{% if not low_bandwidth %}
|
||||||
<script nonce="{{ session['nonce'] }}">
|
{% if markdown_editor %}
|
||||||
window.addEventListener("load", function () {
|
<script nonce="{{ session['nonce'] }}">
|
||||||
var downarea = new DownArea({
|
window.addEventListener("load", function () {
|
||||||
elem: document.querySelector('#body'),
|
var downarea = new DownArea({
|
||||||
resize: DownArea.RESIZE_VERTICAL,
|
elem: document.querySelector('#body'),
|
||||||
hide: ['heading', 'bold-italic'],
|
resize: DownArea.RESIZE_VERTICAL,
|
||||||
|
hide: ['heading', 'bold-italic'],
|
||||||
|
});
|
||||||
|
setupAutoResize('body');
|
||||||
});
|
});
|
||||||
setupAutoResize('body');
|
</script>
|
||||||
});
|
{% else %}
|
||||||
</script>
|
<a href="#" aria-hidden="true" id="post_reply_markdown_editor_enabler" class="markdown_editor_enabler" data-id="body">{{ _('Enable markdown editor') }}</a>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,7 +49,7 @@
|
||||||
<p><a href="{{ url_for('auth.validation_required') }}">{{ _('Verify your email address to comment') }}</a></p>
|
<p><a href="{{ url_for('auth.validation_required') }}">{{ _('Verify your email address to comment') }}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p><a href="{{ url_for('auth.login') }}">{{ _('Log in to comment') }}</a></p>
|
<p><a href="{{ url_for('auth.login', next='/post/' + str(post.id)) }}">{{ _('Log in to comment') }}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{{ _('Comments are disabled.') }}</p>
|
<p>{{ _('Comments are disabled.') }}</p>
|
||||||
|
|
|
@ -80,6 +80,9 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<p class="mt-4"><a class="btn btn-primary" href="/communities">{{ _('Explore communities') }}</a></p>
|
<p class="mt-4"><a class="btn btn-primary" href="/communities">{{ _('Explore communities') }}</a></p>
|
||||||
|
<p>
|
||||||
|
<a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> </a><a href="{{ rss_feed }}" rel="nofollow">RSS feed</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -24,6 +24,23 @@
|
||||||
{{ render_field(form.password_field) }}
|
{{ render_field(form.password_field) }}
|
||||||
<hr />
|
<hr />
|
||||||
{{ render_field(form.about) }}
|
{{ render_field(form.about) }}
|
||||||
|
{% if not low_bandwidth %}
|
||||||
|
{% if markdown_editor %}
|
||||||
|
<script nonce="{{ session['nonce'] }}">
|
||||||
|
window.addEventListener("load", function () {
|
||||||
|
var downarea = new DownArea({
|
||||||
|
elem: document.querySelector('#about'),
|
||||||
|
resize: DownArea.RESIZE_VERTICAL,
|
||||||
|
hide: ['heading', 'bold-italic'],
|
||||||
|
value: {{ form.about.data | tojson | safe }}
|
||||||
|
});
|
||||||
|
setupAutoResize('about');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% else %}
|
||||||
|
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="about">{{ _('Enable markdown editor') }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{{ render_field(form.matrixuserid) }}
|
{{ render_field(form.matrixuserid) }}
|
||||||
<small class="field_hint">e.g. @something:matrix.org. Include leading @ and use : before server</small>
|
<small class="field_hint">e.g. @something:matrix.org. Include leading @ and use : before server</small>
|
||||||
{{ render_field(form.profile_file) }}
|
{{ render_field(form.profile_file) }}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from datetime import timedelta
|
from datetime import timedelta, timezone
|
||||||
from random import randint
|
from random import randint
|
||||||
|
|
||||||
from flask import request, flash, json, url_for, current_app, redirect, abort
|
from feedgen.feed import FeedGenerator
|
||||||
|
from flask import request, flash, json, url_for, current_app, redirect, abort, make_response, g
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from flask_babel import _
|
from flask_babel import _
|
||||||
from sqlalchemy import text, desc, or_
|
from sqlalchemy import text, desc, or_
|
||||||
|
@ -14,7 +15,7 @@ from app.topic import bp
|
||||||
from app import db, celery, cache
|
from app import db, celery, cache
|
||||||
from app.topic.forms import ChooseTopicsForm
|
from app.topic.forms import ChooseTopicsForm
|
||||||
from app.utils import render_template, user_filters_posts, moderating_communities, joined_communities, \
|
from app.utils import render_template, user_filters_posts, moderating_communities, joined_communities, \
|
||||||
community_membership, blocked_domains, validation_required
|
community_membership, blocked_domains, validation_required, mimetype_from_url
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/topic/<topic_name>', methods=['GET'])
|
@bp.route('/topic/<topic_name>', methods=['GET'])
|
||||||
|
@ -79,6 +80,8 @@ def show_topic(topic_name):
|
||||||
return render_template('topic/show_topic.html', title=_(topic.name), posts=posts, topic=topic, sort=sort,
|
return render_template('topic/show_topic.html', title=_(topic.name), posts=posts, topic=topic, sort=sort,
|
||||||
page=page, post_layout=post_layout, next_url=next_url, prev_url=prev_url,
|
page=page, post_layout=post_layout, next_url=next_url, prev_url=prev_url,
|
||||||
topic_communities=topic_communities, content_filters=content_filters,
|
topic_communities=topic_communities, content_filters=content_filters,
|
||||||
|
rss_feed=f"https://{current_app.config['SERVER_NAME']}/topic/{topic_name}.rss",
|
||||||
|
rss_feed_name=f"{topic.name} on {g.site.name}",
|
||||||
show_post_community=True, moderating_communities=moderating_communities(current_user.get_id()),
|
show_post_community=True, moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_communities(current_user.get_id()),
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
inoculation=inoculation[randint(0, len(inoculation) - 1)],
|
inoculation=inoculation[randint(0, len(inoculation) - 1)],
|
||||||
|
@ -87,6 +90,48 @@ def show_topic(topic_name):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/topic/<topic_name>.rss', methods=['GET'])
|
||||||
|
@cache.cached(timeout=600)
|
||||||
|
def show_topic_rss(topic_name):
|
||||||
|
topic = Topic.query.filter(Topic.machine_name == topic_name.strip().lower()).first()
|
||||||
|
|
||||||
|
if topic:
|
||||||
|
posts = Post.query.join(Community, Post.community_id == Community.id).filter(Community.topic_id == topic.id,
|
||||||
|
Community.banned == False)
|
||||||
|
posts = posts.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False)
|
||||||
|
posts = posts.order_by(desc(Post.created_at)).limit(100).all()
|
||||||
|
|
||||||
|
fg = FeedGenerator()
|
||||||
|
fg.id(f"https://{current_app.config['SERVER_NAME']}/topic/{topic_name}")
|
||||||
|
fg.title(f'{topic.name} on {g.site.name}')
|
||||||
|
fg.link(href=f"https://{current_app.config['SERVER_NAME']}/topic/{topic_name}", rel='alternate')
|
||||||
|
fg.logo(f"https://{current_app.config['SERVER_NAME']}/static/images/apple-touch-icon.png")
|
||||||
|
fg.subtitle(' ')
|
||||||
|
fg.link(href=f"https://{current_app.config['SERVER_NAME']}/topic/{topic_name}.rss", rel='self')
|
||||||
|
fg.language('en')
|
||||||
|
|
||||||
|
for post in posts:
|
||||||
|
fe = fg.add_entry()
|
||||||
|
fe.title(post.title)
|
||||||
|
fe.link(href=f"https://{current_app.config['SERVER_NAME']}/post/{post.id}")
|
||||||
|
if post.url:
|
||||||
|
type = mimetype_from_url(post.url)
|
||||||
|
if type and not type.startswith('text/'):
|
||||||
|
fe.enclosure(post.url, type=type)
|
||||||
|
fe.description(post.body_html)
|
||||||
|
fe.guid(post.profile_id(), permalink=True)
|
||||||
|
fe.author(name=post.author.user_name)
|
||||||
|
fe.pubDate(post.created_at.replace(tzinfo=timezone.utc))
|
||||||
|
|
||||||
|
response = make_response(fg.rss_str())
|
||||||
|
response.headers.set('Content-Type', 'application/rss+xml')
|
||||||
|
response.headers.add_header('ETag', f"{topic.id}_{hash(g.site.last_active)}")
|
||||||
|
response.headers.add_header('Cache-Control', 'no-cache, max-age=600, must-revalidate')
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/choose_topics', methods=['GET', 'POST'])
|
@bp.route('/choose_topics', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def choose_topics():
|
def choose_topics():
|
||||||
|
|
|
@ -140,6 +140,7 @@ def edit_profile(actor):
|
||||||
form.password_field.data = ''
|
form.password_field.data = ''
|
||||||
|
|
||||||
return render_template('user/edit_profile.html', title=_('Edit profile'), form=form, user=current_user,
|
return render_template('user/edit_profile.html', title=_('Edit profile'), form=form, user=current_user,
|
||||||
|
markdown_editor=current_user.markdown_editor,
|
||||||
moderating_communities=moderating_communities(current_user.get_id()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_communities(current_user.get_id())
|
joined_communities=joined_communities(current_user.get_id())
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import mimetypes
|
||||||
import random
|
import random
|
||||||
import urllib
|
import urllib
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
@ -56,6 +57,7 @@ def return_304(etag, content_type=None):
|
||||||
resp = make_response('', 304)
|
resp = make_response('', 304)
|
||||||
resp.headers.add_header('ETag', request.headers['If-None-Match'])
|
resp.headers.add_header('ETag', request.headers['If-None-Match'])
|
||||||
resp.headers.add_header('Cache-Control', 'no-cache, max-age=600, must-revalidate')
|
resp.headers.add_header('Cache-Control', 'no-cache, max-age=600, must-revalidate')
|
||||||
|
resp.headers.add_header('Vary', 'Accept, Accept-Encoding, Cookie')
|
||||||
if content_type:
|
if content_type:
|
||||||
resp.headers.set('Content-Type', content_type)
|
resp.headers.set('Content-Type', content_type)
|
||||||
return resp
|
return resp
|
||||||
|
@ -327,6 +329,13 @@ def ensure_directory_exists(directory):
|
||||||
rebuild_directory += '/'
|
rebuild_directory += '/'
|
||||||
|
|
||||||
|
|
||||||
|
def mimetype_from_url(url):
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
path = parsed_url.path.split('?')[0] # Strip off anything after '?'
|
||||||
|
mime_type, _ = mimetypes.guess_type(path)
|
||||||
|
return mime_type
|
||||||
|
|
||||||
|
|
||||||
def validation_required(func):
|
def validation_required(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args, **kwargs):
|
||||||
|
|
|
@ -17,6 +17,8 @@ services:
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env.docker
|
- ./.env.docker
|
||||||
entrypoint: ./entrypoint_celery.sh
|
entrypoint: ./entrypoint_celery.sh
|
||||||
|
volumes:
|
||||||
|
- ./media/:/app/app/static/media/
|
||||||
web:
|
web:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
@ -27,7 +29,6 @@ services:
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env.docker
|
- ./.env.docker
|
||||||
volumes:
|
volumes:
|
||||||
- ./.env:/app/.env
|
|
||||||
- ./.gunicorn.conf.py:/app/gunicorn.conf.py
|
- ./.gunicorn.conf.py:/app/gunicorn.conf.py
|
||||||
- ./media/:/app/app/static/media/
|
- ./media/:/app/app/static/media/
|
||||||
ports:
|
ports:
|
||||||
|
|
|
@ -14,3 +14,5 @@ CACHE_REDIS_URL='redis://localhost:6379/1'
|
||||||
BOUNCE_HOST=''
|
BOUNCE_HOST=''
|
||||||
BOUNCE_USERNAME=''
|
BOUNCE_USERNAME=''
|
||||||
BOUNCE_PASSWORD=''
|
BOUNCE_PASSWORD=''
|
||||||
|
|
||||||
|
FLASK_APP=pyfedi.py
|
||||||
|
|
Loading…
Reference in a new issue