home page, expanded admin area and domain blocking

This commit is contained in:
rimu 2023-12-17 00:12:49 +13:00
parent 7b91250e3a
commit c3839e6873
26 changed files with 476 additions and 49 deletions

View file

@ -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'

View file

@ -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

View file

@ -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,

View file

@ -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'))

View file

@ -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'),

View file

@ -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')

View file

@ -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)))

View file

@ -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()

View file

@ -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):

View file

@ -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():
...

View file

@ -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))

View file

@ -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()

View 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>

View file

@ -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">

View 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 %}

View file

@ -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 %}

View 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 %}

View 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 %}

View 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 %}

View file

@ -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>

View file

@ -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">&larr;</span> {{ _('Previous page') }}
</a>
{% endif %}
{% if next_url %}
<a href="{{ next_url }}" class="btn btn-primary">
{{ _('Next page') }} <span aria-hidden="true">&rarr;</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 %}

View file

@ -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
View file

View file

@ -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)

View 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 ###

View file

@ -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