mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
community
This commit is contained in:
parent
e0e8ccd6fd
commit
c10e46c2e8
7 changed files with 185 additions and 19 deletions
|
@ -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/<actor>', 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('/<actor>/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('/<actor>/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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -27,9 +27,9 @@
|
|||
<div class="row">
|
||||
<div class="col-6">
|
||||
{% if current_user.subscribed(community) %}
|
||||
<a class="w-100 btn btn-primary" href="/c/{{ community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
|
||||
{% else %}
|
||||
<a class="w-100 btn btn-primary" href="/c/{{ community.link() }}/subscribe">{{ _('Subscribe') }}</a>
|
||||
<a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/subscribe">{{ _('Subscribe') }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
|
@ -48,8 +48,27 @@
|
|||
<div class="card-body">
|
||||
<p>{{ community.description }}</p>
|
||||
<p>{{ community.rules }}</p>
|
||||
{% if len(mods) > 0 %}
|
||||
<h3>Moderators</h3>
|
||||
<ol>
|
||||
{% for mod in mods %}
|
||||
<li><a href="/u/{{ mod.user_name }}">{{ mod.user_name }}</a></li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if is_moderator %}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ _('Community Settings') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
||||
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
|
@ -5,10 +5,17 @@
|
|||
<div class="row g-2 justify-content-between">
|
||||
<div class="col-auto">
|
||||
<div class="btn-group">
|
||||
<a href="#" class="btn btn-outline-secondary">{{ _('All') }}</a>
|
||||
<a href="#" class="btn btn-outline-secondary">{{ _('Local') }}</a>
|
||||
<a href="#" class="btn btn-outline-secondary">{{ _('Subscribed') }}</a>
|
||||
<a href="/communities" class="btn {{ 'btn-primary' if request.path == '/communities' else 'btn-outline-secondary' }}">
|
||||
{{ _('All') }}
|
||||
</a>
|
||||
<a href="/communities/local" class="btn {{ 'btn-primary' if request.path == '/communities/local' else 'btn-outline-secondary' }}">
|
||||
{{ _('Local') }}
|
||||
</a>
|
||||
<a href="/communities/subscribed" class="btn {{ 'btn-primary' if request.path == '/communities/subscribed' else 'btn-outline-secondary' }}">
|
||||
{{ _('Subscribed') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
|
||||
|
@ -17,7 +24,9 @@
|
|||
<a href="{{ url_for('community.add_local') }}" class="btn btn-outline-secondary">{{ _('Create local') }}</a>
|
||||
<a href="{{ url_for('community.add_remote') }}" class="btn btn-outline-secondary">{{ _('Add remote') }}</a>
|
||||
</div>
|
||||
<input type="search" placeholder="Find a community" class="form-control">
|
||||
<form method="get" action="/communities">
|
||||
<input name='search' type="search" placeholder="Find a community" class="form-control" value="{{ search }}" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% if len(communities) > 0 %}
|
||||
|
@ -41,9 +50,9 @@
|
|||
<td>{{ community.post_reply_count }}</td>
|
||||
<td>{{ moment(community.last_active).fromNow(refresh=True) }}</td>
|
||||
<td>{% if current_user.subscribed(community) %}
|
||||
<a class="btn btn-primary btn-sm" href="/c/{{ community.link() }}/unsubscribe">Unsubscribe</a>
|
||||
<a class="btn btn-primary btn-sm" href="/community/{{ community.link() }}/unsubscribe">Unsubscribe</a>
|
||||
{% else %}
|
||||
<a class="btn btn-primary btn-sm" href="/c/{{ community.link() }}/subscribe">Subscribe</a>
|
||||
<a class="btn btn-primary btn-sm" href="/community/{{ community.link() }}/subscribe">Subscribe</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
32
migrations/versions/755fa58fd603_private_mods.py
Normal file
32
migrations/versions/755fa58fd603_private_mods.py
Normal file
|
@ -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 ###
|
Loading…
Reference in a new issue