Huge checkin: central project "kepi" to use the "django_kepi" library.

This has shown up some bugs. Checking in the fixes for them too.
2019-08-17
Marnanel Thurman 2019-07-11 18:25:06 +01:00
rodzic c59228c58f
commit 18565c6afe
14 zmienionych plików z 402 dodań i 88 usunięć

1
.gitignore vendored
Wyświetl plik

@ -4,5 +4,6 @@ __pycache__
.eggs
.tox
*.egg-info
*.sqlite3
build/
dist/

Wyświetl plik

@ -0,0 +1,27 @@
from django.contrib import admin
from polymorphic.admin import *
from django_kepi.models import *
class ThingChildAdmin(PolymorphicChildModelAdmin):
base_model = Thing
@admin.register(Actor)
class ActorChildAdmin(ThingChildAdmin):
base_model = Actor
@admin.register(Item)
class ItemChildAdmin(ThingChildAdmin):
base_model = Item
@admin.register(Activity)
class ActivityChildAdmin(ThingChildAdmin):
base_model = Activity
@admin.register(Thing)
class ThingParentAdmin(PolymorphicParentModelAdmin):
base_model = Thing
child_models = (
Actor,
Item,
Activity,
)

Wyświetl plik

@ -0,0 +1,18 @@
# Generated by Django 2.2.1 on 2019-07-11 17:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_kepi', '0007_actor_f_preferredusername'),
]
operations = [
migrations.AlterField(
model_name='thing',
name='f_type',
field=models.CharField(choices=[('Accept', 'Accept'), ('Activity', 'Activity'), ('Actor', 'Actor'), ('Add', 'Add'), ('Announce', 'Announce'), ('Application', 'Application'), ('Article', 'Article'), ('Audio', 'Audio'), ('Create', 'Create'), ('Delete', 'Delete'), ('Document', 'Document'), ('Event', 'Event'), ('Follow', 'Follow'), ('Group', 'Group'), ('Image', 'Image'), ('Like', 'Like'), ('Note', 'Note'), ('Object', 'Object'), ('Organization', 'Organization'), ('Page', 'Page'), ('Person', 'Person'), ('Place', 'Place'), ('Profile', 'Profile'), ('Reject', 'Reject'), ('Relationship', 'Relationship'), ('Remove', 'Remove'), ('Service', 'Service'), ('Undo', 'Undo'), ('Update', 'Update'), ('Video', 'Video')], max_length=255),
),
]

Wyświetl plik

@ -0,0 +1,37 @@
# Generated by Django 2.2.1 on 2019-07-11 17:08
from django.db import migrations, models
import django_kepi.models.thing
class Migration(migrations.Migration):
dependencies = [
('django_kepi', '0008_auto_20190711_1703'),
]
operations = [
migrations.RemoveField(
model_name='thing',
name='f_actor',
),
migrations.RemoveField(
model_name='thing',
name='f_name',
),
migrations.AlterField(
model_name='thing',
name='number',
field=models.CharField(default=django_kepi.models.thing._new_number, max_length=8, primary_key=True, serialize=False, unique=True),
),
migrations.AlterField(
model_name='thing',
name='other_fields',
field=models.TextField(blank=True, default=''),
),
migrations.AlterField(
model_name='thing',
name='remote_url',
field=models.URLField(blank=True, default=None, max_length=255, null=True, unique=True),
),
]

Wyświetl plik

@ -0,0 +1,23 @@
# Generated by Django 2.2.1 on 2019-07-11 17:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_kepi', '0009_auto_20190711_1708'),
]
operations = [
migrations.AlterField(
model_name='actor',
name='privateKey',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='actor',
name='publicKey',
field=models.TextField(blank=True, null=True),
),
]

Wyświetl plik

