From cb7d4fc40fb08a6e1b9199d9cfd31141f44c7524 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:07:58 +1300 Subject: [PATCH] community setting: accept downvotes from community members only --- app/community/forms.py | 9 ++++ app/community/routes.py | 2 + app/constants.py | 5 +++ app/models.py | 3 +- app/templates/community/community_edit.html | 10 +++-- app/utils.py | 21 ++++++++++ .../03258111eef1_downvote_accept_mode.py | 41 +++++++++++++++++++ 7 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 migrations/versions/03258111eef1_downvote_accept_mode.py diff --git a/app/community/forms.py b/app/community/forms.py index 970dacda..cd034504 100644 --- a/app/community/forms.py +++ b/app/community/forms.py @@ -8,6 +8,8 @@ from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Le from flask_babel import _, lazy_gettext as _l from app import db +from app.constants import DOWNVOTE_ACCEPT_ALL, DOWNVOTE_ACCEPT_MEMBERS, DOWNVOTE_ACCEPT_INSTANCE, \ + DOWNVOTE_ACCEPT_TRUSTED from app.models import Community, utcnow from app.utils import domain_from_url, MultiCheckboxField from PIL import Image, ImageOps @@ -54,6 +56,13 @@ class EditCommunityForm(FlaskForm): local_only = BooleanField(_l('Only accept posts from current instance')) restricted_to_mods = BooleanField(_l('Only moderators can post')) new_mods_wanted = BooleanField(_l('New moderators wanted')) + downvote_accept_modes = [(DOWNVOTE_ACCEPT_ALL, _l('Everyone')), + (DOWNVOTE_ACCEPT_MEMBERS, _l('Community members')), + (DOWNVOTE_ACCEPT_INSTANCE, _l('This instance')), + (DOWNVOTE_ACCEPT_TRUSTED, _l('Trusted instances')), + + ] + downvote_accept_mode = SelectField(_l('Accept downvotes from'), coerce=int, choices=downvote_accept_modes, validators=[Optional()], render_kw={'class': 'form-select'}) topic = SelectField(_l('Topic'), coerce=int, validators=[Optional()], render_kw={'class': 'form-select'}) languages = SelectMultipleField(_l('Languages'), coerce=int, validators=[Optional()], render_kw={'class': 'form-select'}) layouts = [('', _l('List')), diff --git a/app/community/routes.py b/app/community/routes.py index 4f6c8962..efee21e4 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -1019,6 +1019,7 @@ def community_edit(community_id: int): community.new_mods_wanted = form.new_mods_wanted.data community.topic_id = form.topic.data if form.topic.data != 0 else None community.default_layout = form.default_layout.data + community.downvote_accept_mode = form.downvote_accept_mode.data icon_file = request.files['icon_file'] if icon_file and icon_file.filename != '': @@ -1067,6 +1068,7 @@ def community_edit(community_id: int): 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.downvote_accept_mode.data = community.downvote_accept_mode return render_template('community/community_edit.html', title=_('Edit community'), form=form, current_app=current_app, current="edit_settings", community=community, moderating_communities=moderating_communities(current_user.get_id()), diff --git a/app/constants.py b/app/constants.py index b53aa12a..ba60315b 100644 --- a/app/constants.py +++ b/app/constants.py @@ -35,6 +35,11 @@ NOTIF_REPLY = 4 ROLE_STAFF = 3 ROLE_ADMIN = 4 +DOWNVOTE_ACCEPT_ALL = 0 +DOWNVOTE_ACCEPT_MEMBERS = 2 +DOWNVOTE_ACCEPT_INSTANCE = 4 +DOWNVOTE_ACCEPT_TRUSTED = 6 + MICROBLOG_APPS = ["mastodon", "misskey", "akkoma", "iceshrimp", "pleroma"] APLOG_IN = True diff --git a/app/models.py b/app/models.py index 985f48cf..9714869b 100644 --- a/app/models.py +++ b/app/models.py @@ -75,7 +75,7 @@ class Instance(db.Model): start_trying_again = db.Column(db.DateTime) # When to start trying again. Should grow exponentially with each failure. gone_forever = db.Column(db.Boolean, default=False) # True once this instance is considered offline forever - never start trying again ip_address = db.Column(db.String(50)) - trusted = db.Column(db.Boolean, default=False) + trusted = db.Column(db.Boolean, default=False, index=True) posting_warning = db.Column(db.String(512)) nodeinfo_href = db.Column(db.String(100)) @@ -438,6 +438,7 @@ class Community(db.Model): topic_id = db.Column(db.Integer, db.ForeignKey('topic.id'), index=True) default_layout = db.Column(db.String(15)) posting_warning = db.Column(db.String(512)) + downvote_accept_mode = db.Column(db.Integer, default=0) # 0 = All, 2 = Community members, 4 = This instance, 6 = Trusted instances ap_id = db.Column(db.String(255), index=True) ap_profile_id = db.Column(db.String(255), index=True, unique=True) diff --git a/app/templates/community/community_edit.html b/app/templates/community/community_edit.html index 767debf1..55706bb9 100644 --- a/app/templates/community/community_edit.html +++ b/app/templates/community/community_edit.html @@ -59,20 +59,22 @@ {{ render_field(form.restricted_to_mods) }} {{ render_field(form.local_only) }} {{ render_field(form.new_mods_wanted) }} + {{ render_field(form.downvote_accept_mode) }} {{ render_field(form.topic) }} {{ render_field(form.languages) }} {{ render_field(form.default_layout) }} -
+
{{ render_field(form.submit) }}
-
+
+
+
{% if community.is_local() and (community.is_owner() or current_user.is_admin()) %}

Delete community

{% endif %} +
- -
diff --git a/app/utils.py b/app/utils.py index f35beabd..cb352aa7 100644 --- a/app/utils.py +++ b/app/utils.py @@ -19,6 +19,8 @@ from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning import warnings import jwt +from app.constants import DOWNVOTE_ACCEPT_ALL, DOWNVOTE_ACCEPT_TRUSTED, DOWNVOTE_ACCEPT_INSTANCE, \ + DOWNVOTE_ACCEPT_MEMBERS warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning) import os @@ -715,6 +717,20 @@ def can_downvote(user, community: Community, site=None) -> bool: if (user.attitude and user.attitude < -0.40) or user.reputation < -10: # this should exclude about 3.7% of users. return False + if community.downvote_accept_mode != DOWNVOTE_ACCEPT_ALL: + if community.downvote_accept_mode == DOWNVOTE_ACCEPT_MEMBERS: + if not community.is_member(user): + return False + elif community.downvote_accept_mode == DOWNVOTE_ACCEPT_INSTANCE: + if user.instance_id != community.instance_id: + return False + elif community.downvote_accept_mode == DOWNVOTE_ACCEPT_TRUSTED: + if community.instance_id == user.instance_id: + pass + else: + if user.instance_id not in trusted_instance_ids(): + return False + if community.id in communities_banned_from(user.id): return False @@ -812,6 +828,11 @@ def reply_is_stupid(body) -> bool: return False +@cache.memoize(timeout=10) +def trusted_instance_ids() -> List[int]: + return [instance.id for instance in Instance.query.filter(Instance.trusted == True)] + + def inbox_domain(inbox: str) -> str: inbox = inbox.lower() if 'https://' in inbox or 'http://' in inbox: diff --git a/migrations/versions/03258111eef1_downvote_accept_mode.py b/migrations/versions/03258111eef1_downvote_accept_mode.py new file mode 100644 index 00000000..9c94b87e --- /dev/null +++ b/migrations/versions/03258111eef1_downvote_accept_mode.py @@ -0,0 +1,41 @@ +"""downvote accept mode + +Revision ID: 03258111eef1 +Revises: 9df13396fd54 +Create Date: 2025-01-11 11:32:54.226698 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '03258111eef1' +down_revision = '9df13396fd54' +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('downvote_accept_mode', sa.Integer(), nullable=True)) + + with op.batch_alter_table('community', schema=None) as batch_op: + batch_op.execute('UPDATE "community" SET downvote_accept_mode = 0') + + with op.batch_alter_table('instance', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_instance_trusted'), ['trusted'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('instance', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_instance_trusted')) + + with op.batch_alter_table('community', schema=None) as batch_op: + batch_op.drop_column('downvote_accept_mode') + + # ### end Alembic commands ###