view community, view posts in community

This commit is contained in:
rimu 2023-10-02 22:16:44 +13:00
parent c8f76c2d54
commit 2c0fc55e35
18 changed files with 365 additions and 28 deletions

View file

@ -5,7 +5,7 @@ starting any large pieces of work to ensure alignment with the roadmap, architec
The general style and philosphy behind the way things have been constructed is well described by The general style and philosphy behind the way things have been constructed is well described by
[The Grug Brained Developer](https://grugbrain.dev/). If that page resonates with you then you'll [The Grug Brained Developer](https://grugbrain.dev/). If that page resonates with you then you'll
probably enjoy your time here! Our code needs to be simple enough that new developers of all probably enjoy your time here! The codebase needs to be simple enough that new developers of all
skill levels can easily understand what's going on and onboard quickly without a lot of upfront skill levels can easily understand what's going on and onboard quickly without a lot of upfront
time investment. Sometimes this will mean writing slightly more verbose/boring code or avoiding the time investment. Sometimes this will mean writing slightly more verbose/boring code or avoiding the
use of advanced design patterns. use of advanced design patterns.
@ -33,6 +33,8 @@ VS Code coders are encouraged to try the free community edition of PyCharm but i
Use PEP 8 conventions for line length, naming, indentation. Use descriptive commit messages. Use PEP 8 conventions for line length, naming, indentation. Use descriptive commit messages.
Database model classes are singular. As in "Car", not "Cars".
### Directory structure ### Directory structure
Where possible, the structure should match the URL structure of the site. e.g. "domain.com/admin" Where possible, the structure should match the URL structure of the site. e.g. "domain.com/admin"
@ -50,8 +52,6 @@ helpful functions that pertain to modules in that directory only.
Changes to this file are turned into changes in the DB by using '[migrations](https://www.onlinetutorialspoint.com/flask/flask-how-to-upgrade-or-downgrade-database-migrations.html)'. Changes to this file are turned into changes in the DB by using '[migrations](https://www.onlinetutorialspoint.com/flask/flask-how-to-upgrade-or-downgrade-database-migrations.html)'.
- /community/* pertains to viewing, posting within and managing communities. - /community/* pertains to viewing, posting within and managing communities.
Python developers who are new to Flask will be able to quickly become productive with
# Code of conduct # Code of conduct
## Our Pledge ## Our Pledge

View file

@ -271,6 +271,7 @@ def shared_inbox():
if post is not None: if post is not None:
db.session.add(post) db.session.add(post)
community.post_count += 1
db.session.commit() db.session.commit()
else: else:
post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to) post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to)
@ -294,6 +295,7 @@ def shared_inbox():
if post_reply is not None: if post_reply is not None:
db.session.add(post_reply) db.session.add(post_reply)
community.post_reply_count += 1
db.session.commit() db.session.commit()
else: else:
activity_log.exception_message = 'Unacceptable type: ' + object_type activity_log.exception_message = 'Unacceptable type: ' + object_type
@ -398,6 +400,7 @@ def shared_inbox():
if join_request: if join_request:
member = CommunityMember(user_id=user.id, community_id=community.id) member = CommunityMember(user_id=user.id, community_id=community.id)
db.session.add(member) db.session.add(member)
community.subscriptions_count += 1
db.session.commit() db.session.commit()
activity_log.result = 'success' activity_log.result = 'success'
else: else:

View file

@ -68,3 +68,8 @@ class CreatePost(FlaskForm):
return False return False
return True return True
class NewReplyForm(FlaskForm):
body = TextAreaField(_l('Body'), render_kw={'placeholder': 'What are your thoughts?'})
submit = SubmitField(_l('Comment'))

View file

@ -2,13 +2,13 @@ from datetime import date, datetime, timedelta
import markdown2 import markdown2
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort
from flask_login import login_user, logout_user, current_user from flask_login import login_user, logout_user, current_user, login_required
from flask_babel import _ from flask_babel import _
from sqlalchemy import or_ from sqlalchemy import or_
from app import db from app import db
from app.activitypub.signature import RsaKeys, HttpSignature from app.activitypub.signature import RsaKeys, HttpSignature
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePost from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePost, NewReplyForm
from app.community.util import search_for_community, community_url_exists, actor_to_community from app.community.util import search_for_community, community_url_exists, actor_to_community
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post
@ -70,8 +70,8 @@ def add_remote():
def show_community(community: Community): def show_community(community: Community):
mods = community.moderators() mods = community.moderators()
is_moderator = any(mod.user_id == current_user.id for mod in mods) is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
is_owner = any(mod.user_id == current_user.id and mod.is_owner == True for mod in mods) is_owner = current_user.is_authenticated and any(mod.user_id == current_user.id and mod.is_owner == True for mod in mods)
if community.private_mods: if community.private_mods:
mod_list = [] mod_list = []
@ -80,10 +80,11 @@ def show_community(community: Community):
mod_list = User.query.filter(User.id.in_(mod_user_ids)).all() mod_list = User.query.filter(User.id.in_(mod_user_ids)).all()
return render_template('community/community.html', community=community, title=community.title, return render_template('community/community.html', community=community, title=community.title,
is_moderator=is_moderator, is_owner=is_owner, mods=mod_list) is_moderator=is_moderator, is_owner=is_owner, mods=mod_list, posts=community.posts)
@bp.route('/<actor>/subscribe', methods=['GET']) @bp.route('/<actor>/subscribe', methods=['GET'])
@login_required
def subscribe(actor): def subscribe(actor):
remote = False remote = False
actor = actor.strip() actor = actor.strip()
@ -137,6 +138,7 @@ def subscribe(actor):
@bp.route('/<actor>/unsubscribe', methods=['GET']) @bp.route('/<actor>/unsubscribe', methods=['GET'])
@login_required
def unsubscribe(actor): def unsubscribe(actor):
community = actor_to_community(actor) community = actor_to_community(actor)
@ -162,6 +164,7 @@ def unsubscribe(actor):
@bp.route('/<actor>/submit', methods=['GET', 'POST']) @bp.route('/<actor>/submit', methods=['GET', 'POST'])
@login_required
def add_post(actor): def add_post(actor):
community = actor_to_community(actor) community = actor_to_community(actor)
form = CreatePost() form = CreatePost()
@ -194,8 +197,11 @@ def add_post(actor):
else: else:
raise Exception('invalid post type') raise Exception('invalid post type')
db.session.add(post) db.session.add(post)
community.post_count += 1
db.session.commit() db.session.commit()
# todo: federate post creation out to followers
flash('Post has been added') flash('Post has been added')
return redirect(f"/c/{community.link()}") return redirect(f"/c/{community.link()}")
else: else:
@ -204,3 +210,13 @@ def add_post(actor):
return render_template('community/add_post.html', title=_('Add post to community'), form=form, community=community, return render_template('community/add_post.html', title=_('Add post to community'), form=form, community=community,
images_disabled=images_disabled) images_disabled=images_disabled)
@bp.route('/post/<int:post_id>')
def show_post(post_id: int):
post = Post.query.get_or_404(post_id)
mods = post.community.moderators()
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
form = NewReplyForm()
return render_template('community/post.html', title=post.title, post=post, is_moderator=is_moderator,
canonical=post.ap_id, form=form)

View file

@ -2,13 +2,13 @@ from datetime import datetime
from app import db from app import db
from app.main import bp from app.main import bp
from flask import g, jsonify, flash, request from flask import g, session, flash, request
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
from sqlalchemy_searchable import search from sqlalchemy_searchable import search
from app.utils import render_template, get_setting from app.utils import render_template, get_setting, gibberish
from app.models import Community, CommunityMember from app.models import Community, CommunityMember
@ -43,9 +43,3 @@ def list_local_communities():
def list_subscribed_communities(): def list_subscribed_communities():
communities = Community.query.join(CommunityMember).filter(CommunityMember.user_id == current_user.id).all() communities = Community.query.join(CommunityMember).filter(CommunityMember.user_id == current_user.id).all()
return render_template('list_communities.html', communities=communities) return render_template('list_communities.html', communities=communities)
@bp.before_app_request
def before_request():
g.locale = str(get_locale())

View file

@ -255,8 +255,8 @@ class Post(db.Model):
nsfl = db.Column(db.Boolean, default=False) nsfl = db.Column(db.Boolean, default=False)
sticky = db.Column(db.Boolean, default=False) sticky = db.Column(db.Boolean, default=False)
indexable = db.Column(db.Boolean, default=False) indexable = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow) created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow) # this is when the content arrived here
posted_at = db.Column(db.DateTime, index=True, default=datetime.utcnow) posted_at = db.Column(db.DateTime, index=True, default=datetime.utcnow) # this is when the original server created it
last_active = db.Column(db.DateTime, index=True, default=datetime.utcnow) last_active = db.Column(db.DateTime, index=True, default=datetime.utcnow)
ip = db.Column(db.String(50)) ip = db.Column(db.String(50))
up_votes = db.Column(db.Integer, default=0) up_votes = db.Column(db.Integer, default=0)
@ -435,6 +435,23 @@ class ActivityPubLog(db.Model):
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
class Filter(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(50))
filter_posts = db.Column(db.Boolean, default=True)
filter_replies = db.Column(db.Boolean, default=False)
hide_type = db.Column(db.Integer, default=0) # 0 = hide with warning, 1 = hide completely
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
class FilterKeyword(db.Model):
id = db.Column(db.Integer, primary_key=True)
keyword = db.Column(db.String(100))
filter_id = db.Column(db.Integer, db.ForeignKey('filter.id'))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
@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))

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="6.3531513mm" height="6.3394589mm" viewBox="0 0 6.3531516 6.3394588" version="1.1" id="svg8" inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" sodipodi:docname="external_link_web.svg">
<defs id="defs2"/>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="3.959798" inkscape:cx="33.546238" inkscape:cy="62.365132" inkscape:document-units="mm" inkscape:current-layer="layer1" inkscape:document-rotation="0" showgrid="false" inkscape:window-width="1872" inkscape:window-height="1039" inkscape:window-x="48" inkscape:window-y="0" inkscape:window-maximized="1" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0"/>
<metadata id="metadata5">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1">
<path style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.67451;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 2.4720007,1.0436168 -1.65871069,0.0019 c -0.2368211,-0.01848 -0.4529512,-0.0047 -0.4359367,0.373061 l -0.039823,4.025901 c -0.00912,0.444233 0.1987852,0.590785 0.5416462,0.552243 l 3.81418619,0.0014 c 0.425508,0.02448 0.594604,-0.164973 0.588101,-0.500924 l -0.0065,-1.611746" id="path24" sodipodi:nodetypes="cccccccc"/>
<path style="fill:none;stroke:#000000;stroke-width:0.67451;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 3.8509437,0.33937181 1.910781,-0.0021 c 0.171289,0.01557 0.273418,0.09464 0.252261,0.286884 l 0.0011,1.87940799" id="path851" sodipodi:nodetypes="cccc"/>
<path style="fill:none;stroke:#000000;stroke-width:0.67451;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 2.3884557,3.9829408 3.471423,-3.51892699 v 0" id="path853"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -2,3 +2,6 @@ $primary-colour: #0071CE;
$primary-colour-hover: #0A5CA0; $primary-colour-hover: #0A5CA0;
$green: #00b550; $green: #00b550;
$green-hover: #83f188; $green-hover: #83f188;
$light-grey: #ddd;
$grey: #bbb;
$dark-grey: #777;

View file

@ -277,4 +277,17 @@ fieldset legend {
height: auto; height: auto;
} }
.post_list .post_teaser {
border-bottom: solid 2px #ddd;
padding-top: 8px;
padding-bottom: 8px;
}
.post_list .post_teaser h3 {
font-size: 120%;
margin-top: 8px;
}
.post_list .post_teaser h3 a {
text-decoration: none;
}
/*# sourceMappingURL=structure.css.map */ /*# sourceMappingURL=structure.css.map */

View file

@ -64,3 +64,21 @@ nav, etc which are used site-wide */
height: auto; height: auto;
} }
} }
.post_list {
.post_teaser {
h3 {
font-size: 120%;
margin-top: 8px;
a {
text-decoration: none;
}
}
border-bottom: solid 2px $light-grey;
padding-top: 8px;
padding-bottom: 8px;
}
}

