add impressum, update README, add abandoned guild claim and adding moderators
This commit is contained in:
parent
f97e613f7a
commit
b821f39bbf
9 changed files with 158 additions and 13 deletions
|
|
@ -9,9 +9,12 @@
|
||||||
- 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
|
- Moderators (and admins) have now access to mod tools
|
||||||
+ Allowed operations: change display name, description, restriction status, and exile (guild-local ban) members
|
+ Allowed operations: change display name, description, restriction status, and exile (guild-local ban) members
|
||||||
- Implemented guild subscriptions
|
+ Site administrators and guild owners can add moderators
|
||||||
|
- Administrators can claim ownership of abandoned guilds
|
||||||
|
- Implemented guild subscriptions (not as in $$$, yes as in the follow button)
|
||||||
- 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`
|
||||||
|
- You can now add an impressum in .env, e.g. `IMPRESSUM='Acme Ltd.::1 Short Island::Old York, Divided States::Responsible: ::Donald Duck'` Lines are separated by two colons. Version before 0.4.0 CAN'T BE RUN in German-speaking countries as of 2025.
|
||||||
|
|
||||||
## 0.3.3
|
## 0.3.3
|
||||||
|
|
||||||
|
|
|
||||||
57
README.md
57
README.md
|
|
@ -11,26 +11,73 @@
|
||||||
* Unix-like OS (Docker container, Linux or MacOS are all good).
|
* Unix-like OS (Docker container, Linux or MacOS are all good).
|
||||||
* **Python** >=3.10. Recommended to use a virtualenv (unless in Docker lol).
|
* **Python** >=3.10. Recommended to use a virtualenv (unless in Docker lol).
|
||||||
* **PostgreSQL** at least 16.
|
* **PostgreSQL** at least 16.
|
||||||
* **Redis**/Valkey (as of 0.4.0 unused in codebase).
|
* **Redis**/Valkey (as of 0.4.0 unused in codebase -_-).
|
||||||
|
* **Docker** and **Docker Compose**.
|
||||||
* A server machine with a public IP address and shell access (mandatory for production, optional for development/staging).
|
* 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` !!!
|
* First time? I recommend a VPS. The cheapest one starts at €5/month, half a Spotify subscription.
|
||||||
|
* You must have **shell access**. FTP only is not enough.
|
||||||
|
* A domain (mandatory for production).
|
||||||
|
* You must have bought it beforehand. Don't have? `.xyz` are like $2 or $3 on Namecheap[^1]
|
||||||
|
* For development, tweaking `/etc/hosts` or plain running on `localhost:5000` is usually enough.
|
||||||
|
* A reverse proxy (i.e. Caddy or nginx) listening on ports 80 and 443. Reminder to set `APP_IS_BEHIND_PROXY=1` in `.env` !!!
|
||||||
* Electricity.
|
* Electricity.
|
||||||
* Will to not give up.
|
* Will to not give up.
|
||||||
* Clone this repository.
|
* Clone this repository.
|
||||||
* Fill in `.env` with the necessary information.
|
* 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])
|
* `DOMAIN_NAME` (see above)
|
||||||
* `APP_NAME`
|
* `APP_NAME`
|
||||||
* `DATABASE_URL` (hint: `postgresql://username:password@localhost/dbname`)
|
* `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`)
|
* `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)
|
* `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)
|
* `APP_IS_BEHIND_PROXY` (mandatory if behind reverse proxy or NAT)
|
||||||
* ...
|
* `IMPRESSUM` (if you host or serve your site in Germany[^2]. Lines are separated by double colons `::`)
|
||||||
|
* Adjust `docker-compose.yml` to your liking.
|
||||||
|
* Run `docker compose build`.
|
||||||
|
* Create a systemd unit file looking like this:
|
||||||
|
```systemd
|
||||||
|
[Unit]
|
||||||
|
Description=Freak
|
||||||
|
## using Caddy? replace nginx.service with Caddy.service. Yes, twice
|
||||||
|
Wants=nginx.service docker.service
|
||||||
|
After=nginx.service docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
## REPLACE it with your path
|
||||||
|
WorkingDirectory=/path/to/repository/freak
|
||||||
|
ExecStart=/usr/bin/docker compose up
|
||||||
|
ExecReload=/usr/bin/docker compose run freak bash ./docker-run.sh r
|
||||||
|
ExecStop=/usr/bin/docker compose down
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
* Copy the file to `/usr/lib/systemd/system` (with root access)
|
||||||
|
* Run `sudo systemctl enable --now freak.service`
|
||||||
|
* Expect no red text or weird error gibberish. If there is, you did not follow the tutorial: read it from the start again.
|
||||||
|
* Congratulations! Your Freak instance is up and running
|
||||||
|
|
||||||
|
|
||||||
[^1]: Namecheap is an American company. Don't trust American companies.
|
[^1]: Namecheap is an American company. Don't trust American companies.
|
||||||
|
[^2]: Not legal advice.
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
...
|
### Why another Reddit clone?
|
||||||
|
|
||||||
|
I felt like it.
|
||||||
|
|
||||||
|
### Will Freak be federated?
|
||||||
|
|
||||||
|
It's on the roadmap. However, it probably won't be fully functional if not after at least twenty feature releases. Therefore, wait patiently.
|
||||||
|
|
||||||
|
Freak is currently implementing the [SIS](https://yusur.moe/protocols/sis.html).
|
||||||
|
|
||||||
|
### What is your legal contact / Impressum?
|
||||||
|
|
||||||
|
You have to configure it yourself by setting `IMPRESSUM` in `.env`.
|
||||||
|
|
||||||
|
I only write the code. I am not accountable for Your use (see [License](#license)).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
14
docker-compose.yml.example
Normal file
14
docker-compose.yml.example
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
services:
|
||||||
|
freak:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
image: freak
|
||||||
|
ports:
|
||||||
|
- 5000:5000
|
||||||
|
volumes:
|
||||||
|
- .:/opt/live-app:ro
|
||||||
|
extra_hosts:
|
||||||
|
- 'postgres.docker.internal:172.17.0.1'
|
||||||
|
restart: on-failure:3
|
||||||
|
|
||||||
|
|
@ -22,6 +22,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
from suou.configparse import ConfigOptions, ConfigValue
|
from suou.configparse import ConfigOptions, ConfigValue
|
||||||
|
|
||||||
from .colors import color_themes, theme_classes
|
from .colors import color_themes, theme_classes
|
||||||
|
from .utils import twocolon_list
|
||||||
|
|
||||||
__version__ = '0.4.0-dev28'
|
__version__ = '0.4.0-dev28'
|
||||||
|
|
||||||
|
|
@ -38,6 +39,7 @@ class AppConfig(ConfigOptions):
|
||||||
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_is_behind_proxy = ConfigValue(cast=bool, default=False)
|
||||||
|
impressum = ConfigValue(cast=twocolon_list, default=None)
|
||||||
|
|
||||||
app_config = AppConfig()
|
app_config = AppConfig()
|
||||||
|
|
||||||
|
|
@ -97,7 +99,8 @@ def _inject_variables():
|
||||||
'post_count': Post.count(),
|
'post_count': Post.count(),
|
||||||
'user_count': User.active_count(),
|
'user_count': User.active_count(),
|
||||||
'colors': color_themes,
|
'colors': color_themes,
|
||||||
'theme_classes': theme_classes
|
'theme_classes': theme_classes,
|
||||||
|
'impressum': '\n'.join(app_config.impressum).replace('_', ' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
|
|
|
||||||
|
|
@ -360,18 +360,21 @@ class Guild(Base):
|
||||||
if self.owner:
|
if self.owner:
|
||||||
yield ModeratorInfo(self.owner, True)
|
yield ModeratorInfo(self.owner, True)
|
||||||
for mem in db.session.execute(select(Member).where(Member.guild_id == self.id, Member.is_moderator == True)).scalars():
|
for mem in db.session.execute(select(Member).where(Member.guild_id == self.id, Member.is_moderator == True)).scalars():
|
||||||
if mem.user != self.owner and not mem.user.is_banned:
|
if mem.user != self.owner and not mem.is_banned:
|
||||||
yield ModeratorInfo(mem.user, False)
|
yield ModeratorInfo(mem.user, False)
|
||||||
|
|
||||||
def update_member(self, u: User | Member, /, **values):
|
def update_member(self, u: User | Member, /, **values):
|
||||||
if isinstance(u, User):
|
if isinstance(u, User):
|
||||||
m = db.session.execute(select(Member).where(Member.user_id == u.id, Member.guild_id == self.id)).scalar()
|
m = db.session.execute(select(Member).where(Member.user_id == u.id, Member.guild_id == self.id)).scalar()
|
||||||
if m is None:
|
if m is None:
|
||||||
return db.session.execute(insert(Member).values(
|
m = db.session.execute(insert(Member).values(
|
||||||
guild_id = self.id,
|
guild_id = self.id,
|
||||||
user_id = u.id,
|
user_id = u.id,
|
||||||
**values
|
**values
|
||||||
).returning(Member)).scalar()
|
).returning(Member)).scalar()
|
||||||
|
if m is None:
|
||||||
|
raise RuntimeError
|
||||||
|
return m
|
||||||
else:
|
else:
|
||||||
m = u
|
m = u
|
||||||
if len(values):
|
if len(values):
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,13 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>License</h2>
|
<h2>License</h2>
|
||||||
<p>Source code is available at: <a href="https://github.com/sakuragasaki46/freak">https://github.com/sakuragasaki46/freak</a></p>
|
<p>Source code is available at: <a href="https://github.com/yusurko/freak">https://github.com/yusurko/freak</a></p>
|
||||||
|
|
||||||
|
{% if impressum %}
|
||||||
|
<h2>Legal Contacts</h2>
|
||||||
|
<pre>{{ impressum }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,30 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% from "macros/icon.html" import icon with context %}
|
||||||
{% from "macros/title.html" import title_tag with context %}
|
{% from "macros/title.html" import title_tag with context %}
|
||||||
{% from "macros/create.html" import checked_if with context %}
|
{% from "macros/create.html" import checked_if with context %}
|
||||||
|
|
||||||
{% block title %}{{ title_tag('Settings for ' + gu.handle()) }}{% endblock %}
|
{% block title %}{{ title_tag('Settings for ' + gu.handle()) }}{% endblock %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
<h1><span class="faint">Settings:</span> {{ gu.handle() }}</h1>
|
<h1><span class="faint">Settings:</span> <a href="{{ gu.url() }}">{{ gu.handle() }}</a></h1>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
{% if gu.owner == None and current_user.is_administrator %}
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<input type="hidden" name="transfer_owner" value="{{ current_user.username }}" />
|
||||||
|
<section class="card">
|
||||||
|
<h2 class="error">{{ icon('spoiler') }} {{ gu.handle() }} is <u>unmoderated</u></h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="primary">Claim ownership</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<section class="card">
|
<section class="card">
|
||||||
|
|
@ -55,5 +71,25 @@
|
||||||
<button type="submit" class="primary">Save</button>
|
<button type="submit" class="primary">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Management</h2>
|
||||||
|
<!-- TODO: make moderation consensual -->
|
||||||
|
{% if gu.owner == current_user or current_user.is_administrator %}
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Add user as moderator:
|
||||||
|
<input type="text" name="moderator_name" placeholder="username" />
|
||||||
|
</label><br />
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="moderator_consent" value="1" autocomplete="off" />
|
||||||
|
<u>I understand that new moderators may damage my community and the above user is trusted</u>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -38,3 +38,8 @@ def timed_cache(ttl: int, maxsize: int = 128, typed: bool = False):
|
||||||
|
|
||||||
def is_b32l(username: str) -> bool:
|
def is_b32l(username: str) -> bool:
|
||||||
return re.fullmatch(r'[a-z2-7]+', username)
|
return re.fullmatch(r'[a-z2-7]+', username)
|
||||||
|
|
||||||
|
def twocolon_list(s: str | None) -> list[str]:
|
||||||
|
if not s:
|
||||||
|
return []
|
||||||
|
return [x.strip() for x in s.split('::')]
|
||||||
|
|
@ -19,12 +19,21 @@ def guild_settings(name: str):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
if current_user.is_administrator and request.form.get('transfer_owner') == current_user.username:
|
||||||
|
gu.owner_id = current_user.id
|
||||||
|
db.session.add(gu)
|
||||||
|
db.session.commit()
|
||||||
|
flash(f'Claimed ownership of {gu.handle()}')
|
||||||
|
return render_template('guildsettings.html', gu=gu)
|
||||||
|
|
||||||
changes = False
|
changes = False
|
||||||
display_name = request.form.get('display_name')
|
display_name = request.form.get('display_name')
|
||||||
description = request.form.get('description')
|
description = request.form.get('description')
|
||||||
exile_name = request.form.get('exile_name')
|
exile_name = request.form.get('exile_name')
|
||||||
exile_reverse = 'exile_reverse' in request.form
|
exile_reverse = 'exile_reverse' in request.form
|
||||||
restricted = 'restricted' in request.form
|
restricted = 'restricted' in request.form
|
||||||
|
moderator_name = request.form.get('moderator_name')
|
||||||
|
moderator_consent = 'moderator_consent' in request.form
|
||||||
|
|
||||||
if description and description != gu.description:
|
if description and description != gu.description:
|
||||||
changes, gu.description = True, description.strip()
|
changes, gu.description = True, description.strip()
|
||||||
|
|
@ -47,11 +56,30 @@ def guild_settings(name: str):
|
||||||
flash(f'User \'{exile_name}\' not found, can\'t exile')
|
flash(f'User \'{exile_name}\' not found, can\'t exile')
|
||||||
if restricted and restricted != gu.is_restricted:
|
if restricted and restricted != gu.is_restricted:
|
||||||
changes, gu.is_restricted = True, restricted
|
changes, gu.is_restricted = True, restricted
|
||||||
|
if moderator_consent and moderator_name:
|
||||||
|
mu = db.session.execute(select(User).where(User.username == moderator_name)).scalar()
|
||||||
|
if mu is None:
|
||||||
|
flash(f'User \'{moderator_name}\' not found')
|
||||||
|
elif mu.is_disabled:
|
||||||
|
flash('Suspended users can\'t be moderators')
|
||||||
|
elif mu.has_blocked(current_user):
|
||||||
|
flash(f'User \'{moderator_name}\' not found')
|
||||||
|
else:
|
||||||
|
mm = gu.update_member(mu)
|
||||||
|
if mm.is_moderator:
|
||||||
|
flash(f'{mu.handle()} is already a moderator')
|
||||||
|
elif mm.is_banned:
|
||||||
|
flash('Exiled users can\'t be moderators')
|
||||||
|
else:
|
||||||
|
mm.is_moderator = True
|
||||||
|
db.session.add(mm)
|
||||||
|
changes = True
|
||||||
|
|
||||||
|
|
||||||
if changes:
|
if changes:
|
||||||
db.session.add(gu)
|
db.session.add(gu)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Changes saved!')
|
flash('Changes saved!')
|
||||||
|
|
||||||
return render_template('guildsettings.html', gu=gu)
|
return render_template('guildsettings.html', gu=gu)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue