2023-09-16 00:09:04 -07:00
|
|
|
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort
|
2023-10-02 02:16:44 -07:00
|
|
|
from flask_login import login_user, logout_user, current_user, login_required
|
2023-08-29 03:01:06 -07:00
|
|
|
from flask_babel import _
|
2023-11-27 01:05:35 -08:00
|
|
|
from pillow_heif import register_heif_opener
|
2023-11-21 02:05:07 -08:00
|
|
|
from sqlalchemy import or_, desc
|
2023-09-17 02:19:51 -07:00
|
|
|
|
2023-10-16 01:38:36 -07:00
|
|
|
from app import db, constants
|
2023-09-08 01:04:01 -07:00
|
|
|
from app.activitypub.signature import RsaKeys, HttpSignature
|
2023-11-29 23:57:51 -08:00
|
|
|
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm
|
2023-11-29 09:36:08 -08:00
|
|
|
from app.community.util import search_for_community, community_url_exists, actor_to_community, \
|
2023-11-29 23:57:51 -08:00
|
|
|
ensure_directory_exists, opengraph_parse, url_to_thumbnail_file, save_post
|
2023-09-17 02:19:51 -07:00
|
|
|
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE
|
2023-11-29 09:36:08 -08:00
|
|
|
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
|
2023-11-29 23:57:51 -08:00
|
|
|
File, PostVote
|
2023-08-29 03:01:06 -07:00
|
|
|
from app.community import bp
|
2023-10-23 00:18:46 -07:00
|
|
|
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
2023-11-27 01:05:35 -08:00
|
|
|
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish
|
|
|
|
import os
|
|
|
|
from PIL import Image, ImageOps
|
2023-11-29 23:57:51 -08:00
|
|
|
from datetime import datetime
|
2023-08-29 03:01:06 -07:00
|
|
|
|
|
|
|
|
|
|
|
@bp.route('/add_local', methods=['GET', 'POST'])
|
2023-10-22 17:03:35 -07:00
|
|
|
@login_required
|
2023-08-29 03:01:06 -07:00
|
|
|
def add_local():
|
|
|
|
form = AddLocalCommunity()
|
2023-09-02 21:30:20 -07:00
|
|
|
if get_setting('allow_nsfw', False) is False:
|
|
|
|
form.nsfw.render_kw = {'disabled': True}
|
|
|
|
|
2023-09-05 01:25:02 -07:00
|
|
|
if form.validate_on_submit() and not community_url_exists(form.url.data):
|
|
|
|
# todo: more intense data validation
|
2023-09-17 02:19:51 -07:00
|
|
|
if form.url.data.strip().lower().startswith('/c/'):
|
2023-09-05 01:25:02 -07:00
|
|
|
form.url.data = form.url.data[3:]
|
2023-09-02 21:30:20 -07:00
|
|
|
private_key, public_key = RsaKeys.generate_keypair()
|
|
|
|
community = Community(title=form.community_name.data, name=form.url.data, description=form.description.data,
|
2023-09-08 01:04:01 -07:00
|
|
|
rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key,
|
2023-10-15 01:13:32 -07:00
|
|
|
public_key=public_key,
|
|
|
|
ap_profile_id=current_app.config['SERVER_NAME'] + '/c/' + form.url.data,
|
2023-09-02 21:30:20 -07:00
|
|
|
subscriptions_count=1)
|
|
|
|
db.session.add(community)
|
|
|
|
db.session.commit()
|
|
|
|
membership = CommunityMember(user_id=current_user.id, community_id=community.id, is_moderator=True,
|
|
|
|
is_owner=True)
|
|
|
|
db.session.add(membership)
|
|
|
|
db.session.commit()
|
|
|
|
flash(_('Your new community has been created.'))
|
|
|
|
return redirect('/c/' + community.name)
|
|
|
|
|
|
|
|
return render_template('community/add_local.html', title=_('Create community'), form=form)
|
2023-08-29 03:01:06 -07:00
|
|
|
|
|
|
|
|
|
|
|
@bp.route('/add_remote', methods=['GET', 'POST'])
|
2023-10-22 17:03:35 -07:00
|
|
|
@login_required
|
2023-08-29 03:01:06 -07:00
|
|
|
def add_remote():
|
|
|
|
form = SearchRemoteCommunity()
|
|
|
|
new_community = None
|
|
|
|
if form.validate_on_submit():
|
|
|
|
address = form.address.data.strip()
|
|
|
|
if address.startswith('!') and '@' in address:
|
|
|
|
new_community = search_for_community(address)
|
|
|
|
elif address.startswith('@') and '@' in address[1:]:
|
|
|
|
# todo: the user is searching for a person instead
|
|
|
|
...
|
|
|
|
elif '@' in address:
|
|
|
|
new_community = search_for_community('!' + address)
|
|
|
|
else:
|
2023-09-08 01:04:01 -07:00
|
|
|
message = Markup(
|
|
|
|
'Type address in the format !community@server.name. Search on <a href="https://lemmyverse.net/communities">Lemmyverse.net</a> to find some.')
|
2023-08-29 03:01:06 -07:00
|
|
|
flash(message, 'error')
|
|
|
|
|
|
|
|
return render_template('community/add_remote.html',
|
|
|
|
title=_('Add remote community'), form=form, new_community=new_community,
|
|
|
|
subscribed=current_user.subscribed(new_community) >= SUBSCRIPTION_MEMBER)
|
|
|
|
|
|
|
|
|
|
|
|
# @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):
|
2023-09-17 02:19:51 -07:00
|
|
|
mods = community.moderators()
|
2023-09-05 01:25:02 -07:00
|
|
|
|
2023-10-02 02:16:44 -07:00
|
|
|
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
2023-10-15 01:13:32 -07:00
|
|
|
is_owner = current_user.is_authenticated and any(
|
|
|
|
mod.user_id == current_user.id and mod.is_owner == True for mod in mods)
|
2023-09-05 01:25:02 -07:00
|
|
|
|
|
|
|
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()
|
|
|
|
|
2023-10-16 01:38:36 -07:00
|
|
|
if current_user.is_anonymous or current_user.ignore_bots:
|
2023-11-21 02:05:07 -08:00
|
|
|
posts = community.posts.filter(Post.from_bot == False).order_by(desc(Post.last_active)).all()
|
2023-10-07 01:32:19 -07:00
|
|
|
else:
|
2023-11-29 01:12:55 -08:00
|
|
|
posts = community.posts.order_by(desc(Post.last_active)).all()
|
2023-10-07 01:32:19 -07:00
|
|
|
|
2023-10-23 00:18:46 -07:00
|
|
|
description = shorten_string(community.description, 150) if community.description else None
|
|
|
|
og_image = community.image.source_url if community.image_id else None
|
|
|
|
|
2023-09-05 01:25:02 -07:00
|
|
|
return render_template('community/community.html', community=community, title=community.title,
|
2023-10-23 00:18:46 -07:00
|
|
|
is_moderator=is_moderator, is_owner=is_owner, mods=mod_list, posts=posts, description=description,
|
2023-11-27 01:05:35 -08:00
|
|
|
og_image=og_image, POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK)
|
2023-09-05 01:25:02 -07:00
|
|
|
|
|
|
|
|
|
|
|
@bp.route('/<actor>/subscribe', methods=['GET'])
|
2023-10-02 02:16:44 -07:00
|
|
|
@login_required
|
2023-10-22 17:03:35 -07:00
|
|
|
@validation_required
|
2023-09-05 01:25:02 -07:00
|
|
|
def subscribe(actor):
|
2023-09-08 01:04:01 -07:00
|
|
|
remote = False
|
2023-09-05 01:25:02 -07:00
|
|
|
actor = actor.strip()
|
|
|
|
if '@' in actor:
|
|
|
|
community = Community.query.filter_by(banned=False, ap_id=actor).first()
|
2023-09-08 01:04:01 -07:00
|
|
|
remote = True
|
2023-09-05 01:25:02 -07:00
|
|
|
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):
|
2023-09-08 01:04:01 -07:00
|
|
|
if remote:
|
|
|
|
# send ActivityPub message to remote community, asking to follow. Accept message will be sent to our shared inbox
|
|
|
|
join_request = CommunityJoinRequest(user_id=current_user.id, community_id=community.id)
|
|
|
|
db.session.add(join_request)
|
|
|
|
db.session.commit()
|
|
|
|
follow = {
|
|
|
|
"actor": f"https://{current_app.config['SERVER_NAME']}/u/{current_user.user_name}",
|
2023-11-17 01:02:44 -08:00
|
|
|
"to": [community.ap_profile_id],
|
|
|
|
"object": community.ap_profile_id,
|
2023-09-08 01:04:01 -07:00
|
|
|
"type": "Follow",
|
2023-09-17 02:19:51 -07:00
|
|
|
"id": f"https://{current_app.config['SERVER_NAME']}/activities/follow/{join_request.id}"
|
2023-09-08 01:04:01 -07:00
|
|
|
}
|
|
|
|
try:
|
|
|
|
message = HttpSignature.signed_request(community.ap_inbox_url, follow, current_user.private_key,
|
2023-11-17 01:02:44 -08:00
|
|
|
current_user.profile_id() + '#main-key')
|
2023-09-08 01:04:01 -07:00
|
|
|
if message.status_code == 200:
|
|
|
|
flash('Your request to subscribe has been sent to ' + community.title)
|
|
|
|
else:
|
|
|
|
flash('Response status code was not 200', 'warning')
|
|
|
|
current_app.logger.error('Response code for subscription attempt was ' +
|
|
|
|
str(message.status_code) + ' ' + message.text)
|
|
|
|
except Exception as ex:
|
|
|
|
flash('Failed to send request to subscribe: ' + str(ex), 'error')
|
|
|
|
current_app.logger.error("Exception while trying to subscribe" + str(ex))
|
2023-10-15 01:13:32 -07:00
|
|
|
else: # for local communities, joining is instant
|
2023-09-08 01:04:01 -07:00
|
|
|
banned = CommunityBan.query.filter_by(user_id=current_user.id, community_id=community.id).first()
|
|
|
|
if banned:
|
|
|
|
flash('You cannot join this community')
|
|
|
|
member = CommunityMember(user_id=current_user.id, community_id=community.id)
|
|
|
|
db.session.add(member)
|
|
|
|
db.session.commit()
|
|
|
|
flash('You are subscribed to ' + community.title)
|
2023-09-05 01:25:02 -07:00
|
|
|
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'])
|
2023-10-02 02:16:44 -07:00
|
|
|
@login_required
|
2023-09-05 01:25:02 -07:00
|
|
|
def unsubscribe(actor):
|
2023-09-17 02:19:51 -07:00
|
|
|
community = actor_to_community(actor)
|
2023-09-05 01:25:02 -07:00
|
|
|
|
|
|
|
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)
|
2023-09-17 02:19:51 -07:00
|
|
|
|
|
|
|
|
|
|
|
@bp.route('/<actor>/submit', methods=['GET', 'POST'])
|
2023-10-02 02:16:44 -07:00
|
|
|
@login_required
|
2023-10-22 17:03:35 -07:00
|
|
|
@validation_required
|
2023-09-17 02:19:51 -07:00
|
|
|
def add_post(actor):
|
|
|
|
community = actor_to_community(actor)
|
2023-11-29 23:57:51 -08:00
|
|
|
form = CreatePostForm()
|
2023-09-17 02:19:51 -07:00
|
|
|
if get_setting('allow_nsfw', False) is False:
|
|
|
|
form.nsfw.render_kw = {'disabled': True}
|
|
|
|
if get_setting('allow_nsfl', False) is False:
|
|
|
|
form.nsfl.render_kw = {'disabled': True}
|
|
|
|
images_disabled = 'disabled' if not get_setting('allow_local_image_posts', True) else ''
|
|
|
|
|
|
|
|
form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
|
|
|
|
|
|
|
|
if form.validate_on_submit():
|
2023-11-29 23:57:51 -08:00
|
|
|
post = Post(user_id=current_user.id, community_id=form.communities.data)
|
|
|
|
save_post(form, post)
|
2023-10-02 02:16:44 -07:00
|
|
|
community.post_count += 1
|
2023-11-29 23:57:51 -08:00
|
|
|
community.last_active = datetime.utcnow()
|
2023-09-17 02:19:51 -07:00
|
|
|
db.session.commit()
|
|
|
|
|
2023-11-29 23:57:51 -08:00
|
|
|
|
2023-10-02 02:16:44 -07:00
|
|
|
# todo: federate post creation out to followers
|
|
|
|
|
2023-09-17 02:19:51 -07:00
|
|
|
flash('Post has been added')
|
|
|
|
return redirect(f"/c/{community.link()}")
|
|
|
|
else:
|
|
|
|
form.communities.data = community.id
|
2023-11-29 23:57:51 -08:00
|
|
|
form.notify_author.data = True
|
2023-09-17 02:19:51 -07:00
|
|
|
|
|
|
|
return render_template('community/add_post.html', title=_('Add post to community'), form=form, community=community,
|
|
|
|
images_disabled=images_disabled)
|
2023-11-29 23:57:51 -08:00
|
|
|
|
|
|
|
|
|
|
|
|