add language choices to UI #51

This commit is contained in:
rimu 2024-05-09 17:54:30 +12:00
parent 29c2a05d38
commit af4dee7ea3
24 changed files with 287 additions and 59 deletions

View file

@ -21,11 +21,11 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti
upvote_post, delete_post_or_comment, community_members, \ upvote_post, delete_post_or_comment, community_members, \
user_removed_from_remote_server, create_post, create_post_reply, update_post_reply_from_activity, \ user_removed_from_remote_server, create_post, create_post_reply, update_post_reply_from_activity, \
update_post_from_activity, undo_vote, undo_downvote, post_to_page, get_redis_connection, find_reported_object, \ update_post_from_activity, undo_vote, undo_downvote, post_to_page, get_redis_connection, find_reported_object, \
process_report, ensure_domains_match process_report, ensure_domains_match, can_edit, can_delete
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, render_template, \ from app.utils import gibberish, get_setting, is_image_url, allowlist_html, render_template, \
domain_from_url, markdown_to_html, community_membership, ap_datetime, ip_address, can_downvote, \ domain_from_url, markdown_to_html, community_membership, ap_datetime, 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, blocked_users community_moderators
import werkzeug.exceptions import werkzeug.exceptions
@ -933,17 +933,23 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
if request_json['object']['type'] == 'Page': # Editing a post if request_json['object']['type'] == 'Page': # Editing a post
post = Post.query.filter_by(ap_id=request_json['object']['id']).first() post = Post.query.filter_by(ap_id=request_json['object']['id']).first()
if post: if post:
if can_edit(request_json['actor'], post):
update_post_from_activity(post, request_json) update_post_from_activity(post, request_json)
announce_activity_to_followers(post.community, post.author, request_json) announce_activity_to_followers(post.community, post.author, request_json)
activity_log.result = 'success' activity_log.result = 'success'
else:
activity_log.exception_message = 'Edit attempt denied'
else: else:
activity_log.exception_message = 'Post not found' activity_log.exception_message = 'Post not found'
elif request_json['object']['type'] == 'Note': # Editing a reply elif request_json['object']['type'] == 'Note': # Editing a reply
reply = PostReply.query.filter_by(ap_id=request_json['object']['id']).first() reply = PostReply.query.filter_by(ap_id=request_json['object']['id']).first()
if reply: if reply:
if can_edit(request_json['actor'], reply):
update_post_reply_from_activity(reply, request_json) update_post_reply_from_activity(reply, request_json)
announce_activity_to_followers(reply.community, reply.author, request_json) announce_activity_to_followers(reply.community, reply.author, request_json)
activity_log.result = 'success' activity_log.result = 'success'
else:
activity_log.exception_message = 'Edit attempt denied'
else: else:
activity_log.exception_message = 'PostReply not found' activity_log.exception_message = 'PostReply not found'
elif request_json['type'] == 'Delete': elif request_json['type'] == 'Delete':
@ -954,6 +960,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
post = Post.query.filter_by(ap_id=ap_id).first() post = Post.query.filter_by(ap_id=ap_id).first()
# Delete post # Delete post
if post: if post:
if can_delete(request_json['actor'], post):
if post.url and post.cross_posts is not None: if post.url and post.cross_posts is not None:
old_cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all() old_cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all()
post.cross_posts.clear() post.cross_posts.clear()
@ -966,16 +973,21 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
db.session.delete(post) db.session.delete(post)
db.session.commit() db.session.commit()
activity_log.result = 'success' activity_log.result = 'success'
else:
activity_log.exception_message = 'Delete attempt denied'
else: else:
# Delete PostReply # Delete PostReply
reply = PostReply.query.filter_by(ap_id=ap_id).first() reply = PostReply.query.filter_by(ap_id=ap_id).first()
if reply: if reply:
if can_delete(request_json['actor'], reply):
reply.body_html = '<p><em>deleted</em></p>' reply.body_html = '<p><em>deleted</em></p>'
reply.body = 'deleted' reply.body = 'deleted'
reply.post.reply_count -= 1 reply.post.reply_count -= 1
announce_activity_to_followers(reply.community, reply.author, request_json) announce_activity_to_followers(reply.community, reply.author, request_json)
db.session.commit() db.session.commit()
activity_log.result = 'success' activity_log.result = 'success'
else:
activity_log.exception_message = 'Delete attempt denied'
else: else:
# Delete User # Delete User
user = find_actor_or_create(ap_id, create_if_not_found=False) user = find_actor_or_create(ap_id, create_if_not_found=False)
@ -1399,7 +1411,11 @@ def comment_ap(comment_id):
'mediaType': 'text/html', 'mediaType': 'text/html',
'published': ap_datetime(reply.created_at), 'published': ap_datetime(reply.created_at),
'distinguished': False, 'distinguished': False,
'audience': reply.community.profile_id() 'audience': reply.community.profile_id(),
'language': {
'identifier': reply.language_code(),
'name': reply.language_name()
}
} }
if reply.edited_at: if reply.edited_at:
reply_data['updated'] = ap_datetime(reply.edited_at) reply_data['updated'] = ap_datetime(reply.edited_at)

View file

