mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-30 03:50:40 +00:00
Use custom model fields in user model
This commit is contained in:
parent
96563598bf
commit
74a58e5267
8 changed files with 429 additions and 124 deletions
|
@ -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
|
||||||
|
|
|
@ -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 '''
|
||||||
|
|
|
@ -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 '''
|
||||||
|
|
188
bookwyrm/migrations/0017_auto_20201130_1819.py
Normal file
188
bookwyrm/migrations/0017_auto_20201130_1819.py
Normal 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),
|
||||||
|
]
|
|
@ -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
165
bookwyrm/models/fields.py
Normal 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 '''
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue