diff --git a/.env.example b/.env.dev.example similarity index 75% rename from .env.example rename to .env.dev.example index 2397a5b15..5e605d744 100644 --- a/.env.example +++ b/.env.dev.example @@ -5,6 +5,7 @@ SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr" DEBUG=true DOMAIN=your.domain.here +#EMAIL=your@email.here ## Leave unset to allow all hosts # ALLOWED_HOSTS="localhost,127.0.0.1,[::1]" @@ -26,14 +27,24 @@ POSTGRES_HOST=db MAX_STREAM_LENGTH=200 REDIS_ACTIVITY_HOST=redis_activity REDIS_ACTIVITY_PORT=6379 +#REDIS_ACTIVITY_PASSWORD=redispassword345 -# Celery config with redis broker +# Redis as celery broker +#REDIS_BROKER_PORT=6379 +#REDIS_BROKER_PASSWORD=redispassword123 CELERY_BROKER=redis://redis_broker:6379/0 CELERY_RESULT_BACKEND=redis://redis_broker:6379/0 +FLOWER_PORT=8888 +#FLOWER_USER=mouse +#FLOWER_PASSWORD=changeme + EMAIL_HOST="smtp.mailgun.org" EMAIL_PORT=587 EMAIL_HOST_USER=mail@your.domain.here EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_USE_TLS=true EMAIL_USE_SSL=false + +# Set this to true when initializing certbot for domain, false when not +CERTBOT_INIT=false diff --git a/.env.prod.example b/.env.prod.example new file mode 100644 index 000000000..0013bf9d2 --- /dev/null +++ b/.env.prod.example @@ -0,0 +1,50 @@ +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG=false + +DOMAIN=your.domain.here +EMAIL=your@email.here + +## Leave unset to allow all hosts +# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]" + +OL_URL=https://openlibrary.org + +## Database backend to use. +## Default is postgres, sqlite is for dev quickstart only (NOT production!!!) +BOOKWYRM_DATABASE_BACKEND=postgres + +MEDIA_ROOT=images/ + +POSTGRES_PASSWORD=securedbpassword123 +POSTGRES_USER=fedireads +POSTGRES_DB=fedireads +POSTGRES_HOST=db + +# Redis activity stream manager +MAX_STREAM_LENGTH=200 +REDIS_ACTIVITY_HOST=redis_activity +REDIS_ACTIVITY_PORT=6379 +REDIS_ACTIVITY_PASSWORD=redispassword345 + +# Redis as celery broker +REDIS_BROKER_PORT=6379 +REDIS_BROKER_PASSWORD=redispassword123 +CELERY_BROKER=redis://:${REDIS_BROKER_PASSWORD}@redis_broker:${REDIS_BROKER_PORT}/0 +CELERY_RESULT_BACKEND=redis://:${REDIS_BROKER_PASSWORD}@redis_broker:${REDIS_BROKER_PORT}/0 + +FLOWER_PORT=8888 +FLOWER_USER=mouse +FLOWER_PASSWORD=changeme + +EMAIL_HOST="smtp.mailgun.org" +EMAIL_PORT=587 +EMAIL_HOST_USER=mail@your.domain.here +EMAIL_HOST_PASSWORD=emailpassword123 +EMAIL_USE_TLS=true +EMAIL_USE_SSL=false + +# Set this to true when initializing certbot for domain, false when not +CERTBOT_INIT=false diff --git a/.gitignore b/.gitignore index 71fa61bfa..cf88e9878 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ #Node tools /node_modules/ + +#nginx +nginx/default.conf diff --git a/README.md b/README.md index b4c358006..91a9aaafa 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,8 @@ Social reading and reviewing, decentralized with ActivityPub - [What it is and isn't](#what-it-is-and-isnt) - [The role of federation](#the-role-of-federation) - [Features](#features) - - [Setting up the developer environment](#setting-up-the-developer-environment) - - [Installing in Production](#installing-in-production) - [Book data](#book-data) + - [Set up Bookwyrm](#set-up-bookwyrm) ## Joining BookWyrm BookWyrm is still a young piece of software, and isn't at the level of stability and feature-richness that you'd find in a production-ready application. But it does what it says on the box! If you'd like to join an instance, you can check out the [instances](https://github.com/mouse-reeve/bookwyrm/blob/main/instances.md) list. @@ -60,11 +59,12 @@ Since the project is still in its early stages, the features are growing every d ### The Tech Stack Web backend - - [Django](https://www.djangoproject.com/) web server - - [PostgreSQL](https://www.postgresql.org/) database - - [ActivityPub](http://activitypub.rocks/) federation - - [Celery](http://celeryproject.org/) task queuing - - [Redis](https://redis.io/) task backend +- [Django](https://www.djangoproject.com/) web server +- [PostgreSQL](https://www.postgresql.org/) database +- [ActivityPub](https://activitypub.rocks/) federation +- [Celery](https://docs.celeryproject.org/) task queuing +- [Redis](https://redis.io/) task backend +- [Redis (again)](https://redis.io/) activity stream manager Front end - Django templates @@ -72,11 +72,14 @@ Front end - Vanilla JavaScript, in moderation Deployment - - [Docker](https://www.docker.com/) and docker-compose - - [Gunicorn](https://gunicorn.org/) web runner - - [Flower](https://github.com/mher/flower) celery monitoring - - [Nginx](https://nginx.org/en/) HTTP server +- [Docker](https://www.docker.com/) and docker-compose +- [Gunicorn](https://gunicorn.org/) web runner +- [Flower](https://github.com/mher/flower) celery monitoring +- [Nginx](https://nginx.org/en/) HTTP server + + +## Book data +The application is set up to share book and author data between instances, and get book data from arbitrary outside sources. Right now, the only connector is to OpenLibrary, but other connectors could be written. ## Set up Bookwyrm - See the [installation instructions](https://github.com/mouse-reeve/bookwyrm/blob/main/INSTALLATION.md) on how to set up Bookwyrm in developer environment or production. diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 2483cc62b..2fe5d825c 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -219,6 +219,12 @@ def dict_from_mappings(data, mappings): def get_data(url, params=None): """ wrapper for request.get """ + # check if the url is blocked + if models.FederatedServer.is_blocked(url): + raise ConnectorException( + "Attempting to load data from blocked url: {:s}".format(url) + ) + try: resp = requests.get( url, diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index b159a89ef..7c41323c0 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -281,3 +281,9 @@ class ReportForm(CustomForm): class Meta: model = models.Report fields = ["user", "reporter", "statuses", "note"] + + +class ServerForm(CustomForm): + class Meta: + model = models.FederatedServer + exclude = ["remote_id"] diff --git a/bookwyrm/management/commands/initdb.py b/bookwyrm/management/commands/initdb.py index d6101c877..a86a1652e 100644 --- a/bookwyrm/management/commands/initdb.py +++ b/bookwyrm/management/commands/initdb.py @@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand, CommandError from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType -from bookwyrm.models import Connector, SiteSettings, User +from bookwyrm.models import Connector, FederatedServer, SiteSettings, User from bookwyrm.settings import DOMAIN @@ -107,6 +107,16 @@ def init_connectors(): ) +def init_federated_servers(): + """ big no to nazis """ + built_in_blocks = ["gab.ai", "gab.com"] + for server in built_in_blocks: + FederatedServer.objects.create( + server_name=server, + status="blocked", + ) + + def init_settings(): SiteSettings.objects.create() @@ -118,4 +128,5 @@ class Command(BaseCommand): init_groups() init_permissions() init_connectors() + init_federated_servers() init_settings() diff --git a/bookwyrm/migrations/0063_auto_20210407_1827.py b/bookwyrm/migrations/0063_auto_20210407_1827.py new file mode 100644 index 000000000..0bd0f2ae4 --- /dev/null +++ b/bookwyrm/migrations/0063_auto_20210407_1827.py @@ -0,0 +1,37 @@ +# Generated by Django 3.1.6 on 2021-04-07 18:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0062_auto_20210407_1545"), + ] + + operations = [ + migrations.AddField( + model_name="federatedserver", + name="notes", + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name="federatedserver", + name="application_type", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="federatedserver", + name="application_version", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="federatedserver", + name="status", + field=models.CharField( + choices=[("federated", "Federated"), ("blocked", "Blocked")], + default="federated", + max_length=255, + ), + ), + ] diff --git a/bookwyrm/migrations/0064_merge_20210410_1633.py b/bookwyrm/migrations/0064_merge_20210410_1633.py new file mode 100644 index 000000000..77ad541e9 --- /dev/null +++ b/bookwyrm/migrations/0064_merge_20210410_1633.py @@ -0,0 +1,13 @@ +# Generated by Django 3.1.8 on 2021-04-10 16:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0063_auto_20210408_1556"), + ("bookwyrm", "0063_auto_20210407_1827"), + ] + + operations = [] diff --git a/bookwyrm/migrations/0065_merge_20210411_1702.py b/bookwyrm/migrations/0065_merge_20210411_1702.py new file mode 100644 index 000000000..2bdc425dc --- /dev/null +++ b/bookwyrm/migrations/0065_merge_20210411_1702.py @@ -0,0 +1,13 @@ +# Generated by Django 3.1.8 on 2021-04-11 17:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0064_auto_20210408_2208"), + ("bookwyrm", "0064_merge_20210410_1633"), + ] + + operations = [] diff --git a/bookwyrm/migrations/0066_user_deactivation_reason.py b/bookwyrm/migrations/0066_user_deactivation_reason.py new file mode 100644 index 000000000..bb3173a7c --- /dev/null +++ b/bookwyrm/migrations/0066_user_deactivation_reason.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.8 on 2021-04-12 15:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0065_merge_20210411_1702"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="deactivation_reason", + field=models.CharField( + blank=True, + choices=[ + ("self_deletion", "Self Deletion"), + ("moderator_deletion", "Moderator Deletion"), + ("domain_block", "Domain Block"), + ], + max_length=255, + null=True, + ), + ), + ] diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index d0ab829d9..ce16460e6 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -153,7 +153,7 @@ class ActivitypubMixin: # unless it's a dm, all the followers should receive the activity if privacy != "direct": # we will send this out to a subset of all remote users - queryset = user_model.objects.filter( + queryset = user_model.viewer_aware_objects(user).filter( local=False, ) # filter users first by whether they're using the desired software diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index cb2fc851e..261c96868 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -31,6 +31,36 @@ class BookWyrmModel(models.Model): """ how to link to this object in the local app """ return self.get_remote_id().replace("https://%s" % DOMAIN, "") + def visible_to_user(self, viewer): + """ is a user authorized to view an object? """ + # make sure this is an object with privacy owned by a user + if not hasattr(self, "user") or not hasattr(self, "privacy"): + return None + + # viewer can't see it if the object's owner blocked them + if viewer in self.user.blocks.all(): + return False + + # you can see your own posts and any public or unlisted posts + if viewer == self.user or self.privacy in ["public", "unlisted"]: + return True + + # you can see the followers only posts of people you follow + if ( + self.privacy == "followers" + and self.user.followers.filter(id=viewer.id).first() + ): + return True + + # you can see dms you are tagged in + if hasattr(self, "mention_users"): + if ( + self.privacy == "direct" + and self.mention_users.filter(id=viewer.id).first() + ): + return True + return False + @receiver(models.signals.post_save) # pylint: disable=unused-argument diff --git a/bookwyrm/models/federated_server.py b/bookwyrm/models/federated_server.py index 8f7d903e4..aa2b2f6af 100644 --- a/bookwyrm/models/federated_server.py +++ b/bookwyrm/models/federated_server.py @@ -1,17 +1,51 @@ """ connections to external ActivityPub servers """ +from urllib.parse import urlparse from django.db import models from .base_model import BookWyrmModel +FederationStatus = models.TextChoices( + "Status", + [ + "federated", + "blocked", + ], +) + class FederatedServer(BookWyrmModel): """ store which servers we federate with """ server_name = models.CharField(max_length=255, unique=True) - # federated, blocked, whatever else - status = models.CharField(max_length=255, default="federated") + status = models.CharField( + max_length=255, default="federated", choices=FederationStatus.choices + ) # is it mastodon, bookwyrm, etc - application_type = models.CharField(max_length=255, null=True) - application_version = models.CharField(max_length=255, null=True) + application_type = models.CharField(max_length=255, null=True, blank=True) + application_version = models.CharField(max_length=255, null=True, blank=True) + notes = models.TextField(null=True, blank=True) + def block(self): + """ block a server """ + self.status = "blocked" + self.save() -# TODO: blocked servers + # deactivate all associated users + self.user_set.filter(is_active=True).update( + is_active=False, deactivation_reason="domain_block" + ) + + def unblock(self): + """ unblock a server """ + self.status = "federated" + self.save() + + self.user_set.filter(deactivation_reason="domain_block").update( + is_active=True, deactivation_reason=None + ) + + @classmethod + def is_blocked(cls, url): + """ look up if a domain is blocked """ + url = urlparse(url) + domain = url.netloc + return cls.objects.filter(server_name=domain, status="blocked").exists() diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index c519f76c9..15ceb19bd 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -24,6 +24,16 @@ from .federated_server import FederatedServer from . import fields, Review +DeactivationReason = models.TextChoices( + "DeactivationReason", + [ + "self_deletion", + "moderator_deletion", + "domain_block", + ], +) + + class User(OrderedCollectionPageMixin, AbstractUser): """ a user who wants to read books """ @@ -111,6 +121,9 @@ class User(OrderedCollectionPageMixin, AbstractUser): default=str(pytz.utc), max_length=255, ) + deactivation_reason = models.CharField( + max_length=255, choices=DeactivationReason.choices, null=True, blank=True + ) name_field = "username" property_fields = [("following_link", "following")] @@ -138,7 +151,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): def viewer_aware_objects(cls, viewer): """ the user queryset filtered for the context of the logged in user """ queryset = cls.objects.filter(is_active=True) - if viewer.is_authenticated: + if viewer and viewer.is_authenticated: queryset = queryset.exclude(blocks=viewer) return queryset diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 146d4fff4..7ea8c5950 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -98,6 +98,7 @@ WSGI_APPLICATION = "bookwyrm.wsgi.application" # redis/activity streams settings REDIS_ACTIVITY_HOST = env("REDIS_ACTIVITY_HOST", "localhost") REDIS_ACTIVITY_PORT = env("REDIS_ACTIVITY_PORT", 6379) +REDIS_ACTIVITY_PASSWORD = env("REDIS_ACTIVITY_PASSWORD", None) MAX_STREAM_LENGTH = int(env("MAX_STREAM_LENGTH", 200)) STREAMS = ["home", "local", "federated"] @@ -166,7 +167,7 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/2.0/howto/static-files/ +# https://docs.djangoproject.com/en/3.1/howto/static-files/ PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) STATIC_URL = "/static/" diff --git a/bookwyrm/templates/settings/admin_layout.html b/bookwyrm/templates/settings/admin_layout.html index 9340da9e1..4f71a2284 100644 --- a/bookwyrm/templates/settings/admin_layout.html +++ b/bookwyrm/templates/settings/admin_layout.html @@ -6,7 +6,14 @@ {% block content %}
-

