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