diff --git a/README.md b/README.md index 6a624ab3..c8b6e92b 100644 --- a/README.md +++ b/README.md @@ -112,36 +112,42 @@ Once the build is complete, you can access the instance at `localhost:1333` ## Installing in Production This project is still young and isn't, at the momoment, very stable, so please procede with caution when running in production. + ### Server setup - Get a domain name and set up DNS for your server - Set your server up with appropriate firewalls for running a web application (this instruction set is tested again Ubuntu 20.04) - - Set up a mailgun account and the appropriate DNS settings + - Set up an email service (such as mailgun) and the appropriate SMTP/DNS settings - Install Docker and docker-compose + ### Install and configure BookWyrm + +The `production` branch of BookWyrm contains a number of tools not on the `main` branch that are suited for running in production, such as `docker-compose` changes to update the default commands or configuration of containers, and indivudal changes to container config to enable things like SSL or regular backups. + +Instructions for running BookWyrm in production: + - Get the application code: `git clone git@github.com:mouse-reeve/bookwyrm.git` - Switch to the `production` branch `git checkout production` - Create your environment variables file `cp .env.example .env` - - Add your domain, email address, mailgun credentials + - Add your domain, email address, SMTP credentials - Set a secure redis password and secret key - Set a secure database password for postgres - Update your nginx configuration in `nginx/default.conf` - Replace `your-domain.com` with your domain name - - Run the application (this should also set up a Certbot ssl cert for your domain) - `docker-compose up --build` - Make sure all the images build successfully + - 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 - When docker has built successfully, stop the process with `CTRL-C` - Comment out the `command: certonly...` line in `docker-compose.yml` - - Run docker-compose in the background - `docker-compose up -d` - - Initialize the database - `./bw-dev initdb` - - Set up schedule backups with cron that runs that `docker-compose exec db pg_dump -U ` and saves the backup to a safe locationgi - - Congrats! You did it, go to your domain and enjoy the fruits of your labors + - Run docker-compose in the background with: `docker-compose up -d` + - Initialize the database with: `./bw-dev initdb` + - Set up schedule backups with cron that runs that `docker-compose exec db pg_dump -U ` and saves the backup to a safe location + +Congrats! You did it, go to your domain and enjoy the fruits of your labors. + ### Configure your instance - - Register a user account in the applcation UI + - Register a user account in the application UI - Make your account a superuser (warning: do *not* use django's `createsuperuser` command) - On your server, open the django shell `./bw-dev shell` diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 18db1069..e0e69055 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -188,7 +188,7 @@ def handle_block(activity): ''' blocking a user ''' # create "block" databse entry activitypub.Block(**activity).to_model(models.UserBlocks) - # the removing relationships is handled in post-save hook in model + # the removing relationships is handled in model save @app.task diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index e2db5468..f4df3b83 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -1,8 +1,7 @@ ''' defines relationships between users ''' from django.apps import apps -from django.db import models, transaction +from django.db import models, transaction, IntegrityError from django.db.models import Q -from django.dispatch import receiver from bookwyrm import activitypub from .activitypub_mixin import ActivitypubMixin, ActivityMixin @@ -61,6 +60,20 @@ class UserFollows(ActivitypubMixin, UserRelationship): status = 'follows' activity_serializer = activitypub.Follow + def save(self, *args, **kwargs): + ''' really really don't let a user follow someone who blocked them ''' + # blocking in either direction is a no-go + if UserBlocks.objects.filter( + Q( + user_subject=self.user_subject, + user_object=self.user_object, + ) | Q( + user_subject=self.user_object, + user_object=self.user_subject, + ) + ).exists(): + raise IntegrityError() + super().save(*args, **kwargs) @classmethod def from_request(cls, follow_request): @@ -79,23 +92,25 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): def save(self, *args, broadcast=True, **kwargs): ''' make sure the follow or block relationship doesn't already exist ''' - try: - UserFollows.objects.get( + # don't create a request if a follow already exists + if UserFollows.objects.filter( user_subject=self.user_subject, user_object=self.user_object, - ) - # blocking in either direction is a no-go - UserBlocks.objects.get( - user_subject=self.user_subject, - user_object=self.user_object, - ) - UserBlocks.objects.get( - user_subject=self.user_object, - user_object=self.user_subject, - ) - return None - except (UserFollows.DoesNotExist, UserBlocks.DoesNotExist): - super().save(*args, **kwargs) + ).exists(): + raise IntegrityError() + # blocking in either direction is a no-go + if UserBlocks.objects.filter( + Q( + user_subject=self.user_subject, + user_object=self.user_object, + ) | Q( + user_subject=self.user_object, + user_object=self.user_subject, + ) + ).exists(): + raise IntegrityError() + + super().save(*args, **kwargs) if broadcast and self.user_subject.local and not self.user_object.local: self.broadcast(self.to_activity(), self.user_subject) @@ -143,20 +158,15 @@ class UserBlocks(ActivityMixin, UserRelationship): status = 'blocks' activity_serializer = activitypub.Block + def save(self, *args, **kwargs): + ''' remove follow or follow request rels after a block is created ''' + super().save(*args, **kwargs) -@receiver(models.signals.post_save, sender=UserBlocks) -#pylint: disable=unused-argument -def execute_after_save(sender, instance, created, *args, **kwargs): - ''' remove follow or follow request rels after a block is created ''' - UserFollows.objects.filter( - Q(user_subject=instance.user_subject, - user_object=instance.user_object) | \ - Q(user_subject=instance.user_object, - user_object=instance.user_subject) - ).delete() - UserFollowRequest.objects.filter( - Q(user_subject=instance.user_subject, - user_object=instance.user_object) | \ - Q(user_subject=instance.user_object, - user_object=instance.user_subject) - ).delete() + UserFollows.objects.filter( + Q(user_subject=self.user_subject, user_object=self.user_object) | \ + Q(user_subject=self.user_object, user_object=self.user_subject) + ).delete() + UserFollowRequest.objects.filter( + Q(user_subject=self.user_subject, user_object=self.user_object) | \ + Q(user_subject=self.user_object, user_object=self.user_subject) + ).delete() diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index da717d2e..7b0f9a91 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -6,7 +6,6 @@ from django.apps import apps from django.contrib.auth.models import AbstractUser from django.core.validators import MinValueValidator from django.db import models -from django.dispatch import receiver from django.utils import timezone from bookwyrm import activitypub @@ -172,15 +171,23 @@ class User(OrderedCollectionPageMixin, AbstractUser): def save(self, *args, **kwargs): ''' populate fields for new local users ''' - # this user already exists, no need to populate fields if not self.local and not re.match(regex.full_username, self.username): # generate a username that uses the domain (webfinger format) actor_parts = urlparse(self.remote_id) self.username = '%s@%s' % (self.username, actor_parts.netloc) - return super().save(*args, **kwargs) + super().save(*args, **kwargs) + return - if self.id or not self.local: - return super().save(*args, **kwargs) + # this user already exists, no need to populate fields + if self.id: + super().save(*args, **kwargs) + return + + # this is a new remote user, we need to set their remote server field + if not self.local: + super().save(*args, **kwargs) + set_remote_server.delay(self.id) + return # populate fields for local users self.remote_id = 'https://%s/user/%s' % (DOMAIN, self.localname) @@ -188,7 +195,32 @@ class User(OrderedCollectionPageMixin, AbstractUser): self.shared_inbox = 'https://%s/inbox' % DOMAIN self.outbox = '%s/outbox' % self.remote_id - return super().save(*args, **kwargs) + # an id needs to be set before we can proceed with related models + super().save(*args, **kwargs) + + # create keys and shelves for new local users + self.key_pair = KeyPair.objects.create( + remote_id='%s/#main-key' % self.remote_id) + self.save(broadcast=False) + + shelves = [{ + 'name': 'To Read', + 'identifier': 'to-read', + }, { + 'name': 'Currently Reading', + 'identifier': 'reading', + }, { + 'name': 'Read', + 'identifier': 'read', + }] + + for shelf in shelves: + Shelf( + name=shelf['name'], + identifier=shelf['identifier'], + user=self, + editable=False + ).save(broadcast=False) @property def local_path(self): @@ -280,42 +312,6 @@ class AnnualGoal(BookWyrmModel): finish_date__year__gte=self.year).count() - -@receiver(models.signals.post_save, sender=User) -#pylint: disable=unused-argument -def execute_after_save(sender, instance, created, *args, **kwargs): - ''' create shelves for new users ''' - if not created: - return - - if not instance.local: - set_remote_server.delay(instance.id) - return - - instance.key_pair = KeyPair.objects.create( - remote_id='%s/#main-key' % instance.remote_id) - instance.save(broadcast=False) - - shelves = [{ - 'name': 'To Read', - 'identifier': 'to-read', - }, { - 'name': 'Currently Reading', - 'identifier': 'reading', - }, { - 'name': 'Read', - 'identifier': 'read', - }] - - for shelf in shelves: - Shelf( - name=shelf['name'], - identifier=shelf['identifier'], - user=instance, - editable=False - ).save(broadcast=False) - - @app.task def set_remote_server(user_id): ''' figure out the user's remote server in the background '''