From c10e46c2e8399b298c7c9641690445065e5acfa9 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Tue, 5 Sep 2023 20:25:02 +1200 Subject: [PATCH] community --- app/community/routes.py | 83 +++++++++++++++++-- app/community/util.py | 9 +- app/main/routes.py | 27 +++++- app/models.py | 9 +- app/templates/community/community.html | 23 ++++- app/templates/list_communities.html | 21 +++-- .../versions/755fa58fd603_private_mods.py | 32 +++++++ 7 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 migrations/versions/755fa58fd603_private_mods.py diff --git a/app/community/routes.py b/app/community/routes.py index 9b70b250..4c87f57d 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -1,15 +1,16 @@ from datetime import date, datetime, timedelta -from flask import render_template, redirect, url_for, flash, request, make_response, session, Markup, current_app +from flask import render_template, redirect, url_for, flash, request, make_response, session, Markup, current_app, abort from flask_login import login_user, logout_user, current_user from flask_babel import _ from app import db from app.activitypub.signature import RsaKeys from app.community.forms import SearchRemoteCommunity, AddLocalCommunity -from app.community.util import search_for_community -from app.constants import SUBSCRIPTION_MEMBER +from app.community.util import search_for_community, community_url_exists +from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER from app.models import User, Community, CommunityMember from app.community import bp from app.utils import get_setting +from sqlalchemy import or_ @bp.route('/add_local', methods=['GET', 'POST']) @@ -18,7 +19,10 @@ def add_local(): if get_setting('allow_nsfw', False) is False: form.nsfw.render_kw = {'disabled': True} - if form.validate_on_submit(): + if form.validate_on_submit() and not community_url_exists(form.url.data): + # todo: more intense data validation + if form.url.data.trim().lower().startswith('/c/'): + form.url.data = form.url.data[3:] private_key, public_key = RsaKeys.generate_keypair() community = Community(title=form.community_name.data, name=form.url.data, description=form.description.data, rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key, public_key=public_key, @@ -59,4 +63,73 @@ def add_remote(): # @bp.route('/c/', methods=['GET']) - defined in activitypub/routes.py, which calls this function for user requests. A bit weird. def show_community(community: Community): - return render_template('community/community.html', community=community, title=community.title) + mods = CommunityMember.query.filter((CommunityMember.community_id == community.id) & + (or_( + CommunityMember.is_owner, + CommunityMember.is_moderator + )) + ).all() + + is_moderator = any(mod.user_id == current_user.id for mod in mods) + is_owner = any(mod.user_id == current_user.id and mod.is_owner == True for mod in mods) + + if community.private_mods: + mod_list = [] + else: + mod_user_ids = [mod.user_id for mod in mods] + mod_list = User.query.filter(User.id.in_(mod_user_ids)).all() + + return render_template('community/community.html', community=community, title=community.title, + is_moderator=is_moderator, is_owner=is_owner, mods=mod_list) + + +@bp.route('//subscribe', methods=['GET']) +def subscribe(actor): + actor = actor.strip() + if '@' in actor: + community = Community.query.filter_by(banned=False, ap_id=actor).first() + else: + community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first() + + if community is not None: + if not current_user.subscribed(community): + membership = CommunityMember(user_id=current_user.id, community_id=community.id) + db.session.add(membership) + db.session.commit() + flash('You have subscribed to ' + community.title) + referrer = request.headers.get('Referer', None) + if referrer is not None: + return redirect(referrer) + else: + return redirect('/c/' + actor) + else: + abort(404) + + +@bp.route('//unsubscribe', methods=['GET']) +def unsubscribe(actor): + actor = actor.strip() + if '@' in actor: + community = Community.query.filter_by(banned=False, ap_id=actor).first() + else: + community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first() + + if community is not None: + subscription = current_user.subscribed(community) + if subscription: + if subscription != SUBSCRIPTION_OWNER: + db.session.query(CommunityMember).filter_by(user_id=current_user.id, community_id=community.id).delete() + db.session.commit() + flash('You are unsubscribed from ' + community.title) + else: + # todo: community deletion + flash('You need to make someone else the owner before unsubscribing.', 'warning') + + # send them back where they came from + referrer = request.headers.get('Referer', None) + if referrer is not None: + return redirect(referrer) + else: + return redirect('/c/' + actor) + else: + abort(404) diff --git a/app/community/util.py b/app/community/util.py index c7634b4c..e5ffcac2 100644 --- a/app/community/util.py +++ b/app/community/util.py @@ -12,7 +12,7 @@ def search_for_community(address: str): banned = BannedInstances.query.filter_by(domain=server).first() if banned: reason = f" Reason: {banned.reason}" if banned.reason is not None else '' - raise Exception(f"{server} is blocked.{reason}") # todo: create custom exception class hierarchy + raise Exception(f"{server} is blocked.{reason}") # todo: create custom exception class hierarchy already_exists = Community.query.filter_by(ap_id=address[1:]).first() if already_exists: @@ -25,7 +25,7 @@ def search_for_community(address: str): if webfinger_data.status_code == 200: webfinger_json = webfinger_data.json() for links in webfinger_json['links']: - if 'rel' in links and links['rel'] == 'self': # this contains the URL of the activitypub profile + if 'rel' in links and links['rel'] == 'self': # this contains the URL of the activitypub profile type = links['type'] if 'type' in links else 'application/activity+json' # retrieve the activitypub profile community_data = get_request(links['href'], headers={'Accept': type}) @@ -64,3 +64,8 @@ def search_for_community(address: str): db.session.commit() return community return None + + +def community_url_exists(url) -> bool: + community = Community.query.filter_by(url=url).first() + return community is not None diff --git a/app/main/routes.py b/app/main/routes.py index 3874fb3a..efae5c33 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -1,12 +1,15 @@ from datetime import datetime +from app import db from app.main import bp -from flask import g, jsonify, render_template, flash +from flask import g, jsonify, render_template, flash, request from flask_moment import moment from flask_login import current_user from flask_babel import _, get_locale +from sqlalchemy import select +from sqlalchemy_searchable import search -from app.models import Community +from app.models import Community, CommunityMember @bp.route('/', methods=['GET', 'POST']) @@ -19,7 +22,25 @@ def index(): @bp.route('/communities', methods=['GET']) def list_communities(): - communities = Community.query.all() + search_param = request.args.get('search', '') + if search_param == '': + communities = Community.query.all() + else: + query = search(select(Community), search_param, sort=True) + communities = db.session.scalars(query).all() + + return render_template('list_communities.html', communities=communities, search=search_param) + + +@bp.route('/communities/local', methods=['GET']) +def list_local_communities(): + communities = Community.query.filter_by(ap_id=None).all() + return render_template('list_communities.html', communities=communities) + + +@bp.route('/communities/subscribed', methods=['GET']) +def list_subscribed_communities(): + communities = Community.query.join(CommunityMember).filter(CommunityMember.user_id == current_user.id).all() return render_template('list_communities.html', communities=communities) diff --git a/app/models.py b/app/models.py index 4528d43a..53464e10 100644 --- a/app/models.py +++ b/app/models.py @@ -55,6 +55,7 @@ class Community(db.Model): banned = db.Column(db.Boolean, default=False) restricted_to_mods = db.Column(db.Boolean, default=False) searchable = db.Column(db.Boolean, default=True) + private_mods = db.Column(db.Boolean, default=False) search_vector = db.Column(TSVectorType('name', 'title', 'description', 'rules')) @@ -180,7 +181,7 @@ class User(UserMixin, db.Model): return True return self.expires < datetime(2019, 9, 1) - def subscribed(self, community) -> int: + def subscribed(self, community: Community) -> int: if community is None: return False subscription:CommunityMember = CommunityMember.query.filter_by(user_id=self.id, community_id=community.id).first() @@ -340,6 +341,12 @@ class Settings(db.Model): value = db.Column(db.String(1024)) +class Interest(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50)) + communities = db.Column(db.Text) + + @login.user_loader def load_user(id): return User.query.get(int(id)) diff --git a/app/templates/community/community.html b/app/templates/community/community.html index 6379c3d6..982740b9 100644 --- a/app/templates/community/community.html +++ b/app/templates/community/community.html @@ -27,9 +27,9 @@
{% if current_user.subscribed(community) %} - {{ _('Unsubscribe') }} + {{ _('Unsubscribe') }} {% else %} - {{ _('Subscribe') }} + {{ _('Subscribe') }} {% endif %}
@@ -48,8 +48,27 @@