View file

@ -288,4 +288,11 @@ nav.navbar {
left: 26px; left: 26px;
} }
.external_link_icon {
width: 12px;
height: 12px;
margin-left: 4px;
margin-bottom: 3px;
}
/*# sourceMappingURL=styles.css.map */ /*# sourceMappingURL=styles.css.map */

View file

@ -83,4 +83,11 @@ nav.navbar {
top: 104px; top: 104px;
left: 26px; left: 26px;
} }
.external_link_icon {
width: 12px;
height: 12px;
margin-left: 4px;
margin-bottom: 3px;
}

View file

@ -26,6 +26,9 @@
<meta name="msapplication-TileColor" content="#da532c"> <meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="/static/browserconfig.xml"> <meta name="msapplication-config" content="/static/browserconfig.xml">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
{% if canonical %}
<link rel="canonical" href="{{ canonical }}" />
{% endif %}
{% endblock %} {% endblock %}
</head> </head>
<body class="d-flex flex-column" style="padding-top: 43px;"> <body class="d-flex flex-column" style="padding-top: 43px;">
@ -77,8 +80,8 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
{{ moment.include_moment() }} {{ str(moment.include_moment()).replace('<script>', '<script nonce="' + session['nonce'] + '">')|safe }}
{{ moment.lang(g.locale) }} {{ str(moment.lang(g.locale)).replace('<script>', '<script nonce="' + session['nonce'] + '">')|safe }}
{% endblock %} {% endblock %}
{{ bootstrap.load_js() }} {{ bootstrap.load_js() }}
<script type="text/javascript" src="{{ url_for('static', filename='js/htmx.min.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/htmx.min.js') }}"></script>

View file

@ -20,7 +20,43 @@
{% else %} {% else %}
<h1 class="mt-3">{{ community.title }}</h1> <h1 class="mt-3">{{ community.title }}</h1>
{% endif %} {% endif %}
<div class="post_list">
{% for post in posts %}
<div class="post_teaser">
<div class="row">
<div class="col">{{ post.author.user_name }} · {{ moment(post.posted_at).fromNow() }}</div>
</div>
<div class="row">
<div class="col{% if post.image_id %}-8{% endif %}">
<h3>
<a href="{{ url_for('community.show_post', post_id=post.id) }}">{{ post.title }}</a>
{% if post.type == post_type_link %}
<span class="domain_link">(<a href="/d/{{ post.domain_id }}">domain</a>)</span>
{% endif %}
</h3>
</div>
{% if post.image_id %}
<div class="col-4">
<img src="{{ post.image.source_url}}" alt="{{ post.image.alt_text }}"
width="100" />
</div>
{% endif %}
</div>
<div class="row">
<div class="col-4">
up {{ post.score }} down
</div>
<div class="col-6">
<a href="{{ url_for('community.show_post', post_id=post.id, _anchor='replies') }}">{{post.reply_count}}</a> additional tools
</div>
<div class="col-2">...</div>
</div>
</div>
{% else %}
<p>{{ _('No posts in this community yet.') }}</p>
{% endfor %}
</div>
</div> </div>
<div class="col-4"> <div class="col-4">
@ -28,7 +64,7 @@
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
{% if current_user.subscribed(community) %} {% if current_user.is_authenticated and current_user.subscribed(community) %}
<a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a> <a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
{% else %} {% else %}
<a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/subscribe">{{ _('Subscribe') }}</a> <a class="w-100 btn btn-primary" href="/community/{{ community.link() }}/subscribe">{{ _('Subscribe') }}</a>
@ -39,7 +75,7 @@
</div> </div>
</div> </div>
<form method="get"> <form method="get">
<input type="search" name="search" class="form-control mt-2" placeholder="Search this community" /> <input type="search" name="search" class="form-control mt-2" placeholder="{{ _('Search this community') }}" />
</form> </form>
</div> </div>
</div> </div>

View file

@ -0,0 +1,120 @@
{% extends "base.html" %}
{% from 'bootstrap/form.html' import render_form %}
{% block app_content %}
<div class="row">
<div class="col-8 position-relative">
<div class="row">
{% if post.image_id %}
<div class="col-8">
<h1 class="mt-3">{{ post.title }}</h1>
{% if post.url %}
<p><small><a href="{{ post.url }}" rel="nofollow ugc">{{ post.url|shorten_url }}</a>
{% if post.type == post_type_link %}
<span class="domain_link">(<a href="/d/{{ post.domain_id }}">domain</a>)</span>
{% endif %}
</small></p>
{% endif %}
<p><small>submitted {{ moment(post.posted_at).fromNow() }} by {{ post.author.user_name }}</small></p>
</div>
<div class="col-4">
{% if post.url %}
<a href="post.url" rel="nofollow ugc"><img src="{{ post.image.source_url }}" alt="{{ post.image.alt_text }}"
width="100" /></a>
{% else %}
<a href="post.image.source_url"><img src="{{ post.image.source_url }}" alt="{{ post.image.alt_text }}"
width="100" /></a>
{% endif %}
</div>
{% else %}
<h1 class="mt-3">{{ post.title }}</h1>
{% if post.url %}
<p><small><a href="{{ post.url }}" rel="nofollow ugc">{{ post.url|shorten_url }}
<img src="/static/images/external_link_black.svg" class="external_link_icon" alt="External link" /></a>
{% if post.type == post_type_link %}
<span class="domain_link">(<a href="/d/{{ post.domain_id }}">domain</a>)</span>
{% endif %}
</small></p>
{% endif %}
<p><small>submitted {{ moment(post.posted_at).fromNow() }} by {{ post.author.user_name }}</small></p>
{% endif %}
</div>
{% if post.body_html %}
<div class="row post_full">
<div class="col">
{{ post.body_html|safe }}
</div>
<hr />
</div>
{% endif %}
{% if post.comments_enabled %}
<div class="row post_reply_form">
<div class="col">
{{ render_form(form) }}
</div>
<hr class="mt-4" />
</div>
{% else %}
<p>{{ _('Comments are disabled for this post.') }}</p>
{% endif %}
<div class="row post_replies">
<div class="col">
</div>
</div>
</div>
<div class="col-4">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-6">
{% if current_user.is_authenticated and current_user.subscribed(post.community) %}
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/unsubscribe">{{ _('Unsubscribe') }}</a>
{% else %}
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/subscribe">{{ _('Subscribe') }}</a>
{% endif %}
</div>
<div class="col-6">
<a class="w-100 btn btn-primary" href="/community/{{ post.community.link() }}/submit">{{ _('Create a post') }}</a>
</div>
</div>
<form method="get">
<input type="search" name="search" class="form-control mt-2" placeholder="{{ _('Search this community') }}" />
</form>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h2>{{ _('About community') }}</h2>
</div>
<div class="card-body">
<p>{{ post.community.description|safe }}</p>
<p>{{ post.community.rules|safe }}</p>
{% if len(mods) > 0 and not post.community.private_mods %}
<h3>Moderators</h3>
<ol>
{% for mod in mods %}
<li><a href="/u/{{ mod.user_name }}">{{ mod.user_name }}</a></li>
{% endfor %}
</ol>
{% endif %}
</div>
</div>
{% if is_moderator %}
<div class="card mt-3">
<div class="card-header">
<h2>{{ _('Community Settings') }}</h2>
</div>
<div class="card-body">
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
<p><a href="#" class="btn btn-primary">{{ _('Settings') }}</a></p>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -143,3 +143,13 @@ def domain_from_url(url: str) -> Domain:
domain = Domain.query.filter_by(name=parsed_url.hostname.lower()).first() domain = Domain.query.filter_by(name=parsed_url.hostname.lower()).first()
return domain return domain
def shorten_string(input_str, max_length=50):
if len(input_str) <= max_length:
return input_str
else:
return input_str[:max_length - 3] + ''
def shorten_url(input: str, max_length=20):
return shorten_string(input.replace('https://', '').replace('http://', ''))

