kopia lustrzana https://gitlab.com/marnanel/chapeau
The id field in local AcActors now consists of "@" plus the username.
This is similar to what we did in b76404, but specifically for AcActors. The reasoning behind it is identical. The f_preferredUsername field is thus removed; preferredUsername should be stored in ThingField when it's needed. Added some logging to webfinger.trilby
rodzic
f869198108
commit
1c05ce625d
|
@ -18,7 +18,6 @@ class PersonAdminForm(forms.ModelForm):
|
|||
model = kepi_models.AcPerson
|
||||
|
||||
fields = [
|
||||
'f_preferredUsername',
|
||||
'f_summary',
|
||||
'icon',
|
||||
'header',
|
||||
|
|
|
@ -58,76 +58,19 @@ class KepiCommand(BaseCommand):
|
|||
|
||||
def objects_by_keywords(keywords):
|
||||
"""
|
||||
Finds a set of kepi objects specified by a series of keywords.
|
||||
|
||||
"keywords" is a list of strings.
|
||||
Returns a list of objects on success.
|
||||
|
||||
An ID number consists of eight hex digits.
|
||||
|
||||
If any of the strings in "keywords" contain one or more
|
||||
bracketed ID numbers, this function returns a list of the
|
||||
objects those numbers represent, in order.
|
||||
In this case, no other representation of an object will be considered.
|
||||
It doesn't matter if some of the lines don't contain any
|
||||
bracketed numbers at all.
|
||||
If any of the numbers don't correspond to a current object,
|
||||
raises KeyError.
|
||||
|
||||
Otherwise, each string in "keywords" must represent either:
|
||||
- an ID number, as above
|
||||
- a username preceded by @
|
||||
If any string doesn't represent either, raise KeyError.
|
||||
Otherwise, we return a list of the objects referred to,
|
||||
in the same order.
|
||||
Finds a set of kepi objects specified by their ids.
|
||||
"""
|
||||
|
||||
def object_by_number(number):
|
||||
try:
|
||||
result = AcObject.objects.get(
|
||||
id = '/'+number,
|
||||
)
|
||||
result = []
|
||||
|
||||
for keyword in keywords:
|
||||
try:
|
||||
result.append(AcObject.objects.get(
|
||||
id = keyword,
|
||||
))
|
||||
except AcObject.DoesNotExist:
|
||||
raise KeyError(
|
||||
'There is nothing with the number %s.' % (number,)
|
||||
'I can\'t find %s.' % (keyword,)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
bracketed_eight_digits_match = re.findall(r'\(([0-9a-f]{8})\)',
|
||||
' '.join(keywords),
|
||||
re.IGNORECASE,
|
||||
)
|
||||
if bracketed_eight_digits_match:
|
||||
return [object_by_number(n) for n in bracketed_eight_digits_match]
|
||||
|
||||
result = []
|
||||
for keyword in keywords:
|
||||
username_match = re.match(r'@([a-z0-9_-]+)$', keyword,
|
||||
re.IGNORECASE)
|
||||
|
||||
if username_match:
|
||||
try:
|
||||
somebody = AcActor.objects.get_local_only(
|
||||
f_preferredUsername = username_match.group(1),
|
||||
)
|
||||
|
||||
result.append(somebody)
|
||||
continue
|
||||
|
||||
except AcActor.DoesNotExist:
|
||||
raise KeyError(
|
||||
'There is no user named %s.' % (keyword,)
|
||||
)
|
||||
|
||||
eight_digits_match = re.match(r'([0-9a-f]{8})$', keyword)
|
||||
if eight_digits_match:
|
||||
result.append(object_by_number(eight_digits_match.group(1)))
|
||||
continue
|
||||
|
||||
raise KeyError(
|
||||
'I don\'t know what %s means.' % (keyword,)
|
||||
)
|
||||
|
||||
return result
|
||||
|
|
|
@ -58,7 +58,7 @@ class Command(KepiCommand):
|
|||
]
|
||||
|
||||
self._display_table(result,
|
||||
title='@'+somebody.f_preferredUsername,
|
||||
title=somebody.id,
|
||||
)
|
||||
|
||||
def _show_activity(self, activity, *args, **options):
|
||||
|
|
|
@ -53,7 +53,7 @@ class Command(KepiCommand):
|
|||
|
||||
spec = {
|
||||
'type': 'Person',
|
||||
'preferredUsername': new_name,
|
||||
'id': '@'+new_name,
|
||||
}
|
||||
|
||||
logger.debug('Creating object with spec %s',
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
# Generated by Django 2.2.4 on 2019-09-12 22:06
|
||||
# Generated by Django 2.2.4 on 2019-09-15 17:53
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import django_kepi.models.acobject
|
||||
import uuid
|
||||
|
||||
|
||||
|
@ -19,7 +18,8 @@ class Migration(migrations.Migration):
|
|||
migrations.CreateModel(
|
||||
name='AcObject',
|
||||
fields=[
|
||||
('id', models.CharField(default=django_kepi.models.acobject._new_number, editable=False, max_length=255, primary_key=True, serialize=False, unique=True)),
|
||||
('id', models.CharField(default=None, editable=False, max_length=255, primary_key=True, serialize=False, unique=True)),
|
||||
('published', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_django_kepi.acobject_set+', to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
# Generated by Django 2.2.4 on 2019-09-13 21:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [('django_kepi', '0002_acobject_published'), ('django_kepi', '0003_auto_20190913_2151')]
|
||||
|
||||
dependencies = [
|
||||
('django_kepi', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='acobject',
|
||||
name='published',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 2.2.4 on 2019-09-15 19:11
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('django_kepi', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='acactor',
|
||||
name='f_preferredUsername',
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 2.2.4 on 2019-09-13 22:19
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('django_kepi', '0002_acobject_published_squashed_0003_auto_20190913_2151'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='acobject',
|
||||
name='published',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
]
|
|
@ -6,21 +6,17 @@ from polymorphic.managers import PolymorphicManager
|
|||
from django_kepi.models.audience import Audience, AUDIENCE_FIELD_NAMES
|
||||
from django_kepi.models.thingfield import ThingField
|
||||
from django_kepi.models.mention import Mention
|
||||
from .. import ATSIGN_CONTEXT
|
||||
from .. import ATSIGN_CONTEXT, URL_REGEXP, SERIAL_NUMBER_REGEXP
|
||||
import django_kepi.side_effects as side_effects
|
||||
import logging
|
||||
import random
|
||||
import warnings
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(name='django_kepi')
|
||||
|
||||
######################
|
||||
|
||||
def _new_number():
|
||||
return '/%08x' % (random.randint(0, 0xffffffff),)
|
||||
|
||||
######################
|
||||
|
||||
class KepiManager(PolymorphicManager):
|
||||
|
||||
# TODO: This should allow filtering on names
|
||||
|
@ -53,7 +49,7 @@ class AcObject(PolymorphicModel):
|
|||
primary_key=True,
|
||||
unique=True,
|
||||
blank=False,
|
||||
default=_new_number,
|
||||
default=None,
|
||||
editable=False,
|
||||
)
|
||||
|
||||
|
@ -70,6 +66,11 @@ class AcObject(PolymorphicModel):
|
|||
'number': self.id[1:],
|
||||
'hostname': settings.KEPI['LOCAL_OBJECT_HOSTNAME'],
|
||||
}
|
||||
elif self.id.startswith('@'):
|
||||
return settings.KEPI['USER_URL_FORMAT'] % {
|
||||
'username': self.id[1:],
|
||||
'hostname': settings.KEPI['LOCAL_OBJECT_HOSTNAME'],
|
||||
}
|
||||
else:
|
||||
return self.id
|
||||
|
||||
|
@ -82,15 +83,11 @@ class AcObject(PolymorphicModel):
|
|||
|
||||
def __str__(self):
|
||||
|
||||
if self.is_local:
|
||||
details = '(%s)' % (self.id[1:],)
|
||||
else:
|
||||
details = self.id
|
||||
|
||||
result = '[%s %s]' % (
|
||||
details,
|
||||
self.id,
|
||||
self.f_type,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
|
@ -304,7 +301,7 @@ class AcObject(PolymorphicModel):
|
|||
|
||||
@property
|
||||
def is_local(self):
|
||||
return self.id.startswith('/')
|
||||
return self.id[0] in '/@'
|
||||
|
||||
def entomb(self):
|
||||
logger.info('%s: entombing', self)
|
||||
|
@ -323,22 +320,65 @@ class AcObject(PolymorphicModel):
|
|||
self.save()
|
||||
logger.info('%s: entombed', self)
|
||||
|
||||
def _generate_id(self):
|
||||
"""
|
||||
Returns a value for "id" on a new object, where
|
||||
the caller has omitted to supply an "id" value.
|
||||
The new value should be unique.
|
||||
|
||||
If this method returns None, the object will
|
||||
not be created.
|
||||
"""
|
||||
return '/%08x' % (random.randint(0, 0xffffffff),)
|
||||
|
||||
def _check_provided_id(self):
|
||||
"""
|
||||
Checks self.id to see whether it's valid for
|
||||
this kind of AcObject. It may normalise the value.
|
||||
|
||||
If the value is valid, returns.
|
||||
If the value is invalid, raises ValueError.
|
||||
|
||||
This method is not called if self.id is a valid
|
||||
URL, because that means it's a remote object
|
||||
and our naming rules won't apply.
|
||||
"""
|
||||
if re.match(SERIAL_NUMBER_REGEXP, self.id,
|
||||
re.IGNORECASE):
|
||||
|
||||
self.id = self.id.lower()
|
||||
logger.debug('id==%s which is a valid serial number',
|
||||
self.id)
|
||||
return
|
||||
|
||||
raise ValueError("Object IDs begin with a slash "+\
|
||||
"followed by eight characters from "+\
|
||||
"0-9 or a-f. "+\
|
||||
"You gave: "+self.id)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
if self.id is None:
|
||||
self.id = self._generate_id()
|
||||
|
||||
if self.id is None:
|
||||
raise ValueError("You need to specify an id "+\
|
||||
"on %s objects." % (self.__class__.__name__,))
|
||||
else:
|
||||
if re.match(URL_REGEXP, self.id,
|
||||
re.IGNORECASE):
|
||||
logger.debug('id==%s which is a valid URL',
|
||||
self.id)
|
||||
else:
|
||||
self._check_provided_id()
|
||||
|
||||
try:
|
||||
super().save(*args, **kwargs)
|
||||
logger.debug('%s: saved', self)
|
||||
except IntegrityError as ie:
|
||||
if self.is_local and kwargs.get('_tries_left',0)>0:
|
||||
logger.info('Integrity error on save (%s); retrying',
|
||||
ie)
|
||||
self.id = _new_number()
|
||||
kwargs['_tries_left'] -= 1
|
||||
return self.save(*args, **kwargs)
|
||||
else:
|
||||
logger.info('Integrity error on save (%s); failed',
|
||||
ie)
|
||||
raise ie
|
||||
logger.info('Integrity error on save (%s); failed',
|
||||
ie)
|
||||
raise ie
|
||||
|
||||
@classmethod
|
||||
def get_by_url(cls, url):
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.conf import settings
|
|||
from . import acobject
|
||||
import django_kepi.crypto
|
||||
import logging
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(name='django_kepi')
|
||||
|
||||
|
@ -35,12 +36,6 @@ class AcActor(acobject.AcObject):
|
|||
help_text="If True, follow requests will be accepted automatically.",
|
||||
)
|
||||
|
||||
f_preferredUsername = models.CharField(
|
||||
max_length=255,
|
||||
help_text="Something short, like 'alice'.",
|
||||
verbose_name='username',
|
||||
)
|
||||
|
||||
f_summary = models.TextField(
|
||||
max_length=255,
|
||||
help_text="Your biography. Something like "+\
|
||||
|
@ -62,18 +57,22 @@ class AcActor(acobject.AcObject):
|
|||
verbose_name='header image',
|
||||
)
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
if self.is_local:
|
||||
return '@{}'.format(self.f_preferredUsername)
|
||||
else:
|
||||
return __super__.short_id
|
||||
def _generate_id(self):
|
||||
return None
|
||||
|
||||
def _check_provided_id(self):
|
||||
if not re.match(r'@[a-z0-9_-]+$', self.id,
|
||||
re.IGNORECASE):
|
||||
raise ValueError("Actor IDs begin with an @ "+\
|
||||
"followed by one or more characters from "+\
|
||||
"A-Z, a-z, 0-9, underscore, or hyphen. "+\
|
||||
"You gave: "+self.id)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
if self.is_local:
|
||||
return settings.KEPI['USER_URL_FORMAT'] % {
|
||||
'username': self.f_preferredUsername,
|
||||
'username': self.id[1:],
|
||||
'hostname': settings.KEPI['LOCAL_OBJECT_HOSTNAME'],
|
||||
}
|
||||
else:
|
||||
|
@ -95,12 +94,11 @@ class AcActor(acobject.AcObject):
|
|||
|
||||
def __str__(self):
|
||||
if self.is_local:
|
||||
return '({}) @{}'.format(
|
||||
return '[{}]'.format(
|
||||
self.id,
|
||||
self.f_preferredUsername,
|
||||
)
|
||||
else:
|
||||
return '({}) [remote user]'.format(
|
||||
return '[remote user {}]'.format(
|
||||
self.id,
|
||||
)
|
||||
|
||||
|
@ -114,7 +112,7 @@ class AcActor(acobject.AcObject):
|
|||
def list_url(self, name):
|
||||
return settings.KEPI['COLLECTION_URL'] % {
|
||||
'hostname': settings.KEPI['LOCAL_OBJECT_HOSTNAME'],
|
||||
'username': self.f_preferredUsername,
|
||||
'username': self.id[1:],
|
||||
'listname': name,
|
||||
}
|
||||
|
||||
|
@ -174,7 +172,7 @@ class AcActor(acobject.AcObject):
|
|||
result[listname] = self.list_url(listname)
|
||||
|
||||
result['url'] = self.url
|
||||
result['name'] = self.f_preferredUsername
|
||||
result['name'] = self.id[1:]
|
||||
|
||||
result['endpoints'] = {}
|
||||
if 'SHARED_INBOX' in settings.KEPI:
|
||||
|
|
|
@ -65,8 +65,8 @@ class Collection(models.Model):
|
|||
username, collectionname)
|
||||
|
||||
try:
|
||||
owner = AcActor.objects.get_local_only(
|
||||
f_preferredUsername = username,
|
||||
owner = AcActor.objects.get(
|
||||
id = '@'+username,
|
||||
)
|
||||
except AcActor.DoesNotExist:
|
||||
logger.info(" -- can't get %s because %s doesn't exist",
|
||||
|
|
|
@ -303,8 +303,8 @@ class ActorView(ThingView):
|
|||
kwargs['username'])
|
||||
|
||||
try:
|
||||
activity_object = AcActor.objects.get_local_only(
|
||||
f_preferredUsername=kwargs['username'],
|
||||
activity_object = AcActor.objects.get(
|
||||
id='@'+kwargs['username'],
|
||||
)
|
||||
|
||||
except AcActor.DoesNotExist:
|
||||
|
@ -344,8 +344,8 @@ class FollowingView(KepiView):
|
|||
|
||||
logger.debug('Finding following of %s:', kwargs['username'])
|
||||
|
||||
person = AcActor.objects.get_local_only(
|
||||
f_preferredUsername=kwargs['username'],
|
||||
person = AcActor.objects.get(
|
||||
id='@'+kwargs['username'],
|
||||
)
|
||||
|
||||
logger.debug('Finding followers of %s: %s',
|
||||
|
@ -363,8 +363,8 @@ class FollowersView(KepiView):
|
|||
|
||||
logger.debug('Finding followers of %s:', kwargs['username'])
|
||||
|
||||
person = AcActor.objects.get_local_only(
|
||||
f_preferredUsername=kwargs['username'],
|
||||
person = AcActor.objects.get(
|
||||
id='@'+kwargs['username'],
|
||||
)
|
||||
|
||||
|
||||
|
@ -402,7 +402,7 @@ class UserCollectionView(KepiView):
|
|||
username, listname)
|
||||
try:
|
||||
the_collection = Collection.objects.get(
|
||||
owner__f_preferredUsername = username,
|
||||
owner__id = '@'+username,
|
||||
name = listname)
|
||||
|
||||
logger.debug(' -- found collection: %s',
|
||||
|
@ -434,7 +434,7 @@ class UserCollectionView(KepiView):
|
|||
username, listname)
|
||||
try:
|
||||
the_collection = Collection.objects.get(
|
||||
owner__f_preferredUsername = username,
|
||||
owner__id = '@'+username,
|
||||
name = listname)
|
||||
|
||||
logger.debug(' -- found collection: %s. Appending %s.',
|
||||
|
@ -448,8 +448,8 @@ class UserCollectionView(KepiView):
|
|||
logger.debug(' -- does not exist; creating it')
|
||||
|
||||
try:
|
||||
owner = AcActor.objects.get_local_only(
|
||||
f_preferredUsername = username,
|
||||
owner = AcActor.objects.get(
|
||||
id = '@'+username,
|
||||
)
|
||||
except AcActor.DoesNotExist:
|
||||
logger.debug(' -- but user %s doesn\'t exist; bailing',
|
||||
|
|
|
@ -32,6 +32,7 @@ class Webfinger(django.views.View):
|
|||
try:
|
||||
user = request.GET['resource']
|
||||
except:
|
||||
logger.info('webfinger request had no username specified')
|
||||
return HttpResponse(
|
||||
status = 400,
|
||||
reason = 'no resource for webfinger',
|
||||
|
@ -42,8 +43,10 @@ class Webfinger(django.views.View):
|
|||
# Generally, user resources should be prefaced with "acct:",
|
||||
# per RFC7565. We support this, but we don't enforce it.
|
||||
user = re.sub(r'^acct:', '', user)
|
||||
logger.info('webfinger request for %s', user)
|
||||
|
||||
if '@' not in user:
|
||||
logger.info(' -- no @ sign; bailing')
|
||||
return HttpResponse(
|
||||
status = 404,
|
||||
reason = 'absolute name required',
|
||||
|
@ -54,6 +57,8 @@ class Webfinger(django.views.View):
|
|||
username, hostname = user.split('@', 2)
|
||||
|
||||
if hostname not in settings.ALLOWED_HOSTS:
|
||||
logger.info(' -- %s is not local; bailing',
|
||||
hostname)
|
||||
return HttpResponse(
|
||||
status = 404,
|
||||
reason = 'not this server',
|
||||
|
@ -61,10 +66,12 @@ class Webfinger(django.views.View):
|
|||
)
|
||||
|
||||
try:
|
||||
whoever = AcActor.objects.get_local_only(
|
||||
f_preferredUsername = username,
|
||||
whoever = AcActor.objects.get(
|
||||
id = '@'+username,
|
||||
)
|
||||
except AcActor.DoesNotExist:
|
||||
logger.info(' -- we don\'t have anyone called %s',
|
||||
username)
|
||||
return HttpResponse(
|
||||
status = 404,
|
||||
reason = 'no such user',
|
||||
|
@ -91,6 +98,9 @@ class Webfinger(django.views.View):
|
|||
},
|
||||
]}
|
||||
|
||||
logger.debug(' -- webfinger for %s was successful',
|
||||
user)
|
||||
|
||||
return HttpResponse(
|
||||
status = 200,
|
||||
reason = 'Here you go',
|
||||
|
|
|
@ -53,7 +53,7 @@ def create_local_person(name='jemima',
|
|||
|
||||
spec = {
|
||||
'name': name,
|
||||
'preferredUsername': name,
|
||||
'id': '@'+name,
|
||||
'type': 'Person',
|
||||
'endpoints': {
|
||||
'sharedInbox': settings.KEPI['SHARED_INBOX'] % {
|
||||
|
|
|
@ -28,7 +28,7 @@ class TestCreate(TestCase):
|
|||
):
|
||||
|
||||
if sender is None:
|
||||
sender = self._fred.id
|
||||
sender = self._fred.url
|
||||
|
||||
if 'id' not in object_form:
|
||||
object_form['id'] = sender+'#bar'
|
||||
|
|
|
@ -80,7 +80,7 @@ class TestCommandView(TestCase):
|
|||
)
|
||||
|
||||
for ourname, acname in [
|
||||
('username', 'preferredUsername'),
|
||||
('username', 'id'),
|
||||
('bio', 'summary'),
|
||||
]:
|
||||
self.assertEqual(
|
||||
|
|
|
@ -23,6 +23,7 @@ class TestPolymorph(TestCase):
|
|||
def test_person(self):
|
||||
t = create(
|
||||
f_type = 'Person',
|
||||
id = '@wombat',
|
||||
)
|
||||
self.assertIsInstance(t, AcActor)
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ class TestKepiView(TestCase):
|
|||
'name': 'alice',
|
||||
'id': 'https://testserver/users/alice',
|
||||
'type': 'Person',
|
||||
'preferredUsername': 'alice',
|
||||
},
|
||||
result,
|
||||
)
|
||||
|
|
Ładowanie…
Reference in New Issue