mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36: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
|
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()
|
migrate = Migrate()
|
||||||
login = LoginManager()
|
login = LoginManager()
|
||||||
login.login_view = 'auth.login'
|
login.login_view = 'auth.login'
|
||||||
|
|
|
@ -2,7 +2,7 @@ from datetime import datetime
|
||||||
|
|
||||||
from app import db, constants, cache
|
from app import db, constants, cache
|
||||||
from app.activitypub import bp
|
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.activitypub.signature import HttpSignature
|
||||||
from app.community.routes import show_community
|
from app.community.routes import show_community
|
||||||
|
@ -290,7 +290,7 @@ def shared_inbox():
|
||||||
community = find_actor_or_create(community_ap_id)
|
community = find_actor_or_create(community_ap_id)
|
||||||
user = find_actor_or_create(user_ap_id)
|
user = find_actor_or_create(user_ap_id)
|
||||||
if user and community:
|
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']
|
object_type = request_json['object']['type']
|
||||||
new_content_types = ['Page', 'Article', 'Link', 'Note']
|
new_content_types = ['Page', 'Article', 'Link', 'Note']
|
||||||
|
@ -403,7 +403,7 @@ def shared_inbox():
|
||||||
community = find_actor_or_create(community_ap_id)
|
community = find_actor_or_create(community_ap_id)
|
||||||
user = find_actor_or_create(user_ap_id)
|
user = find_actor_or_create(user_ap_id)
|
||||||
if user and community:
|
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']
|
object_type = request_json['object']['object']['type']
|
||||||
new_content_types = ['Page', 'Article', 'Link', 'Note']
|
new_content_types = ['Page', 'Article', 'Link', 'Note']
|
||||||
if object_type in new_content_types: # create a new post
|
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 flask import current_app, request
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from app import db, cache
|
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 time
|
||||||
import base64
|
import base64
|
||||||
import requests
|
import requests
|
||||||
|
@ -410,32 +410,34 @@ def is_activitypub_request():
|
||||||
|
|
||||||
|
|
||||||
def lemmy_site_data():
|
def lemmy_site_data():
|
||||||
|
site = Site.query.get(1)
|
||||||
data = {
|
data = {
|
||||||
"site_view": {
|
"site_view": {
|
||||||
"site": {
|
"site": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "PieFed",
|
"name": site.name,
|
||||||
"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)",
|
"sidebar": site.sidebar,
|
||||||
"published": "2023-06-02T09:46:21.972257",
|
"published": site.created_at.isoformat(),
|
||||||
"updated": "2023-11-03T03:22:35.594456",
|
"updated": site.updated.isoformat(),
|
||||||
"icon": "https://lemmy.nz/pictrs/image/d308ef8d-4381-4a7a-b047-569ed5b8dd88.png",
|
"icon": "https://lemmy.nz/pictrs/image/d308ef8d-4381-4a7a-b047-569ed5b8dd88.png",
|
||||||
"banner": "https://lemmy.nz/pictrs/image/68beebd5-4e01-44b6-bd4e-008b0d443ac1.png",
|
"banner": "https://lemmy.nz/pictrs/image/68beebd5-4e01-44b6-bd4e-008b0d443ac1.png",
|
||||||
"description": "PieFed development",
|
"description": site.description,
|
||||||
"actor_id": "https://lemmy.nz/",
|
"actor_id": f"https://{current_app.config['SERVER_NAME']}/",
|
||||||
"last_refreshed_at": "2023-06-02T09:46:21.960383",
|
"last_refreshed_at": site.updated.isoformat(),
|
||||||
"inbox_url": "https://lemmy.nz/site_inbox",
|
"inbox_url": f"https://{current_app.config['SERVER_NAME']}/inbox",
|
||||||
"public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx6cROxTmUbuWDHM3DcIx\nAWVy4O+cYlnMU3s89gbzhgVioPHqoajDbxNzVavqLd093ZhGPG6pEoGAGEgI9zG/\nnxpCcRC8uoMcu6Yh8E707VWRXFiXDsONyldBKnFmQouQDFAEmPaEOkYX3l1Qe6Q+\np4XKQRcD5hZWMvJVYpGsEa1euOcKrZvQffA+HQ1xcbU2Kts92ZiGkuXcEzOT8YR2\nX82Y/JkpeGkFlW4AociJ1ohfsH9i4OV+C215SgpCPxnEa9oEpluOvql8d7lg0yPA\nIisxtLb6hQtx5hiueILv7WB7kq1dh57RZQmvt7fuBsEk9rK5Lqc/ee9hxseqZKH8\nxwIDAQAB\n-----END PUBLIC KEY-----\n",
|
"public_key": site.public_key,
|
||||||
"instance_id": 1
|
"instance_id": 1
|
||||||
},
|
},
|
||||||
"local_site": {
|
"local_site": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"site_id": 1,
|
"site_id": 1,
|
||||||
"site_setup": True,
|
"site_setup": True,
|
||||||
"enable_downvotes": True,
|
"enable_downvotes": site.enable_downvotes,
|
||||||
"enable_nsfw": True,
|
"enable_nsfw": site.enable_nsfw,
|
||||||
"community_creation_admin_only": True,
|
"enable_nsfl": site.enable_nsfl,
|
||||||
|
"community_creation_admin_only": site.community_creation_admin_only,
|
||||||
"require_email_verification": True,
|
"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,
|
"private_instance": False,
|
||||||
"default_theme": "browser",
|
"default_theme": "browser",
|
||||||
"default_post_listing_type": "All",
|
"default_post_listing_type": "All",
|
||||||
|
@ -445,10 +447,10 @@ def lemmy_site_data():
|
||||||
"federation_enabled": True,
|
"federation_enabled": True,
|
||||||
"captcha_enabled": True,
|
"captcha_enabled": True,
|
||||||
"captcha_difficulty": "medium",
|
"captcha_difficulty": "medium",
|
||||||
"published": "2023-06-02T09:46:22.153520",
|
"published": site.created_at.isoformat(),
|
||||||
"updated": "2023-11-03T03:22:35.600601",
|
"updated": site.updated.isoformat(),
|
||||||
"registration_mode": "RequireApplication",
|
"registration_mode": site.registration_mode,
|
||||||
"reports_email_admins": True
|
"reports_email_admins": site.reports_email_admins
|
||||||
},
|
},
|
||||||
"local_site_rate_limit": {
|
"local_site_rate_limit": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
@ -465,7 +467,7 @@ def lemmy_site_data():
|
||||||
"comment_per_second": 600,
|
"comment_per_second": 600,
|
||||||
"search": 999,
|
"search": 999,
|
||||||
"search_per_second": 600,
|
"search_per_second": 600,
|
||||||
"published": "2023-06-02T09:46:22.156933"
|
"published": site.created_at.isoformat(),
|
||||||
},
|
},
|
||||||
"counts": {
|
"counts": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|
|
@ -1,10 +1,37 @@
|
||||||
from flask_wtf import FlaskForm
|
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 wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length
|
||||||
from flask_babel import _, lazy_gettext as _l
|
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'))
|
use_allowlist = BooleanField(_l('Allowlist instead of blocklist'))
|
||||||
allowlist = TextAreaField(_l('Allow federation with these instances'))
|
allowlist = TextAreaField(_l('Allow federation with these instances'))
|
||||||
use_blocklist = BooleanField(_l('Blocklist instead of allowlist'))
|
use_blocklist = BooleanField(_l('Blocklist instead of allowlist'))
|
||||||
|
|
|
@ -6,8 +6,8 @@ from flask_babel import _
|
||||||
from sqlalchemy import text, desc
|
from sqlalchemy import text, desc
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.admin.forms import AdminForm
|
from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm
|
||||||
from app.models import AllowedInstances, BannedInstances, ActivityPubLog
|
from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site
|
||||||
from app.utils import render_template, permission_required, set_setting, get_setting
|
from app.utils import render_template, permission_required, set_setting, get_setting
|
||||||
from app.admin import bp
|
from app.admin import bp
|
||||||
|
|
||||||
|
@ -16,7 +16,81 @@ from app.admin import bp
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required('change instance settings')
|
@permission_required('change instance settings')
|
||||||
def admin_home():
|
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.validate_on_submit():
|
||||||
if form.use_allowlist.data:
|
if form.use_allowlist.data:
|
||||||
set_setting('use_allowlist', True)
|
set_setting('use_allowlist', True)
|
||||||
|
@ -41,7 +115,7 @@ def admin_home():
|
||||||
instances = AllowedInstances.query.all()
|
instances = AllowedInstances.query.all()
|
||||||
form.allowlist.data = '\n'.join([instance.domain for instance in instances])
|
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'])
|
@bp.route('/activities', methods=['GET'])
|
||||||
|
@ -49,7 +123,7 @@ def admin_home():
|
||||||
@permission_required('change instance settings')
|
@permission_required('change instance settings')
|
||||||
def admin_activities():
|
def admin_activities():
|
||||||
db.session.query(ActivityPubLog).filter(
|
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()
|
db.session.commit()
|
||||||
|
|
||||||
return render_template('admin/activities.html', title=_('ActivityPub Log'),
|
return render_template('admin/activities.html', title=_('ActivityPub Log'),
|
||||||
|
|
|
@ -160,3 +160,8 @@ def verify_email(token):
|
||||||
@bp.route('/validation_required')
|
@bp.route('/validation_required')
|
||||||
def validation_required():
|
def validation_required():
|
||||||
return render_template('auth/validation_required.html')
|
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 click
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from app.activitypub.signature import RsaKeys
|
||||||
from app.auth.email import send_verification_email
|
from app.auth.email import send_verification_email
|
||||||
from app.auth.util import random_token
|
from app.auth.util import random_token
|
||||||
from app.models import Settings, BannedInstances, Interest, Role, User, RolePermission, Domain, ActivityPubLog, \
|
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
|
from app.utils import file_get_contents, retrieve_block_list
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,6 +54,9 @@ def register(app):
|
||||||
db.drop_all()
|
db.drop_all()
|
||||||
db.configure_mappers()
|
db.configure_mappers()
|
||||||
db.create_all()
|
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_nsfw', value=json.dumps(False)))
|
||||||
db.session.add(Settings(name='allow_nsfl', 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)))
|
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,
|
rules=form.rules.data, nsfw=form.nsfw.data, private_key=private_key,
|
||||||
public_key=public_key,
|
public_key=public_key,
|
||||||
ap_profile_id='https://' + current_app.config['SERVER_NAME'] + '/c/' + form.url.data,
|
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']
|
icon_file = request.files['icon_file']
|
||||||
if icon_file and icon_file.filename != '':
|
if icon_file and icon_file.filename != '':
|
||||||
file = save_icon_file(icon_file)
|
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
|
# If nothing has changed since their last visit, return HTTP 304
|
||||||
current_etag = f"{community.id}_{hash(community.last_active)}"
|
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)
|
return return_304(current_etag)
|
||||||
|
|
||||||
page = request.args.get('page', 1, type=int)
|
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()]
|
form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
|
||||||
|
|
||||||
if form.validate_on_submit():
|
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)
|
save_post(form, post)
|
||||||
community.post_count += 1
|
community.post_count += 1
|
||||||
community.last_active = utcnow()
|
community.last_active = utcnow()
|
||||||
|
|
|
@ -3,7 +3,7 @@ from typing import List
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps
|
||||||
from flask import request, abort
|
from flask import request, abort, g
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from pillow_heif import register_heif_opener
|
from pillow_heif import register_heif_opener
|
||||||
|
|
||||||
|
@ -63,6 +63,7 @@ def search_for_community(address: str):
|
||||||
ap_domain=server,
|
ap_domain=server,
|
||||||
public_key=community_json['publicKey']['publicKeyPem'],
|
public_key=community_json['publicKey']['publicKeyPem'],
|
||||||
# language=community_json['language'][0]['identifier'] # todo: language
|
# language=community_json['language'][0]['identifier'] # todo: language
|
||||||
|
# todo: set instance_id
|
||||||
)
|
)
|
||||||
if 'icon' in community_json:
|
if 'icon' in community_json:
|
||||||
# todo: retrieve icon, save to disk, save more complete File record
|
# todo: retrieve icon, save to disk, save more complete File record
|
||||||
|
@ -237,6 +238,7 @@ def save_post(form, post: Post):
|
||||||
post.score = 1
|
post.score = 1
|
||||||
db.session.add(postvote)
|
db.session.add(postvote)
|
||||||
db.session.add(post)
|
db.session.add(post)
|
||||||
|
g.site.last_active = utcnow()
|
||||||
|
|
||||||
|
|
||||||
def remove_old_file(file_id):
|
def remove_old_file(file_id):
|
||||||
|
|
|
@ -1,22 +1,51 @@
|
||||||
|
from sqlalchemy.sql.operators import or_
|
||||||
|
|
||||||
from app import db, cache
|
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 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_moment import moment
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_babel import _, get_locale
|
from flask_babel import _, get_locale
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select, desc
|
||||||
from sqlalchemy_searchable import search
|
from sqlalchemy_searchable import search
|
||||||
from app.utils import render_template, get_setting, gibberish
|
from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains
|
||||||
from app.models import Community, CommunityMember
|
from app.models import Community, CommunityMember, Post, Site, User
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/', methods=['GET', 'POST'])
|
@bp.route('/', methods=['GET', 'POST'])
|
||||||
@bp.route('/index', methods=['GET', 'POST'])
|
@bp.route('/index', methods=['GET', 'POST'])
|
||||||
def index():
|
def index():
|
||||||
verification_warning()
|
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'])
|
@bp.route('/communities', methods=['GET'])
|
||||||
|
@ -30,7 +59,8 @@ def list_communities():
|
||||||
communities = db.session.scalars(query).all()
|
communities = db.session.scalars(query).all()
|
||||||
|
|
||||||
return render_template('list_communities.html', communities=communities, search=search_param, title=_('Communities'),
|
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'])
|
@bp.route('/communities/local', methods=['GET'])
|
||||||
|
@ -49,6 +79,18 @@ def list_subscribed_communities():
|
||||||
SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER)
|
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')
|
@bp.route('/test')
|
||||||
def test():
|
def test():
|
||||||
...
|
...
|
||||||
|
|
|
@ -383,6 +383,8 @@ class User(UserMixin, db.Model):
|
||||||
files = File.query.join(Post).filter(Post.user_id == self.id).all()
|
files = File.query.join(Post).filter(Post.user_id == self.id).all()
|
||||||
for file in files:
|
for file in files:
|
||||||
file.delete_from_disk()
|
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(ActivityLog).filter(ActivityLog.user_id == self.id).delete()
|
||||||
db.session.query(PostVote).filter(PostVote.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()
|
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
|
ranking = db.Column(db.Integer, default=0) # used for 'hot' ranking
|
||||||
language = db.Column(db.String(10))
|
language = db.Column(db.String(10))
|
||||||
edited_at = db.Column(db.DateTime)
|
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_id = db.Column(db.String(255), index=True)
|
||||||
ap_create_id = db.Column(db.String(100))
|
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()
|
return cls.query.filter_by(ap_id=ap_id).first()
|
||||||
|
|
||||||
def delete_dependencies(self):
|
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)'),
|
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})
|
{'post_id': self.id})
|
||||||
db.session.execute(text('DELETE 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
|
ranking = db.Column(db.Integer, default=0, index=True) # used for 'hot' sorting
|
||||||
language = db.Column(db.String(10))
|
language = db.Column(db.String(10))
|
||||||
edited_at = db.Column(db.DateTime)
|
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_id = db.Column(db.String(255), index=True)
|
||||||
ap_create_id = db.Column(db.String(100))
|
ap_create_id = db.Column(db.String(100))
|
||||||
|
@ -752,6 +757,33 @@ class Report(db.Model):
|
||||||
created_at = db.Column(db.DateTime, default=utcnow)
|
created_at = db.Column(db.DateTime, default=utcnow)
|
||||||
updated = 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
|
@login.user_loader
|
||||||
def load_user(id):
|
def load_user(id):
|
||||||
return User.query.get(int(id))
|
return User.query.get(int(id))
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from datetime import datetime
|
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_login import login_user, logout_user, current_user, login_required
|
||||||
from flask_babel import _
|
from flask_babel import _
|
||||||
from sqlalchemy import or_, desc
|
from sqlalchemy import or_, desc
|
||||||
|
@ -419,6 +419,7 @@ def post_delete(post_id: int):
|
||||||
post.delete_dependencies()
|
post.delete_dependencies()
|
||||||
post.flush_cache()
|
post.flush_cache()
|
||||||
db.session.delete(post)
|
db.session.delete(post)
|
||||||
|
g.site.last_active = community.last_active = utcnow()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Post deleted.'))
|
flash(_('Post deleted.'))
|
||||||
return redirect(url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name))
|
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}",
|
url=f"https://{current_app.config['SERVER_NAME']}/post/{post.id}",
|
||||||
author_id=current_user.id)
|
author_id=current_user.id)
|
||||||
db.session.add(notification)
|
db.session.add(notification)
|
||||||
|
post.reports += 1
|
||||||
# todo: Also notify admins for certain types of report
|
# todo: Also notify admins for certain types of report
|
||||||
db.session.commit()
|
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 %}
|
{% from 'bootstrap/form.html' import render_form %}
|
||||||
|
|
||||||
{% block app_content %}
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
{% include 'admin/_nav.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<table class="table">
|
<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 %}
|
{% from 'bootstrap/form.html' import render_form %}
|
||||||
|
|
||||||
{% block app_content %}
|
{% block app_content %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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 %}
|
{% block navbar %}
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
|
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
|
||||||
<div class="container-lg">
|
<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">
|
<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>
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
|
|
@ -2,5 +2,68 @@
|
||||||
{% from 'bootstrap5/form.html' import render_form %}
|
{% from 'bootstrap5/form.html' import render_form %}
|
||||||
|
|
||||||
{% block app_content %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
<td>{{ community.post_reply_count }}</td>
|
<td>{{ community.post_reply_count }}</td>
|
||||||
<td>{{ moment(community.last_active).fromNow(refresh=True) }}</td>
|
<td>{{ moment(community.last_active).fromNow(refresh=True) }}</td>
|
||||||
<td>{% if current_user.is_authenticated %}
|
<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>
|
<a class="btn btn-primary btn-sm" href="/community/{{ community.link() }}/unsubscribe">Unsubscribe</a>
|
||||||
{% elif community_membership(current_user, community) == SUBSCRIPTION_PENDING %}
|
{% elif community_membership(current_user, community) == SUBSCRIPTION_PENDING %}
|
||||||
<a class="btn btn-outline-secondary btn-sm" href="/community/{{ community.link() }}/unsubscribe">Pending</a>
|
<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
|
import random
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import markdown2
|
import markdown2
|
||||||
import math
|
import math
|
||||||
|
@ -17,7 +18,7 @@ from sqlalchemy import text
|
||||||
from wtforms.fields import SelectField, SelectMultipleField
|
from wtforms.fields import SelectField, SelectMultipleField
|
||||||
from wtforms.widgets import Select, html_params, ListWidget, CheckboxInput
|
from wtforms.widgets import Select, html_params, ListWidget, CheckboxInput
|
||||||
from app import db, cache
|
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
|
# 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
|
return has_access is not None
|
||||||
|
|
||||||
|
|
||||||
@cache.memoize(timeout=500)
|
@cache.memoize(timeout=86400)
|
||||||
def community_membership(user: User, community: Community) -> int:
|
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.
|
# @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
|
# 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)
|
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():
|
def retrieve_block_list():
|
||||||
try:
|
try:
|
||||||
response = requests.get('https://github.com/rimu/no-qanon/blob/master/domains.txt', timeout=1)
|
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
|
import os, click
|
||||||
from flask import session, g, json
|
from flask import session, g, json
|
||||||
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
|
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
|
from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access, community_membership
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
@ -40,6 +41,7 @@ with app.app_context():
|
||||||
def before_request():
|
def before_request():
|
||||||
session['nonce'] = gibberish()
|
session['nonce'] = gibberish()
|
||||||
g.locale = str(get_locale())
|
g.locale = str(get_locale())
|
||||||
|
g.site = Site.query.get(1)
|
||||||
|
|
||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
|
|
Loading…
Reference in a new issue