Merge pull request #9 from mouse-reeve/main

Merge
This commit is contained in:
tofuwabohu 2021-04-12 20:01:49 +02:00 committed by GitHub
commit 0ba8fe862d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 976 additions and 185 deletions

View file

@ -5,6 +5,7 @@ SECRET_KEY="7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr"
DEBUG=true DEBUG=true
DOMAIN=your.domain.here DOMAIN=your.domain.here
#EMAIL=your@email.here
## Leave unset to allow all hosts ## Leave unset to allow all hosts
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]" # ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
@ -26,14 +27,24 @@ POSTGRES_HOST=db
MAX_STREAM_LENGTH=200 MAX_STREAM_LENGTH=200
REDIS_ACTIVITY_HOST=redis_activity REDIS_ACTIVITY_HOST=redis_activity
REDIS_ACTIVITY_PORT=6379 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_BROKER=redis://redis_broker:6379/0
CELERY_RESULT_BACKEND=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_HOST="smtp.mailgun.org"
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_HOST_USER=mail@your.domain.here EMAIL_HOST_USER=mail@your.domain.here
EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_HOST_PASSWORD=emailpassword123
EMAIL_USE_TLS=true EMAIL_USE_TLS=true
EMAIL_USE_SSL=false EMAIL_USE_SSL=false
# Set this to true when initializing certbot for domain, false when not
CERTBOT_INIT=false

50
.env.prod.example Normal file
View file

@ -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

3
.gitignore vendored
View file

@ -24,3 +24,6 @@
#Node tools #Node tools
/node_modules/ /node_modules/
#nginx
nginx/default.conf

View file

@ -73,8 +73,8 @@ Since the project is still in its early stages, the features are growing every d
Web backend Web backend
- [Django](https://www.djangoproject.com/) web server - [Django](https://www.djangoproject.com/) web server
- [PostgreSQL](https://www.postgresql.org/) database - [PostgreSQL](https://www.postgresql.org/) database
- [ActivityPub](http://activitypub.rocks/) federation - [ActivityPub](https://activitypub.rocks/) federation
- [Celery](http://celeryproject.org/) task queuing - [Celery](https://docs.celeryproject.org/) task queuing
- [Redis](https://redis.io/) task backend - [Redis](https://redis.io/) task backend
- [Redis (again)](https://redis.io/) activity stream manager - [Redis (again)](https://redis.io/) activity stream manager
@ -91,10 +91,15 @@ Deployment
## Setting up the developer environment ## Setting up the developer environment
Set up the environment file: Set up the development environment file:
``` bash ``` bash
cp .env.example .env cp .env.dev.example .env
```
Set up nginx for development `nginx/default.conf`:
``` bash
cp nginx/development nginx/default.conf
``` ```
For most testing, you'll want to use ngrok. Remember to set the DOMAIN in `.env` to your ngrok domain. For most testing, you'll want to use ngrok. Remember to set the DOMAIN in `.env` to your ngrok domain.
@ -108,7 +113,7 @@ docker-compose run --rm web python manage.py initdb
docker-compose up docker-compose up
``` ```
Once the build is complete, you can access the instance at `localhost:1333` Once the build is complete, you can access the instance at `http://localhost:1333`
### Editing static files ### Editing static files
If you edit the CSS or JavaScript, you will need to run Django's `collectstatic` command in order for your changes to have effect. You can do this by running: If you edit the CSS or JavaScript, you will need to run Django's `collectstatic` command in order for your changes to have effect. You can do this by running:
@ -160,26 +165,35 @@ Instructions for running BookWyrm in production:
- Get the application code: - Get the application code:
`git clone git@github.com:mouse-reeve/bookwyrm.git` `git clone git@github.com:mouse-reeve/bookwyrm.git`
- Switch to the `production` branch - Switch to the `production` branch:
`git checkout production` `git checkout production`
- Create your environment variables file - Create your environment variables file, `cp .env.prod.example .env`, and update the following:
`cp .env.example .env` - `SECRET_KEY` | A difficult to guess, secret string of characers
- Add your domain, email address, SMTP credentials - `DOMAIN` | Your web domain
- Set a secure redis password and secret key - `EMAIL` | Email address to be used for certbot domain verification
- Set a secure database password for postgres - `POSTGRES_PASSWORD` | Set a secure password for the database
- `REDIS_ACTIVITY_PASSWORD` | Set a secure password for Redis Activity subsystem
- `REDIS_BROKER_PASSWORD` | Set a secure password for Redis queue broker subsystem
- `FLOWER_USER` | Your own username for accessing Flower queue monitor
- `FLOWER_PASSWORD` | Your own secure password for accessing Flower queue monitor
- Update your nginx configuration in `nginx/default.conf` - Update your nginx configuration in `nginx/default.conf`
- Replace `your-domain.com` with your domain name - Replace `your-domain.com` with your domain name
- If you aren't using the `www` subdomain, remove the www.your-domain.com version of the domain from the `server_name` in the first server block in `nginx/default.conf` and remove the `-d www.${DOMAIN}` flag at the end of the `certbot` command in `docker-compose.yml`. - Configure nginx
- If you are running another web-server on your host machine, you will need to follow the [reverse-proxy instructions](#running-bookwyrm-behind-a-reverse-proxy) - Make a copy of the production template config and set it for use in nginx `cp nginx/production nginx/default.conf`
- Update `nginx/default.conf`:
- Replace `your-domain.com` with your domain name
- If you aren't using the `www` subdomain, remove the www.your-domain.com version of the domain from the `server_name` in the first server block in `nginx/default.conf` and remove the `-d www.${DOMAIN}` flag at the end of the `certbot` command in `docker-compose.yml`.
- If you are running another web-server on your host machine, you will need to follow the [reverse-proxy instructions](#running-bookwyrm-behind-a-reverse-proxy)
- If you need to initialize your certbot for your domain, set `CERTBOT_INIT=true` in your `.env` file
- Run the application (this should also set up a Certbot ssl cert for your domain) with - Run the application (this should also set up a Certbot ssl cert for your domain) with
`docker-compose up --build`, and make sure all the images build successfully `docker-compose up --build`, and make sure all the images build successfully
- If you are running other services on your host machine, you may run into errors where services fail when attempting to bind to a port. - If you are running other services on your host machine, you may run into errors where services fail when attempting to bind to a port.
See the [troubleshooting guide](#port-conflicts) for advice on resolving this. See the [troubleshooting guide](#port-conflicts) for advice on resolving this.
- When docker has built successfully, stop the process with `CTRL-C` - When docker has built successfully, stop the process with `CTRL-C`
- Comment out the `command: certonly...` line in `docker-compose.yml`, and uncomment the following line (`command: renew ...`) so that the certificate will be automatically renewed. - If you set `CERTBOT_INIT=true` earlier, set it now as `CERTBOT_INIT=false` so that certbot runs in renew mode
- Uncomment the https redirect and `server` block in `nginx/default.conf` (lines 17-48).
- Run docker-compose in the background with: `docker-compose up -d` - Run docker-compose in the background with: `docker-compose up -d`
- Initialize the database with: `./bw-dev initdb` - Initialize the database with: `./bw-dev initdb`
- Set up schedule backups with cron that runs that `docker-compose exec db pg_dump -U <databasename>` and saves the backup to a safe location
Congrats! You did it, go to your domain and enjoy the fruits of your labors. Congrats! You did it, go to your domain and enjoy the fruits of your labors.

View file

@ -219,6 +219,12 @@ def dict_from_mappings(data, mappings):
def get_data(url, params=None): def get_data(url, params=None):
""" wrapper for request.get """ """ 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: try:
resp = requests.get( resp = requests.get(
url, url,

View file

@ -281,3 +281,9 @@ class ReportForm(CustomForm):
class Meta: class Meta:
model = models.Report model = models.Report
fields = ["user", "reporter", "statuses", "note"] fields = ["user", "reporter", "statuses", "note"]
class ServerForm(CustomForm):
class Meta:
model = models.FederatedServer
exclude = ["remote_id"]

View file

@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType 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 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(): def init_settings():
SiteSettings.objects.create() SiteSettings.objects.create()
@ -118,4 +128,5 @@ class Command(BaseCommand):
init_groups() init_groups()
init_permissions() init_permissions()
init_connectors() init_connectors()
init_federated_servers()
init_settings() init_settings()

View file

@ -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,
),
),
]

View file

@ -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 = []

View file

@ -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 = []

View file

@ -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,
),
),
]

View file

@ -153,7 +153,7 @@ class ActivitypubMixin:
# unless it's a dm, all the followers should receive the activity # unless it's a dm, all the followers should receive the activity
if privacy != "direct": if privacy != "direct":
# we will send this out to a subset of all remote users # 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, local=False,
) )
# filter users first by whether they're using the desired software # filter users first by whether they're using the desired software

View file

@ -31,6 +31,36 @@ class BookWyrmModel(models.Model):
""" how to link to this object in the local app """ """ how to link to this object in the local app """
return self.get_remote_id().replace("https://%s" % DOMAIN, "") 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) @receiver(models.signals.post_save)
# pylint: disable=unused-argument # pylint: disable=unused-argument

View file

@ -1,17 +1,51 @@
""" connections to external ActivityPub servers """ """ connections to external ActivityPub servers """
from urllib.parse import urlparse
from django.db import models from django.db import models
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
FederationStatus = models.TextChoices(
"Status",
[
"federated",
"blocked",
],
)
class FederatedServer(BookWyrmModel): class FederatedServer(BookWyrmModel):
""" store which servers we federate with """ """ store which servers we federate with """
server_name = models.CharField(max_length=255, unique=True) server_name = models.CharField(max_length=255, unique=True)
# federated, blocked, whatever else status = models.CharField(
status = models.CharField(max_length=255, default="federated") max_length=255, default="federated", choices=FederationStatus.choices
)
# is it mastodon, bookwyrm, etc # is it mastodon, bookwyrm, etc
application_type = 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) 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()

View file

@ -24,6 +24,16 @@ from .federated_server import FederatedServer
from . import fields, Review from . import fields, Review
DeactivationReason = models.TextChoices(
"DeactivationReason",
[
"self_deletion",
"moderator_deletion",
"domain_block",
],
)
class User(OrderedCollectionPageMixin, AbstractUser): class User(OrderedCollectionPageMixin, AbstractUser):
""" a user who wants to read books """ """ a user who wants to read books """
@ -111,6 +121,9 @@ class User(OrderedCollectionPageMixin, AbstractUser):
default=str(pytz.utc), default=str(pytz.utc),
max_length=255, max_length=255,
) )
deactivation_reason = models.CharField(
max_length=255, choices=DeactivationReason.choices, null=True, blank=True
)
name_field = "username" name_field = "username"
property_fields = [("following_link", "following")] property_fields = [("following_link", "following")]
@ -138,7 +151,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
def viewer_aware_objects(cls, viewer): def viewer_aware_objects(cls, viewer):
""" the user queryset filtered for the context of the logged in user """ """ the user queryset filtered for the context of the logged in user """
queryset = cls.objects.filter(is_active=True) queryset = cls.objects.filter(is_active=True)
if viewer.is_authenticated: if viewer and viewer.is_authenticated:
queryset = queryset.exclude(blocks=viewer) queryset = queryset.exclude(blocks=viewer)
return queryset return queryset

View file

@ -98,6 +98,7 @@ WSGI_APPLICATION = "bookwyrm.wsgi.application"
# redis/activity streams settings # redis/activity streams settings
REDIS_ACTIVITY_HOST = env("REDIS_ACTIVITY_HOST", "localhost") REDIS_ACTIVITY_HOST = env("REDIS_ACTIVITY_HOST", "localhost")
REDIS_ACTIVITY_PORT = env("REDIS_ACTIVITY_PORT", 6379) 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)) MAX_STREAM_LENGTH = int(env("MAX_STREAM_LENGTH", 200))
STREAMS = ["home", "local", "federated"] STREAMS = ["home", "local", "federated"]
@ -166,7 +167,7 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # 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__)) PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_URL = "/static/" STATIC_URL = "/static/"

View file

@ -6,7 +6,14 @@
{% block content %} {% block content %}
<header class="block column is-offset-one-quarter pl-1"> <header class="block column is-offset-one-quarter pl-1">
<h1 class="title">{% block header %}{% endblock %}</h1> <div class="columns is-mobile">
<div class="column">
<h1 class="title">{% block header %}{% endblock %}</h1>
</div>
<div class="column is-narrow">
{% block edit-button %}{% endblock %}
</div>
</div>
</header> </header>
<div class="block columns"> <div class="block columns">

View file

@ -0,0 +1,58 @@
{% extends 'settings/admin_layout.html' %}
{% load i18n %}
{% block title %}{% trans "Add server" %}{% endblock %}
{% block header %}
{% trans "Add server" %}
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to server list" %}</a>
{% endblock %}
{% block panel %}
<form method="POST" action="{% url 'settings-add-federated-server' %}">
{% csrf_token %}
<div class="columns">
<div class="column is-half">
<div>
<label class="label" for="id_server_name">{% trans "Instance:" %}</label>
<input type="text" name="server_name" maxlength="255" class="input" required id="id_server_name" value="{{ form.server_name.value|default:'' }}" placeholder="domain.com">
{% for error in form.server_name.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div>
<label class="label" for="id_status">{% trans "Status:" %}</label>
<div class="select">
<select name="status" class="" id="id_status">
<option value="federated" {% if form.status.value == "federated" or not form.status.value %}selected=""{% endif %}>{% trans "Federated" %}</option>
<option value="blocked" {% if form.status.value == "blocked" %}selected{% endif %}>{% trans "Blocked" %}</option>
</select>
</div>
</div>
</div>
<div class="column is-half">
<div>
<label class="label" for="id_application_type">{% trans "Software:" %}</label>
<input type="text" name="application_type" maxlength="255" class="input" id="id_application_type" value="{{ form.application_type.value|default:'' }}">
{% for error in form.application_type.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div>
<label class="label" for="id_application_version">{% trans "Version:" %}</label>
<input type="text" name="application_version" maxlength="255" class="input" id="id_application_version" value="{{ form.application_version.value|default:'' }}">
{% for error in form.application_version.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
</div>
<p>
<label class="label" for="id_notes">{% trans "Notes:" %}</label>
<textarea name="notes" cols="None" rows="None" class="textarea" id="id_notes">{{ form.notes.value|default:'' }}</textarea>
</p>
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
</form>
{% endblock %}

View file

@ -4,64 +4,112 @@
{% block header %} {% block header %}
{{ server.server_name }} {{ server.server_name }}
{% if server.status == "blocked" %}<span class="icon icon-x has-text-danger is-size-5" title="{% trans 'Blocked' %}"><span class="is-sr-only">{% trans "Blocked" %}</span></span>
{% endif %}
<a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to server list" %}</a> <a href="{% url 'settings-federation' %}" class="has-text-weight-normal help">{% trans "Back to server list" %}</a>
{% endblock %} {% endblock %}
{% block panel %} {% block panel %}
<div class="columns">
<section class="column is-half content">
<h2 class="title is-4">{% trans "Details" %}</h2>
<dl>
<div class="is-flex">
<dt>{% trans "Software:" %}</dt>
<dd>{{ server.application_type }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Version:" %}</dt>
<dd>{{ server.application_version }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Status:" %}</dt>
<dd>{{ server.status }}</dd>
</div>
</dl>
</section>
<section class="column is-half content">
<h2 class="title is-4">{% trans "Activity" %}</h2>
<dl>
<div class="is-flex">
<dt>{% trans "Users:" %}</dt>
<dd>
{{ users.count }}
{% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.id }}">{% trans "View all" %}</a>){% endif %}
</dd>
</div>
<div class="is-flex">
<dt>{% trans "Reports:" %}</dt>
<dd>
{{ reports.count }}
{% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.id }}">{% trans "View all" %}</a>){% endif %}
</dd>
</div>
<div class="is-flex">
<dt>{% trans "Followed by us:" %}</dt>
<dd>
{{ followed_by_us.count }}
</dd>
</div>
<div class="is-flex">
<dt>{% trans "Followed by them:" %}</dt>
<dd>
{{ followed_by_them.count }}
</dd>
</div>
<div class="is-flex">
<dt>{% trans "Blocked by us:" %}</dt>
<dd>
{{ blocked_by_us.count }}
</dd>
</div>
</dl>
</section>
</div>
<section class="block content"> <section class="block content">
<h2 class="title is-4">{% trans "Details" %}</h2> <header class="columns is-mobile">
<dl> <div class="column">
<div class="is-flex"> <h2 class="title is-4 mb-0">{% trans "Notes" %}</h2>
<dt>{% trans "Software:" %}</dt>
<dd>{{ server.application_type }}</dd>
</div> </div>
<div class="is-flex"> <div class="column is-narrow">
<dt>{% trans "Version:" %}</dt> {% trans "Edit" as button_text %}
<dd>{{ server.application_version }}</dd> {% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-notes" %}
</div> </div>
<div class="is-flex"> </header>
<dt>{% trans "Status:" %}</dt> {% if server.notes %}
<dd>Federated</dd> <p id="hide-edit-notes">{{ server.notes }}</p>
</div> {% endif %}
</dl> <form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit-notes">
{% csrf_token %}
<p>
<label class="is-sr-only" for="id_notes">Notes:</label>
<textarea name="notes" cols="None" rows="None" class="textarea" id="id_notes">{{ server.notes|default:"" }}</textarea>
</p>
<button type="submit" class="button is-primary">{% trans "Save" %}</button>
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="edit-notes" %}
</form>
</section> </section>
<section class="block content"> <section class="block content">
<h2 class="title is-4">{% trans "Activity" %}</h2> <h2 class="title is-4">{% trans "Actions" %}</h2>
<dl> {% if server.status != 'blocked' %}
<div class="is-flex"> <form class="block" method="post" action="{% url 'settings-federated-server-block' server.id %}">
<dt>{% trans "Users:" %}</dt> {% csrf_token %}
<dd> <button class="button is-danger">{% trans "Block" %}</button>
{{ users.count }} <p class="help">{% trans "All users from this instance will be deactivated." %}</p>
{% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.id }}">{% trans "View all" %}</a>){% endif %} </form>
</dd> {% else %}
</div> <form class="block" method="post" action="{% url 'settings-federated-server-unblock' server.id %}">
<div class="is-flex"> {% csrf_token %}
<dt>{% trans "Reports:" %}</dt> <button class="button">{% trans "Un-block" %}</button>
<dd> <p class="help">{% trans "All users from this instance will be re-activated." %}</p>
{{ reports.count }} </form>
{% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.id }}">{% trans "View all" %}</a>){% endif %} {% endif %}
</dd>
</div>
<div class="is-flex">
<dt>{% trans "Followed by us:" %}</dt>
<dd>
{{ followed_by_us.count }}
</dd>
</div>
<div class="is-flex">
<dt>{% trans "Followed by them:" %}</dt>
<dd>
{{ followed_by_them.count }}
</dd>
</div>
<div class="is-flex">
<dt>{% trans "Blocked by us:" %}</dt>
<dd>
{{ blocked_by_us.count }}
</dd>
</div>
</dl>
</section> </section>
{% endblock %} {% endblock %}

View file

@ -4,8 +4,15 @@
{% block header %}{% trans "Federated Servers" %}{% endblock %} {% block header %}{% trans "Federated Servers" %}{% endblock %}
{% block panel %} {% block edit-button %}
<a href="{% url 'settings-add-federated-server' %}">
<span class="icon icon-plus" title="{% trans 'Add server' %}">
<span class="is-sr-only">{% trans "Add server" %}</span>
</span>
</a>
{% endblock %}
{% block panel %}
<table class="table is-striped"> <table class="table is-striped">
<tr> <tr>
{% url 'settings-federation' as url %} {% url 'settings-federation' as url %}

View file

@ -1,4 +1,5 @@
""" testing models """ """ testing models """
from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
from bookwyrm import models from bookwyrm import models
@ -9,6 +10,22 @@ from bookwyrm.settings import DOMAIN
class BaseModel(TestCase): class BaseModel(TestCase):
""" functionality shared across models """ """ 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): def test_remote_id(self):
""" these should be generated """ """ these should be generated """
instance = base_model.BookWyrmModel() instance = base_model.BookWyrmModel()
@ -18,11 +35,8 @@ class BaseModel(TestCase):
def test_remote_id_with_user(self): def test_remote_id_with_user(self):
""" format of remote id when there's a user object """ """ 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 = base_model.BookWyrmModel()
instance.user = user instance.user = self.local_user
instance.id = 1 instance.id = 1
expected = instance.get_remote_id() expected = instance.get_remote_id()
self.assertEqual(expected, "https://%s/user/mouse/bookwyrmmodel/1" % DOMAIN) self.assertEqual(expected, "https://%s/user/mouse/bookwyrmmodel/1" % DOMAIN)
@ -42,3 +56,66 @@ class BaseModel(TestCase):
instance.remote_id = None instance.remote_id = None
base_model.set_remote_id(None, instance, False) base_model.set_remote_id(None, instance, False)
self.assertIsNone(instance.remote_id) 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))

View file

@ -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")

View file

@ -4,8 +4,9 @@ from unittest.mock import patch
from django.http import HttpResponseNotAllowed, HttpResponseNotFound from django.http import HttpResponseNotAllowed, HttpResponseNotFound
from django.test import TestCase, Client 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 # pylint: disable=too-many-public-methods
@ -15,6 +16,7 @@ class Inbox(TestCase):
def setUp(self): def setUp(self):
""" basic user and book data """ """ basic user and book data """
self.client = Client() self.client = Client()
self.factory = RequestFactory()
local_user = models.User.objects.create_user( local_user = models.User.objects.create_user(
"mouse@example.com", "mouse@example.com",
"mouse@mouse.com", "mouse@mouse.com",
@ -106,3 +108,26 @@ class Inbox(TestCase):
"/inbox", json.dumps(activity), content_type="application/json" "/inbox", json.dumps(activity), content_type="application/json"
) )
self.assertEqual(result.status_code, 200) 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))

View file

@ -47,6 +47,39 @@ class BookViews(TestCase):
) )
models.SiteSettings.objects.create() 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'<input type="date" name="first_published_date" class="input" id="id_first_published_date" value="{first_published_date}">',
)
self.assertContains(
result,
f'<input type="date" name="published_date" class="input" id="id_published_date" value="{published_date}">',
)
def test_book_page(self): def test_book_page(self):
""" there are so many views, this just makes sure it LOADS """ """ there are so many views, this just makes sure it LOADS """
view = views.Book.as_view() view = views.Book.as_view()

View file

@ -1,9 +1,10 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import forms, models, views
class FederationViews(TestCase): class FederationViews(TestCase):
@ -19,6 +20,16 @@ class FederationViews(TestCase):
local=True, local=True,
localname="mouse", 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() models.SiteSettings.objects.create()
def test_federation_page(self): def test_federation_page(self):
@ -44,3 +55,75 @@ class FederationViews(TestCase):
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() result.render()
self.assertEqual(result.status_code, 200) 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")

View file

@ -146,6 +146,15 @@ class ViewsHelpers(TestCase):
self.assertIsInstance(result, models.User) self.assertIsInstance(result, models.User)
self.assertEqual(result.username, "mouse@example.com") 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, _): def test_handle_reading_status_to_read(self, _):
""" posts shelve activities """ """ posts shelve activities """
shelf = self.local_user.shelf_set.get(identifier="to-read") shelf = self.local_user.shelf_set.get(identifier="to-read")
@ -190,66 +199,6 @@ class ViewsHelpers(TestCase):
) )
self.assertFalse(models.GeneratedNote.objects.exists()) 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, _): def test_get_annotated_users(self, _):
""" list of people you might know """ """ list of people you might know """
user_1 = models.User.objects.create_user( user_1 = models.User.objects.create_user(

View file

@ -68,6 +68,21 @@ urlpatterns = [
views.FederatedServer.as_view(), views.FederatedServer.as_view(),
name="settings-federated-server", name="settings-federated-server",
), ),
re_path(
r"^settings/federation/(?P<server>\d+)/block?$",
views.federation.block_server,
name="settings-federated-server-block",
),
re_path(
r"^settings/federation/(?P<server>\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( re_path(
r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites" r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites"
), ),

View file

@ -5,7 +5,8 @@ from .block import Block, unblock
from .books import Book, EditBook, ConfirmEditBook, Editions from .books import Book, EditBook, ConfirmEditBook, Editions
from .books import upload_cover, add_description, switch_edition, resolve_book from .books import upload_cover, add_description, switch_edition, resolve_book
from .directory import Directory 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 .feed import DirectMessage, Feed, Replies, Status
from .follow import follow, unfollow from .follow import follow, unfollow
from .follow import accept_follow_request, delete_follow_request from .follow import accept_follow_request, delete_follow_request

View file

@ -1,12 +1,13 @@
""" manage federated servers """ """ manage federated servers """
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.core.paginator import Paginator 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.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View 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 from bookwyrm.settings import PAGE_LENGTH
@ -30,14 +31,38 @@ class Federation(View):
sort = request.GET.get("sort") sort = request.GET.get("sort")
sort_fields = ["created_date", "application_type", "server_name"] sort_fields = ["created_date", "application_type", "server_name"]
if sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]: if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
servers = servers.order_by(sort) sort = "created_date"
servers = servers.order_by(sort)
paginated = Paginator(servers, PAGE_LENGTH) 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) 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(login_required, name="dispatch")
@method_decorator( @method_decorator(
permission_required("bookwyrm.control_federation", raise_exception=True), permission_required("bookwyrm.control_federation", raise_exception=True),
@ -61,3 +86,32 @@ class FederatedServer(View):
), ),
} }
return TemplateResponse(request, "settings/federated_server.html", data) 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)

View file

@ -12,7 +12,7 @@ from bookwyrm import activitystreams, forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH, STREAMS from bookwyrm.settings import PAGE_LENGTH, STREAMS
from .helpers import get_user_from_username, privacy_filter, get_suggested_users 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 # pylint: disable= no-self-use
@ -113,7 +113,7 @@ class Status(View):
return HttpResponseNotFound() return HttpResponseNotFound()
# make sure the user is authorized to see the status # 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() return HttpResponseNotFound()
if is_api_request(request): if is_api_request(request):

View file

@ -10,7 +10,7 @@ from django.views.decorators.http import require_POST
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.status import create_generated_note 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 # pylint: disable= no-self-use
@ -26,7 +26,7 @@ class Goal(View):
if not goal and user != request.user: if not goal and user != request.user:
return HttpResponseNotFound() 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() return HttpResponseNotFound()
data = { data = {

View file

@ -32,30 +32,6 @@ def is_bookwyrm_request(request):
return True 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): def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
""" filter objects that have "user" and "privacy" fields """ """ filter objects that have "user" and "privacy" fields """
privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"] privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"]

View file

@ -1,9 +1,10 @@
""" incoming activities """ """ incoming activities """
import json import json
import re
from urllib.parse import urldefrag from urllib.parse import urldefrag
from django.http import HttpResponse from django.http import HttpResponse, HttpResponseNotFound
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@ -12,6 +13,7 @@ import requests
from bookwyrm import activitypub, models from bookwyrm import activitypub, models
from bookwyrm.tasks import app from bookwyrm.tasks import app
from bookwyrm.signatures import Signature from bookwyrm.signatures import Signature
from bookwyrm.utils import regex
@method_decorator(csrf_exempt, name="dispatch") @method_decorator(csrf_exempt, name="dispatch")
@ -21,6 +23,10 @@ class Inbox(View):
def post(self, request, username=None): def post(self, request, username=None):
""" only works as POST request """ """ 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 # make sure the user's inbox even exists
if username: if username:
try: try:
@ -34,6 +40,10 @@ class Inbox(View):
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
# let's be extra sure we didn't block this domain
if is_blocked_activity(activity_json):
return HttpResponseForbidden()
if ( if (
not "object" in activity_json not "object" in activity_json
or not "type" in activity_json or not "type" in activity_json
@ -54,6 +64,25 @@ class Inbox(View):
return HttpResponse() 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 @app.task
def activity_task(activity_json): def activity_task(activity_json):
""" do something with this json we think is legit """ """ do something with this json we think is legit """

View file

@ -13,7 +13,7 @@ from django.views.decorators.http import require_POST
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager 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 from .helpers import get_user_from_username
# pylint: disable=no-self-use # pylint: disable=no-self-use
@ -92,7 +92,7 @@ class List(View):
def get(self, request, list_id): def get(self, request, list_id):
""" display a book list """ """ display a book list """
book_list = get_object_or_404(models.List, id=list_id) 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() return HttpResponseNotFound()
if is_api_request(request): if is_api_request(request):
@ -176,7 +176,7 @@ class Curate(View):
def add_book(request): def add_book(request):
""" put a book on a list """ """ put a book on a list """
book_list = get_object_or_404(models.List, id=request.POST.get("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() return HttpResponseNotFound()
book = get_object_or_404(models.Edition, id=request.POST.get("book")) book = get_object_or_404(models.Edition, id=request.POST.get("book"))

View file

@ -16,7 +16,7 @@ from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request, get_edition, get_user_from_username 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 # pylint: disable= no-self-use
@ -43,7 +43,7 @@ class Shelf(View):
shelf = user.shelf_set.get(identifier=shelf_identifier) shelf = user.shelf_set.get(identifier=shelf_identifier)
except models.Shelf.DoesNotExist: except models.Shelf.DoesNotExist:
return HttpResponseNotFound() return HttpResponseNotFound()
if not object_visible_to_user(request.user, shelf): if not shelf.visible_to_user(request.user):
return HttpResponseNotFound() return HttpResponseNotFound()
# this is a constructed "all books" view, with a fake "shelf" obj # this is a constructed "all books" view, with a fake "shelf" obj
else: else:

View file

@ -17,7 +17,7 @@ from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from .helpers import get_user_from_username, is_api_request 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 # pylint: disable= no-self-use
@ -80,7 +80,7 @@ class User(View):
goal = models.AnnualGoal.objects.filter( goal = models.AnnualGoal.objects.filter(
user=user, year=timezone.now().year user=user, year=timezone.now().year
).first() ).first()
if not object_visible_to_user(request.user, goal): if goal and not goal.visible_to_user(request.user):
goal = None goal = None
data = { data = {
"user": user, "user": user,

View file

@ -149,7 +149,7 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # 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_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static")) STATIC_ROOT = os.path.join(BASE_DIR, env("STATIC_ROOT", "static"))

19
certbot.sh Normal file
View file

@ -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

View file

@ -20,6 +20,8 @@ services:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
networks: networks:
- main - main
ports:
- 5432:5432
web: web:
build: . build: .
env_file: .env env_file: .env

72
nginx/production Normal file
View file

@ -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/;
# }
# }