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
|
||||
- Moderators (and admins) have now access to mod tools
|
||||
+ 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✨
|
||||
- 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
|
||||
|
||||
|
|
|
|||
57
README.md
57
README.md
|
|
@ -11,26 +11,73 @@
|
|||
* 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).
|
||||
* **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 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.
|
||||
* 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])
|
||||
* `DOMAIN_NAME` (see above)
|
||||
* `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)
|
||||
* ...
|
||||
* `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.
|
||||
[^2]: Not legal advice.
|
||||
|
||||
## 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
|
||||
|
||||
|
|
|
|||
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 .colors import color_themes, theme_classes
|
||||
from .utils import twocolon_list
|
||||
|
||||
__version__ = '0.4.0-dev28'
|
||||
|
||||
|
|
@ -38,6 +39,7 @@ class AppConfig(ConfigOptions):
|
|||
private_assets = ConfigValue(cast=ssv_list)
|
||||
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)
|
||||
impressum = ConfigValue(cast=twocolon_list, default=None)
|
||||
|
||||
app_config = AppConfig()
|
||||
|
||||
|
|
@ -97,7 +99,8 @@ def _inject_variables():
|
|||
'post_count': Post.count(),
|
||||
'user_count': User.active_count(),
|
||||
'colors': color_themes,
|
||||
'theme_classes': theme_classes
|
||||
'theme_classes': theme_classes,
|
||||
'impressum': '\n'.join(app_config.impressum).replace('_', ' ')
|
||||
}
|
||||
|
||||
@login_manager.user_loader
|
||||
|
|
|
|||
|
|
@ -360,18 +360,21 @@ class Guild(Base):
|
|||
if self.owner:
|
||||
yield ModeratorInfo(self.owner, True)
|
||||
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)
|
||||
|
||||
def update_member(self, u: User | Member, /, **values):
|
||||
if isinstance(u, User):
|
||||
m = db.session.execute(select(Member).where(Member.user_id == u.id, Member.guild_id == self.id)).scalar()
|
||||
if m is None:
|
||||
return db.session.execute(insert(Member).values(
|
||||
m = db.session.execute(insert(Member).values(
|
||||
guild_id = self.id,
|
||||
user_id = u.id,
|
||||
**values
|
||||
).returning(Member)).scalar()
|
||||
if m is None:
|
||||
raise RuntimeError
|
||||
return m
|
||||
else:
|
||||
m = u
|
||||
if len(values):
|
||||
|
|
|
|||
|
|
@ -24,7 +24,13 @@
|
|||
</ul>
|
||||
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,30 @@
|
|||
{% extends "base.html" %}
|
||||
{% from "macros/icon.html" import icon with context %}
|
||||
{% 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>
|
||||
<h1><span class="faint">Settings:</span> <a href="{{ gu.url() }}">{{ gu.handle() }}</a></h1>
|
||||
{% endblock %}
|
||||
|
||||
{% 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">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<section class="card">
|
||||
|
|
@ -55,5 +71,25 @@
|
|||
<button type="submit" class="primary">Save</button>
|
||||
</div>
|
||||
</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>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -38,3 +38,8 @@ def timed_cache(ttl: int, maxsize: int = 128, typed: bool = False):
|
|||
|
||||
def is_b32l(username: str) -> bool:
|
||||
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)
|
||||
|
||||
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
|
||||
display_name = request.form.get('display_name')
|
||||
description = request.form.get('description')
|
||||
exile_name = request.form.get('exile_name')
|
||||
exile_reverse = 'exile_reverse' 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:
|
||||
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')
|
||||
if restricted and restricted != gu.is_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:
|
||||
db.session.add(gu)
|
||||
db.session.commit()
|
||||
flash('Changes saved!')
|
||||
flash('Changes saved!')
|
||||
|
||||
return render_template('guildsettings.html', gu=gu)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue