From b821f39bbf766456c80e0f5412cda750a7d4509b Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Wed, 16 Jul 2025 16:35:22 +0200 Subject: [PATCH] add impressum, update README, add abandoned guild claim and adding moderators --- CHANGELOG.md | 5 ++- README.md | 57 +++++++++++++++++++++++++++--- docker-compose.yml.example | 14 ++++++++ freak/__init__.py | 5 ++- freak/models.py | 7 ++-- freak/templates/about.html | 8 ++++- freak/templates/guildsettings.html | 38 +++++++++++++++++++- freak/utils.py | 7 +++- freak/website/moderation.py | 30 +++++++++++++++- 9 files changed, 158 insertions(+), 13 deletions(-) create mode 100644 docker-compose.yml.example diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7ac82..6ba5f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 20c781e..1308d66 100644 --- a/README.md +++ b/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 diff --git a/docker-compose.yml.example b/docker-compose.yml.example new file mode 100644 index 0000000..cc8f2fe --- /dev/null +++ b/docker-compose.yml.example @@ -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 + diff --git a/freak/__init__.py b/freak/__init__.py index 653388a..30df9de 100644 --- a/freak/__init__.py +++ b/freak/__init__.py @@ -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 diff --git a/freak/models.py b/freak/models.py index 9349519..f2f4657 100644 --- a/freak/models.py +++ b/freak/models.py @@ -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): diff --git a/freak/templates/about.html b/freak/templates/about.html index b7be8b7..a7d0840 100644 --- a/freak/templates/about.html +++ b/freak/templates/about.html @@ -24,7 +24,13 @@

License

-

Source code is available at: https://github.com/sakuragasaki46/freak

+

Source code is available at: https://github.com/yusurko/freak

+ + {% if impressum %} +

Legal Contacts

+
{{ impressum }}
+ {% endif %} + {% endblock %} diff --git a/freak/templates/guildsettings.html b/freak/templates/guildsettings.html index 99e9e2e..f7cbecb 100644 --- a/freak/templates/guildsettings.html +++ b/freak/templates/guildsettings.html @@ -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 %} -

Settings: {{ gu.handle() }}

+

Settings: {{ gu.handle() }}

{% endblock %} {% block content %} + +{% if gu.owner == None and current_user.is_administrator %} +
+ + +
+

{{ icon('spoiler') }} {{ gu.handle() }} is unmoderated

+ +
+ +
+
+
+{% endif %} +
@@ -55,5 +71,25 @@
+ +
+

Management

+ + {% if gu.owner == current_user or current_user.is_administrator %} +
+
+ +
+ {% endif %} +
+ +
+
{% endblock %} diff --git a/freak/utils.py b/freak/utils.py index 24dacd3..d6739c2 100644 --- a/freak/utils.py +++ b/freak/utils.py @@ -37,4 +37,9 @@ def timed_cache(ttl: int, maxsize: int = 128, typed: bool = False): return decorator def is_b32l(username: str) -> bool: - return re.fullmatch(r'[a-z2-7]+', username) \ No newline at end of file + 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('::')] \ No newline at end of file diff --git a/freak/website/moderation.py b/freak/website/moderation.py index 3563ebd..39e48c0 100644 --- a/freak/website/moderation.py +++ b/freak/website/moderation.py @@ -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)