View file

@ -0,0 +1,47 @@
"""filter keywords
Revision ID: b1d3fc38b1f7
Revises: f8200275644a
Create Date: 2023-09-20 19:35:04.332862
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b1d3fc38b1f7'
down_revision = 'f8200275644a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('filter',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=50), nullable=True),
sa.Column('filter_posts', sa.Boolean(), nullable=True),
sa.Column('filter_replies', sa.Boolean(), nullable=True),
sa.Column('hide_type', sa.Integer(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('filter_keyword',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('keyword', sa.String(length=100), nullable=True),
sa.Column('filter_id', sa.Integer(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['filter_id'], ['filter.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('filter_keyword')
op.drop_table('filter')
# ### end Alembic commands ###

View file

@ -1,11 +1,12 @@
# This file is part of pyfedi, which is licensed under the GNU General Public License (GPL) version 3.0. # This file is part of pyfedi, which is licensed under the GNU General Public License (GPL) version 3.0.
# You should have received a copy of the GPL along with this program. If not, see <http://www.gnu.org/licenses/>. # You should have received a copy of the GPL along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask_babel import get_locale
from app import create_app, db, cli from app import create_app, db, cli
import os, click import os, click
from flask import session, g
from app.utils import getmtime from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
from app.utils import getmtime, gibberish, shorten_string, shorten_url
app = create_app() app = create_app()
cli.register(app) cli.register(app)
@ -15,7 +16,7 @@ cli.register(app)
def app_context_processor(): # NB there needs to be an identical function in cb.wsgi to make this work in production def app_context_processor(): # NB there needs to be an identical function in cb.wsgi to make this work in production
def getmtime(filename): def getmtime(filename):
return os.path.getmtime('app/static/' + filename) return os.path.getmtime('app/static/' + filename)
return dict(getmtime=getmtime) return dict(getmtime=getmtime, post_type_link=POST_TYPE_LINK, post_type_image=POST_TYPE_IMAGE, post_type_article=POST_TYPE_ARTICLE)
@app.shell_context_processor @app.shell_context_processor
@ -25,4 +26,22 @@ def make_shell_context():
with app.app_context(): with app.app_context():
app.jinja_env.globals['getmtime'] = getmtime app.jinja_env.globals['getmtime'] = getmtime
app.jinja_env.globals['len'] = len app.jinja_env.globals['len'] = len
app.jinja_env.globals['str'] = str
app.jinja_env.filters['shorten'] = shorten_string
app.jinja_env.filters['shorten_url'] = shorten_url
@app.before_request
def before_request():
session['nonce'] = gibberish()
g.locale = str(get_locale())
@app.after_request
def after_request(response):
response.headers['Content-Security-Policy'] = f"script-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net 'nonce-{session['nonce']}'"
response.headers['Strict-Transport-Security'] = 'max-age=63072000; includeSubDomains; preload'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
return response