Use custom model fields in user model

This commit is contained in:
Mouse Reeve 2020-11-30 10:32:13 -08:00
parent 96563598bf
commit 74a58e5267
8 changed files with 429 additions and 124 deletions

View file

@ -2,7 +2,7 @@
import inspect import inspect
import sys import sys
from .base_activity import ActivityEncoder, PublicKey, Signature from .base_activity import ActivityEncoder, Signature
from .base_activity import Link, Mention from .base_activity import Link, Mention
from .base_activity import ActivitySerializerError, resolve_remote_id from .base_activity import ActivitySerializerError, resolve_remote_id
from .image import Image from .image import Image
@ -10,7 +10,7 @@ from .note import Note, GeneratedNote, Article, Comment, Review, Quotation
from .note import Tombstone from .note import Tombstone
from .interaction import Boost, Like from .interaction import Boost, Like
from .ordered_collection import OrderedCollection, OrderedCollectionPage from .ordered_collection import OrderedCollection, OrderedCollectionPage
from .person import Person from .person import Person, PublicKey
from .book import Edition, Work, Author from .book import Edition, Work, Author
from .verbs import Create, Delete, Undo, Update from .verbs import Create, Delete, Undo, Update
from .verbs import Follow, Accept, Reject from .verbs import Follow, Accept, Reject

View file

@ -13,7 +13,6 @@ from django.db.models.fields import DateTimeField
from django.db.models.fields.files import ImageFileDescriptor from django.db.models.fields.files import ImageFileDescriptor
from django.db.models.query_utils import DeferredAttribute from django.db.models.query_utils import DeferredAttribute
from django.utils import timezone from django.utils import timezone
import requests
from bookwyrm.connectors import ConnectorException, get_data, get_image from bookwyrm.connectors import ConnectorException, get_data, get_image
@ -41,14 +40,6 @@ class Mention(Link):
type: str = 'Mention' type: str = 'Mention'
@dataclass
class PublicKey:
''' public key block '''
id: str
owner: str
publicKeyPem: str
@dataclass @dataclass
class Signature: class Signature:
''' public key block ''' ''' public key block '''

View file

@ -2,9 +2,18 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict from typing import Dict
from .base_activity import ActivityObject, PublicKey from .base_activity import ActivityObject
from .image import Image from .image import Image
@dataclass(init=False)
class PublicKey(ActivityObject):
''' public key block '''
owner: str
publicKeyPem: str
type: str = 'PublicKey'
@dataclass(init=False) @dataclass(init=False)
class Person(ActivityObject): class Person(ActivityObject):
''' actor activitypub json ''' ''' actor activitypub json '''

View file

