Compare commits

..

No commits in common. "793c0b6612d9dfcdf6207e64a3c3c50eb6b226e2" and "e47103d0ee349cd2f3734c01bc075b49af4262a3" have entirely different histories.

16 changed files with 10 additions and 244 deletions

View file

@ -5,7 +5,6 @@
- Added dependency to [SUOU](https://github.com/sakuragasaki46/suou) library - Added dependency to [SUOU](https://github.com/sakuragasaki46/suou) library
- Added user blocks - Added user blocks
- Added user strikes: a strike logs the content of a removed message for future use - Added user strikes: a strike logs the content of a removed message for future use
- Posts may now be deleted by author. If it has comments, comments are not spared
- Implemented guild subscriptions - Implemented guild subscriptions
+ Blocking a user prevents them from seeing your comments, posts (standalone or in feed) and profile + Blocking a user prevents them from seeing your comments, posts (standalone or in feed) and profile
- Added ✨color themes✨ - Added ✨color themes✨

View file

@ -1,34 +0,0 @@
"""empty message
Revision ID: 6d418df3c72f
Revises: 90c7d0098efe
Create Date: 2025-07-07 13:37:51.667620
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '6d418df3c72f'
down_revision: Union[str, None] = '90c7d0098efe'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('comment_parent_post_id', 'freak_comment', type_='foreignkey')
op.create_foreign_key('comment_parent_post_id', 'freak_comment', 'freak_post', ['parent_post_id'], ['id'], ondelete='cascade')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('comment_parent_post_id', 'freak_comment', type_='foreignkey')
op.create_foreign_key('comment_parent_post_id', 'freak_comment', 'freak_post', ['parent_post_id'], ['id'])
# ### end Alembic commands ###

View file

@ -17,13 +17,12 @@ from sqlalchemy.exc import SQLAlchemyError
from suou import Snowflake, ssv_list from suou import Snowflake, ssv_list
from werkzeug.routing import BaseConverter from werkzeug.routing import BaseConverter
from sassutils.wsgi import SassMiddleware from sassutils.wsgi import SassMiddleware
from werkzeug.middleware.proxy_fix import ProxyFix
from suou.configparse import ConfigOptions, ConfigValue from suou.configparse import ConfigOptions, ConfigValue
from freak.colors import color_themes, theme_classes from freak.colors import color_themes, theme_classes
__version__ = '0.4.0-dev27' __version__ = '0.4.0-dev24'
APP_BASE_DIR = os.path.dirname(os.path.dirname(__file__)) APP_BASE_DIR = os.path.dirname(os.path.dirname(__file__))
@ -37,7 +36,6 @@ class AppConfig(ConfigOptions):
domain_name = ConfigValue() domain_name = ConfigValue()
private_assets = ConfigValue(cast=ssv_list) private_assets = ConfigValue(cast=ssv_list)
jquery_url = ConfigValue(default='https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js') jquery_url = ConfigValue(default='https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js')
app_is_behind_proxy = ConfigValue(cast=bool, default=False)
app_config = AppConfig() app_config = AppConfig()
@ -53,12 +51,6 @@ app.wsgi_app = SassMiddleware(app.wsgi_app, dict(
freak=('static/sass', 'static/css', '/static/css', True) freak=('static/sass', 'static/css', '/static/css', True)
)) ))
# proxy fix
if app_config.app_is_behind_proxy:
app.wsgi_app = ProxyFix(
app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
)
class SlugConverter(BaseConverter): class SlugConverter(BaseConverter):
regex = r'[a-z0-9]+(?:-[a-z0-9]+)*' regex = r'[a-z0-9]+(?:-[a-z0-9]+)*'

View file

@ -40,9 +40,3 @@ def append(text, l: list):
l.append(text) l.append(text)
return None return None
@app.template_filter()
def faint_paren(text: str):
if not '(' in text:
return text
t1, t2, t3 = text.partition('(')
return Markup('{0} <span class="faint">{1}</span>').format(t1, t2 + t3)

View file

@ -245,6 +245,7 @@ class User(Base):
target_id = target target_id = target
qq= ~select(UserBlock).where(UserBlock.c.actor_id == actor_id, UserBlock.c.target_id == target_id).exists() qq= ~select(UserBlock).where(UserBlock.c.actor_id == actor_id, UserBlock.c.target_id == target_id).exists()
print(qq)
return qq return qq
def recompute_karma(self): def recompute_karma(self):
@ -436,7 +437,7 @@ class Comment(Base):
id = snowflake_column() id = snowflake_column()
author_id = Column(BigInteger, ForeignKey('freak_user.id', name='comment_author_id'), nullable=True) author_id = Column(BigInteger, ForeignKey('freak_user.id', name='comment_author_id'), nullable=True)
parent_post_id = Column(BigInteger, ForeignKey('freak_post.id', name='comment_parent_post_id', ondelete='cascade'), nullable=False) parent_post_id = Column(BigInteger, ForeignKey('freak_post.id', name='comment_parent_post_id'), nullable=False)
parent_comment_id = Column(BigInteger, ForeignKey('freak_comment.id', name='comment_parent_comment_id'), nullable=True) parent_comment_id = Column(BigInteger, ForeignKey('freak_comment.id', name='comment_parent_comment_id'), nullable=True)
text_content = Column(String(16384), nullable=False) text_content = Column(String(16384), nullable=False)
created_at = Column(DateTime, server_default=func.current_timestamp(), index=True) created_at = Column(DateTime, server_default=func.current_timestamp(), index=True)

View file

@ -1,8 +1,6 @@
@import "constants.sass" @import "constants.sass"
body
margin: 0
.content-container .content-container
display: flex display: flex
@ -20,7 +18,6 @@ body
main main
min-height: 70vh min-height: 70vh
margin: 12px auto
// __ header styles __ // // __ header styles __ //
@ -31,6 +28,7 @@ header.header
overflow: hidden overflow: hidden
height: 3em height: 3em
padding: .75em 1.5em padding: .75em 1.5em
margin: -12px
line-height: 1 line-height: 1
h1 h1
margin: 0 margin: 0

View file

@ -10,58 +10,7 @@
grid-template-columns: 1fr 1fr grid-template-columns: 1fr 1fr
.nomobile .nomobile
display: none !important display: none
body
position: relative
footer.mobile-nav
position: sticky
bottom: 0
left: 0
width: 100%
overflow: hidden
margin: 0
padding: 0
background-color: var(--background)
box-shadow: 0 0 6px var(--border)
z-index: 150
> ul
display: flex
list-style: none
margin: 0
padding: 0
flex-direction: row
align-items: stretch
justify-content: stretch
> li
flex: 1
padding: .5em
margin: 0
text-align: center
a
text-decoration: none
.icon
font-size: 2rem
.content-nav
margin: 1em
width: unset
header.header h1
margin-top: 4px
margin-left: 6px
.content-header
text-align: center
.big-search-bar form
flex-direction: column
[type="submit"]
width: unset
margin: 12px auto
@media screen and (max-width: 960px) @media screen and (max-width: 960px)
.header-username .header-username
@ -84,4 +33,4 @@
@media screen and (min-width: 801px) @media screen and (min-width: 801px)
.mobileonly .mobileonly
display: none !important display: none

View file

@ -8,8 +8,5 @@
<li> <li>
<h2><a href="{{ url_for('admin.strikes') }}">Strikes</a></h2> <h2><a href="{{ url_for('admin.strikes') }}">Strikes</a></h2>
</li> </li>
<li>
<h2><a href="{{ url_for('admin.users') }}">Users</a></h2>
</li>
</ul> </ul>
{% endblock %} {% endblock %}

View file

@ -1,30 +0,0 @@
{% extends "admin/admin_base.html" %}
{% from "macros/feed.html" import stop_scrolling, no_more_scrolling with context %}
{% block content %}
<ul>
{% for u in user_list %}
<li>
<p><a href="{{ u.url() }}">{{ u.handle() }}</a> (#{{ u.id | to_b32l }})
{%- if u.is_administrator %}
<span>(Admin)</span>
{% endif -%}
{% if u == current_user %}
<span>(You)</span>
{% endif -%}
</p>
<ul class="inline">
<li>Age: {{ u.age() }} years old ({{ u.gdpr_birthday.strftime("%B %d, %Y") }})</li>
<li>Registered at: {{ u.joined_at.strftime("%B %d, %Y %H:%M %z") }}</li>
<li>Registered from IP address: {{ u.joined_ip }}</li>
<li>Status: {{ account_status_string(u) | faint_paren }}</li>
</ul>
</li>
{% endfor %}
{% if user_list.has_next %}
{{ stop_scrolling(user_list.page) }}
{% else %}
{{ no_more_scrolling(user_list.page) }}
{% endif %}
</ul>
{% endblock %}

View file

@ -46,11 +46,11 @@
{% if g.no_user %} {% if g.no_user %}
<!-- no user --> <!-- no user -->
{% elif current_user.is_authenticated %} {% elif current_user.is_authenticated %}
<li class="nomobile"> <li>
<a class="round border-accent" href="/create" title="Create a post"> <a class="round border-accent" href="/create" title="Create a post">
<i class="icon icon-add"></i> <i class="icon icon-add"></i>
<span class="a11y">create</span> <span class="a11y">create</span>
<span>New post</span> <span class="nomobile">New post</span>
</a> </a>
</li> </li>
<li><a href="{{ current_user.url() }}" title="{{ current_user.handle() }}'s profile">{{ icon('profile')}}<span class="a11y">profile</span></a> <li><a href="{{ current_user.url() }}" title="{{ current_user.handle() }}'s profile">{{ icon('profile')}}<span class="a11y">profile</span></a>
@ -99,17 +99,6 @@
<li><a href="https://github.com/sakuragasaki46/freak">GitHub</a></li> <li><a href="https://github.com/sakuragasaki46/freak">GitHub</a></li>
</ul> </ul>
</footer> </footer>
{% if current_user and current_user.is_authenticated %}
<footer class="mobile-nav mobileonly">
<ul>
<li><a href="/" title="Homepage">{{ icon('home') }}</a></li>
<li><a href="/search" title="Search">{{ icon('search') }}</a></li>
<li><a href="/create" title="Create">{{ icon('add') }}</a></li>
<li><a href="{{ current_user.url() }}" title="Messages">{{ icon('message') }}</a></li>
<li><a href="https://trollface.dk" title="Notifications">{{ icon('notification') }}</a></li>
</ul>
</footer>
{% endif %}
<script> <script>
function changeAccentColorTime() { function changeAccentColorTime() {
let hours = (new Date).getHours(); let hours = (new Date).getHours();

View file

@ -1,27 +0,0 @@
{% extends "base.html" %}
{% from "macros/title.html" import title_tag with context %}
{% from "macros/icon.html" import icon, callout with context %}
{% block title %}{{ title_tag('Confirm deletion: ' + p.title, False) }}{% endblock %}
{% block heading %}
<h2><span class="faint">Confirm deletion:</span> {{ p.title }}</h2>
{% endblock %}
{% block content %}
<div class="card">
<form action="/delete/post/{{ p.id | to_b32l }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div>
<p>You are about to delete <u>permanently</u> <a href="{{ p.url() }}">your post on {{ p.topic_or_user().handle() }}</a>.</p>
{% call callout('spoiler', 'error') %}This action <u><b>cannot be undone</b></u>.{% endcall %}
{% if (p.comments | count) %}
{% call callout('spoiler') %}Your post has <strong>{{ (p.comments | count) }} comments</strong>. Your post will be deleted <u>along with ALL the comments</u>.{% endcall %}
{% endif %}
</div>
<div>
<button type="submit" class="primary">{{ icon('delete') }} Delete</button>
</div>
</form>
</div>
{% endblock %}

View file

@ -17,9 +17,6 @@ blueprints.append(bp)
from .edit import bp from .edit import bp
blueprints.append(bp) blueprints.append(bp)
from .delete import bp
blueprints.append(bp)
from .about import bp from .about import bp
blueprints.append(bp) blueprints.append(bp)

View file

@ -4,7 +4,6 @@ import datetime
from typing import Mapping from typing import Mapping
from flask import Blueprint, abort, render_template, request, redirect, flash from flask import Blueprint, abort, render_template, request, redirect, flash
from flask_login import login_required, login_user, logout_user, current_user from flask_login import login_required, login_user, logout_user, current_user
from werkzeug.exceptions import Forbidden
from ..models import REPORT_REASONS, db, User from ..models import REPORT_REASONS, db, User
from ..utils import age_and_days from ..utils import age_and_days
from sqlalchemy import select, insert from sqlalchemy import select, insert
@ -54,13 +53,9 @@ def validate_register_form() -> dict:
try: try:
f['gdpr_birthday'] = datetime.date.fromisoformat(request.form['birthday']) f['gdpr_birthday'] = datetime.date.fromisoformat(request.form['birthday'])
if age_and_days(f['gdpr_birthday']) == (0, 0):
# block bot attempt to register
raise Forbidden
if age_and_days(f['gdpr_birthday']) < (14,): if age_and_days(f['gdpr_birthday']) < (14,):
f['banned_at'] = datetime.datetime.now() f['banned_at'] = datetime.datetime.now()
f['banned_reason'] = REPORT_REASONS['underage'] f['banned_reason'] = REPORT_REASONS['underage']
except ValueError: except ValueError:
raise ValueError('Invalid date format') raise ValueError('Invalid date format')
f['username'] = request.form['username'].lower() f['username'] = request.form['username'].lower()

View file

@ -30,21 +30,6 @@ TARGET_TYPES = {
Comment: REPORT_TARGET_COMMENT Comment: REPORT_TARGET_COMMENT
} }
def account_status_string(u: User):
if u.is_active:
return 'Active'
elif u.banned_at:
s = 'Suspended'
if u.banned_until:
s += f' until {u.banned_until:%b %d, %Y %H:%M}'
if u.banned_reason in REPORT_REASON_STRINGS:
s += f' ({REPORT_REASON_STRINGS[u.banned_reason]})'
return s
elif u.is_disabled_by_user:
return 'Paused'
else:
return 'Inactive'
def remove_content(target, reason_code: int): def remove_content(target, reason_code: int):
if isinstance(target, Post): if isinstance(target, Post):
target.removed_at = datetime.datetime.now() target.removed_at = datetime.datetime.now()
@ -161,11 +146,3 @@ def strikes():
strike_list = db.paginate(select(UserStrike).order_by(UserStrike.id.desc())) strike_list = db.paginate(select(UserStrike).order_by(UserStrike.id.desc()))
return render_template('admin/admin_strikes.html', return render_template('admin/admin_strikes.html',
strike_list=strike_list, report_reasons=REPORT_REASON_STRINGS) strike_list=strike_list, report_reasons=REPORT_REASON_STRINGS)
@bp.route('/admin/users/')
@admin_required
def users():
user_list = db.paginate(select(User).order_by(User.joined_at.desc()))
return render_template('admin/admin_users.html',
user_list=user_list, account_status_string=account_status_string)

View file

@ -1,31 +0,0 @@
from flask import Blueprint, abort, flash, redirect, render_template, request
from flask_login import current_user, login_required
from sqlalchemy import delete, select
from ..models import Post, db
bp = Blueprint('delete', __name__)
@bp.route('/delete/post/<b32l:id>', methods=['GET', 'POST'])
@login_required
def delete_post(id: int):
p = db.session.execute(select(Post).where(Post.id == id, Post.author == current_user)).scalar()
if p is None:
abort(404)
if p.author != current_user:
abort(403)
pt = p.topic_or_user()
if request.method == 'POST':
db.session.execute(delete(Post).where(Post.id == id, Post.author == current_user))
db.session.commit()
flash('Your post has been deleted')
return redirect(pt.url()), 303
return render_template('singledelete.html', p=p)

View file

@ -14,7 +14,7 @@ bp = Blueprint('edit', __name__)
@bp.route('/edit/post/<b32l:id>', methods=['GET', 'POST']) @bp.route('/edit/post/<b32l:id>', methods=['GET', 'POST'])
@login_required @login_required
def edit_post(id): def edit_post(id):
p: Post | None = db.session.execute(select(Post).where(Post.id == id, Post.author == current_user)).scalar() p: Post | None = db.session.execute(select(Post).where(Post.id == id)).scalar()
if p is None: if p is None:
abort(404) abort(404)