{{ community.description }}

{{ community.rules }}

+ {% if len(mods) > 0 %} +

Moderators

+
    + {% for mod in mods %} +
  1. {{ mod.user_name }}
  2. + {% endfor %} +
+ {% endif %}
+ {% if is_moderator %} +
+
+

{{ _('Community Settings') }}

+
+ +
+ {% endif %}
diff --git a/app/templates/list_communities.html b/app/templates/list_communities.html index f04a781a..4ed4d961 100644 --- a/app/templates/list_communities.html +++ b/app/templates/list_communities.html @@ -5,10 +5,17 @@
{% if len(communities) > 0 %} @@ -41,9 +50,9 @@ {{ community.post_reply_count }} {{ moment(community.last_active).fromNow(refresh=True) }} {% if current_user.subscribed(community) %} - Unsubscribe + Unsubscribe {% else %} - Subscribe + Subscribe {% endif %} diff --git a/migrations/versions/755fa58fd603_private_mods.py b/migrations/versions/755fa58fd603_private_mods.py new file mode 100644 index 00000000..918e90ba --- /dev/null +++ b/migrations/versions/755fa58fd603_private_mods.py @@ -0,0 +1,32 @@ +"""private mods + +Revision ID: 755fa58fd603 +Revises: feef49234599 +Create Date: 2023-09-03 18:26:50.925553 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '755fa58fd603' +down_revision = 'feef49234599' +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('private_mods', 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('private_mods') + + # ### end Alembic commands ###