let admin override language for remote communities #51

This commit is contained in:
rimu 2024-05-08 21:07:22 +12:00
parent cba60c2aee
commit 3bc30ec99c
14 changed files with 97 additions and 9 deletions

View file

@ -535,7 +535,7 @@ def refresh_community_profile_task(community_id):
community.image = image community.image = image
db.session.add(image) db.session.add(image)
cover_changed = True cover_changed = True
if 'language' in activity_json and isinstance(activity_json['language'], list): if 'language' in activity_json and isinstance(activity_json['language'], list) and not community.ignore_remote_language:
for ap_language in activity_json['language']: for ap_language in activity_json['language']:
new_language = find_language_or_create(ap_language['identifier'], ap_language['name']) new_language = find_language_or_create(ap_language['identifier'], ap_language['name'])
if new_language not in community.languages: if new_language not in community.languages:

View file

@ -3,6 +3,7 @@ from flask_wtf.file import FileRequired, FileAllowed
from sqlalchemy import func from sqlalchemy import func
from wtforms import StringField, PasswordField, SubmitField, EmailField, HiddenField, BooleanField, TextAreaField, SelectField, \ from wtforms import StringField, PasswordField, SubmitField, EmailField, HiddenField, BooleanField, TextAreaField, SelectField, \
FileField, IntegerField FileField, IntegerField
from wtforms.fields.choices import SelectMultipleField
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
@ -83,6 +84,8 @@ class EditCommunityForm(FlaskForm):
('masonry_wide', _l('Wide masonry'))] ('masonry_wide', _l('Wide masonry'))]
default_layout = SelectField(_l('Layout'), coerce=str, choices=layouts, validators=[Optional()]) default_layout = SelectField(_l('Layout'), coerce=str, choices=layouts, validators=[Optional()])
posting_warning = StringField(_l('Posting warning'), validators=[Optional(), Length(min=3, max=512)]) posting_warning = StringField(_l('Posting warning'), validators=[Optional(), Length(min=3, max=512)])
languages = SelectMultipleField(_l('Languages'), coerce=int, validators=[Optional()], render_kw={'class': 'form-select'})
ignore_remote_language = BooleanField(_l('Override remote language setting'))
submit = SubmitField(_l('Save')) submit = SubmitField(_l('Save'))
def validate(self, extra_validators=None): def validate(self, extra_validators=None):

View file

@ -18,10 +18,10 @@ from app.admin.util import unsubscribe_from_everything_then_delete, unsubscribe_
from app.community.util import save_icon_file, save_banner_file from app.community.util import save_icon_file, save_banner_file
from app.constants import REPORT_STATE_NEW, REPORT_STATE_ESCALATED from app.constants import REPORT_STATE_NEW, REPORT_STATE_ESCALATED
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, Role, Post, PostReply User, Instance, File, Report, Topic, UserRegistration, Role, Post, PostReply, Language
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, blocked_phrases, blocked_referrers, \ moderating_communities, joined_communities, finalize_user_setup, theme_list, blocked_phrases, blocked_referrers, \
topic_tree topic_tree, languages_for_form
from app.admin import bp from app.admin import bp
@ -236,6 +236,7 @@ def admin_community_edit(community_id):
form = EditCommunityForm() form = EditCommunityForm()
community = Community.query.get_or_404(community_id) community = Community.query.get_or_404(community_id)
form.topic.choices = topics_for_form(0) form.topic.choices = topics_for_form(0)
form.languages.choices = languages_for_form()
if form.validate_on_submit(): if form.validate_on_submit():
community.name = form.url.data community.name = form.url.data
community.title = form.title.data community.title = form.title.data
@ -256,6 +257,7 @@ def admin_community_edit(community_id):
community.topic_id = form.topic.data if form.topic.data != 0 else None community.topic_id = form.topic.data if form.topic.data != 0 else None
community.default_layout = form.default_layout.data community.default_layout = form.default_layout.data
community.posting_warning = form.posting_warning.data community.posting_warning = form.posting_warning.data
community.ignore_remote_language = form.ignore_remote_language.data
icon_file = request.files['icon_file'] icon_file = request.files['icon_file']
if icon_file and icon_file.filename != '': if icon_file and icon_file.filename != '':
@ -272,6 +274,14 @@ def admin_community_edit(community_id):
if file: if file:
community.image = file community.image = file
# Languages of the community
db.session.execute(text('DELETE FROM "community_language" WHERE community_id = :community_id'),
{'community_id': community_id})
for language_choice in form.languages.data:
community.languages.append(Language.query.get(language_choice))
# Always include the undetermined language, so posts with no language will be accepted
community.languages.append(Language.query.filter(Language.code == 'und').first())
db.session.commit() db.session.commit()
if community.topic_id: if community.topic_id:
community.topic.num_communities = community.topic.communities.count() community.topic.num_communities = community.topic.communities.count()
@ -298,6 +308,8 @@ def admin_community_edit(community_id):
form.topic.data = community.topic_id if community.topic_id else None form.topic.data = community.topic_id if community.topic_id else None
form.default_layout.data = community.default_layout form.default_layout.data = community.default_layout
form.posting_warning.data = community.posting_warning form.posting_warning.data = community.posting_warning
form.languages.data = community.language_ids()
form.ignore_remote_language.data = community.ignore_remote_language
return render_template('admin/edit_community.html', title=_('Edit community'), form=form, community=community, return render_template('admin/edit_community.html', title=_('Edit community'), form=form, community=community,
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()),

View file

@ -7,7 +7,7 @@ from flask_babel import _
from app import db, cache, celery from app import db, cache, celery
from app.activitypub.signature import post_request, default_context from app.activitypub.signature import post_request, default_context
from app.models import User, Community, Instance, Site, ActivityPubLog, CommunityMember, Topic from app.models import User, Community, Instance, Site, ActivityPubLog, CommunityMember, Language
from app.utils import gibberish, topic_tree from app.utils import gibberish, topic_tree
@ -124,3 +124,6 @@ def topics_for_form_children(topics, current_topic: int, depth: int) -> List[Tup
if topic['children']: if topic['children']:
result.extend(topics_for_form_children(topic['children'], current_topic, depth + 1)) result.extend(topics_for_form_children(topic['children'], current_topic, depth + 1))
return result return result

View file

@ -19,7 +19,7 @@ from app.auth.util import random_token
from app.constants import NOTIF_COMMUNITY, NOTIF_POST, NOTIF_REPLY from app.constants import NOTIF_COMMUNITY, NOTIF_POST, NOTIF_REPLY
from app.email import send_verification_email, send_email from app.email import send_verification_email, send_email
from app.models import Settings, BannedInstances, Interest, Role, User, RolePermission, Domain, ActivityPubLog, \ from app.models import Settings, BannedInstances, Interest, Role, User, RolePermission, Domain, ActivityPubLog, \
utcnow, Site, Instance, File, Notification, Post, CommunityMember, NotificationSubscription, PostReply utcnow, Site, Instance, File, Notification, Post, CommunityMember, NotificationSubscription, PostReply, Language
from app.utils import file_get_contents, retrieve_block_list, blocked_domains, retrieve_peertube_block_list, \ from app.utils import file_get_contents, retrieve_block_list, blocked_domains, retrieve_peertube_block_list, \
shorten_string shorten_string
@ -86,6 +86,7 @@ def register(app):
db.session.add(Settings(name='allow_local_image_posts', value=json.dumps(True))) db.session.add(Settings(name='allow_local_image_posts', value=json.dumps(True)))
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'))
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

@ -3,6 +3,7 @@ from flask_login import current_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField, BooleanField, HiddenField, SelectField, FileField, \ from wtforms import StringField, SubmitField, TextAreaField, BooleanField, HiddenField, SelectField, FileField, \
DateField DateField
from wtforms.fields.choices import SelectMultipleField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Regexp, Optional from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length, Regexp, Optional
from flask_babel import _, lazy_gettext as _l from flask_babel import _, lazy_gettext as _l
@ -23,6 +24,7 @@ class AddCommunityForm(FlaskForm):
rules = TextAreaField(_l('Rules')) rules = TextAreaField(_l('Rules'))
nsfw = BooleanField('NSFW') nsfw = BooleanField('NSFW')
local_only = BooleanField('Local only') local_only = BooleanField('Local only')
languages = SelectMultipleField(_l('Languages'), coerce=int, validators=[Optional()], render_kw={'class': 'form-select'})
submit = SubmitField(_l('Create')) submit = SubmitField(_l('Create'))
def validate(self, extra_validators=None): def validate(self, extra_validators=None):
@ -53,6 +55,7 @@ class EditCommunityForm(FlaskForm):
restricted_to_mods = BooleanField(_l('Only moderators can post')) restricted_to_mods = BooleanField(_l('Only moderators can post'))
new_mods_wanted = BooleanField(_l('New moderators wanted')) new_mods_wanted = BooleanField(_l('New moderators wanted'))
topic = SelectField(_l('Topic'), coerce=int, validators=[Optional()]) topic = SelectField(_l('Topic'), coerce=int, validators=[Optional()])
languages = SelectMultipleField(_l('Languages'), coerce=int, validators=[Optional()], render_kw={'class': 'form-select'})
layouts = [('', _l('List')), layouts = [('', _l('List')),
('masonry', _l('Masonry')), ('masonry', _l('Masonry')),
('masonry_wide', _l('Wide masonry'))] ('masonry_wide', _l('Wide masonry'))]

View file

@ -25,7 +25,7 @@ from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LIN
from app.inoculation import inoculation from app.inoculation import inoculation
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \ from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog, Topic, Conversation, PostReply, \ File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog, Topic, Conversation, PostReply, \
NotificationSubscription, UserFollower, Instance NotificationSubscription, UserFollower, Instance, Language
from app.community import bp from app.community import bp
from app.user.utils import search_for_user from app.user.utils import search_for_user
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, \
@ -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 blocked_users, post_ranking, languages_for_form
from feedgen.feed import FeedGenerator from feedgen.feed import FeedGenerator
from datetime import timezone, timedelta from datetime import timezone, timedelta
@ -47,6 +47,8 @@ def add_local():
if g.site.enable_nsfw is False: if g.site.enable_nsfw is False:
form.nsfw.render_kw = {'disabled': True} form.nsfw.render_kw = {'disabled': True}
form.languages.choices = languages_for_form()
if form.validate_on_submit(): if form.validate_on_submit():
if form.url.data.strip().lower().startswith('/c/'): if form.url.data.strip().lower().startswith('/c/'):
form.url.data = form.url.data[3:] form.url.data = form.url.data[3:]
@ -76,6 +78,11 @@ def add_local():
membership = CommunityMember(user_id=current_user.id, community_id=community.id, is_moderator=True, membership = CommunityMember(user_id=current_user.id, community_id=community.id, is_moderator=True,
is_owner=True) is_owner=True)
db.session.add(membership) db.session.add(membership)
# Languages of the community
for language_choice in form.languages.data:
community.languages.append(Language.query.get(language_choice))
# Always include the undetermined language, so posts with no language will be accepted
community.languages.append(Language.query.filter(Language.code == 'und').first())
db.session.commit() db.session.commit()
flash(_('Your new community has been created.')) flash(_('Your new community has been created.'))
cache.delete_memoized(community_membership, current_user, community) cache.delete_memoized(community_membership, current_user, community)
@ -940,6 +947,7 @@ def community_edit(community_id: int):
if community.is_owner() or current_user.is_admin(): if community.is_owner() or current_user.is_admin():
form = EditCommunityForm() form = EditCommunityForm()
form.topic.choices = topics_for_form(0) form.topic.choices = topics_for_form(0)
form.languages.choices = languages_for_form()
if form.validate_on_submit(): if form.validate_on_submit():
community.title = form.title.data community.title = form.title.data
community.description = form.description.data community.description = form.description.data
@ -968,6 +976,14 @@ def community_edit(community_id: int):
if file: if file:
community.image = file community.image = file
# Languages of the community
db.session.execute(text('DELETE FROM "community_language" WHERE community_id = :community_id'),
{'community_id': community_id})
for language_choice in form.languages.data:
community.languages.append(Language.query.get(language_choice))
# Always include the undetermined language, so posts with no language will be accepted
community.languages.append(Language.query.filter(Language.code == 'und').first())
db.session.commit() db.session.commit()
if community.topic: if community.topic:
community.topic.num_communities = community.topic.communities.count() community.topic.num_communities = community.topic.communities.count()
@ -983,6 +999,7 @@ def community_edit(community_id: int):
form.new_mods_wanted.data = community.new_mods_wanted form.new_mods_wanted.data = community.new_mods_wanted
form.restricted_to_mods.data = community.restricted_to_mods form.restricted_to_mods.data = community.restricted_to_mods
form.topic.data = community.topic_id if community.topic_id else None form.topic.data = community.topic_id if community.topic_id else None
form.languages.data = community.language_ids()
form.default_layout.data = community.default_layout form.default_layout.data = community.default_layout
return render_template('community/community_edit.html', title=_('Edit community'), form=form, return render_template('community/community_edit.html', title=_('Edit community'), form=form,
current_app=current_app, current="edit_settings", current_app=current_app, current="edit_settings",

View file

@ -13,7 +13,7 @@ from app.activitypub.signature import post_request, default_context
from app.activitypub.util import find_actor_or_create, actor_json_to_model, post_json_to_model, ensure_domains_match from app.activitypub.util import find_actor_or_create, actor_json_to_model, post_json_to_model, ensure_domains_match
from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_VIDEO, NOTIF_POST from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_VIDEO, NOTIF_POST
from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site, \ from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site, \
Instance, Notification, User, ActivityPubLog, NotificationSubscription Instance, Notification, User, ActivityPubLog, NotificationSubscription, Language
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, allowlist_html, \ from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, allowlist_html, \
is_image_url, ensure_directory_exists, inbox_domain, post_ranking, shorten_string, parse_page, \ is_image_url, ensure_directory_exists, inbox_domain, post_ranking, shorten_string, parse_page, \
remove_tracking_from_link, ap_datetime, instance_banned, blocked_phrases remove_tracking_from_link, ap_datetime, instance_banned, blocked_phrases

View file

@ -394,6 +394,8 @@ class Community(db.Model):
show_popular = db.Column(db.Boolean, default=True) show_popular = db.Column(db.Boolean, default=True)
show_all = db.Column(db.Boolean, default=True) show_all = db.Column(db.Boolean, default=True)
ignore_remote_language = db.Column(db.Boolean, default=False)
search_vector = db.Column(TSVectorType('name', 'title', 'description', 'rules')) search_vector = db.Column(TSVectorType('name', 'title', 'description', 'rules'))
posts = db.relationship('Post', lazy='dynamic', cascade="all, delete-orphan") posts = db.relationship('Post', lazy='dynamic', cascade="all, delete-orphan")
@ -402,6 +404,9 @@ class Community(db.Model):
image = db.relationship('File', foreign_keys=[image_id], single_parent=True, cascade="all, delete-orphan") image = db.relationship('File', foreign_keys=[image_id], single_parent=True, cascade="all, delete-orphan")
languages = db.relationship('Language', lazy='dynamic', secondary=community_language, backref=db.backref('communities', lazy='dynamic')) languages = db.relationship('Language', lazy='dynamic', secondary=community_language, backref=db.backref('communities', lazy='dynamic'))
def language_ids(self):
return [language.id for language in self.languages.all()]
@cache.memoize(timeout=500) @cache.memoize(timeout=500)
def icon_image(self, size='default') -> str: def icon_image(self, size='default') -> str:
if self.icon_id is not None: if self.icon_id is not None:

View file

@ -33,6 +33,7 @@
{{ render_field(form.rules) }} {{ render_field(form.rules) }}
{{ render_field(form.nsfw) }} {{ render_field(form.nsfw) }}
{{ render_field(form.restricted_to_mods) }} {{ render_field(form.restricted_to_mods) }}
{{ render_field(form.languages) }}
{% if not community.is_local() %} {% if not community.is_local() %}
<fieldset class="border pl-2 pt-2 mb-4"> <fieldset class="border pl-2 pt-2 mb-4">
<legend>{{ _('Will not be overwritten by remote server') }}</legend> <legend>{{ _('Will not be overwritten by remote server') }}</legend>
@ -48,6 +49,7 @@
{{ render_field(form.topic) }} {{ render_field(form.topic) }}
{{ render_field(form.default_layout) }} {{ render_field(form.default_layout) }}
{{ render_field(form.posting_warning) }} {{ render_field(form.posting_warning) }}
{{ render_field(form.ignore_remote_language) }}
{% if not community.is_local() %} {% if not community.is_local() %}
</fieldset> </fieldset>
{% endif %} {% endif %}

View file

@ -29,6 +29,7 @@
{{ render_field(form.nsfw) }} {{ render_field(form.nsfw) }}
{{ render_field(form.local_only) }} {{ render_field(form.local_only) }}
<small class="field_hint">{{ _('Only people using %(name)s can post or reply', name=current_app.config['SERVER_NAME']) }}.</small> <small class="field_hint">{{ _('Only people using %(name)s can post or reply', name=current_app.config['SERVER_NAME']) }}.</small>
{{ render_field(form.languages) }}
{{ render_field(form.submit) }} {{ render_field(form.submit) }}
</form> </form>
</div> </div>

View file

@ -46,6 +46,7 @@
{{ render_field(form.local_only) }} {{ render_field(form.local_only) }}
{{ render_field(form.new_mods_wanted) }} {{ render_field(form.new_mods_wanted) }}
{{ render_field(form.topic) }} {{ render_field(form.topic) }}
{{ render_field(form.languages) }}
{{ render_field(form.default_layout) }} {{ render_field(form.default_layout) }}
<div class="row"> <div class="row">
<div class="col-auto"> <div class="col-auto">

View file

@ -32,7 +32,7 @@ from PIL import Image
from app.email import send_welcome_email from app.email import send_welcome_email
from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan, \ from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan, \
Site, Post, PostReply, utcnow, Filter, CommunityMember, InstanceBlock, CommunityBan, Topic, UserBlock Site, Post, PostReply, utcnow, Filter, CommunityMember, InstanceBlock, CommunityBan, Topic, UserBlock, Language
# Flask's render_template function, with support for themes added # Flask's render_template function, with support for themes added
@ -976,3 +976,11 @@ def recently_downvoted_post_replies(user_id) -> List[int]:
reply_ids = db.session.execute(text('SELECT post_reply_id FROM "post_reply_vote" WHERE user_id = :user_id AND effect < 0 ORDER BY id DESC LIMIT 1000'), reply_ids = db.session.execute(text('SELECT post_reply_id FROM "post_reply_vote" WHERE user_id = :user_id AND effect < 0 ORDER BY id DESC LIMIT 1000'),
{'user_id': user_id}).scalars() {'user_id': user_id}).scalars()
return sorted(reply_ids) return sorted(reply_ids)
def languages_for_form():
result = []
for language in Language.query.order_by(Language.name).all():
if language.code != 'und':
result.append((language.id, language.name))
return result

View file

@ -0,0 +1,32 @@
"""community remote language
Revision ID: 94828ddc7c63
Revises: 5487a1886c62
Create Date: 2024-05-08 20:55:23.821386
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '94828ddc7c63'
down_revision = '5487a1886c62'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('community', schema=None) as batch_op:
batch_op.add_column(sa.Column('ignore_remote_language', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('community', schema=None) as batch_op:
batch_op.drop_column('ignore_remote_language')
# ### end Alembic commands ###