mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-01-08 16:25:27 +00:00
commit
0ba8fe862d
41 changed files with 976 additions and 185 deletions
|
@ -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
|
50
.env.prod.example
Normal file
50
.env.prod.example
Normal 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
3
.gitignore
vendored
|
@ -24,3 +24,6 @@
|
|||
|
||||
#Node tools
|
||||
/node_modules/
|
||||
|
||||
#nginx
|
||||
nginx/default.conf
|
||||
|
|
44
README.md
44
README.md
|
@ -73,8 +73,8 @@ Since the project is still in its early stages, the features are growing every d
|
|||
Web backend
|
||||
- [Django](https://www.djangoproject.com/) web server
|
||||
- [PostgreSQL](https://www.postgresql.org/) database
|
||||
- [ActivityPub](http://activitypub.rocks/) federation
|
||||
- [Celery](http://celeryproject.org/) task queuing
|
||||
- [ActivityPub](https://activitypub.rocks/) federation
|
||||
- [Celery](https://docs.celeryproject.org/) task queuing
|
||||
- [Redis](https://redis.io/) task backend
|
||||
- [Redis (again)](https://redis.io/) activity stream manager
|
||||
|
||||
|
@ -91,10 +91,15 @@ Deployment
|
|||
|
||||
## Setting up the developer environment
|
||||
|
||||
Set up the environment file:
|
||||
Set up the development environment file:
|
||||
|
||||
``` bash
|
||||
cp .env.example .env
|
||||
cp .env.dev.example .env
|
||||
```
|
||||
|
||||
Set up nginx for development `nginx/default.conf`:
|
||||
``` bash
|
||||
cp nginx/development nginx/default.conf
|
||||
```
|
||||
|
||||
For most testing, you'll want to use ngrok. Remember to set the DOMAIN in `.env` to your ngrok domain.
|
||||
|
@ -108,7 +113,7 @@ docker-compose run --rm web python manage.py initdb
|
|||
docker-compose up
|
||||
```
|
||||
|
||||
Once the build is complete, you can access the instance at `localhost:1333`
|
||||
Once the build is complete, you can access the instance at `http://localhost:1333`
|
||||
|
||||
### Editing static files
|
||||
If you edit the CSS or JavaScript, you will need to run Django's `collectstatic` command in order for your changes to have effect. You can do this by running:
|
||||
|
@ -160,26 +165,35 @@ Instructions for running BookWyrm in production:
|
|||
|
||||
- Get the application code:
|
||||
`git clone git@github.com:mouse-reeve/bookwyrm.git`
|
||||
- Switch to the `production` branch
|
||||
- Switch to the `production` branch:
|
||||
`git checkout production`
|
||||
- Create your environment variables file
|
||||
`cp .env.example .env`
|
||||
- Add your domain, email address, SMTP credentials
|
||||
- Set a secure redis password and secret key
|
||||
- Set a secure database password for postgres
|
||||
- Create your environment variables file, `cp .env.prod.example .env`, and update the following:
|
||||
- `SECRET_KEY` | A difficult to guess, secret string of characers
|
||||
- `DOMAIN` | Your web domain
|
||||
- `EMAIL` | Email address to be used for certbot domain verification
|
||||
- `POSTGRES_PASSWORD` | Set a secure password for the database
|
||||
- `REDIS_ACTIVITY_PASSWORD` | Set a secure password for Redis Activity subsystem
|
||||
- `REDIS_BROKER_PASSWORD` | Set a secure password for Redis queue broker subsystem
|
||||
- `FLOWER_USER` | Your own username for accessing Flower queue monitor
|
||||
- `FLOWER_PASSWORD` | Your own secure password for accessing Flower queue monitor
|
||||
- Update your nginx configuration in `nginx/default.conf`
|
||||
- Replace `your-domain.com` with your domain name
|
||||
- If you aren't using the `www` subdomain, remove the www.your-domain.com version of the domain from the `server_name` in the first server block in `nginx/default.conf` and remove the `-d www.${DOMAIN}` flag at the end of the `certbot` command in `docker-compose.yml`.
|
||||
- If you are running another web-server on your host machine, you will need to follow the [reverse-proxy instructions](#running-bookwyrm-behind-a-reverse-proxy)
|
||||
- Configure nginx
|
||||
- Make a copy of the production template config and set it for use in nginx `cp nginx/production nginx/default.conf`
|
||||
- Update `nginx/default.conf`:
|
||||
- Replace `your-domain.com` with your domain name
|
||||
- If you aren't using the `www` subdomain, remove the www.your-domain.com version of the domain from the `server_name` in the first server block in `nginx/default.conf` and remove the `-d www.${DOMAIN}` flag at the end of the `certbot` command in `docker-compose.yml`.
|
||||
- If you are running another web-server on your host machine, you will need to follow the [reverse-proxy instructions](#running-bookwyrm-behind-a-reverse-proxy)
|
||||
- If you need to initialize your certbot for your domain, set `CERTBOT_INIT=true` in your `.env` file
|
||||
- Run the application (this should also set up a Certbot ssl cert for your domain) with
|
||||
`docker-compose up --build`, and make sure all the images build successfully
|
||||
- If you are running other services on your host machine, you may run into errors where services fail when attempting to bind to a port.
|
||||
See the [troubleshooting guide](#port-conflicts) for advice on resolving this.
|
||||
- When docker has built successfully, stop the process with `CTRL-C`
|
||||
- Comment out the `command: certonly...` line in `docker-compose.yml`, and uncomment the following line (`command: renew ...`) so that the certificate will be automatically renewed.
|
||||
- Uncomment the https redirect and `server` block in `nginx/default.conf` (lines 17-48).
|
||||
- If you set `CERTBOT_INIT=true` earlier, set it now as `CERTBOT_INIT=false` so that certbot runs in renew mode
|
||||
- Run docker-compose in the background with: `docker-compose up -d`
|
||||
- Initialize the database with: `./bw-dev initdb`
|
||||
- Set up schedule backups with cron that runs that `docker-compose exec db pg_dump -U <databasename>` and saves the backup to a safe location
|
||||
|
||||
Congrats! You did it, go to your domain and enjoy the fruits of your labors.
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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()
|
||||
|
|
37
bookwyrm/migrations/0063_auto_20210407_1827.py
Normal file
37
bookwyrm/migrations/0063_auto_20210407_1827.py
Normal 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,
|
||||
),
|
||||
),
|
||||
]
|
13
bookwyrm/migrations/0064_merge_20210410_1633.py
Normal file
13
bookwyrm/migrations/0064_merge_20210410_1633.py
Normal 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 = []
|
13
bookwyrm/migrations/0065_merge_20210411_1702.py
Normal file
13
bookwyrm/migrations/0065_merge_20210411_1702.py
Normal 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 = []
|
27
bookwyrm/migrations/0066_user_deactivation_reason.py
Normal file
27
bookwyrm/migrations/0066_user_deactivation_reason.py
Normal 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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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/"
|
||||
|
|
|
@ -6,7 +6,14 @@
|
|||
{% block content %}
|
||||
|
||||
<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>
|
||||
|
||||
<div class="block columns">
|
||||
|
|
58
bookwyrm/templates/settings/edit_server.html
Normal file
58
bookwyrm/templates/settings/edit_server.html
Normal 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 %}
|
|
@ -4,64 +4,112 @@
|
|||
|
||||
{% block header %}
|
||||
{{ 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>
|
||||
{% endblock %}
|
||||
|
||||
{% 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">
|
||||
<h2 class="title is-4">{% trans "Details" %}</h2>
|
||||
<dl>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Software:" %}</dt>
|
||||
<dd>{{ server.application_type }}</dd>
|
||||
<header class="columns is-mobile">
|
||||
<div class="column">
|
||||
<h2 class="title is-4 mb-0">{% trans "Notes" %}</h2>
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Version:" %}</dt>
|
||||
<dd>{{ server.application_version }}</dd>
|
||||
<div class="column is-narrow">
|
||||
{% trans "Edit" as button_text %}
|
||||
{% include 'snippets/toggle/open_button.html' with text=button_text icon="pencil" controls_text="edit-notes" %}
|
||||
</div>
|
||||
<div class="is-flex">
|
||||
<dt>{% trans "Status:" %}</dt>
|
||||
<dd>Federated</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</header>
|
||||
{% if server.notes %}
|
||||
<p id="hide-edit-notes">{{ server.notes }}</p>
|
||||
{% endif %}
|
||||
<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 class="block 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>
|
||||
<h2 class="title is-4">{% trans "Actions" %}</h2>
|
||||
{% if server.status != 'blocked' %}
|
||||
<form class="block" method="post" action="{% url 'settings-federated-server-block' server.id %}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-danger">{% trans "Block" %}</button>
|
||||
<p class="help">{% trans "All users from this instance will be deactivated." %}</p>
|
||||
</form>
|
||||
{% else %}
|
||||
<form class="block" method="post" action="{% url 'settings-federated-server-unblock' server.id %}">
|
||||
{% csrf_token %}
|
||||
<button class="button">{% trans "Un-block" %}</button>
|
||||
<p class="help">{% trans "All users from this instance will be re-activated." %}</p>
|
||||
</form>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -4,8 +4,15 @@
|
|||
|
||||
{% 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">
|
||||
<tr>
|
||||
{% url 'settings-federation' as url %}
|
||||
|
|
|
@ -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))
|
||||
|
|
67
bookwyrm/tests/models/test_federated_server.py
Normal file
67
bookwyrm/tests/models/test_federated_server.py
Normal 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")
|
|
@ -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))
|
||||
|
|
|
@ -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'<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):
|
||||
""" there are so many views, this just makes sure it LOADS """
|
||||
view = views.Book.as_view()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -68,6 +68,21 @@ urlpatterns = [
|
|||
views.FederatedServer.as_view(),
|
||||
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(
|
||||
r"^settings/invites/?$", views.ManageInvites.as_view(), name="settings-invites"
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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 """
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"))
|
||||
|
|
19
certbot.sh
Normal file
19
certbot.sh
Normal 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
|
|
@ -20,6 +20,8 @@ services:
|
|||
- pgdata:/var/lib/postgresql/data
|
||||
networks:
|
||||
- main
|
||||
ports:
|
||||
- 5432:5432
|
||||
web:
|
||||
build: .
|
||||
env_file: .env
|
||||
|
|
72
nginx/production
Normal file
72
nginx/production
Normal 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/;
|
||||
# }
|
||||
# }
|
Loading…
Reference in a new issue