{% block header %}{% endblock %}

+
+
+

{% block header %}{% endblock %}

+
+
+ {% block edit-button %}{% endblock %} +
+
diff --git a/bookwyrm/templates/settings/edit_server.html b/bookwyrm/templates/settings/edit_server.html new file mode 100644 index 000000000..6ae227898 --- /dev/null +++ b/bookwyrm/templates/settings/edit_server.html @@ -0,0 +1,58 @@ +{% extends 'settings/admin_layout.html' %} +{% load i18n %} +{% block title %}{% trans "Add server" %}{% endblock %} + +{% block header %} +{% trans "Add server" %} +{% trans "Back to server list" %} +{% endblock %} + +{% block panel %} + +
+ {% csrf_token %} +
+
+
+ + + {% for error in form.server_name.errors %} +

{{ error | escape }}

+ {% endfor %} +
+
+ +
+ +
+
+
+
+
+ + + {% for error in form.application_type.errors %} +

{{ error | escape }}

+ {% endfor %} +
+
+ + + {% for error in form.application_version.errors %} +

{{ error | escape }}

+ {% endfor %} +
+
+
+

+ + +

+ + +
+ +{% endblock %} diff --git a/bookwyrm/templates/settings/federated_server.html b/bookwyrm/templates/settings/federated_server.html index 13715bfb2..6996557d8 100644 --- a/bookwyrm/templates/settings/federated_server.html +++ b/bookwyrm/templates/settings/federated_server.html @@ -4,64 +4,112 @@ {% block header %} {{ server.server_name }} + +{% if server.status == "blocked" %}{% trans "Blocked" %} +{% endif %} + {% trans "Back to server list" %} {% endblock %} {% block panel %} +
+
+