@ -156,7 +156,11 @@ def post_to_activity(post: Post, community: Community):
"sensitive": post.nsfw or post.nsfl, "sensitive": post.nsfw or post.nsfl,
"published": ap_datetime(post.created_at), "published": ap_datetime(post.created_at),
"stickied": post.sticky, "stickied": post.sticky,
"audience": f"https://{current_app.config['SERVER_NAME']}/c/{community.name}" "audience": f"https://{current_app.config['SERVER_NAME']}/c/{community.name}",
'language': {
'identifier': post.language_code(),
'name': post.language_name()
}
}, },
"cc": [ "cc": [
f"https://{current_app.config['SERVER_NAME']}/c/{community.name}" f"https://{current_app.config['SERVER_NAME']}/c/{community.name}"
@ -756,6 +760,11 @@ def post_json_to_model(activity_log, post_json, user, community) -> Post:
domain.post_count += 1 domain.post_count += 1
post.domain = domain post.domain = domain
if 'language' in post_json:
language = find_language_or_create(post_json['language']['identifier'], post_json['language']['name'])
if language:
post.language_id = language.id
if post is not None: if post is not None:
if 'image' in post_json and post.image is None: if 'image' in post_json and post.image is None:
image = File(source_url=post_json['image']['url']) image = File(source_url=post_json['image']['url'])
@ -1312,6 +1321,11 @@ def create_post_reply(activity_log: ActivityPubLog, community: Community, in_rep
elif 'content' in request_json['object']: # Kbin elif 'content' in request_json['object']: # Kbin
post_reply.body_html = allowlist_html(request_json['object']['content']) post_reply.body_html = allowlist_html(request_json['object']['content'])
post_reply.body = '' post_reply.body = ''
if 'language' in request_json['object'] and isinstance(request_json['object']['language'], dict):
language = find_language_or_create(request_json['object']['language']['identifier'],
request_json['object']['language']['name'])
post_reply.language_id = language.id
if post_id is not None: if post_id is not None:
# Discard post_reply if it contains certain phrases. Good for stopping spam floods. # Discard post_reply if it contains certain phrases. Good for stopping spam floods.
if post_reply.body: if post_reply.body:
@ -1579,6 +1593,10 @@ def update_post_reply_from_activity(reply: PostReply, request_json: dict):
elif 'content' in request_json['object']: elif 'content' in request_json['object']:
reply.body_html = allowlist_html(request_json['object']['content']) reply.body_html = allowlist_html(request_json['object']['content'])
reply.body = '' reply.body = ''
# Language
if 'language' in request_json['object'] and isinstance(request_json['object']['language'], dict):
language = find_language_or_create(request_json['object']['language']['identifier'], request_json['object']['language']['name'])
reply.language_id = language.id
reply.edited_at = utcnow() reply.edited_at = utcnow()
db.session.commit() db.session.commit()
@ -1636,7 +1654,7 @@ def update_post_from_activity(post: Post, request_json: dict):
db.session.add(image) db.session.add(image)
post.image = image post.image = image
elif is_video_url(post.url): elif is_video_url(post.url):
post.type == POST_TYPE_VIDEO post.type = POST_TYPE_VIDEO
image = File(source_url=post.url) image = File(source_url=post.url)
db.session.add(image) db.session.add(image)
post.image = image post.image = image
@ -2049,3 +2067,16 @@ def ensure_domains_match(activity: dict) -> bool:
return False return False
def can_edit(user_ap_id, post):
user = find_actor_or_create(user_ap_id, create_if_not_found=False)
if user:
if post.user_id == user.id:
return True
if post.community.is_moderator(user) or post.community.is_owner(user) or post.community.is_instance_admin(user):
return True
return False
def can_delete(user_ap_id, post):
return can_edit(user_ap_id, post)

View file

@ -87,6 +87,7 @@ def register(app):
db.session.add(Settings(name='allow_remote_image_posts', value=json.dumps(True))) db.session.add(Settings(name='allow_remote_image_posts', value=json.dumps(True)))
db.session.add(Settings(name='federation', value=json.dumps(True))) db.session.add(Settings(name='federation', value=json.dumps(True)))
db.session.add(Language(name='Undetermined', code='und')) db.session.add(Language(name='Undetermined', code='und'))
db.session.add(Language(name='English', code='en'))
banned_instances = ['anonib.al','lemmygrad.ml', 'gab.com', 'rqd2.net', 'exploding-heads.com', 'hexbear.net', banned_instances = ['anonib.al','lemmygrad.ml', 'gab.com', 'rqd2.net', 'exploding-heads.com', 'hexbear.net',
'threads.net', 'noauthority.social', 'pieville.net', 'links.hackliberty.org', 'threads.net', 'noauthority.social', 'pieville.net', 'links.hackliberty.org',
'poa.st', 'freespeechextremist.com', 'bae.st', 'nicecrew.digital', 'detroitriotcity.com', 'poa.st', 'freespeechextremist.com', 'bae.st', 'nicecrew.digital', 'detroitriotcity.com',

View file

@ -100,6 +100,7 @@ class CreateDiscussionForm(FlaskForm):
nsfw = BooleanField(_l('NSFW')) nsfw = BooleanField(_l('NSFW'))
nsfl = BooleanField(_l('Gore/gross')) nsfl = BooleanField(_l('Gore/gross'))
notify_author = BooleanField(_l('Notify about replies')) notify_author = BooleanField(_l('Notify about replies'))
language_id = SelectField(_l('Language'), validators=[DataRequired()], coerce=int, render_kw={'class': 'form-select'})
submit = SubmitField(_l('Save')) submit = SubmitField(_l('Save'))
@ -113,6 +114,7 @@ class CreateLinkForm(FlaskForm):
nsfw = BooleanField(_l('NSFW')) nsfw = BooleanField(_l('NSFW'))
nsfl = BooleanField(_l('Gore/gross')) nsfl = BooleanField(_l('Gore/gross'))
notify_author = BooleanField(_l('Notify about replies')) notify_author = BooleanField(_l('Notify about replies'))
language_id = SelectField(_l('Language'), validators=[DataRequired()], coerce=int, render_kw={'class': 'form-select'})
submit = SubmitField(_l('Save')) submit = SubmitField(_l('Save'))
def validate(self, extra_validators=None) -> bool: def validate(self, extra_validators=None) -> bool:
@ -133,6 +135,7 @@ class CreateVideoForm(FlaskForm):
nsfw = BooleanField(_l('NSFW')) nsfw = BooleanField(_l('NSFW'))
nsfl = BooleanField(_l('Gore/gross')) nsfl = BooleanField(_l('Gore/gross'))
notify_author = BooleanField(_l('Notify about replies')) notify_author = BooleanField(_l('Notify about replies'))
language_id = SelectField(_l('Language'), validators=[DataRequired()], coerce=int, render_kw={'class': 'form-select'})
submit = SubmitField(_l('Save')) submit = SubmitField(_l('Save'))
def validate(self, extra_validators=None) -> bool: def validate(self, extra_validators=None) -> bool:
@ -153,6 +156,7 @@ class CreateImageForm(FlaskForm):
nsfw = BooleanField(_l('NSFW')) nsfw = BooleanField(_l('NSFW'))
nsfl = BooleanField(_l('Gore/gross')) nsfl = BooleanField(_l('Gore/gross'))
notify_author = BooleanField(_l('Notify about replies')) notify_author = BooleanField(_l('Notify about replies'))
language_id = SelectField(_l('Language'), validators=[DataRequired()], coerce=int, render_kw={'class': 'form-select'})
submit = SubmitField(_l('Save')) submit = SubmitField(_l('Save'))
def validate(self, extra_validators=None) -> bool: def validate(self, extra_validators=None) -> bool:

View file

@ -33,7 +33,7 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_
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, mimetype_from_url, blocked_instances, \ joined_communities, moderating_communities, blocked_domains, mimetype_from_url, blocked_instances, \
community_moderators, communities_banned_from, show_ban_message, recently_upvoted_posts, recently_downvoted_posts, \ community_moderators, communities_banned_from, show_ban_message, recently_upvoted_posts, recently_downvoted_posts, \
blocked_users, post_ranking, languages_for_form blocked_users, post_ranking, languages_for_form, english_language_id
from feedgen.feed import FeedGenerator from feedgen.feed import FeedGenerator
from datetime import timezone, timedelta from datetime import timezone, timedelta
@ -488,6 +488,8 @@ def add_discussion_post(actor):
if not community_in_list(community.id, form.communities.choices): if not community_in_list(community.id, form.communities.choices):
form.communities.choices.append((community.id, community.display_name())) form.communities.choices.append((community.id, community.display_name()))
form.language_id.choices = languages_for_form()
if not can_create_post(current_user, community): if not can_create_post(current_user, community):
abort(401) abort(401)
@ -515,6 +517,7 @@ def add_discussion_post(actor):
else: else:
form.communities.data = community.id form.communities.data = community.id
form.notify_author.data = True form.notify_author.data = True
form.language_id.data = current_user.language_id if current_user.language_id else english_language_id()
if community.posting_warning: if community.posting_warning:
flash(community.posting_warning) flash(community.posting_warning)
@ -551,6 +554,8 @@ def add_image_post(actor):
if not community_in_list(community.id, form.communities.choices): if not community_in_list(community.id, form.communities.choices):
form.communities.choices.append((community.id, community.display_name())) form.communities.choices.append((community.id, community.display_name()))
form.language_id.choices = languages_for_form()
if not can_create_post(current_user, community): if not can_create_post(current_user, community):
abort(401) abort(401)
@ -594,6 +599,7 @@ def add_image_post(actor):
else: else:
form.communities.data = community.id form.communities.data = community.id
form.notify_author.data = True form.notify_author.data = True
form.language_id.data = current_user.language_id if current_user.language_id else english_language_id()
return render_template('community/add_image_post.html', title=_('Add post to community'), form=form, community=community, return render_template('community/add_image_post.html', title=_('Add post to community'), form=form, community=community,
markdown_editor=current_user.markdown_editor, low_bandwidth=False, actor=actor, markdown_editor=current_user.markdown_editor, low_bandwidth=False, actor=actor,
@ -628,6 +634,8 @@ def add_link_post(actor):
if not community_in_list(community.id, form.communities.choices): if not community_in_list(community.id, form.communities.choices):
form.communities.choices.append((community.id, community.display_name())) form.communities.choices.append((community.id, community.display_name()))
form.language_id.choices = languages_for_form()
if not can_create_post(current_user, community): if not can_create_post(current_user, community):
abort(401) abort(401)
@ -671,6 +679,7 @@ def add_link_post(actor):
else: else:
form.communities.data = community.id form.communities.data = community.id
form.notify_author.data = True form.notify_author.data = True
form.language_id.data = current_user.language_id if current_user.language_id else english_language_id()
return render_template('community/add_link_post.html', title=_('Add post to community'), form=form, community=community, return render_template('community/add_link_post.html', title=_('Add post to community'), form=form, community=community,
markdown_editor=current_user.markdown_editor, low_bandwidth=False, actor=actor, markdown_editor=current_user.markdown_editor, low_bandwidth=False, actor=actor,
@ -705,6 +714,8 @@ def add_video_post(actor):
if not community_in_list(community.id, form.communities.choices): if not community_in_list(community.id, form.communities.choices):
form.communities.choices.append((community.id, community.display_name())) form.communities.choices.append((community.id, community.display_name()))
form.language_id.choices = languages_for_form()
if not can_create_post(current_user, community): if not can_create_post(current_user, community):
abort(401) abort(401)
@ -748,6 +759,7 @@ def add_video_post(actor):
else: else:
form.communities.data = community.id form.communities.data = community.id
form.notify_author.data = True form.notify_author.data = True
form.language_id.data = current_user.language_id if current_user.language_id else english_language_id()
return render_template('community/add_video_post.html', title=_('Add post to community'), form=form, community=community, return render_template('community/add_video_post.html', title=_('Add post to community'), form=form, community=community,
markdown_editor=current_user.markdown_editor, low_bandwidth=False, actor=actor, markdown_editor=current_user.markdown_editor, low_bandwidth=False, actor=actor,
@ -780,7 +792,11 @@ def federate_post(community, post):
'nsfl': post.nsfl, 'nsfl': post.nsfl,
'stickied': post.sticky, 'stickied': post.sticky,
'published': ap_datetime(utcnow()), 'published': ap_datetime(utcnow()),
'audience': community.ap_profile_id 'audience': community.ap_profile_id,
'language': {
'identifier': post.language_code(),
'name': post.language_name()
}
} }
create = { create = {
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}", "id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
@ -870,7 +886,11 @@ def federate_post_to_user_followers(post):
'sensitive': post.nsfw, 'sensitive': post.nsfw,
'nsfl': post.nsfl, 'nsfl': post.nsfl,
'stickied': post.sticky, 'stickied': post.sticky,
'published': ap_datetime(utcnow()) 'published': ap_datetime(utcnow()),
'language': {
'identifier': post.language_code(),
'name': post.language_name()
}
} }
create = { create = {
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}", "id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",

View file

@ -216,6 +216,8 @@ def save_post(form, post: Post, type: str):
post.nsfw = form.nsfw.data post.nsfw = form.nsfw.data
post.nsfl = form.nsfl.data post.nsfl = form.nsfl.data
post.notify_author = form.notify_author.data post.notify_author = form.notify_author.data
post.language_id = form.language_id.data
current_user.language_id = form.language_id.data
if type == '' or type == 'discussion': if type == '' or type == 'discussion':
post.title = form.discussion_title.data post.title = form.discussion_title.data
post.body = form.discussion_body.data post.body = form.discussion_body.data

View file

@ -944,10 +944,9 @@ class Post(db.Model):
up_votes = db.Column(db.Integer, default=0) up_votes = db.Column(db.Integer, default=0)
down_votes = db.Column(db.Integer, default=0) down_votes = db.Column(db.Integer, default=0)
ranking = db.Column(db.Integer, default=0, index=True) # used for 'hot' ranking ranking = db.Column(db.Integer, default=0, index=True) # used for 'hot' ranking
language = db.Column(db.String(10))
edited_at = db.Column(db.DateTime) edited_at = db.Column(db.DateTime)
reports = db.Column(db.Integer, default=0) # how many times this post has been reported. Set to -1 to ignore reports reports = db.Column(db.Integer, default=0) # how many times this post has been reported. Set to -1 to ignore reports
language_id = db.Column(db.Integer, index=True) language_id = db.Column(db.Integer, db.ForeignKey('language.id'), index=True)
cross_posts = db.Column(MutableList.as_mutable(ARRAY(db.Integer))) cross_posts = db.Column(MutableList.as_mutable(ARRAY(db.Integer)))
tags = db.relationship('Tag', lazy='dynamic', secondary=post_tag, backref=db.backref('posts', lazy='dynamic')) tags = db.relationship('Tag', lazy='dynamic', secondary=post_tag, backref=db.backref('posts', lazy='dynamic'))
@ -962,6 +961,7 @@ class Post(db.Model):
author = db.relationship('User', lazy='joined', overlaps='posts', foreign_keys=[user_id]) author = db.relationship('User', lazy='joined', overlaps='posts', foreign_keys=[user_id])
community = db.relationship('Community', lazy='joined', overlaps='posts', foreign_keys=[community_id]) community = db.relationship('Community', lazy='joined', overlaps='posts', foreign_keys=[community_id])
replies = db.relationship('PostReply', lazy='dynamic', backref='post') replies = db.relationship('PostReply', lazy='dynamic', backref='post')
language = db.relationship('Language', foreign_keys=[language_id])
def is_local(self): def is_local(self):
return self.ap_id is None or self.ap_id.startswith('https://' + current_app.config['SERVER_NAME']) return self.ap_id is None or self.ap_id.startswith('https://' + current_app.config['SERVER_NAME'])
@ -1006,6 +1006,18 @@ class Post(db.Model):
NotificationSubscription.type == NOTIF_POST).first() NotificationSubscription.type == NOTIF_POST).first()
return existing_notification is not None return existing_notification is not None
def language_code(self):
if self.language_id:
return self.language.code
else:
return 'en'
def language_name(self):
if self.language_id:
return self.language.name
else:
return 'English'
class PostReply(db.Model): class PostReply(db.Model):
query_class = FullTextSearchQuery query_class = FullTextSearchQuery
@ -1033,7 +1045,7 @@ class PostReply(db.Model):
up_votes = db.Column(db.Integer, default=0) up_votes = db.Column(db.Integer, default=0)
down_votes = db.Column(db.Integer, default=0) down_votes = db.Column(db.Integer, default=0)
ranking = db.Column(db.Float, default=0.0, index=True) # used for 'hot' sorting ranking = db.Column(db.Float, default=0.0, index=True) # used for 'hot' sorting
language = db.Column(db.String(10)) language_id = db.Column(db.Integer, db.ForeignKey('language.id'), index=True)
edited_at = db.Column(db.DateTime) edited_at = db.Column(db.DateTime)
reports = db.Column(db.Integer, default=0) # how many times this post has been reported. Set to -1 to ignore reports reports = db.Column(db.Integer, default=0) # how many times this post has been reported. Set to -1 to ignore reports
@ -1045,6 +1057,19 @@ class PostReply(db.Model):
author = db.relationship('User', lazy='joined', foreign_keys=[user_id], single_parent=True, overlaps="post_replies") author = db.relationship('User', lazy='joined', foreign_keys=[user_id], single_parent=True, overlaps="post_replies")
community = db.relationship('Community', lazy='joined', overlaps='replies', foreign_keys=[community_id]) community = db.relationship('Community', lazy='joined', overlaps='replies', foreign_keys=[community_id])
language = db.relationship('Language', foreign_keys=[language_id])
def language_code(self):
if self.language_id:
return self.language.code
else:
return 'en'
def language_name(self):
if self.language_id:
return self.language.name
else:
return 'English'
def is_local(self): def is_local(self):
return self.ap_id is None or self.ap_id.startswith('https://' + current_app.config['SERVER_NAME']) return self.ap_id is None or self.ap_id.startswith('https://' + current_app.config['SERVER_NAME'])

View file

@ -1,5 +1,6 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField, BooleanField, StringField from wtforms import TextAreaField, SubmitField, BooleanField, StringField
from wtforms.fields.choices import SelectField
from wtforms.validators import DataRequired, Length, ValidationError from wtforms.validators import DataRequired, Length, ValidationError
from flask_babel import _, lazy_gettext as _l from flask_babel import _, lazy_gettext as _l
@ -9,6 +10,7 @@ from app.utils import MultiCheckboxField
class NewReplyForm(FlaskForm): class NewReplyForm(FlaskForm):
body = TextAreaField(_l('Body'), render_kw={'placeholder': 'What are your thoughts?', 'rows': 5}, validators={DataRequired(), Length(min=1, max=5000)}) body = TextAreaField(_l('Body'), render_kw={'placeholder': 'What are your thoughts?', 'rows': 5}, validators={DataRequired(), Length(min=1, max=5000)})
notify_author = BooleanField(_l('Notify about replies')) notify_author = BooleanField(_l('Notify about replies'))
language_id = SelectField(_l('Language'), validators=[DataRequired()], coerce=int, render_kw={'class': 'form-select language-float-right'})
submit = SubmitField(_l('Comment')) submit = SubmitField(_l('Comment'))

View file

@ -27,7 +27,8 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_
request_etag_matches, ip_address, user_ip_banned, instance_banned, can_downvote, can_upvote, post_ranking, \ 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, \ reply_already_exists, reply_is_just_link_to_gif_reaction, confidence, moderating_communities, joined_communities, \
blocked_instances, blocked_domains, community_moderators, blocked_phrases, show_ban_message, recently_upvoted_posts, \ blocked_instances, blocked_domains, community_moderators, blocked_phrases, show_ban_message, recently_upvoted_posts, \
recently_downvoted_posts, recently_upvoted_post_replies, recently_downvoted_post_replies, reply_is_stupid recently_downvoted_posts, recently_upvoted_post_replies, recently_downvoted_post_replies, reply_is_stupid, \
languages_for_form, english_language_id
def show_post(post_id: int): def show_post(post_id: int):
@ -58,6 +59,7 @@ def show_post(post_id: int):
# handle top-level comments/replies # handle top-level comments/replies
form = NewReplyForm() form = NewReplyForm()
form.language_id.choices = languages_for_form()
if current_user.is_authenticated and current_user.verified and form.validate_on_submit(): if current_user.is_authenticated and current_user.verified and form.validate_on_submit():
if not post.comments_enabled: if not post.comments_enabled:
@ -98,11 +100,12 @@ def show_post(post_id: int):
reply = PostReply(user_id=current_user.id, post_id=post.id, community_id=community.id, body=form.body.data, reply = PostReply(user_id=current_user.id, post_id=post.id, community_id=community.id, body=form.body.data,
body_html=markdown_to_html(form.body.data), body_html_safe=True, body_html=markdown_to_html(form.body.data), body_html_safe=True,
from_bot=current_user.bot, nsfw=post.nsfw, nsfl=post.nsfl, from_bot=current_user.bot, nsfw=post.nsfw, nsfl=post.nsfl,
notify_author=form.notify_author.data, instance_id=1) notify_author=form.notify_author.data, language_id=form.language_id.data, instance_id=1)
post.last_active = community.last_active = utcnow() post.last_active = community.last_active = utcnow()
post.reply_count += 1 post.reply_count += 1
community.post_reply_count += 1 community.post_reply_count += 1
current_user.language_id = form.language_id.data
db.session.add(reply) db.session.add(reply)
db.session.commit() db.session.commit()
@ -163,7 +166,11 @@ def show_post(post_id: int):
'href': post.author.public_url(), 'href': post.author.public_url(),
'name': post.author.mention_tag(), 'name': post.author.mention_tag(),
'type': 'Mention' 'type': 'Mention'
}] }],
'language': {
'identifier': reply.language_code(),
'name': reply.language_name()
}
} }
create_json = { create_json = {
'type': 'Create', 'type': 'Create',
@ -222,6 +229,7 @@ def show_post(post_id: int):
else: else:
replies = post_replies(post.id, sort) replies = post_replies(post.id, sort)
form.notify_author.data = True form.notify_author.data = True
form.language_id.data = current_user.language_id if current_user.language_id else english_language_id()
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
@ -588,6 +596,7 @@ def add_reply(post_id: int, comment_id: int):
return redirect(url_for('activitypub.post_ap', post_id=post_id)) return redirect(url_for('activitypub.post_ap', post_id=post_id))
form = NewReplyForm() form = NewReplyForm()
form.language_id.choices = languages_for_form()
if form.validate_on_submit(): if form.validate_on_submit():
if reply_already_exists(user_id=current_user.id, post_id=post.id, parent_id=in_reply_to.id, body=form.body.data): if reply_already_exists(user_id=current_user.id, post_id=post.id, parent_id=in_reply_to.id, body=form.body.data):
if in_reply_to.depth <= constants.THREAD_CUTOFF_DEPTH: if in_reply_to.depth <= constants.THREAD_CUTOFF_DEPTH:
@ -617,11 +626,12 @@ def add_reply(post_id: int, comment_id: int):
current_user.last_seen = utcnow() current_user.last_seen = utcnow()
current_user.ip_address = ip_address() current_user.ip_address = ip_address()
current_user.language_id = form.language_id.data
reply = PostReply(user_id=current_user.id, post_id=post.id, parent_id=in_reply_to.id, depth=in_reply_to.depth + 1, reply = PostReply(user_id=current_user.id, post_id=post.id, parent_id=in_reply_to.id, depth=in_reply_to.depth + 1,
community_id=post.community.id, body=form.body.data, community_id=post.community.id, body=form.body.data,
body_html=markdown_to_html(form.body.data), body_html_safe=True, body_html=markdown_to_html(form.body.data), body_html_safe=True,
from_bot=current_user.bot, nsfw=post.nsfw, nsfl=post.nsfl, from_bot=current_user.bot, nsfw=post.nsfw, nsfl=post.nsfl,
notify_author=form.notify_author.data, instance_id=1) notify_author=form.notify_author.data, instance_id=1, language_id=form.language_id.data)
if reply.body: if reply.body:
for blocked_phrase in blocked_phrases(): for blocked_phrase in blocked_phrases():
if blocked_phrase in reply.body: if blocked_phrase in reply.body:
@ -761,6 +771,7 @@ def add_reply(post_id: int, comment_id: int):
return redirect(url_for('post.continue_discussion', post_id=post_id, comment_id=reply.parent_id)) return redirect(url_for('post.continue_discussion', post_id=post_id, comment_id=reply.parent_id))
else: else:
form.notify_author.data = True form.notify_author.data = True
form.language_id.data = current_user.language_id if current_user.language_id else english_language_id()
return render_template('post/add_reply.html', title=_('Discussing %(title)s', title=post.title), post=post, return render_template('post/add_reply.html', title=_('Discussing %(title)s', title=post.title), post=post,
is_moderator=is_moderator, form=form, comment=in_reply_to, markdown_editor=current_user.is_authenticated and current_user.markdown_editor, is_moderator=is_moderator, form=form, comment=in_reply_to, markdown_editor=current_user.is_authenticated and current_user.markdown_editor,
@ -828,6 +839,8 @@ def post_edit_discussion_post(post_id: int):
form.nsfl.data = True form.nsfl.data = True
form.nsfw.render_kw = {'disabled': True} form.nsfw.render_kw = {'disabled': True}
form.language_id.choices = languages_for_form()
if form.validate_on_submit(): if form.validate_on_submit():
save_post(form, post, 'discussion') save_post(form, post, 'discussion')
post.community.last_active = utcnow() post.community.last_active = utcnow()
@ -848,6 +861,7 @@ def post_edit_discussion_post(post_id: int):
form.nsfw.data = post.nsfw form.nsfw.data = post.nsfw
form.nsfl.data = post.nsfl form.nsfl.data = post.nsfl
form.sticky.data = post.sticky form.sticky.data = post.sticky
form.language_id.data = post.language_id
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()): if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
form.sticky.render_kw = {'disabled': True} form.sticky.render_kw = {'disabled': True}
return render_template('post/post_edit_discussion.html', title=_('Edit post'), form=form, post=post, return render_template('post/post_edit_discussion.html', title=_('Edit post'), form=form, post=post,
@ -886,6 +900,8 @@ def post_edit_image_post(post_id: int):
old_url = post.url old_url = post.url
form.language_id.choices = languages_for_form()
if form.validate_on_submit(): if form.validate_on_submit():
save_post(form, post, 'image') save_post(form, post, 'image')
post.community.last_active = utcnow() post.community.last_active = utcnow()
@ -929,6 +945,7 @@ def post_edit_image_post(post_id: int):
form.nsfw.data = post.nsfw form.nsfw.data = post.nsfw
form.nsfl.data = post.nsfl form.nsfl.data = post.nsfl
form.sticky.data = post.sticky form.sticky.data = post.sticky
form.language_id.data = post.language_id
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()): if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
form.sticky.render_kw = {'disabled': True} form.sticky.render_kw = {'disabled': True}
return render_template('post/post_edit_image.html', title=_('Edit post'), form=form, post=post, return render_template('post/post_edit_image.html', title=_('Edit post'), form=form, post=post,
@ -967,6 +984,8 @@ def post_edit_link_post(post_id: int):
old_url = post.url old_url = post.url
form.language_id.choices = languages_for_form()
if form.validate_on_submit(): if form.validate_on_submit():
save_post(form, post, 'link') save_post(form, post, 'link')
post.community.last_active = utcnow() post.community.last_active = utcnow()
@ -1010,6 +1029,7 @@ def post_edit_link_post(post_id: int):
form.nsfw.data = post.nsfw form.nsfw.data = post.nsfw
form.nsfl.data = post.nsfl form.nsfl.data = post.nsfl
form.sticky.data = post.sticky form.sticky.data = post.sticky
form.language_id.data = post.language_id
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()): if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
form.sticky.render_kw = {'disabled': True} form.sticky.render_kw = {'disabled': True}
return render_template('post/post_edit_link.html', title=_('Edit post'), form=form, post=post, return render_template('post/post_edit_link.html', title=_('Edit post'), form=form, post=post,
@ -1048,6 +1068,8 @@ def post_edit_video_post(post_id: int):
old_url = post.url old_url = post.url
form.language_id.choices = languages_for_form()
if form.validate_on_submit(): if form.validate_on_submit():
save_post(form, post, 'video') save_post(form, post, 'video')
post.community.last_active = utcnow() post.community.last_active = utcnow()
@ -1091,6 +1113,7 @@ def post_edit_video_post(post_id: int):
form.nsfw.data = post.nsfw form.nsfw.data = post.nsfw
form.nsfl.data = post.nsfl form.nsfl.data = post.nsfl
form.sticky.data = post.sticky form.sticky.data = post.sticky
form.language_id.data = post.language_id
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()): if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
form.sticky.render_kw = {'disabled': True} form.sticky.render_kw = {'disabled': True}
return render_template('post/post_edit_video.html', title=_('Edit post'), form=form, post=post, return render_template('post/post_edit_video.html', title=_('Edit post'), form=form, post=post,
@ -1126,7 +1149,11 @@ def federate_post_update(post):
'stickied': post.sticky, 'stickied': post.sticky,
'published': ap_datetime(post.posted_at), 'published': ap_datetime(post.posted_at),
'updated': ap_datetime(post.edited_at), 'updated': ap_datetime(post.edited_at),
'audience': post.community.ap_profile_id 'audience': post.community.ap_profile_id,
'language': {
'identifier': post.language_code(),
'name': post.language_name()
}
} }
update_json = { update_json = {
'id': f"https://{current_app.config['SERVER_NAME']}/activities/update/{gibberish(15)}", 'id': f"https://{current_app.config['SERVER_NAME']}/activities/update/{gibberish(15)}",
@ -1487,6 +1514,7 @@ def post_reply_edit(post_id: int, comment_id: int):
else: else:
comment = None comment = None
form = NewReplyForm() form = NewReplyForm()
form.language_id.choices = languages_for_form()
if post_reply.user_id == current_user.id or post.community.is_moderator(): if post_reply.user_id == current_user.id or post.community.is_moderator():
if form.validate_on_submit(): if form.validate_on_submit():
post_reply.body = form.body.data post_reply.body = form.body.data
@ -1494,6 +1522,7 @@ def post_reply_edit(post_id: int, comment_id: int):
post_reply.notify_author = form.notify_author.data post_reply.notify_author = form.notify_author.data
post.community.last_active = utcnow() post.community.last_active = utcnow()
post_reply.edited_at = utcnow() post_reply.edited_at = utcnow()
post_reply.language_id = form.language_id.data
db.session.commit() db.session.commit()
flash(_('Your changes have been saved.'), 'success') flash(_('Your changes have been saved.'), 'success')
@ -1528,6 +1557,10 @@ def post_reply_edit(post_id: int, comment_id: int):
'audience': post.community.public_url(), 'audience': post.community.public_url(),
'contentMap': { 'contentMap': {
'en': post_reply.body_html 'en': post_reply.body_html
},
'language': {
'identifier': post_reply.language_code(),
'name': post_reply.language_name()
} }
} }
update_json = { update_json = {
@ -1599,6 +1632,7 @@ def post_reply_edit(post_id: int, comment_id: int):
else: else:
form.body.data = post_reply.body form.body.data = post_reply.body
form.notify_author.data = post_reply.notify_author form.notify_author.data = post_reply.notify_author
form.language_id.data = post_reply.language_id
return render_template('post/post_reply_edit.html', title=_('Edit comment'), form=form, post=post, post_reply=post_reply, return render_template('post/post_reply_edit.html', title=_('Edit comment'), form=form, post=post, post_reply=post_reply,
comment=comment, markdown_editor=current_user.markdown_editor, moderating_communities=moderating_communities(current_user.get_id()), comment=comment, markdown_editor=current_user.markdown_editor, 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,

View file

@ -1376,4 +1376,22 @@ h1 .warning_badge {
max-height: 90vh; max-height: 90vh;
} }
@media (min-width: 768px) {
.post_language_chooser label {
display: none;
}
.post_language_chooser select {
max-width: 150px;
float: right;
margin-top: -5px;
}
}
@media (min-width: 768px) {
.language-float-right {
max-width: 150px;
float: right;
}
}
/*# sourceMappingURL=structure.css.map */ /*# sourceMappingURL=structure.css.map */

View file

@ -1051,3 +1051,23 @@ h1 .warning_badge {
max-width: 100%; max-width: 100%;
max-height: 90vh; max-height: 90vh;
} }
.post_language_chooser {
@include breakpoint(phablet) {
label {
display: none;
}
select {
max-width: 150px;
float: right;
margin-top: -5px;
}
}
}
.language-float-right {
@include breakpoint(phablet) {
max-width: 150px;
float: right;
}
}

View file

@ -58,8 +58,8 @@
<div class="col-md-1"> <div class="col-md-1">
{{ render_field(form.nsfl) }} {{ render_field(form.nsfl) }}
</div> </div>
<div class="col"> <div class="col post_language_chooser">
{{ render_field(form.language_id) }}
</div> </div>
</div> </div>

View file

@ -60,8 +60,8 @@
<div class="col-md-1"> <div class="col-md-1">
{{ render_field(form.nsfl) }} {{ render_field(form.nsfl) }}
</div> </div>
<div class="col"> <div class="col post_language_chooser">
{{ render_field(form.language_id) }}
</div> </div>
</div> </div>

View file

@ -59,8 +59,8 @@
<div class="col-md-1"> <div class="col-md-1">
{{ render_field(form.nsfl) }} {{ render_field(form.nsfl) }}
</div> </div>
<div class="col"> <div class="col post_language_chooser">
{{ render_field(form.language_id) }}
</div> </div>
</div> </div>

View file

@ -60,8 +60,8 @@
<div class="col-md-1"> <div class="col-md-1">
{{ render_field(form.nsfl) }} {{ render_field(form.nsfl) }}
</div> </div>
<div class="col"> <div class="col post_language_chooser">
{{ render_field(form.language_id) }}
</div> </div>
</div> </div>

View file

@ -39,7 +39,7 @@
}); });
</script> </script>
{% else %} {% else %}
<a href="#" aria-hidden="true" id="post_reply_markdown_editor_enabler" class="markdown_editor_enabler" data-id="body">{{ _('Enable markdown editor') }}</a> <!-- <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 %} {% endif %}
</div> </div>

View file

@ -41,8 +41,8 @@
{{ render_field(form.nsfl) }} {{ render_field(form.nsfl) }}
</div> </div>
<div class="col"> <div class="col post_language_chooser">
{{ render_field(form.language_id) }}
</div> </div>
</div> </div>

View file

@ -44,8 +44,8 @@
{{ render_field(form.nsfl) }} {{ render_field(form.nsfl) }}
</div> </div>
<div class="col"> <div class="col post_language_chooser">
{{ render_field(form.language_id) }}
</div> </div>
</div> </div>

View file

@ -41,8 +41,8 @@
{{ render_field(form.nsfl) }} {{ render_field(form.nsfl) }}
</div> </div>
<div class="col"> <div class="col post_language_chooser">
{{ render_field(form.language_id) }}
</div> </div>
</div> </div>

View file

@ -41,8 +41,8 @@
{{ render_field(form.nsfl) }} {{ render_field(form.nsfl) }}
</div> </div>
<div class="col"> <div class="col post_language_chooser">
{{ render_field(form.language_id) }}
</div> </div>
</div> </div>

View file

@ -13,9 +13,11 @@
<div class="card-title">{{ _('Options for "%(post_title)s"', post_title=post.title) }}</div> <div class="card-title">{{ _('Options for "%(post_title)s"', post_title=post.title) }}</div>
<ul class="option_list"> <ul class="option_list">
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
{% if post.user_id == current_user.id or post.community.is_moderator() or current_user.is_admin() %} {% if post.user_id == current_user.id %}
<li><a href="{{ url_for('post.post_edit', post_id=post.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-edit"></span> <li><a href="{{ url_for('post.post_edit', post_id=post.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-edit"></span>
{{ _('Edit') }}</a></li> {{ _('Edit') }}</a></li>
{% endif %}
{% if post.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() %}
<li><a href="{{ url_for('post.post_delete', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span> <li><a href="{{ url_for('post.post_delete', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
{{ _('Delete') }}</a></li> {{ _('Delete') }}</a></li>
{% endif %} {% endif %}

View file

@ -13,9 +13,11 @@
<div class="card-title">{{ _('Options for comment on "%(post_title)s"', post_title=post.title) }}</div> <div class="card-title">{{ _('Options for comment on "%(post_title)s"', post_title=post.title) }}</div>
<ul class="option_list"> <ul class="option_list">
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
{% if post_reply.user_id == current_user.id or post.community.is_moderator() or current_user.is_admin() %} {% if post_reply.user_id == current_user.id %}
<li><a href="{{ url_for('post.post_reply_edit', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-edit"></span> <li><a href="{{ url_for('post.post_reply_edit', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-edit"></span>
{{ _('Edit') }}</a></li> {{ _('Edit') }}</a></li>
{% endif %}
{% if post_reply.user_id == current_user.id or post.community.is_moderator() or post.community.is_owner() or current_user.is_admin() %}
<li><a href="{{ url_for('post.post_reply_delete', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span> <li><a href="{{ url_for('post.post_reply_delete', post_id=post.id, comment_id=post_reply.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span>
{{ _('Delete') }}</a></li> {{ _('Delete') }}</a></li>
{% endif %} {% endif %}

View file

@ -984,3 +984,8 @@ def languages_for_form():
if language.code != 'und': if language.code != 'und':
result.append((language.id, language.name)) result.append((language.id, language.name))
return result return result
def english_language_id():
english = Language.query.filter(Language.code == 'en').first()
return english.id if english else None

View file

@ -0,0 +1,46 @@
"""post_reply language
Revision ID: 9ad372b72d7c
Revises: e73996747d7e
Create Date: 2024-05-09 14:26:48.888908
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9ad372b72d7c'
down_revision = 'e73996747d7e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('post', schema=None) as batch_op:
batch_op.create_foreign_key(None, 'language', ['language_id'], ['id'])
batch_op.drop_column('language')
with op.batch_alter_table('post_reply', schema=None) as batch_op:
batch_op.add_column(sa.Column('language_id', sa.Integer(), nullable=True))
batch_op.create_index(batch_op.f('ix_post_reply_language_id'), ['language_id'], unique=False)
batch_op.create_foreign_key(None, 'language', ['language_id'], ['id'])
batch_op.drop_column('language')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('post_reply', schema=None) as batch_op:
batch_op.add_column(sa.Column('language', sa.VARCHAR(length=10), autoincrement=False, nullable=True))
batch_op.drop_constraint(None, type_='foreignkey')
batch_op.drop_index(batch_op.f('ix_post_reply_language_id'))
batch_op.drop_column('language_id')
with op.batch_alter_table('post', schema=None) as batch_op:
batch_op.add_column(sa.Column('language', sa.VARCHAR(length=10), autoincrement=False, nullable=True))
batch_op.drop_constraint(None, type_='foreignkey')
# ### end Alembic commands ###