diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index 51230bfdc3bc023f6cea0c985e5524edb468f82f..97a0883384afc6f5017d0c2cf13089b7bb3b23b9 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -69,6 +69,8 @@ else:
FUNKWHALE_HOSTNAME = _parsed.netloc
FUNKWHALE_PROTOCOL = _parsed.scheme
+FUNKWHALE_PROTOCOL = FUNKWHALE_PROTOCOL.lower()
+FUNKWHALE_HOSTNAME = FUNKWHALE_HOSTNAME.lower()
FUNKWHALE_URL = "{}://{}".format(FUNKWHALE_PROTOCOL, FUNKWHALE_HOSTNAME)
FUNKWHALE_SPA_HTML_ROOT = env(
"FUNKWHALE_SPA_HTML_ROOT", default=FUNKWHALE_URL + "/front/"
@@ -83,7 +85,7 @@ APP_NAME = "Funkwhale"
# XXX: deprecated, see #186
FEDERATION_ENABLED = env.bool("FEDERATION_ENABLED", default=True)
-FEDERATION_HOSTNAME = env("FEDERATION_HOSTNAME", default=FUNKWHALE_HOSTNAME)
+FEDERATION_HOSTNAME = env("FEDERATION_HOSTNAME", default=FUNKWHALE_HOSTNAME).lower()
# XXX: deprecated, see #186
FEDERATION_COLLECTION_PAGE_SIZE = env.int("FEDERATION_COLLECTION_PAGE_SIZE", default=50)
# XXX: deprecated, see #186
diff --git a/api/funkwhale_api/federation/admin.py b/api/funkwhale_api/federation/admin.py
index 98bc65247c9125d6548016ec540e2b1231a1fad2..acb2e5b67b33830e57550d9484516d0071c8c9df 100644
--- a/api/funkwhale_api/federation/admin.py
+++ b/api/funkwhale_api/federation/admin.py
@@ -24,6 +24,12 @@ def redeliver_activities(modeladmin, request, queryset):
redeliver_activities.short_description = "Redeliver"
+@admin.register(models.Domain)
+class DomainAdmin(admin.ModelAdmin):
+ list_display = ["name", "creation_date"]
+ search_fields = ["name"]
+
+
@admin.register(models.Activity)
class ActivityAdmin(admin.ModelAdmin):
list_display = ["type", "fid", "url", "actor", "creation_date"]
diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py
index a52cf88becfafe72c8ed03190f75135af815456e..cbe0bee85fceee231b9d9d86181225aab97390b0 100644
--- a/api/funkwhale_api/federation/factories.py
+++ b/api/funkwhale_api/federation/factories.py
@@ -66,24 +66,39 @@ def create_user(actor):
return user_factories.UserFactory(actor=actor)
+@registry.register
+class Domain(factory.django.DjangoModelFactory):
+ name = factory.Faker("domain_name")
+
+ class Meta:
+ model = "federation.Domain"
+ django_get_or_create = ("name",)
+
+
@registry.register
class ActorFactory(factory.DjangoModelFactory):
public_key = None
private_key = None
preferred_username = factory.Faker("user_name")
summary = factory.Faker("paragraph")
- domain = factory.Faker("domain_name")
+ domain = factory.SubFactory(Domain)
fid = factory.LazyAttribute(
- lambda o: "https://{}/users/{}".format(o.domain, o.preferred_username)
+ lambda o: "https://{}/users/{}".format(o.domain.name, o.preferred_username)
)
followers_url = factory.LazyAttribute(
- lambda o: "https://{}/users/{}followers".format(o.domain, o.preferred_username)
+ lambda o: "https://{}/users/{}followers".format(
+ o.domain.name, o.preferred_username
+ )
)
inbox_url = factory.LazyAttribute(
- lambda o: "https://{}/users/{}/inbox".format(o.domain, o.preferred_username)
+ lambda o: "https://{}/users/{}/inbox".format(
+ o.domain.name, o.preferred_username
+ )
)
outbox_url = factory.LazyAttribute(
- lambda o: "https://{}/users/{}/outbox".format(o.domain, o.preferred_username)
+ lambda o: "https://{}/users/{}/outbox".format(
+ o.domain.name, o.preferred_username
+ )
)
class Meta:
@@ -95,7 +110,9 @@ class ActorFactory(factory.DjangoModelFactory):
return
from funkwhale_api.users.factories import UserFactory
- self.domain = settings.FEDERATION_HOSTNAME
+ self.domain = models.Domain.objects.get_or_create(
+ name=settings.FEDERATION_HOSTNAME
+ )[0]
self.save(update_fields=["domain"])
if not create:
if extracted and hasattr(extracted, "pk"):
diff --git a/api/funkwhale_api/federation/migrations/0013_auto_20181226_1935.py b/api/funkwhale_api/federation/migrations/0013_auto_20181226_1935.py
new file mode 100644
index 0000000000000000000000000000000000000000..98a481271c0c32d196763614641db53359fab58f
--- /dev/null
+++ b/api/funkwhale_api/federation/migrations/0013_auto_20181226_1935.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.0.9 on 2018-12-26 19:35
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [("federation", "0012_auto_20180920_1803")]
+
+ operations = [
+ migrations.AlterField(
+ model_name="actor",
+ name="private_key",
+ field=models.TextField(blank=True, max_length=5000, null=True),
+ ),
+ migrations.AlterField(
+ model_name="actor",
+ name="public_key",
+ field=models.TextField(blank=True, max_length=5000, null=True),
+ ),
+ ]
diff --git a/api/funkwhale_api/federation/migrations/0014_auto_20181205_0958.py b/api/funkwhale_api/federation/migrations/0014_auto_20181205_0958.py
new file mode 100644
index 0000000000000000000000000000000000000000..7be361f871dce7dd45857243108aa44faec6ca44
--- /dev/null
+++ b/api/funkwhale_api/federation/migrations/0014_auto_20181205_0958.py
@@ -0,0 +1,46 @@
+# Generated by Django 2.0.9 on 2018-12-05 09:58
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [("federation", "0013_auto_20181226_1935")]
+
+ operations = [
+ migrations.CreateModel(
+ name="Domain",
+ fields=[
+ (
+ "name",
+ models.CharField(max_length=255, primary_key=True, serialize=False),
+ ),
+ (
+ "creation_date",
+ models.DateTimeField(default=django.utils.timezone.now),
+ ),
+ ],
+ ),
+ migrations.AlterField(
+ model_name="actor",
+ name="domain",
+ field=models.CharField(max_length=1000, null=True),
+ ),
+ migrations.RenameField("actor", "domain", "old_domain"),
+ migrations.AddField(
+ model_name="actor",
+ name="domain",
+ field=models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="actors",
+ to="federation.Domain",
+ ),
+ ),
+ migrations.AlterUniqueTogether(name="actor", unique_together=set()),
+ migrations.AlterUniqueTogether(
+ name="actor", unique_together={("domain", "preferred_username")}
+ ),
+ ]
diff --git a/api/funkwhale_api/federation/migrations/0015_populate_domains.py b/api/funkwhale_api/federation/migrations/0015_populate_domains.py
new file mode 100644
index 0000000000000000000000000000000000000000..0f0036c947fb3cf71ea1eb29062eb7059681d5c7
--- /dev/null
+++ b/api/funkwhale_api/federation/migrations/0015_populate_domains.py
@@ -0,0 +1,56 @@
+# Generated by Django 2.0.9 on 2018-11-14 08:55
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+def populate_domains(apps, schema_editor):
+ Domain = apps.get_model("federation", "Domain")
+ Actor = apps.get_model("federation", "Actor")
+
+ domains = set(
+ [v.lower() for v in Actor.objects.values_list("old_domain", flat=True)]
+ )
+ for domain in sorted(domains):
+ print("Populating domain {}...".format(domain))
+ first_actor = (
+ Actor.objects.order_by("creation_date")
+ .exclude(creation_date=None)
+ .filter(old_domain__iexact=domain)
+ .first()
+ )
+
+ if first_actor:
+ first_seen = first_actor.creation_date
+ else:
+ first_seen = django.utils.timezone.now()
+
+ Domain.objects.update_or_create(
+ name=domain, defaults={"creation_date": first_seen}
+ )
+
+ for domain in Domain.objects.all():
+ Actor.objects.filter(old_domain__iexact=domain.name).update(domain=domain)
+
+
+def skip(apps, schema_editor):
+ pass
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [("federation", "0014_auto_20181205_0958")]
+
+ operations = [
+ migrations.RunPython(populate_domains, skip),
+ migrations.AlterField(
+ model_name="actor",
+ name="domain",
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="actors",
+ to="federation.Domain",
+ ),
+ ),
+ ]
diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py
index 058bb9c46f578336aaeb2a432c9e93a07d5e3c37..7a450e4f50c07276a9c44fd6cf5aa4b6110ef0bf 100644
--- a/api/funkwhale_api/federation/models.py
+++ b/api/funkwhale_api/federation/models.py
@@ -62,6 +62,23 @@ class ActorQuerySet(models.QuerySet):
return qs
+class Domain(models.Model):
+ name = models.CharField(primary_key=True, max_length=255)
+ creation_date = models.DateTimeField(default=timezone.now)
+
+ def __str__(self):
+ return self.name
+
+ def save(self, **kwargs):
+ lowercase_fields = ["name"]
+ for field in lowercase_fields:
+ v = getattr(self, field, None)
+ if v:
+ setattr(self, field, v.lower())
+
+ super().save(**kwargs)
+
+
class Actor(models.Model):
ap_type = "Actor"
@@ -74,7 +91,7 @@ class Actor(models.Model):
shared_inbox_url = models.URLField(max_length=500, null=True, blank=True)
type = models.CharField(choices=TYPE_CHOICES, default="Person", max_length=25)
name = models.CharField(max_length=200, null=True, blank=True)
- domain = models.CharField(max_length=1000)
+ domain = models.ForeignKey(Domain, on_delete=models.CASCADE, related_name="actors")
summary = models.CharField(max_length=500, null=True, blank=True)
preferred_username = models.CharField(max_length=200, null=True, blank=True)
public_key = models.TextField(max_length=5000, null=True, blank=True)
@@ -110,36 +127,9 @@ class Actor(models.Model):
def __str__(self):
return "{}@{}".format(self.preferred_username, self.domain)
- def save(self, **kwargs):
- lowercase_fields = ["domain"]
- for field in lowercase_fields:
- v = getattr(self, field, None)
- if v:
- setattr(self, field, v.lower())
-
- super().save(**kwargs)
-
@property
def is_local(self):
- return self.domain == settings.FEDERATION_HOSTNAME
-
- @property
- def is_system(self):
- from . import actors
-
- return all(
- [
- settings.FEDERATION_HOSTNAME == self.domain,
- self.preferred_username in actors.SYSTEM_ACTORS,
- ]
- )
-
- @property
- def system_conf(self):
- from . import actors
-
- if self.is_system:
- return actors.SYSTEM_ACTORS[self.preferred_username]
+ return self.domain_id == settings.FEDERATION_HOSTNAME
def get_approved_followers(self):
follows = self.received_follows.filter(approved=True)
diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py
index 6c4ffeb587129a55389381ef84e83fc3f95cdbf7..76ab5ba86512054ce151b287b7fcd5a235ad9bdd 100644
--- a/api/funkwhale_api/federation/serializers.py
+++ b/api/funkwhale_api/federation/serializers.py
@@ -114,7 +114,8 @@ class ActorSerializer(serializers.Serializer):
if maf is not None:
kwargs["manually_approves_followers"] = maf
domain = urllib.parse.urlparse(kwargs["fid"]).netloc
- kwargs["domain"] = domain
+ kwargs["domain"] = models.Domain.objects.get_or_create(
+ pk=domain)[0]
for endpoint, url in self.initial_data.get("endpoints", {}).items():
if endpoint == "sharedInbox":
kwargs["shared_inbox_url"] = url
diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py
index 2bc87588ef0ab235ec51bce6270770148c905641..efd02407bb761850e65f122aa476a7ca7db030ba 100644
--- a/api/funkwhale_api/users/models.py
+++ b/api/funkwhale_api/users/models.py
@@ -252,7 +252,9 @@ def get_actor_data(user):
username = federation_utils.slugify_username(user.username)
return {
"preferred_username": username,
- "domain": settings.FEDERATION_HOSTNAME,
+ "domain": federation_models.Domain.objects.get_or_create(
+ name=settings.FEDERATION_HOSTNAME
+ )[0],
"type": "Person",
"name": user.username,
"manually_approves_followers": False,
diff --git a/api/tests/conftest.py b/api/tests/conftest.py
index 99317303c86aa8f9980e7dda2196fcf3e48906f8..22d8f7ebaf7b41a5249ccd1c307282a13efc03ed 100644
--- a/api/tests/conftest.py
+++ b/api/tests/conftest.py
@@ -12,12 +12,16 @@ from faker.providers import internet as internet_provider
import factory
import pytest
+from django.core.management import call_command
from django.contrib.auth.models import AnonymousUser
from django.core.cache import cache as django_cache, caches
from django.core.files import uploadedfile
from django.utils import timezone
from django.test import client
+from django.db import connection
+from django.db.migrations.executor import MigrationExecutor
from django.db.models import QuerySet
+
from dynamic_preferences.registries import global_preferences_registry
from rest_framework import fields as rest_fields
from rest_framework.test import APIClient, APIRequestFactory
@@ -400,3 +404,9 @@ def spa_html(r_mock, settings):
@pytest.fixture
def no_api_auth(preferences):
preferences["common__api_authentication_required"] = False
+
+
+@pytest.fixture()
+def migrator(transactional_db):
+ yield MigrationExecutor(connection)
+ call_command("migrate", interactive=False)
diff --git a/api/tests/federation/test_migrations.py b/api/tests/federation/test_migrations.py
new file mode 100644
index 0000000000000000000000000000000000000000..4a9ce427478a07bf201370106270fdefd7a0f2c8
--- /dev/null
+++ b/api/tests/federation/test_migrations.py
@@ -0,0 +1,34 @@
+def test_domain_14_migration(migrator):
+ a, f, t = ("federation", "0014_auto_20181205_0958", "0015_populate_domains")
+
+ migrator.migrate([(a, f)])
+ old_apps = migrator.loader.project_state([(a, f)]).apps
+ Actor = old_apps.get_model(a, "Actor")
+ a1 = Actor.objects.create(
+ fid="http://test1.com", preferred_username="test1", old_domain="dOmaiN1.com"
+ )
+ a2 = Actor.objects.create(
+ fid="http://test2.com", preferred_username="test2", old_domain="domain1.com"
+ )
+ a3 = Actor.objects.create(
+ fid="http://test3.com", preferred_username="test2", old_domain="domain2.com"
+ )
+
+ migrator.loader.build_graph()
+ migrator.migrate([(a, t)])
+ new_apps = migrator.loader.project_state([(a, t)]).apps
+
+ Actor = new_apps.get_model(a, "Actor")
+ Domain = new_apps.get_model(a, "Domain")
+
+ a1 = Actor.objects.get(pk=a1.pk)
+ a2 = Actor.objects.get(pk=a2.pk)
+ a3 = Actor.objects.get(pk=a3.pk)
+
+ assert Domain.objects.count() == 2
+ assert a1.domain == Domain.objects.get(pk="domain1.com")
+ assert a2.domain == Domain.objects.get(pk="domain1.com")
+ assert a3.domain == Domain.objects.get(pk="domain2.com")
+
+ assert Domain.objects.get(pk="domain1.com").creation_date == a1.creation_date
+ assert Domain.objects.get(pk="domain2.com").creation_date == a3.creation_date
diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py
index 4a6131934994e1e109128d07bebe38ad805587bb..18966430e21bd9ccc9ab4637a6178971b0f10804 100644
--- a/api/tests/federation/test_models.py
+++ b/api/tests/federation/test_models.py
@@ -54,3 +54,16 @@ def test_actor_get_quota(factories):
expected = {"total": 10, "pending": 1, "skipped": 2, "errored": 3, "finished": 4}
assert library.actor.get_current_usage() == expected
+
+
+@pytest.mark.parametrize(
+ "value, expected",
+ [
+ ("Domain.com", "domain.com"),
+ ("hello-WORLD.com", "hello-world.com"),
+ ("posés.com", "posés.com"),
+ ],
+)
+def test_domain_name_saved_properly(value, expected, factories):
+ domain = factories["federation.Domain"](name=value)
+ assert domain.name == expected
diff --git a/api/tests/federation/test_serializers.py b/api/tests/federation/test_serializers.py
index fe0485b52e2bfa9df24e4be3e94c3c9a62ed0eea..fb151b2d7d36d5823e128668ab0277fb8c5c21e0 100644
--- a/api/tests/federation/test_serializers.py
+++ b/api/tests/federation/test_serializers.py
@@ -43,7 +43,7 @@ def test_actor_serializer_from_ap(db):
assert actor.public_key == payload["publicKey"]["publicKeyPem"]
assert actor.preferred_username == payload["preferredUsername"]
assert actor.name == payload["name"]
- assert actor.domain == "test.federation"
+ assert actor.domain.pk == "test.federation"
assert actor.summary == payload["summary"]
assert actor.type == "Person"
assert actor.manually_approves_followers == payload["manuallyApprovesFollowers"]
@@ -71,7 +71,7 @@ def test_actor_serializer_only_mandatory_field_from_ap(db):
assert actor.followers_url == payload["followers"]
assert actor.following_url == payload["following"]
assert actor.preferred_username == payload["preferredUsername"]
- assert actor.domain == "test.federation"
+ assert actor.domain.pk == "test.federation"
assert actor.type == "Person"
assert actor.manually_approves_followers is None
@@ -110,7 +110,7 @@ def test_actor_serializer_to_ap():
public_key=expected["publicKey"]["publicKeyPem"],
preferred_username=expected["preferredUsername"],
name=expected["name"],
- domain="test.federation",
+ domain=models.Domain(pk="test.federation"),
summary=expected["summary"],
type="Person",
manually_approves_followers=False,
@@ -135,7 +135,7 @@ def test_webfinger_serializer():
actor = models.Actor(
fid=expected["links"][0]["href"],
preferred_username="service",
- domain="test.federation",
+ domain=models.Domain(pk="test.federation"),
)
serializer = serializers.ActorWebfingerSerializer(actor)
@@ -898,7 +898,7 @@ def test_local_actor_serializer_to_ap(factories):
public_key=expected["publicKey"]["publicKeyPem"],
preferred_username=expected["preferredUsername"],
name=expected["name"],
- domain="test.federation",
+ domain=models.Domain.objects.create(pk="test.federation"),
summary=expected["summary"],
type="Person",
manually_approves_followers=False,
diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py
index 69d33882848313df762e5f9a9f11d8b896450fe4..8e4ebea9777dcf0c594beb001c55bb4b7cb80c9f 100644
--- a/api/tests/users/test_models.py
+++ b/api/tests/users/test_models.py
@@ -137,7 +137,7 @@ def test_creating_actor_from_user(factories, settings):
actor = models.create_actor(user)
assert actor.preferred_username == "Hello_M_world" # slugified
- assert actor.domain == settings.FEDERATION_HOSTNAME
+ assert actor.domain.pk == settings.FEDERATION_HOSTNAME
assert actor.type == "Person"
assert actor.name == user.username
assert actor.manually_approves_followers is False