improved 404 handling, added mod tools page (stub), CSS .warning, outlawed some usernames
This commit is contained in:
parent
793c0b6612
commit
b0c815ea0a
17 changed files with 236 additions and 34 deletions
|
|
@ -3,11 +3,12 @@
|
||||||
## 0.4.0
|
## 0.4.0
|
||||||
|
|
||||||
- Added dependency to [SUOU](https://github.com/sakuragasaki46/suou) library
|
- Added dependency to [SUOU](https://github.com/sakuragasaki46/suou) library
|
||||||
- Added user blocks
|
- Users can now block each other
|
||||||
|
+ Blocking a user prevents them from seeing your comments, posts (standalone or in feed) and profile
|
||||||
- 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
|
- Posts may now be deleted by author. If it has comments, comments are not spared
|
||||||
|
- Moderators (and admins) have now access to mod tools
|
||||||
- Implemented guild subscriptions
|
- Implemented guild subscriptions
|
||||||
+ Blocking a user prevents them from seeing your comments, posts (standalone or in feed) and profile
|
|
||||||
- Added ✨color themes✨
|
- Added ✨color themes✨
|
||||||
- Users can now set their display name, biography and color theme in `/settings`
|
- Users can now set their display name, biography and color theme in `/settings`
|
||||||
|
|
||||||
|
|
|
||||||
43
README.md
43
README.md
|
|
@ -1,3 +1,44 @@
|
||||||
# Freak
|
# Freak
|
||||||
|
|
||||||
(´ω\`)
|
> \~(´ω\`)\~
|
||||||
|
> (Josip Broz Tito, possibly)
|
||||||
|
|
||||||
|
**Freak** (as in extremely interested into something, NOT as in predator) is a in-development FOSS and sovereign alternative to Reddit (and an attempt to revive Ruqqus from scratch). The socio-moral reasons are beyond the scope of this README.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
* First make sure you have these requirements:
|
||||||
|
* Unix-like OS (Docker container, Linux or MacOS are all good).
|
||||||
|
* **Python** >=3.10. Recommended to use a virtualenv (unless in Docker lol).
|
||||||
|
* **PostgreSQL** at least 16.
|
||||||
|
* **Redis**/Valkey (as of 0.4.0 unused in codebase).
|
||||||
|
* A server machine with a public IP address and shell access (mandatory for production, optional for development/staging).
|
||||||
|
* A reverse proxy listening on ports 80 and 443. Reminder to set `APP_IS_BEHIND_PROXY=1` in `.env` !!!
|
||||||
|
* Electricity.
|
||||||
|
* Will to not give up.
|
||||||
|
* Clone this repository.
|
||||||
|
* Fill in `.env` with the necessary information.
|
||||||
|
* `DOMAIN_NAME` (you must own it. Don't have? `.xyz` are like $2 or $3 on Namecheap[^1])
|
||||||
|
* `APP_NAME`
|
||||||
|
* `DATABASE_URL` (hint: `postgresql://username:password@localhost/dbname`)
|
||||||
|
* `SECRET_KEY` (you can generate one with the command `cat /dev/random | tr -dc A-Za-z0-9_. | head -c 56`)
|
||||||
|
* `PRIVATE_ASSETS` (you must provide the icon stylesheets here. Useful for custom CSS / scripts as well)
|
||||||
|
* `APP_IS_BEHIND_PROXY` (mandatory if behind reverse proxy or NAT)
|
||||||
|
* ...
|
||||||
|
|
||||||
|
[^1]: Namecheap is an American company. Don't trust American companies.
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the [Apache License, Version 2.0](LICENSE), a non-copyleft free and open source license.
|
||||||
|
|
||||||
|
This is a hobby project, made available “AS IS”, with __no warranty__ express or implied.
|
||||||
|
|
||||||
|
I (sakuragasaki46) may NOT be held accountable for Your use of my code.
|
||||||
|
|
||||||
|
> It's pointless to file a lawsuit because you feel damaged, and it's only going to turn against you. What a waste of money you could have spent on a vacation or charity, or invested in stocks.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from sqlite3 import ProgrammingError
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import warnings
|
import warnings
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask, g, render_template,
|
Flask, g, redirect, render_template,
|
||||||
request, send_from_directory, url_for
|
request, send_from_directory, url_for
|
||||||
)
|
)
|
||||||
import os
|
import os
|
||||||
|
|
@ -21,7 +21,7 @@ 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 .colors import color_themes, theme_classes
|
||||||
|
|
||||||
__version__ = '0.4.0-dev27'
|
__version__ = '0.4.0-dev27'
|
||||||
|
|
||||||
|
|
@ -131,8 +131,18 @@ def error_400(body):
|
||||||
def error_403(body):
|
def error_403(body):
|
||||||
return render_template('403.html'), 403
|
return render_template('403.html'), 403
|
||||||
|
|
||||||
|
from .search import find_guild_or_user
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def error_404(body):
|
def error_404(body):
|
||||||
|
try:
|
||||||
|
if mo := re.match(r'/([a-z0-9_-]+)/?', request.path):
|
||||||
|
alternative = find_guild_or_user(mo.group(1))
|
||||||
|
if alternative is not None:
|
||||||
|
return redirect(alternative), 302
|
||||||
|
except Exception as e:
|
||||||
|
warnings.warn(f'Exception in find_guild_or_user: {e}')
|
||||||
|
pass
|
||||||
return render_template('404.html'), 404
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
@app.errorhandler(405)
|
@app.errorhandler(405)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ AJAX hooks for the website.
|
||||||
import re
|
import re
|
||||||
from flask import Blueprint, abort, flash, redirect, request
|
from flask import Blueprint, abort, flash, redirect, request
|
||||||
from sqlalchemy import delete, insert, select
|
from sqlalchemy import delete, insert, select
|
||||||
from .models import Guild, Member, UserBlock, db, User, Post, PostUpvote
|
from .models import Guild, Member, UserBlock, db, User, Post, PostUpvote, username_is_legal
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
current_user: User
|
current_user: User
|
||||||
|
|
@ -18,7 +18,7 @@ bp = Blueprint('ajax', __name__)
|
||||||
@bp.route('/username_availability/<username>')
|
@bp.route('/username_availability/<username>')
|
||||||
@bp.route('/ajax/username_availability/<username>')
|
@bp.route('/ajax/username_availability/<username>')
|
||||||
def username_availability(username: str):
|
def username_availability(username: str):
|
||||||
is_valid = re.fullmatch('[a-z0-9_-]+', username) is not None
|
is_valid = username_is_legal(username)
|
||||||
|
|
||||||
if is_valid:
|
if is_valid:
|
||||||
user = db.session.execute(select(User).where(User.username == username)).scalar()
|
user = db.session.execute(select(User).where(User.username == username)).scalar()
|
||||||
|
|
@ -30,7 +30,7 @@ def username_availability(username: str):
|
||||||
return {
|
return {
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
'is_valid': is_valid,
|
'is_valid': is_valid,
|
||||||
'is_available': is_available,
|
'is_available': is_available
|
||||||
}
|
}
|
||||||
|
|
||||||
@bp.route('/guild_name_availability/<username>')
|
@bp.route('/guild_name_availability/<username>')
|
||||||
|
|
@ -47,7 +47,7 @@ def guild_name_availability(name: str):
|
||||||
return {
|
return {
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
'is_valid': is_valid,
|
'is_valid': is_valid,
|
||||||
'is_available': is_available,
|
'is_available': is_available
|
||||||
}
|
}
|
||||||
|
|
||||||
@bp.route('/comments/<b32l:id>/upvote', methods=['POST'])
|
@bp.route('/comments/<b32l:id>/upvote', methods=['POST'])
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from collections import namedtuple
|
||||||
import datetime
|
import datetime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from operator import or_
|
from operator import or_
|
||||||
|
import re
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, text, \
|
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, text, \
|
||||||
CheckConstraint, Date, DateTime, Boolean, func, BigInteger, \
|
CheckConstraint, Date, DateTime, Boolean, func, BigInteger, \
|
||||||
|
|
@ -62,6 +63,26 @@ REPORT_UPDATE_COMPLETE = 1
|
||||||
REPORT_UPDATE_REJECTED = 2
|
REPORT_UPDATE_REJECTED = 2
|
||||||
REPORT_UPDATE_ON_HOLD = 3
|
REPORT_UPDATE_ON_HOLD = 3
|
||||||
|
|
||||||
|
USERNAME_RE = r'[a-z2-9_-][a-z0-9_-]+'
|
||||||
|
|
||||||
|
ILLEGAL_USERNAMES = (
|
||||||
|
## reserved for masspings and administrative claims
|
||||||
|
'me', 'everyone', 'here', 'admin', 'mod', 'modteam', 'moderator', 'sysop', 'room', 'all', 'any', 'nobody', 'deleted', 'suspended', 'owner', 'administrator', 'ai',
|
||||||
|
## law enforcement corps and slurs because yes
|
||||||
|
'pedo', 'rape', 'rapist', 'nigger', 'retard', 'ncmec', 'police', 'cops', '911', 'childsafety', 'report', 'dmca'
|
||||||
|
)
|
||||||
|
|
||||||
|
def username_is_legal(username: str) -> bool:
|
||||||
|
if len(username) < 2 or len(username) > 100:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if re.fullmatch(USERNAME_RE, username) is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if username in ILLEGAL_USERNAMES:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
## END constants and enums
|
## END constants and enums
|
||||||
|
|
||||||
Base = declarative_base(app_config.domain_name, app_config.secret_key,
|
Base = declarative_base(app_config.domain_name, app_config.secret_key,
|
||||||
|
|
@ -259,6 +280,21 @@ class User(Base):
|
||||||
def strike_count(self) -> int:
|
def strike_count(self) -> int:
|
||||||
return db.session.execute(select(func.count('*')).select_from(UserStrike).where(UserStrike.user_id == self.id)).scalar()
|
return db.session.execute(select(func.count('*')).select_from(UserStrike).where(UserStrike.user_id == self.id)).scalar()
|
||||||
|
|
||||||
|
def moderates(self, gu: Guild) -> bool:
|
||||||
|
## owner
|
||||||
|
if gu.owner_id == self.id:
|
||||||
|
return True
|
||||||
|
## admin or global mod
|
||||||
|
if self.is_administrator:
|
||||||
|
return True
|
||||||
|
memb = db.session.execute(select(Member).where(Member.user_id == self.id, Member.guild_id == gu.id)).scalar()
|
||||||
|
|
||||||
|
if memb is None:
|
||||||
|
return False
|
||||||
|
return memb.is_moderator
|
||||||
|
|
||||||
|
## TODO check banship?
|
||||||
|
|
||||||
# UserBlock table is at the top !!
|
# UserBlock table is at the top !!
|
||||||
|
|
||||||
## END User
|
## END User
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,11 @@
|
||||||
|
|
||||||
|
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
from flask import flash, g
|
||||||
from sqlalchemy import Column, Select, select, or_
|
from sqlalchemy import Column, Select, select, or_
|
||||||
|
|
||||||
|
from .models import Guild, User, db
|
||||||
|
|
||||||
|
|
||||||
class SearchQuery:
|
class SearchQuery:
|
||||||
keywords: Iterable[str]
|
keywords: Iterable[str]
|
||||||
|
|
@ -23,3 +26,25 @@ class SearchQuery:
|
||||||
sq = sq.where(or_(*or_cond) if len(or_cond) > 1 else or_cond[0])
|
sq = sq.where(or_(*or_cond) if len(or_cond) > 1 else or_cond[0])
|
||||||
return sq
|
return sq
|
||||||
|
|
||||||
|
|
||||||
|
def find_guild_or_user(name: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Used in 404 error handler.
|
||||||
|
|
||||||
|
Returns an URL to redirect or None for no redirect.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if hasattr(g, 'no_user'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
gu = db.session.execute(select(Guild).where(Guild.name == name)).scalar()
|
||||||
|
if gu is not None:
|
||||||
|
flash(f'There is nothing at /{name}. Luckily, a guild with name {gu.handle()} happens to exist. Next time, remember to add + before!')
|
||||||
|
return gu.url()
|
||||||
|
|
||||||
|
user = db.session.execute(select(User).where(User.username == name)).scalar()
|
||||||
|
if user is not None:
|
||||||
|
flash(f'There is nothing at /{name}. Luckily, a user named {user.handle()} happens to exist. Next time, remember to add @ before!')
|
||||||
|
return user.url()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
@ -26,7 +26,8 @@
|
||||||
--light-text-alt: #444
|
--light-text-alt: #444
|
||||||
--light-border: #999
|
--light-border: #999
|
||||||
--light-success: #73af00
|
--light-success: #73af00
|
||||||
--light-error: #e04433
|
--light-error: #e04830
|
||||||
|
--light-warning: #dea800
|
||||||
--light-canvas: #eaecee
|
--light-canvas: #eaecee
|
||||||
--light-background: #f9f9f9
|
--light-background: #f9f9f9
|
||||||
--light-bg-sharp: #fdfdff
|
--light-bg-sharp: #fdfdff
|
||||||
|
|
@ -35,7 +36,8 @@
|
||||||
--dark-text-alt: #c0cad3
|
--dark-text-alt: #c0cad3
|
||||||
--dark-border: #777
|
--dark-border: #777
|
||||||
--dark-success: #93cf00
|
--dark-success: #93cf00
|
||||||
--dark-error: #e04433
|
--dark-error: #e04830
|
||||||
|
--dark-warning: #dea800
|
||||||
--dark-canvas: #0a0a0e
|
--dark-canvas: #0a0a0e
|
||||||
--dark-background: #181a21
|
--dark-background: #181a21
|
||||||
--dark-bg-sharp: #080808
|
--dark-bg-sharp: #080808
|
||||||
|
|
@ -51,6 +53,7 @@
|
||||||
--border: var(--light-border)
|
--border: var(--light-border)
|
||||||
--success: var(--light-success)
|
--success: var(--light-success)
|
||||||
--error: var(--light-error)
|
--error: var(--light-error)
|
||||||
|
--warning: var(--light-warning)
|
||||||
--canvas: var(--light-canvas)
|
--canvas: var(--light-canvas)
|
||||||
--background: var(--light-background)
|
--background: var(--light-background)
|
||||||
--bg-sharp: var(--light-bg-sharp)
|
--bg-sharp: var(--light-bg-sharp)
|
||||||
|
|
@ -62,6 +65,7 @@
|
||||||
--border: var(--dark-border)
|
--border: var(--dark-border)
|
||||||
--success: var(--dark-success)
|
--success: var(--dark-success)
|
||||||
--error: var(--dark-error)
|
--error: var(--dark-error)
|
||||||
|
--warning: var(--dark-warning)
|
||||||
--canvas: var(--dark-canvas)
|
--canvas: var(--dark-canvas)
|
||||||
--background: var(--dark-background)
|
--background: var(--dark-background)
|
||||||
--bg-sharp: var(--dark-bg-sharp)
|
--bg-sharp: var(--dark-bg-sharp)
|
||||||
|
|
@ -72,6 +76,7 @@
|
||||||
--border: var(--light-border)
|
--border: var(--light-border)
|
||||||
--success: var(--light-success)
|
--success: var(--light-success)
|
||||||
--error: var(--light-error)
|
--error: var(--light-error)
|
||||||
|
--warning: var(--light-warning)
|
||||||
--canvas: var(--light-canvas)
|
--canvas: var(--light-canvas)
|
||||||
--background: var(--light-background)
|
--background: var(--light-background)
|
||||||
--bg-sharp: var(--light-bg-sharp)
|
--bg-sharp: var(--light-bg-sharp)
|
||||||
|
|
@ -82,6 +87,7 @@
|
||||||
--border: var(--dark-border)
|
--border: var(--dark-border)
|
||||||
--success: var(--dark-success)
|
--success: var(--dark-success)
|
||||||
--error: var(--dark-error)
|
--error: var(--dark-error)
|
||||||
|
--warning: var(--dark-warning)
|
||||||
--canvas: var(--dark-canvas)
|
--canvas: var(--dark-canvas)
|
||||||
--background: var(--dark-background)
|
--background: var(--dark-background)
|
||||||
--bg-sharp: var(--dark-bg-sharp)
|
--bg-sharp: var(--dark-bg-sharp)
|
||||||
|
|
@ -186,3 +192,17 @@ img
|
||||||
|
|
||||||
.faint
|
.faint
|
||||||
opacity: .75
|
opacity: .75
|
||||||
|
strong &
|
||||||
|
font-weight: 400
|
||||||
|
|
||||||
|
.callout
|
||||||
|
color: var(--text-alt)
|
||||||
|
|
||||||
|
.success
|
||||||
|
color: var(--success)
|
||||||
|
|
||||||
|
.error
|
||||||
|
color: var(--error)
|
||||||
|
|
||||||
|
.warning
|
||||||
|
color: var(--warning)
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,6 @@ blockquote
|
||||||
padding-right: 1em
|
padding-right: 1em
|
||||||
border-right: 4px solid var(--border)
|
border-right: 4px solid var(--border)
|
||||||
|
|
||||||
.callout
|
|
||||||
color: var(--text-alt)
|
|
||||||
|
|
||||||
.success
|
|
||||||
color: var(--success)
|
|
||||||
|
|
||||||
.error
|
|
||||||
color: var(--error)
|
|
||||||
|
|
||||||
.message-content
|
.message-content
|
||||||
p
|
p
|
||||||
margin: 4px 0
|
margin: 4px 0
|
||||||
|
|
|
||||||
|
|
@ -378,3 +378,12 @@ button.card
|
||||||
margin: 12px auto
|
margin: 12px auto
|
||||||
font-size: 36px
|
font-size: 36px
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
|
|
||||||
|
textarea.create_text
|
||||||
|
min-height: 8em
|
||||||
|
|
||||||
|
// specificity ew //
|
||||||
|
form.boundaryless &
|
||||||
|
min-height: 8em
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,6 @@
|
||||||
{% from "macros/title.html" import title_tag with context %}
|
{% from "macros/title.html" import title_tag with context %}
|
||||||
{% from "macros/nav.html" import nav_guild, nav_top_communities with context %}
|
{% from "macros/nav.html" import nav_guild, nav_top_communities with context %}
|
||||||
|
|
||||||
{# set feed_title = 'For you' if feed_type == 'foryou' and not feed_title %}
|
|
||||||
{% set feed_title = 'Explore' if feed_type == 'explore' and not feed_title #}
|
|
||||||
|
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{{ title_tag(feed_title) }}
|
{{ title_tag(feed_title) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -24,7 +20,7 @@
|
||||||
{{ nav_guild(guild) }}
|
{{ nav_guild(guild) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<aside class="card">
|
<aside class="card nomobile">
|
||||||
<h3>Don’t miss a post!</h3>
|
<h3>Don’t miss a post!</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong><a id="notificationEnabler" href="#">Enable notifications</a></strong> to continue staying with us 😉</li>
|
<li><strong><a id="notificationEnabler" href="#">Enable notifications</a></strong> to continue staying with us 😉</li>
|
||||||
|
|
@ -42,13 +38,11 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if l.has_next %}
|
{% if l.has_next %}
|
||||||
{{ stop_scrolling(l.page) }}
|
{{ stop_scrolling(l.page) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ no_more_scrolling(l.page) }}
|
{{ no_more_scrolling(l.page) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{# TODO: pagination #}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
|
|
||||||
29
freak/templates/guildsettings.html
Normal file
29
freak/templates/guildsettings.html
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% from "macros/title.html" import title_tag with context %}
|
||||||
|
{% from "macros/create.html" import checked_if with context %}
|
||||||
|
|
||||||
|
{% block title %}{{ title_tag('Settings for ' + gu.handle()) }}{% endblock %}
|
||||||
|
|
||||||
|
{% block heading %}
|
||||||
|
<h1><span class="faint">Settings:</span> {{ gu.handle() }}</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<section class="card">
|
||||||
|
<h2>Community Identity</h2>
|
||||||
|
<div>
|
||||||
|
<label for="GS__display_name">Display name:</label>
|
||||||
|
<input type="text" name="display_name" id="GS__display_name" value="{{ gu.display_name or '' }}" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="GS__description">Description:</label>
|
||||||
|
<textarea name="description" id="GS__description">{{ gu.description or '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -14,6 +14,9 @@
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
|
{% if current_user.moderates(gu) %}
|
||||||
|
<a href="{{ gu.url() }}/settings"><button class="card">{{ icon('settings') }} Mod Tools</button></a>
|
||||||
|
{% endif %}
|
||||||
{{ subscribe_button(gu, gu.has_subscriber(current_user)) }}
|
{{ subscribe_button(gu, gu.has_subscriber(current_user)) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<p>You are about to delete <u>permanently</u> <a href="{{ p.url() }}">your post on {{ p.topic_or_user().handle() }}</a>.</p>
|
<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 %}
|
{% call callout('spoiler', 'error') %}This action <u><b>cannot be undone</b></u>.{% endcall %}
|
||||||
{% if (p.comments | count) %}
|
{% 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 %}
|
{% call callout('spoiler', 'warning') %}Your post has <strong>{{ (p.comments | count) }} comments</strong>. Your post will be deleted <u>along with ALL the comments</u>.{% endcall %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,9 @@
|
||||||
<h2>Identification</h2>
|
<h2>Identification</h2>
|
||||||
<div><label for="US__display_name">Full name:</label>
|
<div><label for="US__display_name">Full name:</label>
|
||||||
<input type="text" name="display_name" id="US__display_name" value="{{ current_user.display_name or '' }}" />
|
<input type="text" name="display_name" id="US__display_name" value="{{ current_user.display_name or '' }}" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div><label for="US__biography">Bio:</label>
|
<div><label for="US__biography">Bio:</label>
|
||||||
<textarea name="biography" id="US__biography">{{ current_user.biography or '' }}</textarea>
|
<textarea name="biography" id="US__biography">{{ current_user.biography or '' }}</textarea>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" class="primary">Save</button>
|
<button type="submit" class="primary">Save</button>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ blueprints.append(bp)
|
||||||
from .delete import bp
|
from .delete import bp
|
||||||
blueprints.append(bp)
|
blueprints.append(bp)
|
||||||
|
|
||||||
|
from .moderation import bp
|
||||||
|
blueprints.append(bp)
|
||||||
|
|
||||||
from .about import bp
|
from .about import bp
|
||||||
blueprints.append(bp)
|
blueprints.append(bp)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
import os, sys
|
import os, sys
|
||||||
import re
|
import re
|
||||||
import datetime
|
import datetime
|
||||||
|
|
@ -10,6 +13,8 @@ from ..utils import age_and_days
|
||||||
from sqlalchemy import select, insert
|
from sqlalchemy import select, insert
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
|
current_user: User
|
||||||
|
|
||||||
bp = Blueprint('accounts', __name__)
|
bp = Blueprint('accounts', __name__)
|
||||||
|
|
||||||
@bp.route('/login', methods=['GET', 'POST'])
|
@bp.route('/login', methods=['GET', 'POST'])
|
||||||
|
|
@ -112,12 +117,13 @@ COLOR_SCHEMES = {'dark': 2, 'light': 1, 'system': 0, 'unset': 0}
|
||||||
@login_required
|
@login_required
|
||||||
def settings():
|
def settings():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
user: User = current_user
|
changes = False
|
||||||
|
user = current_user
|
||||||
color_scheme = COLOR_SCHEMES[request.form.get('color_scheme')] if 'color_scheme' in request.form else None
|
color_scheme = COLOR_SCHEMES[request.form.get('color_scheme')] if 'color_scheme' in request.form else None
|
||||||
color_theme = int(request.form.get('color_theme')) if 'color_theme' in request.form else None
|
color_theme = int(request.form.get('color_theme')) if 'color_theme' in request.form else None
|
||||||
biography = request.form.get('biography')
|
biography = request.form.get('biography')
|
||||||
display_name = request.form.get('display_name')
|
display_name = request.form.get('display_name')
|
||||||
changes = False
|
|
||||||
if display_name and display_name != user.display_name:
|
if display_name and display_name != user.display_name:
|
||||||
changes, user.display_name = True, display_name.strip()
|
changes, user.display_name = True, display_name.strip()
|
||||||
if biography and biography != user.biography:
|
if biography and biography != user.biography:
|
||||||
|
|
|
||||||
36
freak/website/moderation.py
Normal file
36
freak/website/moderation.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
|
||||||
|
from flask import Blueprint, abort, flash, render_template, request
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from ..models import db, User, Guild
|
||||||
|
|
||||||
|
current_user: User
|
||||||
|
|
||||||
|
bp = Blueprint('moderation', __name__)
|
||||||
|
|
||||||
|
@bp.route('/+<name>/settings', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def guild_settings(name: str):
|
||||||
|
gu = db.session.execute(select(Guild).where(Guild.name == name)).scalar()
|
||||||
|
|
||||||
|
if not current_user.moderates(gu):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
changes = False
|
||||||
|
display_name = request.form.get('display_name')
|
||||||
|
description = request.form.get('description')
|
||||||
|
|
||||||
|
if description and description != gu.description:
|
||||||
|
changes, gu.description = True, description.strip()
|
||||||
|
if display_name and display_name != gu.display_name:
|
||||||
|
changes, gu.display_name = True, display_name.strip()
|
||||||
|
|
||||||
|
if changes:
|
||||||
|
db.session.add(gu)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Changes saved!')
|
||||||
|
|
||||||
|
return render_template('guildsettings.html', gu=gu)
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue