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 %}
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 %}
+
+
+
+{% 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' %}
+
+ {% else %}
+
+ {% 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/;
+# }
+# }