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
Marnanel Thurman 2019-09-16 23:16:30 +01:00
rodzic f869198108
commit 1c05ce625d
18 zmienionych plików z 138 dodań i 171 usunięć

Wyświetl plik

@ -18,7 +18,6 @@ class PersonAdminForm(forms.ModelForm):
model = kepi_models.AcPerson
fields = [
'f_preferredUsername',
'f_summary',
'icon',
'header',

Wyświetl plik

@ -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

Wyświetl plik

@ -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):

Wyświetl plik

@ -53,7 +53,7 @@ class Command(KepiCommand):
spec = {
'type': 'Person',
'preferredUsername': new_name,
'id': '@'+new_name,
}
logger.debug('Creating object with spec %s',

Wyświetl plik

@ -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={

Wyświetl plik

@ -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),
),
]

Wyświetl plik

@ -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',
),
]

Wyświetl plik

@ -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),
),
]

Wyświetl plik

@ -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):

Wyświetl plik

@ -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:

Wyświetl plik

@ -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",

Wyświetl plik

@ -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',

Wyświetl plik

@ -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',

Wyświetl plik

@ -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'] % {

Wyświetl plik

@ -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'

Wyświetl plik

@ -80,7 +80,7 @@ class TestCommandView(TestCase):
)
for ourname, acname in [
('username', 'preferredUsername'),
('username', 'id'),
('bio', 'summary'),
]:
self.assertEqual(

Wyświetl plik

@ -23,6 +23,7 @@ class TestPolymorph(TestCase):
def test_person(self):
t = create(
f_type = 'Person',
id = '@wombat',
)
self.assertIsInstance(t, AcActor)

Wyświetl plik

@ -36,7 +36,6 @@ class TestKepiView(TestCase):
'name': 'alice',
'id': 'https://testserver/users/alice',
'type': 'Person',
'preferredUsername': 'alice',
},
result,
)