{% trans "Details" %}

+
+
+
{% trans "Software:" %}
+
{{ server.application_type }}
+
+
+
{% trans "Version:" %}
+
{{ server.application_version }}
+
+
+
{% trans "Status:" %}
+
{{ server.status }}
+
+
+
+ +
+

{% trans "Activity" %}

+
+
+
{% trans "Users:" %}
+
+ {{ users.count }} + {% if server.user_set.count %}({% trans "View all" %}){% endif %} +
+
+
+
{% trans "Reports:" %}
+
+ {{ reports.count }} + {% if reports.count %}({% trans "View all" %}){% endif %} +
+
+
+
{% trans "Followed by us:" %}
+
+ {{ followed_by_us.count }} +
+
+
+
{% trans "Followed by them:" %}
+
+ {{ followed_by_them.count }} +
+
+
+
{% trans "Blocked by us:" %}
+
+ {{ blocked_by_us.count }} +
+
+
+
+
+
-

{% trans "Details" %}

-
-
-
{% trans "Software:" %}
-
{{ server.application_type }}
+
+
+

{% trans "Notes" %}

-
-
{% trans "Version:" %}
-
{{ server.application_version }}
+
+ {% trans "Edit" as button_text %} + {% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-notes" %}
-
-
{% trans "Status:" %}
-
Federated
-
-
+ + {% if server.notes %} +

{{ server.notes }}

+ {% endif %} +
-

{% trans "Activity" %}

-
-
-
{% trans "Users:" %}
-
- {{ users.count }} - {% if server.user_set.count %}({% trans "View all" %}){% endif %} -
-
-
-
{% trans "Reports:" %}
-
- {{ reports.count }} - {% if reports.count %}({% trans "View all" %}){% endif %} -
-
-
-
{% trans "Followed by us:" %}
-
- {{ followed_by_us.count }} -
-
-
-
{% trans "Followed by them:" %}
-
- {{ followed_by_them.count }} -
-
-
-
{% trans "Blocked by us:" %}
-
- {{ blocked_by_us.count }} -
-
-
+

{% trans "Actions" %}

+ {% if server.status != 'blocked' %} +
+ {% csrf_token %} + +

{% trans "All users from this instance will be deactivated." %}

+
+ {% else %} +
+ {% csrf_token %} + +

{% trans "All users from this instance will be re-activated." %}

+
+ {% endif %}
{% endblock %} diff --git a/bookwyrm/templates/settings/federation.html b/bookwyrm/templates/settings/federation.html index 696d7a205..99afb5418 100644 --- a/bookwyrm/templates/settings/federation.html +++ b/bookwyrm/templates/settings/federation.html @@ -4,8 +4,15 @@ {% block header %}{% trans "Federated Servers" %}{% endblock %} -{% block panel %} +{% block edit-button %} + + + {% trans "Add server" %} + + +{% endblock %} +{% block panel %} {% url 'settings-federation' as url %} diff --git a/bookwyrm/tests/models/test_base_model.py b/bookwyrm/tests/models/test_base_model.py index 25a2e7ee6..442f98ca1 100644 --- a/bookwyrm/tests/models/test_base_model.py +++ b/bookwyrm/tests/models/test_base_model.py @@ -1,4 +1,5 @@ """ testing models """ +from unittest.mock import patch from django.test import TestCase from bookwyrm import models @@ -9,6 +10,22 @@ from bookwyrm.settings import DOMAIN class BaseModel(TestCase): """ functionality shared across models """ + def setUp(self): + """ shared data """ + self.local_user = models.User.objects.create_user( + "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" + ) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + def test_remote_id(self): """ these should be generated """ instance = base_model.BookWyrmModel() @@ -18,11 +35,8 @@ class BaseModel(TestCase): def test_remote_id_with_user(self): """ format of remote id when there's a user object """ - user = models.User.objects.create_user( - "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" - ) instance = base_model.BookWyrmModel() - instance.user = user + instance.user = self.local_user instance.id = 1 expected = instance.get_remote_id() self.assertEqual(expected, "https://%s/user/mouse/bookwyrmmodel/1" % DOMAIN) @@ -42,3 +56,66 @@ class BaseModel(TestCase): instance.remote_id = None base_model.set_remote_id(None, instance, False) self.assertIsNone(instance.remote_id) + + @patch("bookwyrm.activitystreams.ActivityStream.add_status") + def test_object_visible_to_user(self, _): + """ does a user have permission to view an object """ + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="public" + ) + self.assertTrue(obj.visible_to_user(self.local_user)) + + obj = models.Shelf.objects.create( + name="test", user=self.remote_user, privacy="unlisted" + ) + self.assertTrue(obj.visible_to_user(self.local_user)) + + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="followers" + ) + self.assertFalse(obj.visible_to_user(self.local_user)) + + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="direct" + ) + self.assertFalse(obj.visible_to_user(self.local_user)) + + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="direct" + ) + obj.mention_users.add(self.local_user) + self.assertTrue(obj.visible_to_user(self.local_user)) + + @patch("bookwyrm.activitystreams.ActivityStream.add_status") + def test_object_visible_to_user_follower(self, _): + """ what you can see if you follow a user """ + self.remote_user.followers.add(self.local_user) + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="followers" + ) + self.assertTrue(obj.visible_to_user(self.local_user)) + + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="direct" + ) + self.assertFalse(obj.visible_to_user(self.local_user)) + + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="direct" + ) + obj.mention_users.add(self.local_user) + self.assertTrue(obj.visible_to_user(self.local_user)) + + @patch("bookwyrm.activitystreams.ActivityStream.add_status") + def test_object_visible_to_user_blocked(self, _): + """ you can't see it if they block you """ + self.remote_user.blocks.add(self.local_user) + obj = models.Status.objects.create( + content="hi", user=self.remote_user, privacy="public" + ) + self.assertFalse(obj.visible_to_user(self.local_user)) + + obj = models.Shelf.objects.create( + name="test", user=self.remote_user, privacy="unlisted" + ) + self.assertFalse(obj.visible_to_user(self.local_user)) diff --git a/bookwyrm/tests/models/test_federated_server.py b/bookwyrm/tests/models/test_federated_server.py new file mode 100644 index 000000000..4e9e8b686 --- /dev/null +++ b/bookwyrm/tests/models/test_federated_server.py @@ -0,0 +1,67 @@ +""" testing models """ +from unittest.mock import patch +from django.test import TestCase + +from bookwyrm import models + + +class FederatedServer(TestCase): + """ federate server management """ + + def setUp(self): + """ we'll need a user """ + self.server = models.FederatedServer.objects.create(server_name="test.server") + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + federated_server=self.server, + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) + self.inactive_remote_user = models.User.objects.create_user( + "nutria", + "nutria@nutria.com", + "nutriaword", + federated_server=self.server, + local=False, + remote_id="https://example.com/users/nutria", + inbox="https://example.com/users/nutria/inbox", + outbox="https://example.com/users/nutria/outbox", + is_active=False, + deactivation_reason="self_deletion", + ) + + def test_block_unblock(self): + """ block a server and all users on it """ + self.assertEqual(self.server.status, "federated") + self.assertTrue(self.remote_user.is_active) + self.assertFalse(self.inactive_remote_user.is_active) + + self.server.block() + + self.assertEqual(self.server.status, "blocked") + self.remote_user.refresh_from_db() + self.assertFalse(self.remote_user.is_active) + self.assertEqual(self.remote_user.deactivation_reason, "domain_block") + + self.inactive_remote_user.refresh_from_db() + self.assertFalse(self.inactive_remote_user.is_active) + self.assertEqual(self.inactive_remote_user.deactivation_reason, "self_deletion") + + # UNBLOCK + self.server.unblock() + + self.assertEqual(self.server.status, "federated") + # user blocked in deactivation is reactivated + self.remote_user.refresh_from_db() + self.assertTrue(self.remote_user.is_active) + self.assertIsNone(self.remote_user.deactivation_reason) + + # deleted user remains deleted + self.inactive_remote_user.refresh_from_db() + self.assertFalse(self.inactive_remote_user.is_active) + self.assertEqual(self.inactive_remote_user.deactivation_reason, "self_deletion") diff --git a/bookwyrm/tests/views/inbox/test_inbox.py b/bookwyrm/tests/views/inbox/test_inbox.py index 12d7a736c..50fb1ecc0 100644 --- a/bookwyrm/tests/views/inbox/test_inbox.py +++ b/bookwyrm/tests/views/inbox/test_inbox.py @@ -4,8 +4,9 @@ from unittest.mock import patch from django.http import HttpResponseNotAllowed, HttpResponseNotFound from django.test import TestCase, Client +from django.test.client import RequestFactory -from bookwyrm import models +from bookwyrm import models, views # pylint: disable=too-many-public-methods @@ -15,6 +16,7 @@ class Inbox(TestCase): def setUp(self): """ basic user and book data """ self.client = Client() + self.factory = RequestFactory() local_user = models.User.objects.create_user( "mouse@example.com", "mouse@mouse.com", @@ -106,3 +108,26 @@ class Inbox(TestCase): "/inbox", json.dumps(activity), content_type="application/json" ) self.assertEqual(result.status_code, 200) + + def test_is_blocked_user_agent(self): + """ check for blocked servers """ + request = self.factory.post( + "", + HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)", + ) + self.assertFalse(views.inbox.is_blocked_user_agent(request)) + + models.FederatedServer.objects.create( + server_name="mastodon.social", status="blocked" + ) + self.assertTrue(views.inbox.is_blocked_user_agent(request)) + + def test_is_blocked_activity(self): + """ check for blocked servers """ + activity = {"actor": "https://mastodon.social/user/whaatever/else"} + self.assertFalse(views.inbox.is_blocked_activity(activity)) + + models.FederatedServer.objects.create( + server_name="mastodon.social", status="blocked" + ) + self.assertTrue(views.inbox.is_blocked_activity(activity)) diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index ade6131d0..a0fa03676 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -47,6 +47,39 @@ class BookViews(TestCase): ) models.SiteSettings.objects.create() + def test_date_regression(self): + """ensure that creating a new book actually saves the published date fields + + this was initially a regression due to using a custom date picker tag + """ + first_published_date = "2021-04-20" + published_date = "2022-04-20" + self.local_user.groups.add(self.group) + view = views.EditBook.as_view() + form = forms.EditionForm( + { + "title": "New Title", + "last_edited_by": self.local_user.id, + "first_published_date": first_published_date, + "published_date": published_date, + } + ) + request = self.factory.post("", form.data) + request.user = self.local_user + + with patch("bookwyrm.connectors.connector_manager.local_search"): + result = view(request) + result.render() + + self.assertContains( + result, + f'', + ) + self.assertContains( + result, + f'', + ) + def test_book_page(self): """ there are so many views, this just makes sure it LOADS """ view = views.Book.as_view() diff --git a/bookwyrm/tests/views/test_federation.py b/bookwyrm/tests/views/test_federation.py index a60ea4327..4dc5d048f 100644 --- a/bookwyrm/tests/views/test_federation.py +++ b/bookwyrm/tests/views/test_federation.py @@ -1,9 +1,10 @@ """ test for app action functionality """ +from unittest.mock import patch from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory -from bookwyrm import models, views +from bookwyrm import forms, models, views class FederationViews(TestCase): @@ -19,6 +20,16 @@ class FederationViews(TestCase): local=True, localname="mouse", ) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) models.SiteSettings.objects.create() def test_federation_page(self): @@ -44,3 +55,75 @@ class FederationViews(TestCase): self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) + + def test_server_page_block(self): + """ block a server """ + server = models.FederatedServer.objects.create(server_name="hi.there.com") + self.remote_user.federated_server = server + self.remote_user.save() + + self.assertEqual(server.status, "federated") + + view = views.federation.block_server + request = self.factory.post("") + request.user = self.local_user + request.user.is_superuser = True + + view(request, server.id) + server.refresh_from_db() + self.remote_user.refresh_from_db() + self.assertEqual(server.status, "blocked") + # and the user was deactivated + self.assertFalse(self.remote_user.is_active) + + def test_server_page_unblock(self): + """ unblock a server """ + server = models.FederatedServer.objects.create( + server_name="hi.there.com", status="blocked" + ) + self.remote_user.federated_server = server + self.remote_user.is_active = False + self.remote_user.deactivation_reason = "domain_block" + self.remote_user.save() + + request = self.factory.post("") + request.user = self.local_user + request.user.is_superuser = True + + views.federation.unblock_server(request, server.id) + server.refresh_from_db() + self.remote_user.refresh_from_db() + self.assertEqual(server.status, "federated") + # and the user was re-activated + self.assertTrue(self.remote_user.is_active) + + def test_add_view_get(self): + """ there are so many views, this just makes sure it LOADS """ + # create mode + view = views.AddFederatedServer.as_view() + request = self.factory.get("") + request.user = self.local_user + request.user.is_superuser = True + + result = view(request) + self.assertIsInstance(result, TemplateResponse) + result.render() + self.assertEqual(result.status_code, 200) + + def test_add_view_post_create(self): + """ create a server entry """ + form = forms.ServerForm() + form.data["server_name"] = "remote.server" + form.data["application_type"] = "coolsoft" + form.data["status"] = "blocked" + + view = views.AddFederatedServer.as_view() + request = self.factory.post("", form.data) + request.user = self.local_user + request.user.is_superuser = True + + view(request) + server = models.FederatedServer.objects.get() + self.assertEqual(server.server_name, "remote.server") + self.assertEqual(server.application_type, "coolsoft") + self.assertEqual(server.status, "blocked") diff --git a/bookwyrm/tests/views/test_helpers.py b/bookwyrm/tests/views/test_helpers.py index 7d2bc42c9..2e5ed82d4 100644 --- a/bookwyrm/tests/views/test_helpers.py +++ b/bookwyrm/tests/views/test_helpers.py @@ -146,6 +146,15 @@ class ViewsHelpers(TestCase): self.assertIsInstance(result, models.User) self.assertEqual(result.username, "mouse@example.com") + def test_user_on_blocked_server(self, _): + """ find a remote user using webfinger """ + models.FederatedServer.objects.create( + server_name="example.com", status="blocked" + ) + + result = views.helpers.handle_remote_webfinger("@mouse@example.com") + self.assertIsNone(result) + def test_handle_reading_status_to_read(self, _): """ posts shelve activities """ shelf = self.local_user.shelf_set.get(identifier="to-read") @@ -190,66 +199,6 @@ class ViewsHelpers(TestCase): ) self.assertFalse(models.GeneratedNote.objects.exists()) - def test_object_visible_to_user(self, _): - """ does a user have permission to view an object """ - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="public" - ) - self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Shelf.objects.create( - name="test", user=self.remote_user, privacy="unlisted" - ) - self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="followers" - ) - self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="direct" - ) - self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="direct" - ) - obj.mention_users.add(self.local_user) - self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) - - def test_object_visible_to_user_follower(self, _): - """ what you can see if you follow a user """ - self.remote_user.followers.add(self.local_user) - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="followers" - ) - self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="direct" - ) - self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="direct" - ) - obj.mention_users.add(self.local_user) - self.assertTrue(views.helpers.object_visible_to_user(self.local_user, obj)) - - def test_object_visible_to_user_blocked(self, _): - """ you can't see it if they block you """ - self.remote_user.blocks.add(self.local_user) - obj = models.Status.objects.create( - content="hi", user=self.remote_user, privacy="public" - ) - self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) - - obj = models.Shelf.objects.create( - name="test", user=self.remote_user, privacy="unlisted" - ) - self.assertFalse(views.helpers.object_visible_to_user(self.local_user, obj)) - def test_get_annotated_users(self, _): """ list of people you might know """ user_1 = models.User.objects.create_user( diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 463988065..c5c528002 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -68,6 +68,21 @@ urlpatterns = [ views.FederatedServer.as_view(), name="settings-federated-server", ), + re_path( + r"^settings/federation/(?P\d+)/block?$", + views.federation.block_server, + name="settings-federated-server-block", + ), + re_path( + r"^settings/federation/(?P\d+)/unblock?$", + views.federation.unblock_server, + name="settings-federated-server-unblock", + ), + re_path( + r"^settings/federation/add/?$", + views.AddFederatedServer.as_view(), + name="settings-add-federated-server", + ), re_path( r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites" ), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index d053e971b..d7bc4e130 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -5,7 +5,8 @@ from .block import Block, unblock from .books import Book, EditBook, ConfirmEditBook, Editions from .books import upload_cover, add_description, switch_edition, resolve_book from .directory import Directory -from .federation import Federation, FederatedServer +from .federation import Federation, FederatedServer, AddFederatedServer +from .federation import block_server, unblock_server from .feed import DirectMessage, Feed, Replies, Status from .follow import follow, unfollow from .follow import accept_follow_request, delete_follow_request diff --git a/bookwyrm/views/federation.py b/bookwyrm/views/federation.py index 464a207ca..f34f7d191 100644 --- a/bookwyrm/views/federation.py +++ b/bookwyrm/views/federation.py @@ -1,12 +1,13 @@ """ manage federated servers """ from django.contrib.auth.decorators import login_required, permission_required from django.core.paginator import Paginator -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views import View +from django.views.decorators.http import require_POST -from bookwyrm import models +from bookwyrm import forms, models from bookwyrm.settings import PAGE_LENGTH @@ -30,14 +31,38 @@ class Federation(View): sort = request.GET.get("sort") sort_fields = ["created_date", "application_type", "server_name"] - if sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]: - servers = servers.order_by(sort) + if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]: + sort = "created_date" + servers = servers.order_by(sort) paginated = Paginator(servers, PAGE_LENGTH) - data = {"servers": paginated.page(page), "sort": sort} + + data = { + "servers": paginated.page(page), + "sort": sort, + "form": forms.ServerForm(), + } return TemplateResponse(request, "settings/federation.html", data) +class AddFederatedServer(View): + """ manually add a server """ + + def get(self, request): + """ add server form """ + data = {"form": forms.ServerForm()} + return TemplateResponse(request, "settings/edit_server.html", data) + + def post(self, request): + """ add a server from the admin panel """ + form = forms.ServerForm(request.POST) + if not form.is_valid(): + data = {"form": form} + return TemplateResponse(request, "settings/edit_server.html", data) + server = form.save() + return redirect("settings-federated-server", server.id) + + @method_decorator(login_required, name="dispatch") @method_decorator( permission_required("bookwyrm.control_federation", raise_exception=True), @@ -61,3 +86,32 @@ class FederatedServer(View): ), } return TemplateResponse(request, "settings/federated_server.html", data) + + def post(self, request, server): # pylint: disable=unused-argument + """ update note """ + server = get_object_or_404(models.FederatedServer, id=server) + server.notes = request.POST.get("notes") + server.save() + return redirect("settings-federated-server", server.id) + + +@login_required +@require_POST +@permission_required("bookwyrm.control_federation", raise_exception=True) +# pylint: disable=unused-argument +def block_server(request, server): + """ block a server """ + server = get_object_or_404(models.FederatedServer, id=server) + server.block() + return redirect("settings-federated-server", server.id) + + +@login_required +@require_POST +@permission_required("bookwyrm.control_federation", raise_exception=True) +# pylint: disable=unused-argument +def unblock_server(request, server): + """ unblock a server """ + server = get_object_or_404(models.FederatedServer, id=server) + server.unblock() + return redirect("settings-federated-server", server.id) diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py index cda115867..d5e644343 100644 --- a/bookwyrm/views/feed.py +++ b/bookwyrm/views/feed.py @@ -12,7 +12,7 @@ from bookwyrm import activitystreams, forms, models from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.settings import PAGE_LENGTH, STREAMS from .helpers import get_user_from_username, privacy_filter, get_suggested_users -from .helpers import is_api_request, is_bookwyrm_request, object_visible_to_user +from .helpers import is_api_request, is_bookwyrm_request # pylint: disable= no-self-use @@ -113,7 +113,7 @@ class Status(View): return HttpResponseNotFound() # make sure the user is authorized to see the status - if not object_visible_to_user(request.user, status): + if not status.visible_to_user(request.user): return HttpResponseNotFound() if is_api_request(request): diff --git a/bookwyrm/views/goal.py b/bookwyrm/views/goal.py index 9c4e117c6..1627d3da3 100644 --- a/bookwyrm/views/goal.py +++ b/bookwyrm/views/goal.py @@ -10,7 +10,7 @@ from django.views.decorators.http import require_POST from bookwyrm import forms, models from bookwyrm.status import create_generated_note -from .helpers import get_user_from_username, object_visible_to_user +from .helpers import get_user_from_username # pylint: disable= no-self-use @@ -26,7 +26,7 @@ class Goal(View): if not goal and user != request.user: return HttpResponseNotFound() - if goal and not object_visible_to_user(request.user, goal): + if goal and not goal.visible_to_user(request.user): return HttpResponseNotFound() data = { diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index 2b6501ff2..57c334377 100644 --- a/bookwyrm/views/helpers.py +++ b/bookwyrm/views/helpers.py @@ -32,30 +32,6 @@ def is_bookwyrm_request(request): return True -def object_visible_to_user(viewer, obj): - """ is a user authorized to view an object? """ - if not obj: - return False - - # viewer can't see it if the object's owner blocked them - if viewer in obj.user.blocks.all(): - return False - - # you can see your own posts and any public or unlisted posts - if viewer == obj.user or obj.privacy in ["public", "unlisted"]: - return True - - # you can see the followers only posts of people you follow - if obj.privacy == "followers" and obj.user.followers.filter(id=viewer.id).first(): - return True - - # you can see dms you are tagged in - if isinstance(obj, models.Status): - if obj.privacy == "direct" and obj.mention_users.filter(id=viewer.id).first(): - return True - return False - - def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False): """ filter objects that have "user" and "privacy" fields """ privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"] diff --git a/bookwyrm/views/inbox.py b/bookwyrm/views/inbox.py index 8c645159e..d1b75997d 100644 --- a/bookwyrm/views/inbox.py +++ b/bookwyrm/views/inbox.py @@ -1,9 +1,10 @@ """ incoming activities """ import json +import re from urllib.parse import urldefrag -from django.http import HttpResponse -from django.http import HttpResponseBadRequest, HttpResponseNotFound +from django.http import HttpResponse, HttpResponseNotFound +from django.http import HttpResponseBadRequest, HttpResponseForbidden from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import csrf_exempt @@ -12,6 +13,7 @@ import requests from bookwyrm import activitypub, models from bookwyrm.tasks import app from bookwyrm.signatures import Signature +from bookwyrm.utils import regex @method_decorator(csrf_exempt, name="dispatch") @@ -21,6 +23,10 @@ class Inbox(View): def post(self, request, username=None): """ only works as POST request """ + # first check if this server is on our shitlist + if is_blocked_user_agent(request): + return HttpResponseForbidden() + # make sure the user's inbox even exists if username: try: @@ -34,6 +40,10 @@ class Inbox(View): except json.decoder.JSONDecodeError: return HttpResponseBadRequest() + # let's be extra sure we didn't block this domain + if is_blocked_activity(activity_json): + return HttpResponseForbidden() + if ( not "object" in activity_json or not "type" in activity_json @@ -54,6 +64,25 @@ class Inbox(View): return HttpResponse() +def is_blocked_user_agent(request): + """ check if a request is from a blocked server based on user agent """ + # check user agent + user_agent = request.headers.get("User-Agent") + if not user_agent: + return False + url = re.search(r"https?://{:s}/?".format(regex.domain), user_agent).group() + return models.FederatedServer.is_blocked(url) + + +def is_blocked_activity(activity_json): + """ get the sender out of activity json and check if it's blocked """ + actor = activity_json.get("actor") + if not actor: + # well I guess it's not even a valid activity so who knows + return False + return models.FederatedServer.is_blocked(actor) + + @app.task def activity_task(activity_json): """ do something with this json we think is legit """ diff --git a/bookwyrm/views/list.py b/bookwyrm/views/list.py index adf9840d4..3d85280d3 100644 --- a/bookwyrm/views/list.py +++ b/bookwyrm/views/list.py @@ -13,7 +13,7 @@ from django.views.decorators.http import require_POST from bookwyrm import forms, models from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.connectors import connector_manager -from .helpers import is_api_request, object_visible_to_user, privacy_filter +from .helpers import is_api_request, privacy_filter from .helpers import get_user_from_username # pylint: disable=no-self-use @@ -92,7 +92,7 @@ class List(View): def get(self, request, list_id): """ display a book list """ book_list = get_object_or_404(models.List, id=list_id) - if not object_visible_to_user(request.user, book_list): + if not book_list.visible_to_user(request.user): return HttpResponseNotFound() if is_api_request(request): @@ -176,7 +176,7 @@ class Curate(View): def add_book(request): """ put a book on a list """ book_list = get_object_or_404(models.List, id=request.POST.get("list")) - if not object_visible_to_user(request.user, book_list): + if not book_list.visible_to_user(request.user): return HttpResponseNotFound() book = get_object_or_404(models.Edition, id=request.POST.get("book")) diff --git a/bookwyrm/views/shelf.py b/bookwyrm/views/shelf.py index 41d1f1358..888999493 100644 --- a/bookwyrm/views/shelf.py +++ b/bookwyrm/views/shelf.py @@ -16,7 +16,7 @@ from bookwyrm import forms, models from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.settings import PAGE_LENGTH from .helpers import is_api_request, get_edition, get_user_from_username -from .helpers import handle_reading_status, privacy_filter, object_visible_to_user +from .helpers import handle_reading_status, privacy_filter # pylint: disable= no-self-use @@ -43,7 +43,7 @@ class Shelf(View): shelf = user.shelf_set.get(identifier=shelf_identifier) except models.Shelf.DoesNotExist: return HttpResponseNotFound() - if not object_visible_to_user(request.user, shelf): + if not shelf.visible_to_user(request.user): return HttpResponseNotFound() # this is a constructed "all books" view, with a fake "shelf" obj else: diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index aba804d8b..d666f064e 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -17,7 +17,7 @@ from bookwyrm import forms, models from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.settings import PAGE_LENGTH from .helpers import get_user_from_username, is_api_request -from .helpers import is_blocked, privacy_filter, object_visible_to_user +from .helpers import is_blocked, privacy_filter # pylint: disable= no-self-use @@ -80,7 +80,7 @@ class User(View): goal = models.AnnualGoal.objects.filter( user=user, year=timezone.now().year ).first() - if not object_visible_to_user(request.user, goal): + if goal and not goal.visible_to_user(request.user): goal = None data = { "user": user, diff --git a/celerywyrm/settings.py b/celerywyrm/settings.py index 952fe5b15..cd5b00ba4 100644 --- a/celerywyrm/settings.py +++ b/celerywyrm/settings.py @@ -149,7 +149,7 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.0/howto/static-files/ +# https://docs.djangoproject.com/en/3.1/howto/static-files/ STATIC_URL = "/static/" STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static")) diff --git a/certbot.sh b/certbot.sh new file mode 100644 index 000000000..6d2c3cd90 --- /dev/null +++ b/certbot.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +source .env; + +if [ "$CERTBOT_INIT" = "true" ] +then + certonly \ + --webroot \ + --webroot-path=/var/www/certbot \ + --email ${EMAIL} \ + --agree-tos \ + --no-eff-email \ + -d ${DOMAIN} \ + -d www.${DOMAIN} +else + renew \ + --webroot \ + --webroot-path \ + /var/www/certbot +fi diff --git a/docker-compose.yml b/docker-compose.yml index 3ee9037f9..60816cc09 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,8 @@ services: - pgdata:/var/lib/postgresql/data networks: - main + ports: + - 5432:5432 web: build: . env_file: .env diff --git a/nginx/default.conf b/nginx/development similarity index 100% rename from nginx/default.conf rename to nginx/development diff --git a/nginx/production b/nginx/production new file mode 100644 index 000000000..c5d83cbf6 --- /dev/null +++ b/nginx/production @@ -0,0 +1,72 @@ +upstream web { + server web:8000; +} + +server { + listen [::]:80; + listen 80; + + server_name your-domain.com www.your-domain.com; + + location ~ /.well-known/acme-challenge { + allow all; + root /var/www/certbot; + } + +# # redirect http to https +# return 301 https://your-domain.com$request_uri; +# } +# +# server { +# listen [::]:443 ssl http2; +# listen 443 ssl http2; +# +# server_name your-domain.com; +# +# # SSL code +# ssl_certificate /etc/nginx/ssl/live/your-domain.com/fullchain.pem; +# ssl_certificate_key /etc/nginx/ssl/live/your-domain.com/privkey.pem; +# +# location ~ /.well-known/acme-challenge { +# allow all; +# root /var/www/certbot; +# } +# +# location / { +# proxy_pass http://web; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header Host $host; +# proxy_redirect off; +# } +# +# location /images/ { +# alias /app/images/; +# } +# +# location /static/ { +# alias /app/static/; +# } +} + +# Reverse-Proxy server +# server { +# listen [::]:8001; +# listen 8001; + +# server_name your-domain.com www.your-domain.com; + +# location / { +# proxy_pass http://web; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header Host $host; +# proxy_redirect off; +# } + +# location /images/ { +# alias /app/images/; +# } + +# location /static/ { +# alias /app/static/; +# } +# }