@ -18,9 +18,13 @@ class Actor(thing.Thing):
"""
privateKey = models.TextField(
blank=True,
null=True,
)
publicKey = models.TextField(
blank=True,
null=True,
)
auto_follow = models.BooleanField(

Wyświetl plik

@ -8,35 +8,18 @@ import random
import json
import datetime
import warnings
from django_kepi.types import ACTIVITYPUB_TYPES
logger = logging.getLogger(name='django_kepi')
######################
ACTIVITY_TYPES = set([
'Create',
'Update',
'Delete',
'Follow',
'Add',
'Remove',
'Like',
'Undo',
'Accept',
'Reject',
])
ACTIVITY_TYPES = sorted(ACTIVITYPUB_TYPES.keys())
ACTIVITY_TYPE_CHOICES = [(x,x) for x in sorted(ACTIVITY_TYPES)]
ACTIVITY_TYPE_CHOICES = [(x,x) for x in ACTIVITY_TYPES]
OTHER_OBJECT_TYPES = set([
# https://www.w3.org/TR/activitystreams-vocabulary/
'Actor', 'Application', 'Group', 'Organization', 'Person', 'Service',
'Article', 'Audio', 'Document', 'Event', 'Image', 'Note', 'Page',
'Place', 'Profile', 'Relationship', 'Video',
])
def _new_number():
return '%08x' % (random.randint(0, 0xffffffff),)
######################
@ -46,7 +29,7 @@ class Thing(PolymorphicModel):
max_length=8,
primary_key=True,
unique=True,
default='',
default=_new_number,
)
f_type = models.CharField(
@ -58,24 +41,16 @@ class Thing(PolymorphicModel):
max_length=255,
unique=True,
null=True,
blank=True,
default=None,
)
f_actor = models.URLField(
max_length=255,
blank=True,
)
f_name = models.CharField(
max_length=255,
blank=True,
)
active = models.BooleanField(
default=True,
)
other_fields = models.TextField(
blank=True,
default='',
)
@ -97,10 +72,7 @@ class Thing(PolymorphicModel):
else:
inactive_warning = ' INACTIVE'
if self.f_name:
details = self.f_name
else:
details = self.url
details = self.url
result = '[%s %s %s%s]' % (
self.number,
@ -131,7 +103,6 @@ class Thing(PolymorphicModel):
items.extend( [
('actor', self.f_actor),
('_name', self.f_name),
('_number', self.number),
('_remote_url', self.remote_url),
] )
@ -177,8 +148,6 @@ class Thing(PolymorphicModel):
if value=='':
value = None
else:
value = json.loads(value)
result[name[2:]] = value
@ -372,33 +341,6 @@ class Thing(PolymorphicModel):
self['object'] = creation
self.save()
elif f_type in OTHER_OBJECT_TYPES:
# XXX only if this came in via a local inbox
logger.debug('New Thing is not an activity: %s',
str(self.activity_form))
logger.debug('We must create a Create wrapper for it.')
from django_kepi.create import create
wrapper = create(
f_type = 'Create',
f_actor = self.f_actor,
to = self['to'],
cc = self['cc'],
f_object = self.url,
run_side_effects = False,
)
wrapper.save()
logger.debug('Created wrapper %s',
str(wrapper.activity_form))
# XXX We copy "to" and "cc" per
# https://www.w3.org/TR/activitypub/#object-without-create
# which also requires us to copy
# the two blind versions, and "audience".
# We don't support those (atm), but
# we should probably copy them anyway.
@property
def is_local(self):
@ -438,13 +380,10 @@ class Thing(PolymorphicModel):
def save(self, *args, **kwargs):
if not self.number:
self.number = '%08x' % (random.randint(0, 0xffffffff),)
try:
super().save(*args, **kwargs)
except IntegrityError:
self.number = None
self.number = _new_number()
return self.save(*args, **kwargs)
######################################
@ -469,3 +408,5 @@ def _normalise_type_for_thing(v):
return v.activity_form
except AttributeError:
return str(v)

Wyświetl plik

@ -191,10 +191,10 @@ class ThingView(KepiView):
)
elif 'name' in kwargs:
logger.debug('Looking up Thing by name==%s',
logger.debug('Looking up Actor by name==%s',
kwargs['name'])
activity_object = Thing.objects.get(
f_name=kwargs['name'],
activity_object = Actor.objects.get(
f_preferredUsername=kwargs['name'],
)
else:
raise ValueError("Need an id or a name")
@ -217,9 +217,8 @@ class FollowingView(KepiView):
logger.debug('Finding following of %s:', kwargs['name'])
person = Thing.objects.get(
f_type='Person',
f_name = kwargs['name'],
person = Actor.objects.get(
f_preferredUsername=kwargs['name'],
)
logging.debug('Finding followers of %s: %s',
@ -237,9 +236,8 @@ class FollowersView(KepiView):
logger.debug('Finding followers of %s:', kwargs['name'])
person = Thing.objects.get(
f_type='Person',
f_name=kwargs['name'],
person = Actor.objects.get(
f_preferredUsername=kwargs['name'],
)
logging.debug('Finding followers of %s: %s',

0
kepi/__init__.py 100644
Wyświetl plik

133
kepi/settings.py 100644
Wyświetl plik

@ -0,0 +1,133 @@
import os
import djcelery
djcelery.setup_loader()
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ROOT_URLCONF = 'kepi.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'kepi.wsgi.application'
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'cmfy8%_q^u#bix$_4bq!p^8eq@=46bb*a7ztmg4i)l8jo(kl%^'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
KEPI = {
'ACTIVITY_URL_FORMAT': 'https://altair.example.com/%s',
'LOCAL_OBJECT_HOSTNAME': 'example.com',
'FOLLOWERS_PATH': '/user/%(username)s/followers',
'FOLLOWING_PATH': '/user/%(username)s/followers',
'INBOX_PATH': '/user/%(username)s/inbox',
'OUTBOX_PATH': '/user/%(username)s/outbox',
}
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'djcelery',
'django_celery_results',
'django_kepi',
'polymorphic',
)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'kepi.sqlite3'),
}
}
ALLOWED_HOSTS = [
'altair.example.com',
'sirius.example.com',
]
ROOT_URLCONF = 'kepi.urls'
CELERY = {
'task_ignore_result': True,
}
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django_kepi': {
'handlers': ['console'],
'level': 'DEBUG',
},
},
}
TEST_RUNNER = 'djcelery.contrib.test_runner.CeleryTestSuiteRunner'
ALLOWED_HOSTS = []
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = 'en-gb'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = '/static/'

25
kepi/urls.py 100644
Wyświetl plik

@ -0,0 +1,25 @@
"""kepi URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, re_path, include
import django_kepi.urls
urlpatterns = [
path(r'admin/', admin.site.urls),
# path('', kepi.views.FrontPageView.as_view()), # or something
path(r'', include(django_kepi.urls)),
]

