mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
home page, expanded admin area and domain blocking
This commit is contained in:
parent
7b91250e3a
commit
c3839e6873
26 changed files with 476 additions and 49 deletions
|
@ -18,7 +18,7 @@ from sqlalchemy_searchable import make_searchable
|
|||
from config import Config
|
||||
|
||||
|
||||
db = SQLAlchemy(session_options={"autoflush": False}) # engine_options={'pool_size': 5, 'max_overflow': 10}
|
||||
db = SQLAlchemy() # engine_options={'pool_size': 5, 'max_overflow': 10} # session_options={"autoflush": False}
|
||||
migrate = Migrate()
|
||||
login = LoginManager()
|
||||
login.login_view = 'auth.login'
|
||||
|
|
|
@ -2,7 +2,7 @@ from datetime import datetime
|
|||
|
||||
from app import db, constants, cache
|
||||
from app.activitypub import bp
|
||||
from flask import request, Response, current_app, abort, jsonify, json
|
||||
from flask import request, Response, current_app, abort, jsonify, json, g
|
||||
|
||||
from app.activitypub.signature import HttpSignature
|
||||
from app.community.routes import show_community
|
||||
|
@ -290,7 +290,7 @@ def shared_inbox():
|
|||
community = find_actor_or_create(community_ap_id)
|
||||
user = find_actor_or_create(user_ap_id)
|
||||
if user and community:
|
||||
user.last_seen = community.last_active = utcnow()
|
||||
user.last_seen = community.last_active = g.site.last_active = utcnow()
|
||||
|
||||
object_type = request_json['object']['type']
|
||||
new_content_types = ['Page', 'Article', 'Link', 'Note']
|
||||
|
@ -403,7 +403,7 @@ def shared_inbox():
|
|||
community = find_actor_or_create(community_ap_id)
|
||||
user = find_actor_or_create(user_ap_id)
|
||||
if user and community:
|
||||
user.last_seen = community.last_active = utcnow()
|
||||
user.last_seen = community.last_active = g.site.last_active = utcnow()
|
||||
object_type = request_json['object']['object']['type']
|
||||
new_content_types = ['Page', 'Article', 'Link', 'Note']
|
||||
if object_type in new_content_types: # create a new post
|
||||
|
|
|
@ -5,7 +5,7 @@ from typing import Union, Tuple
|
|||
from flask import current_app, request
|
||||
from sqlalchemy import text
|
||||
from app import db, cache
|
||||
from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow
|
||||
from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow, Site
|
||||
import time
|
||||
import base64
|
||||
import requests
|
||||
|
@ -410,32 +410,34 @@ def is_activitypub_request():
|
|||
|
||||
|
||||
def lemmy_site_data():
|
||||
site = Site.query.get(1)
|
||||
data = {
|
||||
"site_view": {
|
||||
"site": {
|
||||
"id": 1,
|
||||
"name": "PieFed",
|
||||
"sidebar": "Rules:\n- [Don't be a dick](https://lemmy.nz/post/63098)\n\n[FAQ](https://lemmy.nz/post/31318) ~ [NZ Community List ](https://lemmy.nz/post/63156) ~ [Join Matrix chatroom](https://lemmy.nz/post/169187)",
|
||||
"published": "2023-06-02T09:46:21.972257",
|
||||
"updated": "2023-11-03T03:22:35.594456",
|
||||
"name": site.name,
|
||||
"sidebar": site.sidebar,
|
||||
"published": site.created_at.isoformat(),
|
||||
"updated": site.updated.isoformat(),
|
||||
"icon": "https://lemmy.nz/pictrs/image/d308ef8d-4381-4a7a-b047-569ed5b8dd88.png",
|
||||
"banner": "https://lemmy.nz/pictrs/image/68beebd5-4e01-44b6-bd4e-008b0d443ac1.png",
|
||||
"description": "PieFed development",
|
||||
"actor_id": "https://lemmy.nz/",
|
||||
"last_refreshed_at": "2023-06-02T09:46:21.960383",
|
||||
"inbox_url": "https://lemmy.nz/site_inbox",
|
||||
"public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx6cROxTmUbuWDHM3DcIx\nAWVy4O+cYlnMU3s89gbzhgVioPHqoajDbxNzVavqLd093ZhGPG6pEoGAGEgI9zG/\nnxpCcRC8uoMcu6Yh8E707VWRXFiXDsONyldBKnFmQouQDFAEmPaEOkYX3l1Qe6Q+\np4XKQRcD5hZWMvJVYpGsEa1euOcKrZvQffA+HQ1xcbU2Kts92ZiGkuXcEzOT8YR2\nX82Y/JkpeGkFlW4AociJ1ohfsH9i4OV+C215SgpCPxnEa9oEpluOvql8d7lg0yPA\nIisxtLb6hQtx5hiueILv7WB7kq1dh57RZQmvt7fuBsEk9rK5Lqc/ee9hxseqZKH8\nxwIDAQAB\n-----END PUBLIC KEY-----\n",
|
||||
"description": site.description,
|
||||
"actor_id": f"https://{current_app.config['SERVER_NAME']}/",
|
||||
"last_refreshed_at": site.updated.isoformat(),
|
||||
"inbox_url": f"https://{current_app.config['SERVER_NAME']}/inbox",
|
||||
"public_key": site.public_key,
|
||||
"instance_id": 1
|
||||
},
|
||||
"local_site": {
|
||||
"id": 1,
|
||||
"site_id": 1,
|
||||
"site_setup": True,
|
||||
"enable_downvotes": True,
|
||||
"enable_nsfw": True,
|
||||
"community_creation_admin_only": True,
|
||||
"enable_downvotes": site.enable_downvotes,
|
||||
"enable_nsfw": site.enable_nsfw,
|
||||
"enable_nsfl": site.enable_nsfl,
|
||||
"community_creation_admin_only": site.community_creation_admin_only,
|
||||
"require_email_verification": True,
|
||||
"application_question": "This is a New Zealand instance with a focus on New Zealand content. Most content can be accessed from any instance, see https://join-lemmy.org to find one that suits you.\n\nBecause of a Lemmy-wide spam issue, we have needed to turn on the requirement to apply for an account. We will approve you as soon as possible after reviewing your response.\n\nRemember if you didn't provide an email address, you won't be able to get notified you've been approved, so don't forget to check back.\n\nWhere are you from?",
|
||||
"application_question": site.application_question,
|
||||
"private_instance": False,
|
||||
"default_theme": "browser",
|
||||
"default_post_listing_type": "All",
|
||||
|
@ -445,10 +447,10 @@ def lemmy_site_data():
|
|||
"federation_enabled": True,
|
||||
"captcha_enabled": True,
|
||||
"captcha_difficulty": "medium",
|
||||
"published": "2023-06-02T09:46:22.153520",
|
||||
"updated": "2023-11-03T03:22:35.600601",
|
||||
"registration_mode": "RequireApplication",
|
||||
"reports_email_admins": True
|
||||
"published": site.created_at.isoformat(),
|
||||
"updated": site.updated.isoformat(),
|
||||
"registration_mode": site.registration_mode,
|
||||
"reports_email_admins": site.reports_email_admins
|
||||
},
|
||||
"local_site_rate_limit": {
|
||||
"id": 1,
|
||||
|
@ -465,7 +467,7 @@ def lemmy_site_data():
|
|||
"comment_per_second": 600,
|
||||
"search": 999,
|
||||
"search_per_second": 600,
|
||||
"published": "2023-06-02T09:46:22.156933"
|
||||
"published": site.created_at.isoformat(),
|
||||
},
|
||||
"counts": {
|
||||
"id": 1,
|
||||
|
|
|
@ -1,10 +1,37 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, SubmitField, HiddenField, BooleanField, TextAreaField
|
||||
from flask_wtf.file import FileRequired, FileAllowed
|
||||
from wtforms import StringField, PasswordField, SubmitField, HiddenField, BooleanField, TextAreaField, SelectField, \
|
||||
FileField, IntegerField
|
||||
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length
|
||||
from flask_babel import _, lazy_gettext as _l
|
||||
|
||||
|
||||
class AdminForm(FlaskForm):
|
||||
class SiteProfileForm(FlaskForm):
|
||||
name = StringField(_l('Name'))
|
||||
description = StringField(_l('Tagline'))
|
||||
icon = FileField(_('Icon'), validators=[
|
||||
FileAllowed(['jpg', 'jpeg', 'png', 'webp'], 'Images only!')
|
||||
])
|
||||
sidebar = TextAreaField(_l('Sidebar'))
|
||||
legal_information = TextAreaField(_l('Legal information'))
|
||||
submit = SubmitField(_l('Save'))
|
||||
|
||||
|
||||
class SiteMiscForm(FlaskForm):
|
||||
enable_downvotes = BooleanField(_l('Enable downvotes'))
|
||||
allow_local_image_posts = BooleanField(_l('Allow local image posts'))
|
||||
remote_image_cache_days = IntegerField(_l('Days to cache images from remote instances for'))
|
||||
enable_nsfw = BooleanField(_l('Allow NSFW communities and posts'))
|
||||
enable_nsfl = BooleanField(_l('Allow NSFL communities and posts'))
|
||||
community_creation_admin_only = BooleanField(_l('Only admins can create new local communities'))
|
||||
reports_email_admins = BooleanField(_l('Notify admins about reports, not just moderators'))
|
||||
types = [('Open', _l('Open')), ('RequireApplication', _l('Require application')), ('Closed', _l('Closed'))]
|
||||
registration_mode = SelectField(_l('Registration mode'), choices=types, default=1, coerce=str)
|
||||
application_question = TextAreaField(_l('Question to ask people applying for an account'))
|
||||
submit = SubmitField(_l('Save'))
|
||||
|
||||
|
||||
class FederationForm(FlaskForm):
|
||||
use_allowlist = BooleanField(_l('Allowlist instead of blocklist'))
|
||||
allowlist = TextAreaField(_l('Allow federation with these instances'))
|
||||
use_blocklist = BooleanField(_l('Blocklist instead of allowlist'))
|
||||
|
|
|
@ -6,8 +6,8 @@ from flask_babel import _
|
|||
from sqlalchemy import text, desc
|
||||
|
||||
from app import db
|
||||
from app.admin.forms import AdminForm
|
||||
from app.models import AllowedInstances, BannedInstances, ActivityPubLog
|
||||
from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm
|
||||
from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site
|
||||
from app.utils import render_template, permission_required, set_setting, get_setting
|
||||
from app.admin import bp
|
||||
|
||||
|
@ -16,7 +16,81 @@ from app.admin import bp
|
|||
@login_required
|
||||
@permission_required('change instance settings')
|
||||
def admin_home():
|
||||
form = AdminForm()
|
||||
return render_template('admin/home.html', title=_('Admin'))
|
||||
|
||||
|
||||
@bp.route('/site', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@permission_required('change instance settings')
|
||||
def admin_site():
|
||||
form = SiteProfileForm()
|
||||
site = Site.query.get(1)
|
||||
if site is None:
|
||||
site = Site()
|
||||
if form.validate_on_submit():
|
||||
site.name = form.name.data
|
||||
site.description = form.description.data
|
||||
site.sidebar = form.sidebar.data
|
||||
site.legal_information = form.legal_information.data
|
||||
site.updated = utcnow()
|
||||
if site.id is None:
|
||||
db.session.add(site)
|
||||
db.session.commit()
|
||||
flash('Settings saved.')
|
||||
elif request.method == 'GET':
|
||||
form.name.data = site.name
|
||||
form.description.data = site.description
|
||||
form.sidebar.data = site.sidebar
|
||||
form.legal_information.data = site.legal_information
|
||||
return render_template('admin/site.html', title=_('Site profile'), form=form)
|
||||
|
||||
|
||||
@bp.route('/misc', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@permission_required('change instance settings')
|
||||
def admin_misc():
|
||||
form = SiteMiscForm()
|
||||
site = Site.query.get(1)
|
||||
if site is None:
|
||||
site = Site()
|
||||
if form.validate_on_submit():
|
||||
site.enable_downvotes = form.enable_downvotes.data
|
||||
site.allow_local_image_posts = form.allow_local_image_posts.data
|
||||
site.remote_image_cache_days = form.remote_image_cache_days.data
|
||||
site.enable_nsfw = form.enable_nsfw.data
|
||||
site.enable_nsfl = form.enable_nsfl.data
|
||||
site.community_creation_admin_only = form.community_creation_admin_only.data
|
||||
site.reports_email_admins = form.reports_email_admins.data
|
||||
site.registration_mode = form.registration_mode.data
|
||||
site.application_question = form.application_question.data
|
||||
site.updated = utcnow()
|
||||
if site.id is None:
|
||||
db.session.add(site)
|
||||
db.session.commit()
|
||||
flash('Settings saved.')
|
||||
elif request.method == 'GET':
|
||||
form.enable_downvotes.data = site.enable_downvotes
|
||||
form.allow_local_image_posts.data = site.allow_local_image_posts
|
||||
form.remote_image_cache_days.data = site.remote_image_cache_days
|
||||
form.enable_nsfw.data = site.enable_nsfw
|
||||
form.enable_nsfl.data = site.enable_nsfl
|
||||
form.community_creation_admin_only.data = site.community_creation_admin_only
|
||||
form.reports_email_admins.data = site.reports_email_admins
|
||||
form.registration_mode.data = site.registration_mode
|
||||
form.application_question.data = site.application_question
|
||||
return render_template('admin/misc.html', title=_('Misc settings'), form=form)
|
||||
|
||||
|
||||
@bp.route('/federation', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@permission_required('change instance settings')
|
||||
def admin_federation():
|
||||
form = FederationForm()
|
||||
site = Site.query.get(1)
|
||||
if site is None:
|
||||
site = Site()
|
||||
# todo: finish form
|
||||
site.updated = utcnow()
|
||||
if form.validate_on_submit():
|
||||
if form.use_allowlist.data:
|
||||
set_setting('use_allowlist', True)
|
||||
|
@ -41,7 +115,7 @@ def admin_home():
|
|||
instances = AllowedInstances.query.all()
|
||||
form.allowlist.data = '\n'.join([instance.domain for instance in instances])
|
||||
|
||||
return render_template('admin/home.html', title=_('Admin settings'), form=form)
|
||||
return render_template('admin/federation.html', title=_('Federation settings'), form=form)
|
||||
|
||||
|
||||
@bp.route('/activities', methods=['GET'])
|
||||
|
@ -49,7 +123,7 @@ def admin_home():
|
|||
@permission_required('change instance settings')
|
||||
def admin_activities():
|
||||
db.session.query(ActivityPubLog).filter(
|
||||
ActivityPubLog.created_at < aware_utcnow() - timedelta(days=3)).delete()
|
||||
ActivityPubLog.created_at < utcnow() - timedelta(days=3)).delete()
|
||||
db.session.commit()
|
||||
|
||||
return render_template('admin/activities.html', title=_('ActivityPub Log'),
|
||||
|
|
|
@ -160,3 +160,8 @@ def verify_email(token):
|
|||
@bp.route('/validation_required')
|
||||
def validation_required():
|
||||
return render_template('auth/validation_required.html')
|
||||
|
||||
|
||||
@bp.route('/permission_denied')
|
||||
def permission_denied():
|
||||
return render_template('auth/permission_denied.html')
|
||||
|
|
|
@ -8,10 +8,11 @@ from app import db
|
|||
import click
|
||||
import os
|
||||
|
||||
from app.activitypub.signature import RsaKeys
|
||||
from app.auth.email import send_verification_email
|
||||
from app.auth.util import random_token
|
||||
from app.models import Settings, BannedInstances, Interest, Role, User, RolePermission, Domain, ActivityPubLog, \
|
||||
utcnow
|
||||
utcnow, Site, Instance
|
||||
from app.utils import file_get_contents, retrieve_block_list
|
||||
|
||||
|
||||
|
@ -53,6 +54,9 @@ def register(app):
|
|||
db.drop_all()
|
||||
db.configure_mappers()
|
||||
db.create_all()
|
||||
private_key, public_key = RsaKeys.generate_keypair()
|
||||
db.session.add(Site(name="PieFed", description='', public_key=public_key, private_key=private_key))
|
||||
db.session.add(Instance(domain=app.config['SERVER_NAME'], software='PieFed'))
|
||||
db.session.add(Settings(name='allow_nsfw', value=json.dumps(False)))
|
||||
db.session.add(Settings(name='allow_nsfl', value=json.dumps(False)))
|
||||
db.session.add(Settings(name='allow_dislike', value=json.dumps(True)))
|
||||
|
|
|
@ -37,7 +37,7 @@ def add_local():
|
|||
rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key,
|
||||
public_key=public_key,
|
||||
ap_profile_id='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data,
|
||||
subscriptions_count=1)
|
||||
subscriptions_count=1, instance_id=1)
|
||||
icon_file = request.files['icon_file']
|
||||
if icon_file and icon_file.filename != '':
|
||||
file = save_icon_file(icon_file)
|
||||
|
@ -89,7 +89,7 @@ def show_community(community: Community):
|
|||
|
||||
# If nothing has changed since their last visit, return HTTP 304
|
||||
current_etag = f"{community.id}_{hash(community.last_active)}"
|
||||
if current_user.is_anonymous and request_etag_matches(current_etag):
|
||||
if current_user.is_anonymous and request_etag_matches(current_etag):
|
||||
return return_304(current_etag)
|
||||
|
||||
page = request.args.get('page', 1, type=int)
|
||||
|
@ -314,7 +314,7 @@ def add_post(actor):
|
|||
form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
|
||||
|
||||
if form.validate_on_submit():
|
||||
post = Post(user_id=current_user.id, community_id=form.communities.data)
|
||||
post = Post(user_id=current_user.id, community_id=form.communities.data, instance_id=1)
|
||||
save_post(form, post)
|
||||
community.post_count += 1
|
||||
community.last_active = utcnow()
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import List
|
|||
|
||||
import requests
|
||||
from PIL import Image, ImageOps
|
||||
from flask import request, abort
|
||||
from flask import request, abort, g
|
||||
from flask_login import current_user
|
||||
from pillow_heif import register_heif_opener
|
||||
|
||||
|
@ -63,6 +63,7 @@ def search_for_community(address: str):
|
|||
ap_domain=server,
|
||||
public_key=community_json['publicKey']['publicKeyPem'],
|
||||
# language=community_json['language'][0]['identifier'] # todo: language
|
||||
# todo: set instance_id
|
||||
)
|
||||
if 'icon' in community_json:
|
||||
# todo: retrieve icon, save to disk, save more complete File record
|
||||
|
@ -237,6 +238,7 @@ def save_post(form, post: Post):
|
|||
post.score = 1
|
||||
db.session.add(postvote)
|
||||
db.session.add(post)
|
||||
g.site.last_active = utcnow()
|
||||
|
||||
|
||||
def remove_old_file(file_id):
|
||||
|
|
|
@ -1,22 +1,51 @@
|
|||
from sqlalchemy.sql.operators import or_
|
||||
|
||||
from app import db, cache
|
||||
from app.constants import SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER
|
||||
from app.constants import SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER, POST_TYPE_IMAGE, POST_TYPE_LINK, SUBSCRIPTION_OWNER
|
||||
from app.main import bp
|
||||
from flask import g, session, flash, request
|
||||
from flask import g, session, flash, request, current_app, url_for, redirect, make_response
|
||||
from flask_moment import moment
|
||||
from flask_login import current_user
|
||||
from flask_babel import _, get_locale
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, desc
|
||||
from sqlalchemy_searchable import search
|
||||
from app.utils import render_template, get_setting, gibberish
|
||||
from app.models import Community, CommunityMember
|
||||
from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains
|
||||
from app.models import Community, CommunityMember, Post, Site, User
|
||||
|
||||
|
||||
@bp.route('/', methods=['GET', 'POST'])
|
||||
@bp.route('/index', methods=['GET', 'POST'])
|
||||
def index():
|
||||
verification_warning()
|
||||
return render_template('index.html')
|
||||
|
||||
# If nothing has changed since their last visit, return HTTP 304
|
||||
current_etag = f"home_{hash(str(g.site.last_active))}"
|
||||
if current_user.is_anonymous and request_etag_matches(current_etag):
|
||||
return return_304(current_etag)
|
||||
|
||||
page = request.args.get('page', 1, type=int)
|
||||
|
||||
if current_user.is_anonymous:
|
||||
posts = Post.query.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False)
|
||||
else:
|
||||
posts = Post.query.join(CommunityMember, Post.community_id == CommunityMember.community_id).filter(CommunityMember.is_banned == False)
|
||||
posts = posts.join(User, CommunityMember.user_id == User.id).filter(User.id == current_user.id)
|
||||
domains_ids = blocked_domains(current_user.id)
|
||||
if domains_ids:
|
||||
posts = posts.filter(or_(Post.domain_id.not_in(domains_ids), Post.domain_id == None))
|
||||
|
||||
posts = posts.order_by(desc(Post.last_active)).paginate(page=page, per_page=100, error_out=False)
|
||||
|
||||
next_url = url_for('main.index', page=posts.next_num) if posts.has_next else None
|
||||
prev_url = url_for('main.index', page=posts.prev_num) if posts.has_prev and page != 1 else None
|
||||
|
||||
active_communities = Community.query.filter_by(banned=False).order_by(desc(Community.last_active)).limit(5).all()
|
||||
|
||||
return render_template('index.html', posts=posts, active_communities=active_communities,
|
||||
POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK,
|
||||
SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
|
||||
etag=f"home_{hash(str(g.site.last_active))}", next_url=next_url, prev_url=prev_url,
|
||||
rss_feed=f"https://{current_app.config['SERVER_NAME']}/feed", rss_feed_name=f"Posts on " + g.site.name)
|
||||
|
||||
|
||||
@bp.route('/communities', methods=['GET'])
|
||||
|
@ -30,7 +59,8 @@ def list_communities():
|
|||
communities = db.session.scalars(query).all()
|
||||
|
||||
return render_template('list_communities.html', communities=communities, search=search_param, title=_('Communities'),
|
||||
SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER)
|
||||
SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
|
||||
SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER)
|
||||
|
||||
|
||||
@bp.route('/communities/local', methods=['GET'])
|
||||
|
@ -49,6 +79,18 @@ def list_subscribed_communities():
|
|||
SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER)
|
||||
|
||||
|
||||
@bp.route('/login')
|
||||
def login():
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
|
||||
@bp.route('/robots.txt')
|
||||
def robots():
|
||||
resp = make_response(render_template('robots.txt'))
|
||||
resp.mimetype = 'text/plain'
|
||||
return resp
|
||||
|
||||
|
||||
@bp.route('/test')
|
||||
def test():
|
||||
...
|
||||
|
|
|
@ -383,6 +383,8 @@ class User(UserMixin, db.Model):
|
|||
files = File.query.join(Post).filter(Post.user_id == self.id).all()
|
||||
for file in files:
|
||||
file.delete_from_disk()
|
||||
db.session.query(Report).filter(Report.reporter_id == self.id).delete()
|
||||
db.session.query(Report).filter(Report.suspect_user_id == self.id).delete()
|
||||
db.session.query(ActivityLog).filter(ActivityLog.user_id == self.id).delete()
|
||||
db.session.query(PostVote).filter(PostVote.user_id == self.id).delete()
|
||||
db.session.query(PostReplyVote).filter(PostReplyVote.user_id == self.id).delete()
|
||||
|
@ -448,6 +450,7 @@ class Post(db.Model):
|
|||
ranking = db.Column(db.Integer, default=0) # used for 'hot' ranking
|
||||
language = db.Column(db.String(10))
|
||||
edited_at = db.Column(db.DateTime)
|
||||
reports = db.Column(db.Integer, default=0) # how many times this post has been reported. Set to -1 to ignore reports
|
||||
|
||||
ap_id = db.Column(db.String(255), index=True)
|
||||
ap_create_id = db.Column(db.String(100))
|
||||
|
@ -468,6 +471,7 @@ class Post(db.Model):
|
|||
return cls.query.filter_by(ap_id=ap_id).first()
|
||||
|
||||
def delete_dependencies(self):
|
||||
db.session.query(Report).filter(Report.suspect_post_id == self.id).delete()
|
||||
db.session.execute(text('DELETE FROM post_reply_vote WHERE post_reply_id IN (SELECT id FROM post_reply WHERE post_id = :post_id)'),
|
||||
{'post_id': self.id})
|
||||
db.session.execute(text('DELETE FROM post_reply WHERE post_id = :post_id'), {'post_id': self.id})
|
||||
|
@ -520,6 +524,7 @@ class PostReply(db.Model):
|
|||
ranking = db.Column(db.Integer, default=0, index=True) # used for 'hot' sorting
|
||||
language = db.Column(db.String(10))
|
||||
edited_at = db.Column(db.DateTime)
|
||||
reports = db.Column(db.Integer, default=0) # how many times this post has been reported. Set to -1 to ignore reports
|
||||
|
||||
ap_id = db.Column(db.String(255), index=True)
|
||||
ap_create_id = db.Column(db.String(100))
|
||||
|
@ -752,6 +757,33 @@ class Report(db.Model):
|
|||
created_at = db.Column(db.DateTime, default=utcnow)
|
||||
updated = db.Column(db.DateTime, default=utcnow)
|
||||
|
||||
|
||||
class Site(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(256))
|
||||
description = db.Column(db.String(256))
|
||||
icon_id = db.Column(db.Integer, db.ForeignKey('file.id'))
|
||||
sidebar = db.Column(db.Text, default='')
|
||||
legal_information = db.Column(db.Text, default='')
|
||||
public_key = db.Column(db.Text)
|
||||
private_key = db.Column(db.Text)
|
||||
enable_downvotes = db.Column(db.Boolean, default=True)
|
||||
allow_local_image_posts = db.Column(db.Boolean, default=True)
|
||||
remote_image_cache_days = db.Column(db.Integer, default=30)
|
||||
enable_nsfw = db.Column(db.Boolean, default=False)
|
||||
enable_nsfl = db.Column(db.Boolean, default=False)
|
||||
community_creation_admin_only = db.Column(db.Boolean, default=False)
|
||||
reports_email_admins = db.Column(db.Boolean, default=True)
|
||||
registration_mode = db.Column(db.String(20), default='Closed')
|
||||
application_question = db.Column(db.Text, default='')
|
||||
allow_or_block_list = db.Column(db.Integer, default=2) # 1 = allow list, 2 = block list
|
||||
allowlist = db.Column(db.Text, default='')
|
||||
blocklist = db.Column(db.Text, default='')
|
||||
created_at = db.Column(db.DateTime, default=utcnow)
|
||||
updated = db.Column(db.DateTime, default=utcnow)
|
||||
last_active = db.Column(db.DateTime, default=utcnow)
|
||||
|
||||
|
||||
@login.user_loader
|
||||
def load_user(id):
|
||||
return User.query.get(int(id))
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from datetime import datetime
|
||||
|
||||
from flask import redirect, url_for, flash, current_app, abort, request
|
||||
from flask import redirect, url_for, flash, current_app, abort, request, g
|
||||
from flask_login import login_user, logout_user, current_user, login_required
|
||||
from flask_babel import _
|
||||
from sqlalchemy import or_, desc
|
||||
|
@ -419,6 +419,7 @@ def post_delete(post_id: int):
|
|||
post.delete_dependencies()
|
||||
post.flush_cache()
|
||||
db.session.delete(post)
|
||||
g.site.last_active = community.last_active = utcnow()
|
||||
db.session.commit()
|
||||
flash(_('Post deleted.'))
|
||||
return redirect(url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name))
|
||||
|
@ -440,6 +441,7 @@ def post_report(post_id: int):
|
|||
url=f"https://{current_app.config['SERVER_NAME']}/post/{post.id}",
|
||||
author_id=current_user.id)
|
||||
db.session.add(notification)
|
||||
post.reports += 1
|
||||
# todo: Also notify admins for certain types of report
|
||||
db.session.commit()
|
||||
|
||||
|
|
7
app/templates/admin/_nav.html
Normal file
7
app/templates/admin/_nav.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<nav class="mb-4">
|
||||
<a href="{{ url_for('admin.admin_home') }}">{{ _('Home') }}</a> |
|
||||
<a href="{{ url_for('admin.admin_site') }}">{{ _('Site profile') }}</a> |
|
||||
<a href="{{ url_for('admin.admin_misc') }}">{{ _('Misc settings') }}</a> |
|
||||
<a href="{{ url_for('admin.admin_federation') }}">{{ _('Federation') }}</a> |
|
||||
<a href="{{ url_for('admin.admin_activities') }}">{{ _('Activities') }}</a>
|
||||
</nav>
|
|
@ -2,6 +2,12 @@
|
|||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table">
|
||||
|
|
18
app/templates/admin/federation.html
Normal file
18
app/templates/admin/federation.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -2,9 +2,17 @@
|
|||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ render_form(form) }}
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p>Welcome to the admin area. This page will eventually have some helpful guides and links for new admins but
|
||||
for now it does not.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
17
app/templates/admin/misc.html
Normal file
17
app/templates/admin/misc.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
25
app/templates/admin/site.html
Normal file
25
app/templates/admin/site.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.name) }}
|
||||
{{ render_field(form.description) }}
|
||||
{{ render_field(form.icon) }}
|
||||
{{ render_field(form.sidebar) }}
|
||||
{{ render_field(form.legal_information) }}
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
18
app/templates/auth/permission_denied.html
Normal file
18
app/templates/auth/permission_denied.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
<div class="card mt-5">
|
||||
<div class="card-body p-6">
|
||||
<div class="card-title text-center">{{ _('Sorry') }}</div>
|
||||
<br>
|
||||
<hr />
|
||||
<br>
|
||||
<p>{{ _('Your account does not have access to that area.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -58,7 +58,7 @@
|
|||
{% block navbar %}
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
|
||||
<div class="container-lg">
|
||||
<a class="navbar-brand" href="/" target="_blank"><img src="/static/images/logo2.png" alt="Logo" width="50" height="50" />PieFed</a>
|
||||
<a class="navbar-brand" href="/" target="_blank"><img src="/static/images/logo2.png" alt="Logo" width="50" height="50" />{{ g.site.name }}</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
|
|
|
@ -2,5 +2,68 @@
|
|||
{% from 'bootstrap5/form.html' import render_form %}
|
||||
|
||||
{% block app_content %}
|
||||
<p>List of home page content goes here.</p>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<h1 class="mt-2">{{ _('Recent posts') }}</h1>
|
||||
<div class="post_list">
|
||||
{% for post in posts %}
|
||||
{% include 'post/_post_teaser.html' %}
|
||||
{% else %}
|
||||
<p>{{ _('No posts yet.') }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<nav aria-label="Pagination" class="mt-4">
|
||||
{% if prev_url %}
|
||||
<a href="{{ prev_url }}" class="btn btn-primary">
|
||||
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if next_url %}
|
||||
<a href="{{ next_url }}" class="btn btn-primary">
|
||||
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="get">
|
||||
<input type="search" name="search" class="form-control mt-2" placeholder="{{ _('Search') }}" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ _('Active communities') }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for community in active_communities %}
|
||||
<li class="list-group-item">
|
||||
<a href="/c/{{ community.link() }}"><img src="{{ community.icon_image() }}" class="community_icon rounded-circle" loading="lazy" />{{ community.display_name() }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p class="mt-4"><a class="btn btn-primary" href="/communities">Explore communities</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ _('About %(site_name)s', site_name=g.site.name) }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>{{ g.site.description|safe }}</strong></p>
|
||||
<p>{{ g.site.sidebar|safe }}</p>
|
||||
<p class="mt-4">
|
||||
<a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> RSS feed</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
<td>{{ community.post_reply_count }}</td>
|
||||
<td>{{ moment(community.last_active).fromNow(refresh=True) }}</td>
|
||||
<td>{% if current_user.is_authenticated %}
|
||||
{% if community_membership(current_user, community) == SUBSCRIPTION_MEMBER %}
|
||||
{% if community_membership(current_user, community) in [SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER] %}
|
||||
<a class="btn btn-primary btn-sm" href="/community/{{ community.link() }}/unsubscribe">Unsubscribe</a>
|
||||
{% elif community_membership(current_user, community) == SUBSCRIPTION_PENDING %}
|
||||
<a class="btn btn-outline-secondary btn-sm" href="/community/{{ community.link() }}/unsubscribe">Pending</a>
|
||||
|
|
0
app/templates/robots.txt
Normal file
0
app/templates/robots.txt
Normal file
11
app/utils.py
11
app/utils.py
|
@ -1,5 +1,6 @@
|
|||
import random
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
import markdown2
|
||||
import math
|
||||
|
@ -17,7 +18,7 @@ from sqlalchemy import text
|
|||
from wtforms.fields import SelectField, SelectMultipleField
|
||||
from wtforms.widgets import Select, html_params, ListWidget, CheckboxInput
|
||||
from app import db, cache
|
||||
from app.models import Settings, Domain, Instance, BannedInstances, User, Community
|
||||
from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock
|
||||
|
||||
|
||||
# Flask's render_template function, with support for themes added
|
||||
|
@ -226,7 +227,7 @@ def user_access(permission: str, user_id: int) -> bool:
|
|||
return has_access is not None
|
||||
|
||||
|
||||
@cache.memoize(timeout=500)
|
||||
@cache.memoize(timeout=86400)
|
||||
def community_membership(user: User, community: Community) -> int:
|
||||
# @cache.memoize works with User.subscribed but cache.delete_memoized does not, making it bad to use on class methods.
|
||||
# however cache.memoize and cache.delete_memoized works fine with normal functions
|
||||
|
@ -235,6 +236,12 @@ def community_membership(user: User, community: Community) -> int:
|
|||
return user.subscribed(community.id)
|
||||
|
||||
|
||||
@cache.memoize(timeout=86400)
|
||||
def blocked_domains(user_id) -> List[int]:
|
||||
blocks = DomainBlock.query.filter_by(user_id=user_id)
|
||||
return [block.domain_id for block in blocks]
|
||||
|
||||
|
||||
def retrieve_block_list():
|
||||
try:
|
||||
response = requests.get('https://github.com/rimu/no-qanon/blob/master/domains.txt', timeout=1)
|
||||
|
|
66
migrations/versions/72f3326bdf54_site_settings.py
Normal file
66
migrations/versions/72f3326bdf54_site_settings.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
"""site settings
|
||||
|
||||
Revision ID: 72f3326bdf54
|
||||
Revises: 238faf5c9b8d
|
||||
Create Date: 2023-12-16 20:22:16.446742
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '72f3326bdf54'
|
||||
down_revision = '238faf5c9b8d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('site',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=256), nullable=True),
|
||||
sa.Column('description', sa.String(length=256), nullable=True),
|
||||
sa.Column('icon_id', sa.Integer(), nullable=True),
|
||||
sa.Column('sidebar', sa.Text(), nullable=True),
|
||||
sa.Column('legal_information', sa.Text(), nullable=True),
|
||||
sa.Column('public_key', sa.Text(), nullable=True),
|
||||
sa.Column('private_key', sa.Text(), nullable=True),
|
||||
sa.Column('enable_downvotes', sa.Boolean(), nullable=True),
|
||||
sa.Column('allow_local_image_posts', sa.Boolean(), nullable=True),
|
||||
sa.Column('remote_image_cache_days', sa.Integer(), nullable=True),
|
||||
sa.Column('enable_nsfw', sa.Boolean(), nullable=True),
|
||||
sa.Column('enable_nsfl', sa.Boolean(), nullable=True),
|
||||
sa.Column('community_creation_admin_only', sa.Boolean(), nullable=True),
|
||||
sa.Column('reports_email_admins', sa.Boolean(), nullable=True),
|
||||
sa.Column('registration_mode', sa.String(length=20), nullable=True),
|
||||
sa.Column('application_question', sa.Text(), nullable=True),
|
||||
sa.Column('allow_or_block_list', sa.Integer(), nullable=True),
|
||||
sa.Column('allowlist', sa.Text(), nullable=True),
|
||||
sa.Column('blocklist', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated', sa.DateTime(), nullable=True),
|
||||
sa.Column('last_active', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['icon_id'], ['file.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('reports', sa.Integer(), nullable=True))
|
||||
|
||||
with op.batch_alter_table('post_reply', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('reports', sa.Integer(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('post_reply', schema=None) as batch_op:
|
||||
batch_op.drop_column('reports')
|
||||
|
||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
||||
batch_op.drop_column('reports')
|
||||
|
||||
op.drop_table('site')
|
||||
# ### end Alembic commands ###
|
|
@ -6,6 +6,7 @@ from app import create_app, db, cli
|
|||
import os, click
|
||||
from flask import session, g, json
|
||||
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
|
||||
from app.models import Site
|
||||
from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access, community_membership
|
||||
|
||||
app = create_app()
|
||||
|
@ -40,6 +41,7 @@ with app.app_context():
|
|||
def before_request():
|
||||
session['nonce'] = gibberish()
|
||||
g.locale = str(get_locale())
|
||||
g.site = Site.query.get(1)
|
||||
|
||||
|
||||
@app.after_request
|
||||
|
|
Loading…
Reference in a new issue