@ -0,0 +1,188 @@
# Generated by Django 3.0.7 on 2020-11-30 18:19
import bookwyrm.models.base_model
import bookwyrm.models.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def copy_rsa_keys(app_registry, schema_editor):
db_alias = schema_editor.connection.alias
users = app_registry.get_model('bookwyrm', 'User')
keypair = app_registry.get_model('bookwyrm', 'KeyPair')
for user in users.objects.using(db_alias):
if user.public_key or user.private_key:
user.key_pair = keypair.objects.create(
private_key=user.private_key,
public_key=user.public_key
)
user.save()
class Migration(migrations.Migration):
dependencies = [
('bookwyrm', '0016_auto_20201129_0304'),
]
operations = [
migrations.CreateModel(
name='KeyPair',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(auto_now_add=True)),
('updated_date', models.DateTimeField(auto_now=True)),
('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])),
('private_key', models.TextField(blank=True, null=True)),
('public_key', bookwyrm.models.fields.TextField(blank=True, null=True)),
],
options={
'abstract': False,
},
bases=(bookwyrm.models.base_model.ActivitypubMixin, models.Model),
),
migrations.AddField(
model_name='user',
name='followers',
field=bookwyrm.models.fields.ManyToManyField(related_name='following', through='bookwyrm.UserFollows', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='author',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='book',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='connector',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='favorite',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='federatedserver',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='image',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='notification',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='readthrough',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='shelf',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='shelfbook',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='status',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='tag',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='user',
name='avatar',
field=bookwyrm.models.fields.ImageField(blank=True, null=True, upload_to='avatars/'),
),
migrations.AlterField(
model_name='user',
name='bookwyrm_user',
field=bookwyrm.models.fields.BooleanField(default=True),
),
migrations.AlterField(
model_name='user',
name='inbox',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='user',
name='local',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='user',
name='manually_approves_followers',
field=bookwyrm.models.fields.BooleanField(default=False),
),
migrations.AlterField(
model_name='user',
name='name',
field=bookwyrm.models.fields.CharField(blank=True, max_length=100, null=True),
),
migrations.AlterField(
model_name='user',
name='outbox',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='user',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, unique=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='user',
name='shared_inbox',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='user',
name='summary',
field=bookwyrm.models.fields.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='user',
name='username',
field=bookwyrm.models.fields.UsernameField(),
),
migrations.AlterField(
model_name='userblocks',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='userfollowrequest',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='userfollows',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AlterField(
model_name='usertag',
name='remote_id',
field=bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id]),
),
migrations.AddField(
model_name='user',
name='key_pair',
field=bookwyrm.models.fields.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owner', to='bookwyrm.KeyPair'),
),
migrations.RunPython(copy_rsa_keys),
]

View file

