Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-02-22 08:40:24 -08:00
commit 888987f19d
4 changed files with 100 additions and 88 deletions

View file

@ -112,36 +112,42 @@ Once the build is complete, you can access the instance at `localhost:1333`
## Installing in Production ## 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. This project is still young and isn't, at the momoment, very stable, so please procede with caution when running in production.
### Server setup ### Server setup
- Get a domain name and set up DNS for your server - 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 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 Docker and docker-compose
### Install and configure BookWyrm ### 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: - 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.example .env` `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 redis password and secret key
- Set a secure database password for postgres - Set a secure database password for postgres
- 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
- Run the application (this should also set up a Certbot ssl cert for your domain) - Run the application (this should also set up a Certbot ssl cert for your domain) with
`docker-compose up --build` `docker-compose up --build`, and make sure all the images build successfully
Make sure all the images build successfully
- 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` - Comment out the `command: certonly...` line in `docker-compose.yml`
- Run docker-compose in the background - Run docker-compose in the background with: `docker-compose up -d`
`docker-compose up -d` - Initialize the database with: `./bw-dev initdb`
- Initialize the database - 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
`./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 locationgi 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
### Configure your instance ### 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) - Make your account a superuser (warning: do *not* use django's `createsuperuser` command)
- On your server, open the django shell - On your server, open the django shell
`./bw-dev shell` `./bw-dev shell`

View file

@ -188,7 +188,7 @@ def handle_block(activity):
''' blocking a user ''' ''' blocking a user '''
# create "block" databse entry # create "block" databse entry
activitypub.Block(**activity).to_model(models.UserBlocks) 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 @app.task

View file

@ -1,8 +1,7 @@
''' defines relationships between users ''' ''' defines relationships between users '''
from django.apps import apps 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.db.models import Q
from django.dispatch import receiver
from bookwyrm import activitypub from bookwyrm import activitypub
from .activitypub_mixin import ActivitypubMixin, ActivityMixin from .activitypub_mixin import ActivitypubMixin, ActivityMixin
@ -61,6 +60,20 @@ class UserFollows(ActivitypubMixin, UserRelationship):
status = 'follows' status = 'follows'
activity_serializer = activitypub.Follow 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 @classmethod
def from_request(cls, follow_request): def from_request(cls, follow_request):
@ -79,23 +92,25 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
def save(self, *args, broadcast=True, **kwargs): def save(self, *args, broadcast=True, **kwargs):
''' make sure the follow or block relationship doesn't already exist ''' ''' make sure the follow or block relationship doesn't already exist '''
try: # don't create a request if a follow already exists
UserFollows.objects.get( if UserFollows.objects.filter(
user_subject=self.user_subject, user_subject=self.user_subject,
user_object=self.user_object, user_object=self.user_object,
) ).exists():
# blocking in either direction is a no-go raise IntegrityError()
UserBlocks.objects.get( # blocking in either direction is a no-go
user_subject=self.user_subject, if UserBlocks.objects.filter(
user_object=self.user_object, Q(
) user_subject=self.user_subject,
UserBlocks.objects.get( user_object=self.user_object,
user_subject=self.user_object, ) | Q(
user_object=self.user_subject, user_subject=self.user_object,
) user_object=self.user_subject,
return None )
except (UserFollows.DoesNotExist, UserBlocks.DoesNotExist): ).exists():
super().save(*args, **kwargs) raise IntegrityError()
super().save(*args, **kwargs)
if broadcast and self.user_subject.local and not self.user_object.local: if broadcast and self.user_subject.local and not self.user_object.local:
self.broadcast(self.to_activity(), self.user_subject) self.broadcast(self.to_activity(), self.user_subject)
@ -143,20 +158,15 @@ class UserBlocks(ActivityMixin, UserRelationship):
status = 'blocks' status = 'blocks'
activity_serializer = activitypub.Block 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) UserFollows.objects.filter(
#pylint: disable=unused-argument Q(user_subject=self.user_subject, user_object=self.user_object) | \
def execute_after_save(sender, instance, created, *args, **kwargs): Q(user_subject=self.user_object, user_object=self.user_subject)
''' remove follow or follow request rels after a block is created ''' ).delete()
UserFollows.objects.filter( UserFollowRequest.objects.filter(
Q(user_subject=instance.user_subject, Q(user_subject=self.user_subject, user_object=self.user_object) | \
user_object=instance.user_object) | \ Q(user_subject=self.user_object, user_object=self.user_subject)
Q(user_subject=instance.user_object, ).delete()
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()

View file

@ -6,7 +6,6 @@ from django.apps import apps
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from bookwyrm import activitypub from bookwyrm import activitypub
@ -172,15 +171,23 @@ class User(OrderedCollectionPageMixin, AbstractUser):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
''' populate fields for new local users ''' ''' 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): if not self.local and not re.match(regex.full_username, self.username):
# generate a username that uses the domain (webfinger format) # generate a username that uses the domain (webfinger format)
actor_parts = urlparse(self.remote_id) actor_parts = urlparse(self.remote_id)
self.username = '%s@%s' % (self.username, actor_parts.netloc) 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: # this user already exists, no need to populate fields
return super().save(*args, **kwargs) 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 # populate fields for local users
self.remote_id = 'https://%s/user/%s' % (DOMAIN, self.localname) 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.shared_inbox = 'https://%s/inbox' % DOMAIN
self.outbox = '%s/outbox' % self.remote_id 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 @property
def local_path(self): def local_path(self):
@ -280,42 +312,6 @@ class AnnualGoal(BookWyrmModel):
finish_date__year__gte=self.year).count() 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 @app.task
def set_remote_server(user_id): def set_remote_server(user_id):
''' figure out the user's remote server in the background ''' ''' figure out the user's remote server in the background '''