diff --git a/.env.example b/.env.dev.example similarity index 75% rename from .env.example rename to .env.dev.example index 2397a5b1..5e605d74 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 00000000..0013bf9d --- /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 71fa61bf..cf88e987 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 6dad4178..fbecc2d0 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,8 @@ Since the project is still in its early stages, the features are growing every d 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 +- [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 @@ -91,10 +91,15 @@ Deployment ## Setting up the developer environment -Set up the environment file: +Set up the development environment file: ``` bash -cp .env.example .env +cp .env.dev.example .env +``` + +Set up nginx for development `nginx/default.conf`: +``` bash +cp nginx/development nginx/default.conf ``` For most testing, you'll want to use ngrok. Remember to set the DOMAIN in `.env` to your ngrok domain. @@ -108,7 +113,7 @@ docker-compose run --rm web python manage.py initdb docker-compose up ``` -Once the build is complete, you can access the instance at `localhost:1333` +Once the build is complete, you can access the instance at `http://localhost:1333` ### Editing static files If you edit the CSS or JavaScript, you will need to run Django's `collectstatic` command in order for your changes to have effect. You can do this by running: @@ -160,26 +165,35 @@ Instructions for running BookWyrm in production: - Get the application code: `git clone git@github.com:mouse-reeve/bookwyrm.git` -- Switch to the `production` branch +- Switch to the `production` branch: `git checkout production` -- Create your environment variables file - `cp .env.example .env` - - Add your domain, email address, SMTP credentials - - Set a secure redis password and secret key - - Set a secure database password for postgres +- Create your environment variables file, `cp .env.prod.example .env`, and update the following: + - `SECRET_KEY` | A difficult to guess, secret string of characers + - `DOMAIN` | Your web domain + - `EMAIL` | Email address to be used for certbot domain verification + - `POSTGRES_PASSWORD` | Set a secure password for the database + - `REDIS_ACTIVITY_PASSWORD` | Set a secure password for Redis Activity subsystem + - `REDIS_BROKER_PASSWORD` | Set a secure password for Redis queue broker subsystem + - `FLOWER_USER` | Your own username for accessing Flower queue monitor + - `FLOWER_PASSWORD` | Your own secure password for accessing Flower queue monitor - Update your nginx configuration in `nginx/default.conf` - Replace `your-domain.com` with your domain name - - If you aren't using the `www` subdomain, remove the www.your-domain.com version of the domain from the `server_name` in the first server block in `nginx/default.conf` and remove the `-d www.${DOMAIN}` flag at the end of the `certbot` command in `docker-compose.yml`. - - If you are running another web-server on your host machine, you will need to follow the [reverse-proxy instructions](#running-bookwyrm-behind-a-reverse-proxy) +- Configure nginx + - Make a copy of the production template config and set it for use in nginx `cp nginx/production nginx/default.conf` + - Update `nginx/default.conf`: + - Replace `your-domain.com` with your domain name + - If you aren't using the `www` subdomain, remove the www.your-domain.com version of the domain from the `server_name` in the first server block in `nginx/default.conf` and remove the `-d www.${DOMAIN}` flag at the end of the `certbot` command in `docker-compose.yml`. + - If you are running another web-server on your host machine, you will need to follow the [reverse-proxy instructions](#running-bookwyrm-behind-a-reverse-proxy) +- If you need to initialize your certbot for your domain, set `CERTBOT_INIT=true` in your `.env` file - Run the application (this should also set up a Certbot ssl cert for your domain) with `docker-compose up --build`, and make sure all the images build successfully - If you are running other services on your host machine, you may run into errors where services fail when attempting to bind to a port. See the [troubleshooting guide](#port-conflicts) for advice on resolving this. - When docker has built successfully, stop the process with `CTRL-C` -- Comment out the `command: certonly...` line in `docker-compose.yml`, and uncomment the following line (`command: renew ...`) so that the certificate will be automatically renewed. -- Uncomment the https redirect and `server` block in `nginx/default.conf` (lines 17-48). +- If you set `CERTBOT_INIT=true` earlier, set it now as `CERTBOT_INIT=false` so that certbot runs in renew mode - Run docker-compose in the background with: `docker-compose up -d` - Initialize the database with: `./bw-dev initdb` +- Set up schedule backups with cron that runs that `docker-compose exec db pg_dump -U ` and saves the backup to a safe location Congrats! You did it, go to your domain and enjoy the fruits of your labors. diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 2483cc62..2fe5d825 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 b159a89e..7c41323c 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 d6101c87..a86a1652 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 00000000..0bd0f2ae --- /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 00000000..77ad541e --- /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 00000000..2bdc425d --- /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 00000000..bb3173a7 --- /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 d0ab829d..ce16460e 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 cb2fc851..261c9686 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 8f7d903e..aa2b2f6a 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 c519f76c..15ceb19b 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 146d4fff..7ea8c595 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 9340da9e..4f71a228 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 00000000..6ae22789 --- /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 13715bfb..6996557d 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 696d7a20..99afb541 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 25a2e7ee..442f98ca 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 00000000..4e9e8b68 --- /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 12d7a736..50fb1ecc 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 ade6131d..a0fa0367 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 a60ea432..4dc5d048 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 7d2bc42c..2e5ed82d 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 46398806..c5c52800 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 d053e971..d7bc4e13 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 464a207c..f34f7d19 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 cda11586..d5e64434 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 9c4e117c..1627d3da 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 2b6501ff..57c33437 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 8c645159..d1b75997 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 adf9840d..3d85280d 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 41d1f135..88899949 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 aba804d8..d666f064 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 952fe5b1..cd5b00ba 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 00000000..6d2c3cd9 --- /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 3ee9037f..60816cc0 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 00000000..c5d83cbf --- /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/; +# } +# }