16
kepi/wsgi.py 100644
Wyświetl plik

@ -0,0 +1,16 @@
"""
WSGI config for kepi project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'kepi.settings')
application = get_wsgi_application()

Wyświetl plik

@ -1,13 +1,21 @@
#!/usr/bin/env python
# this is a manage.py for reusable libraries
# it is dedicated to the public domain
# feel free to copy it into your project
"""Django's command-line utility for administrative tasks."""
import os
import sys
from django.core.management import execute_from_command_line
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_settings")
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'kepi.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

Wyświetl plik

@ -0,0 +1,83 @@
from django.test import TestCase
from tests import create_local_note, create_local_person
from django_kepi.create import create
from django_kepi.models import *
import logging
SENDER_ID = 'https://example.com/actor'
SENDER_DOMAIN = 'example.com'
SENDER_FOLLOWERS = 'https://example.com/followers'
logger = logging.getLogger(name='tests')
# XXX Why does this only test updating of profiles?
# XXX I thought we should update items as well.
class TestUpdate(TestCase):
def test_update_profile(self):
sender = create_local_person()
object_json = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'foo',
'type': 'Update',
'actor': sender,
'object': actor_json,
}
self.assertEqual(
sender.display_name,
"Totally modified now",
)
hidden = """
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Update do
let!(:sender) { Fabricate(:account) }
before do
stub_request(:get, actor_json[:outbox]).to_return(status: 404)
stub_request(:get, actor_json[:followers]).to_return(status: 404)
stub_request(:get, actor_json[:following]).to_return(status: 404)
stub_request(:get, actor_json[:featured]).to_return(status: 404)
sender.update!(uri: ActivityPub::TagManager.instance.uri_for(sender))
end
let(:modified_sender) do
sender.dup.tap do |modified_sender|
modified_sender.display_name = 'Totally modified now'
end
end
let(:actor_json) do
ActiveModelSerializers::SerializableResource.new(modified_sender, serializer: ActivityPub::ActorSerializer, key_transform: :camel_lower).as_json
end
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Update',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: actor_json,
}.with_indifferent_access
end
describe '#perform' do
subject { described_class.new(json, sender) }
before do
subject.perform
end
it 'updates profile' do
expect(sender.reload.display_name).to eq 'Totally modified now'
end
end
end
"""