@ -17,6 +17,7 @@ from django.dispatch import receiver
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
from .fields import RemoteIdField
PrivacyLevels = models.TextChoices('Privacy', [ PrivacyLevels = models.TextChoices('Privacy', [
@ -30,7 +31,7 @@ class BookWyrmModel(models.Model):
''' shared fields ''' ''' shared fields '''
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True) updated_date = models.DateTimeField(auto_now=True)
remote_id = models.CharField(max_length=255, null=True) remote_id = RemoteIdField(null=True, activitypub_field='id')
def get_remote_id(self): def get_remote_id(self):
''' generate a url that resolves to the local object ''' ''' generate a url that resolves to the local object '''
@ -61,49 +62,18 @@ class ActivitypubMixin:
def to_activity(self, pure=False): def to_activity(self, pure=False):
''' convert from a model to an activity ''' ''' convert from a model to an activity '''
if pure: activity = {}
# works around bookwyrm-specific fields for vanilla AP services for field in self.__class__._meta.fields:
mappings = self.pure_activity_mappings key, value = field.to_activity(getattr(self, field.name))
else: activity[key] = value
# may include custom fields that bookwyrm instances will understand for related_object in self.__class__.meta.related_objects:
mappings = self.activity_mappings # TODO: check if it's serializable
related_model = related_object.related_model
key = related_object.name
related_values = getattr(self, key)
activity[key] = [i.remote_id for i in related_values]
fields = {} return self.activity_serializer(**activity_json).serialize()
for mapping in mappings:
if not hasattr(self, mapping.model_key) or not mapping.activity_key:
# this field on the model isn't serialized
continue
value = getattr(self, mapping.model_key)
model_field = getattr(self.__class__, mapping.model_key)
if hasattr(value, 'remote_id'):
# this is probably a foreign key field, which we want to
# serialize as just the remote_id url reference
value = value.remote_id
elif isinstance(model_field, \
(ManyToManyDescriptor, ReverseManyToOneDescriptor)):
value = [i.remote_id for i in value.all()]
elif isinstance(value, datetime):
value = value.isoformat()
elif isinstance(model_field, ImageFileDescriptor):
value = image_formatter(value)
# run the custom formatter function set in the model
formatted_value = mapping.activity_formatter(value)
if mapping.activity_key in fields and \
isinstance(fields[mapping.activity_key], list):
# there can be two database fields that map to the same AP list
# this happens in status tags, which combines user and book tags
fields[mapping.activity_key] += formatted_value
else:
fields[mapping.activity_key] = formatted_value
if pure:
return self.pure_activity_serializer(
**fields
).serialize()
return self.activity_serializer(
**fields
).serialize()
def to_create_activity(self, user, pure=False): def to_create_activity(self, user, pure=False):

165
bookwyrm/models/fields.py Normal file
View file

@ -0,0 +1,165 @@
''' activitypub-aware django model fields '''
import re
from uuid import uuid4
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.db import models
from django.utils.translation import gettext_lazy as _
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from bookwyrm.connectors import get_image
def validate_remote_id(value):
''' make sure the remote_id looks like a url '''
if not re.match(r'^http.?:\/\/[^\s]+$', value):
raise ValidationError(
_('%(value)s is not a valid remote_id'),
params={'value': value},
)
def to_camel_case(snake_string):
''' model_field_name to activitypubFieldName '''
components = snake_string.split('_')
return components[0] + ''.join(x.title() for x in components[1:])
class ActivitypubFieldMixin:
''' make a database field serializable '''
def __init__(self, *args, \
activitypub_field=None, activitypub_wrapper=None, **kwargs):
self.activitypub_wrapper = activitypub_wrapper
self.activitypub_field = activitypub_field
super().__init__(*args, **kwargs)
def to_activity(self, value):
''' formatter to convert a model value into activitypub '''
if self.activitypub_wrapper:
value = {self.activitypub_wrapper: value}
return (self.activitypub_field, value)
def from_activity(self, activity_data):
''' formatter to convert activitypub into a model value '''
value = activity_data.get(self.activitypub_field)
if self.activitypub_wrapper:
value = value.get(self.activitypub_wrapper)
return value
class RemoteIdField(ActivitypubFieldMixin, models.CharField):
''' a url that serves as a unique identifier '''
def __init__(self, *args, max_length=255, validators=None, **kwargs):
validators = validators or [validate_remote_id]
super().__init__(
*args, max_length=max_length, validators=validators,
**kwargs
)
class UsernameField(ActivitypubFieldMixin, models.CharField):
''' activitypub-aware username field '''
def __init__(self, activitypub_field='preferredUsername'):
self.activitypub_field = activitypub_field
super(ActivitypubFieldMixin, self).__init__(
_('username'),
max_length=150,
unique=True,
validators=[AbstractUser.username_validator],
error_messages={
'unique': _('A user with that username already exists.'),
},
)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
del kwargs['verbose_name']
del kwargs['max_length']
del kwargs['unique']
del kwargs['validators']
del kwargs['error_messages']
return name, path, args, kwargs
def to_activity(self, value):
return value.split('@')[0]
class ForeignKey(ActivitypubFieldMixin, models.ForeignKey):
''' activitypub-aware foreign key field '''
def to_activity(self, value):
return value.remote_id
def from_activity(self, activity_data):
pass# TODO
class OneToOneField(ActivitypubFieldMixin, models.OneToOneField):
''' activitypub-aware foreign key field '''
def __init__(self, *args, **kwargs):
super(ActivitypubFieldMixin, self).__init__(*args, **kwargs)
def to_activity(self, value):
return value.remote_id
def from_activity(self, activity_data):
pass# TODO
class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
''' activitypub-aware many to many field '''
def __init__(self, *args, link_only=False, **kwargs):
self.link_only = link_only
super().__init__(*args, **kwargs)
def to_activity(self, value):
if self.link_only:
return '%s/followers' % self.instance.remote_id
return [i.remote_id for i in value]
def from_activity(self, activity_data):
if self.link_only:
return
values = super().from_activity(self, activity_data)
return values# TODO
class ImageField(ActivitypubFieldMixin, models.ImageField):
''' activitypub-aware image field '''
def to_activity(self, value):
if value and hasattr(value, 'url'):
url = value.url
else:
return None
url = 'https://%s%s' % (DOMAIN, url)
return activitypub.Image(url=url)
def from_activity(self, activity_data):
image_slug = super().from_activity(activity_data)
# when it's an inline image (User avatar/icon, Book cover), it's a json
# blob, but when it's an attached image, it's just a url
if isinstance(image_slug, dict):
url = image_slug.get('url')
elif isinstance(image_slug, str):
url = image_slug
else:
return None
if not url:
return None
response = get_image(url)
if not response:
return None
image_name = str(uuid4()) + '.' + url.split('.')[-1]
image_content = ContentFile(response.content)
return [image_name, image_content]
class CharField(ActivitypubFieldMixin, models.CharField):
''' activitypub-aware char field '''
class TextField(ActivitypubFieldMixin, models.TextField):
''' activitypub-aware text field '''
class BooleanField(ActivitypubFieldMixin, models.BooleanField):
''' activitypub-aware boolean field '''

View file

@ -1,6 +1,5 @@
''' database schema for user data ''' ''' database schema for user data '''
from urllib.parse import urlparse from urllib.parse import urlparse
import requests
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
@ -13,40 +12,51 @@ from bookwyrm.models.status import Status, Review
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
from bookwyrm.signatures import create_key_pair from bookwyrm.signatures import create_key_pair
from bookwyrm.tasks import app from bookwyrm.tasks import app
from .base_model import ActivityMapping, OrderedCollectionPageMixin from .base_model import OrderedCollectionPageMixin
from .base_model import ActivitypubMixin, BookWyrmModel
from .federated_server import FederatedServer from .federated_server import FederatedServer
from . import fields
class User(OrderedCollectionPageMixin, AbstractUser): class User(OrderedCollectionPageMixin, AbstractUser):
''' a user who wants to read books ''' ''' a user who wants to read books '''
private_key = models.TextField(blank=True, null=True) username = fields.UsernameField()
public_key = models.TextField(blank=True, null=True)
inbox = models.CharField(max_length=255, unique=True) key_pair = fields.OneToOneField(
shared_inbox = models.CharField(max_length=255, blank=True, null=True) 'KeyPair',
on_delete=models.CASCADE,
blank=True, null=True,
related_name='owner'
)
inbox = fields.RemoteIdField(unique=True)
shared_inbox = fields.RemoteIdField(
activitypub_wrapper='endpoints', null=True)
federated_server = models.ForeignKey( federated_server = models.ForeignKey(
'FederatedServer', 'FederatedServer',
on_delete=models.PROTECT, on_delete=models.PROTECT,
null=True, null=True,
blank=True, blank=True,
) )
outbox = models.CharField(max_length=255, unique=True) outbox = fields.RemoteIdField(unique=True)
summary = models.TextField(blank=True, null=True) summary = fields.TextField(blank=True, null=True)
local = models.BooleanField(default=True) local = models.BooleanField(default=False)
bookwyrm_user = models.BooleanField(default=True) bookwyrm_user = fields.BooleanField(default=True)
localname = models.CharField( localname = models.CharField(
max_length=255, max_length=255,
null=True, null=True,
unique=True unique=True
) )
# name is your display name, which you can change at will # name is your display name, which you can change at will
name = models.CharField(max_length=100, blank=True, null=True) name = fields.CharField(max_length=100, blank=True, null=True)
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True) avatar = fields.ImageField(
following = models.ManyToManyField( upload_to='avatars/', blank=True, null=True, activitypub_field='icon')
followers = fields.ManyToManyField(
'self', 'self',
link_only=True,
symmetrical=False, symmetrical=False,
through='UserFollows', through='UserFollows',
through_fields=('user_subject', 'user_object'), through_fields=('user_object', 'user_subject'),
related_name='followers' related_name='following'
) )
follow_requests = models.ManyToManyField( follow_requests = models.ManyToManyField(
'self', 'self',
@ -69,60 +79,13 @@ class User(OrderedCollectionPageMixin, AbstractUser):
through_fields=('user', 'status'), through_fields=('user', 'status'),
related_name='favorite_statuses' related_name='favorite_statuses'
) )
remote_id = models.CharField(max_length=255, null=True, unique=True) remote_id = fields.RemoteIdField(
null=True, unique=True, activitypub_field='id')
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True) updated_date = models.DateTimeField(auto_now=True)
last_active_date = models.DateTimeField(auto_now=True) last_active_date = models.DateTimeField(auto_now=True)
manually_approves_followers = models.BooleanField(default=False) manually_approves_followers = fields.BooleanField(default=False)
# ---- activitypub serialization settings for this model ----- #
@property
def ap_followers(self):
''' generates url for activitypub followers page '''
return '%s/followers' % self.remote_id
@property
def ap_public_key(self):
''' format the public key block for activitypub '''
return activitypub.PublicKey(**{
'id': '%s/#main-key' % self.remote_id,
'owner': self.remote_id,
'publicKeyPem': self.public_key,
})
activity_mappings = [
ActivityMapping('id', 'remote_id'),
ActivityMapping(
'preferredUsername',
'username',
activity_formatter=lambda x: x.split('@')[0]
),
ActivityMapping('name', 'name'),
ActivityMapping('bookwyrmUser', 'bookwyrm_user'),
ActivityMapping('inbox', 'inbox'),
ActivityMapping('outbox', 'outbox'),
ActivityMapping('followers', 'ap_followers'),
ActivityMapping('summary', 'summary'),
ActivityMapping(
'publicKey',
'public_key',
model_formatter=lambda x: x.get('publicKeyPem')
),
ActivityMapping('publicKey', 'ap_public_key'),
ActivityMapping(
'endpoints',
'shared_inbox',
activity_formatter=lambda x: {'sharedInbox': x},
model_formatter=lambda x: x.get('sharedInbox')
),
ActivityMapping('icon', 'avatar'),
ActivityMapping(
'manuallyApprovesFollowers',
'manually_approves_followers'
),
# this field isn't in the activity but should always be false
ActivityMapping(None, 'local', model_formatter=lambda x: False),
]
activity_serializer = activitypub.Person activity_serializer = activitypub.Person
def to_outbox(self, **kwargs): def to_outbox(self, **kwargs):
@ -183,12 +146,30 @@ class User(OrderedCollectionPageMixin, AbstractUser):
self.inbox = '%s/inbox' % self.remote_id self.inbox = '%s/inbox' % self.remote_id
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
if not self.private_key: if not self.key_pair:
self.private_key, self.public_key = create_key_pair() self.key_pair = KeyPair.objects.create()
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
class KeyPair(ActivitypubMixin, BookWyrmModel):
''' public and private keys for a user '''
private_key = models.TextField(blank=True, null=True)
public_key = fields.TextField(
blank=True, null=True, activitypub_field='publicKeyPem')
activity_serializer = activitypub.PublicKey
def get_remote_id(self):
# self.owner is set by the OneToOneField on User
return '%s/#main-key' % self.owner.remote_id
def save(self, *args, **kwargs):
''' create a key pair '''
self.private_key, self.public_key = create_key_pair()
return super().save(*args, **kwargs)
@receiver(models.signals.post_save, sender=User) @receiver(models.signals.post_save, sender=User)
def execute_after_save(sender, instance, created, *args, **kwargs): def execute_after_save(sender, instance, created, *args, **kwargs):
''' create shelves for new users ''' ''' create shelves for new users '''
@ -217,6 +198,7 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
editable=False editable=False
).save() ).save()
@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 '''
@ -227,7 +209,6 @@ def set_remote_server(user_id):
user.save() user.save()
if user.bookwyrm_user: if user.bookwyrm_user:
get_remote_reviews.delay(user.outbox) get_remote_reviews.delay(user.outbox)
return
def get_or_create_remote_server(domain): def get_or_create_remote_server(domain):

View file

@ -84,7 +84,8 @@ def register(request):
} }
return TemplateResponse(request, 'login.html', data) return TemplateResponse(request, 'login.html', data)
user = models.User.objects.create_user(username, email, password) user = models.User.objects.create_user(
username, email, password, local=True)
if invite: if invite:
invite.times_used += 1 invite.times_used += 1
invite.save() invite.save()