Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • funkwhale/funkwhale
  • Luclu7/funkwhale
  • mbothorel/funkwhale
  • EorlBruder/funkwhale
  • tcit/funkwhale
  • JocelynDelalande/funkwhale
  • eneiluj/funkwhale
  • reg/funkwhale
  • ButterflyOfFire/funkwhale
  • m4sk1n/funkwhale
  • wxcafe/funkwhale
  • andybalaam/funkwhale
  • jcgruenhage/funkwhale
  • pblayo/funkwhale
  • joshuaboniface/funkwhale
  • n3ddy/funkwhale
  • gegeweb/funkwhale
  • tohojo/funkwhale
  • emillumine/funkwhale
  • Te-k/funkwhale
  • asaintgenis/funkwhale
  • anoadragon453/funkwhale
  • Sakada/funkwhale
  • ilianaw/funkwhale
  • l4p1n/funkwhale
  • pnizet/funkwhale
  • dante383/funkwhale
  • interfect/funkwhale
  • akhardya/funkwhale
  • svfusion/funkwhale
  • noplanman/funkwhale
  • nykopol/funkwhale
  • roipoussiere/funkwhale
  • Von/funkwhale
  • aurieh/funkwhale
  • icaria36/funkwhale
  • floreal/funkwhale
  • paulwalko/funkwhale
  • comradekingu/funkwhale
  • FurryJulie/funkwhale
  • Legolars99/funkwhale
  • Vierkantor/funkwhale
  • zachhats/funkwhale
  • heyjake/funkwhale
  • sn0w/funkwhale
  • jvoisin/funkwhale
  • gordon/funkwhale
  • Alexander/funkwhale
  • bignose/funkwhale
  • qasim.ali/funkwhale
  • fakegit/funkwhale
  • Kxze/funkwhale
  • stenstad/funkwhale
  • creak/funkwhale
  • Kaze/funkwhale
  • Tixie/funkwhale
  • IISergII/funkwhale
  • lfuelling/funkwhale
  • nhaddag/funkwhale
  • yoasif/funkwhale
  • ifischer/funkwhale
  • keslerm/funkwhale
  • flupe/funkwhale
  • petitminion/funkwhale
  • ariasuni/funkwhale
  • ollie/funkwhale
  • ngaumont/funkwhale
  • techknowlogick/funkwhale
  • Shleeble/funkwhale
  • theflyingfrog/funkwhale
  • jonatron/funkwhale
  • neobrain/funkwhale
  • eorn/funkwhale
  • KokaKiwi/funkwhale
  • u1-liquid/funkwhale
  • marzzzello/funkwhale
  • sirenwatcher/funkwhale
  • newer027/funkwhale
  • codl/funkwhale
  • Zwordi/funkwhale
  • gisforgabriel/funkwhale
  • iuriatan/funkwhale
  • simon/funkwhale
  • bheesham/funkwhale
  • zeoses/funkwhale
  • accraze/funkwhale
  • meliurwen/funkwhale
  • divadsn/funkwhale
  • Etua/funkwhale
  • sdrik/funkwhale
  • Soran/funkwhale
  • kuba-orlik/funkwhale
  • cristianvogel/funkwhale
  • Forceu/funkwhale
  • jeff/funkwhale
  • der_scheibenhacker/funkwhale
  • owlnical/funkwhale
  • jovuit/funkwhale
  • SilverFox15/funkwhale
  • phw/funkwhale
  • mayhem/funkwhale
  • sridhar/funkwhale
  • stromlin/funkwhale
  • rrrnld/funkwhale
  • nitaibezerra/funkwhale
  • jaller94/funkwhale
  • pcouy/funkwhale
  • eduxstad/funkwhale
  • codingHahn/funkwhale
  • captain/funkwhale
  • polyedre/funkwhale
  • leishenailong/funkwhale
  • ccritter/funkwhale
  • lnceballosz/funkwhale
  • fpiesche/funkwhale
  • Fanyx/funkwhale
  • markusblogde/funkwhale
  • Firobe/funkwhale
  • devilcius/funkwhale
  • freaktechnik/funkwhale
  • blopware/funkwhale
  • cone/funkwhale
  • thanksd/funkwhale
  • vachan-maker/funkwhale
  • bbenti/funkwhale
  • tarator/funkwhale
  • prplecake/funkwhale
  • DMarzal/funkwhale
  • lullis/funkwhale
  • hanacgr/funkwhale
  • albjeremias/funkwhale
  • xeruf/funkwhale
  • llelite/funkwhale
  • RoiArthurB/funkwhale
  • cloo/funkwhale
  • nztvar/funkwhale
  • Keunes/funkwhale
  • petitminion/funkwhale-petitminion
  • m-idler/funkwhale
  • SkyLeite/funkwhale
140 results
Select Git revision
Show changes
Showing
with 602 additions and 60 deletions
import requests import requests
from django.conf import settings
from funkwhale_api.common import session from funkwhale_api.common import session
...@@ -12,9 +11,7 @@ def get_library_data(library_url, actor): ...@@ -12,9 +11,7 @@ def get_library_data(library_url, actor):
response = session.get_session().get( response = session.get_session().get(
library_url, library_url,
auth=auth, auth=auth,
timeout=5, headers={"Accept": "application/activity+json"},
verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
headers={"Content-Type": "application/activity+json"},
) )
except requests.ConnectionError: except requests.ConnectionError:
return {"errors": ["This library is not reachable"]} return {"errors": ["This library is not reachable"]}
...@@ -24,7 +21,7 @@ def get_library_data(library_url, actor): ...@@ -24,7 +21,7 @@ def get_library_data(library_url, actor):
elif scode == 403: elif scode == 403:
return {"errors": ["Permission denied while scanning library"]} return {"errors": ["Permission denied while scanning library"]}
elif scode >= 400: elif scode >= 400:
return {"errors": ["Error {} while fetching the library".format(scode)]} return {"errors": [f"Error {scode} while fetching the library"]}
serializer = serializers.LibrarySerializer(data=response.json()) serializer = serializers.LibrarySerializer(data=response.json())
if not serializer.is_valid(): if not serializer.is_valid():
return {"errors": ["Invalid ActivityPub response from remote library"]} return {"errors": ["Invalid ActivityPub response from remote library"]}
...@@ -37,9 +34,7 @@ def get_library_page(library, page_url, actor): ...@@ -37,9 +34,7 @@ def get_library_page(library, page_url, actor):
response = session.get_session().get( response = session.get_session().get(
page_url, page_url,
auth=auth, auth=auth,
timeout=5, headers={"Accept": "application/activity+json"},
verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL,
headers={"Content-Type": "application/activity+json"},
) )
serializer = serializers.CollectionPageSerializer( serializer = serializers.CollectionPageSerializer(
data=response.json(), data=response.json(),
......
...@@ -4,13 +4,12 @@ from funkwhale_api.common import utils ...@@ -4,13 +4,12 @@ from funkwhale_api.common import utils
from funkwhale_api.federation import models as federation_models from funkwhale_api.federation import models as federation_models
from funkwhale_api.music import models as music_models from funkwhale_api.music import models as music_models
MODELS = [ MODELS = [
(music_models.Artist, ["fid"]), (music_models.Artist, ["fid"]),
(music_models.Album, ["fid"]), (music_models.Album, ["fid"]),
(music_models.Track, ["fid"]), (music_models.Track, ["fid"]),
(music_models.Upload, ["fid"]), (music_models.Upload, ["fid"]),
(music_models.Library, ["fid", "followers_url"]), (music_models.Library, ["fid"]),
( (
federation_models.Actor, federation_models.Actor,
[ [
...@@ -31,7 +30,7 @@ MODELS = [ ...@@ -31,7 +30,7 @@ MODELS = [
class Command(BaseCommand): class Command(BaseCommand):
help = """ help = """
Find and replace wrong protocal/domain in local federation ids. Find and replace wrong protocol/domain in local federation ids.
Use with caution and only if you know what you are doing. Use with caution and only if you know what you are doing.
""" """
...@@ -68,9 +67,7 @@ class Command(BaseCommand): ...@@ -68,9 +67,7 @@ class Command(BaseCommand):
for kls, fields in MODELS: for kls, fields in MODELS:
results[kls] = {} results[kls] = {}
for field in fields: for field in fields:
candidates = kls.objects.filter( candidates = kls.objects.filter(**{f"{field}__startswith": old_prefix})
**{"{}__startswith".format(field): old_prefix}
)
results[kls][field] = candidates.count() results[kls][field] = candidates.count()
total = sum([t for k in results.values() for t in k.values()]) total = sum([t for k in results.values() for t in k.values()])
...@@ -78,7 +75,7 @@ class Command(BaseCommand): ...@@ -78,7 +75,7 @@ class Command(BaseCommand):
if total: if total:
self.stdout.write( self.stdout.write(
self.style.WARNING( self.style.WARNING(
"Will replace {} found occurences of '{}' by '{}':".format( "Will replace {} found occurrences of '{}' by '{}':".format(
total, old_prefix, new_prefix total, old_prefix, new_prefix
) )
) )
...@@ -93,9 +90,7 @@ class Command(BaseCommand): ...@@ -93,9 +90,7 @@ class Command(BaseCommand):
) )
else: else:
self.stdout.write( self.stdout.write(f"No objects found with prefix {old_prefix}, exiting.")
"No objects found with prefix {}, exiting.".format(old_prefix)
)
return return
if options["dry_run"]: if options["dry_run"]:
self.stdout.write( self.stdout.write(
...@@ -113,9 +108,7 @@ class Command(BaseCommand): ...@@ -113,9 +108,7 @@ class Command(BaseCommand):
for kls, fields in results.items(): for kls, fields in results.items():
for field, count in fields.items(): for field, count in fields.items():
self.stdout.write( self.stdout.write(f"Replacing {field} on {count} {kls._meta.label}")
"Replacing {} on {} {}…".format(field, count, kls._meta.label)
)
candidates = kls.objects.all() candidates = kls.objects.all()
utils.replace_prefix(candidates, field, old=old_prefix, new=new_prefix) utils.replace_prefix(candidates, field, old=old_prefix, new=new_prefix)
self.stdout.write("") self.stdout.write("")
......
...@@ -75,7 +75,7 @@ class Migration(migrations.Migration): ...@@ -75,7 +75,7 @@ class Migration(migrations.Migration):
"last_fetch_date", "last_fetch_date",
models.DateTimeField(default=django.utils.timezone.now), models.DateTimeField(default=django.utils.timezone.now),
), ),
("manually_approves_followers", models.NullBooleanField(default=None)), ("manually_approves_followers", models.BooleanField(default=None, null=True)),
], ],
) )
] ]
...@@ -77,7 +77,7 @@ class Migration(migrations.Migration): ...@@ -77,7 +77,7 @@ class Migration(migrations.Migration):
models.DateTimeField(default=django.utils.timezone.now), models.DateTimeField(default=django.utils.timezone.now),
), ),
("modification_date", models.DateTimeField(auto_now=True)), ("modification_date", models.DateTimeField(auto_now=True)),
("approved", models.NullBooleanField(default=None)), ("approved", models.BooleanField(default=None, null=True)),
( (
"actor", "actor",
models.ForeignKey( models.ForeignKey(
......
...@@ -14,7 +14,7 @@ class Migration(migrations.Migration): ...@@ -14,7 +14,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name="follow", model_name="follow",
name="approved", name="approved",
field=models.NullBooleanField(default=None), field=models.BooleanField(default=None, null=True),
), ),
migrations.AddField( migrations.AddField(
model_name="library", model_name="library",
......
...@@ -43,7 +43,7 @@ class Migration(migrations.Migration): ...@@ -43,7 +43,7 @@ class Migration(migrations.Migration):
"creation_date", "creation_date",
models.DateTimeField(default=django.utils.timezone.now), models.DateTimeField(default=django.utils.timezone.now),
), ),
("delivered", models.NullBooleanField(default=None)), ("delivered", models.BooleanField(default=None, null=True)),
("delivered_date", models.DateTimeField(blank=True, null=True)), ("delivered_date", models.DateTimeField(blank=True, null=True)),
], ],
), ),
...@@ -69,7 +69,7 @@ class Migration(migrations.Migration): ...@@ -69,7 +69,7 @@ class Migration(migrations.Migration):
models.DateTimeField(default=django.utils.timezone.now), models.DateTimeField(default=django.utils.timezone.now),
), ),
("modification_date", models.DateTimeField(auto_now=True)), ("modification_date", models.DateTimeField(auto_now=True)),
("approved", models.NullBooleanField(default=None)), ("approved", models.BooleanField(default=None, null=True)),
], ],
), ),
migrations.RenameField("actor", "url", "fid"), migrations.RenameField("actor", "url", "fid"),
......
...@@ -10,7 +10,7 @@ def populate_domains(apps, schema_editor): ...@@ -10,7 +10,7 @@ def populate_domains(apps, schema_editor):
Actor = apps.get_model("federation", "Actor") Actor = apps.get_model("federation", "Actor")
domains = set( domains = set(
[v.lower() for v in Actor.objects.values_list("old_domain", flat=True)] [v.lower() for v in Actor.objects.values_list("old_domain", flat=True) if v]
) )
for domain in sorted(domains): for domain in sorted(domains):
print("Populating domain {}...".format(domain)) print("Populating domain {}...".format(domain))
......
# Generated by Django 2.1.7 on 2019-04-17 14:57
import django.contrib.postgres.fields.jsonb
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import funkwhale_api.federation.models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('federation', '0017_auto_20190130_0926'),
]
operations = [
migrations.CreateModel(
name='Fetch',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.URLField(db_index=True, max_length=500)),
('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
('fetch_date', models.DateTimeField(blank=True, null=True)),
('object_id', models.IntegerField(null=True)),
('status', models.CharField(choices=[('pending', 'Pending'), ('errored', 'Errored'), ('finished', 'Finished'), ('skipped', 'Skipped')], default='pending', max_length=20)),
('detail', django.contrib.postgres.fields.jsonb.JSONField(default=funkwhale_api.federation.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=50000)),
('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fetches', to='federation.Actor')),
('object_content_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
),
]
# Generated by Django 2.2.2 on 2019-06-11 08:51
import django.contrib.postgres.fields.jsonb
import django.core.serializers.json
from django.db import migrations, models
import funkwhale_api.federation.models
class Migration(migrations.Migration):
dependencies = [("federation", "0018_fetch")]
operations = [
migrations.AddField(
model_name="domain",
name="allowed",
field=models.BooleanField(default=None, null=True),
)
]
# Generated by Django 2.2.3 on 2019-07-30 08:46
import django.contrib.postgres.fields.jsonb
import django.core.serializers.json
from django.db import migrations
import funkwhale_api.federation.models
class Migration(migrations.Migration):
dependencies = [
('federation', '0019_auto_20190611_0851'),
]
operations = [
migrations.AlterField(
model_name='activity',
name='payload',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=50000),
),
migrations.AlterField(
model_name='fetch',
name='detail',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=50000),
),
migrations.AlterField(
model_name='librarytrack',
name='metadata',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=10000),
),
]
# Generated by Django 2.2.6 on 2019-10-29 12:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('federation', '0020_auto_20190730_0846'),
]
operations = [
migrations.AlterModelOptions(
name='actor',
options={'verbose_name': 'Account'},
),
migrations.AlterField(
model_name='actor',
name='type',
field=models.CharField(choices=[('Person', 'Person'), ('Tombstone', 'Tombstone'), ('Application', 'Application'), ('Group', 'Group'), ('Organization', 'Organization'), ('Service', 'Service')], default='Person', max_length=25),
),
]
# Generated by Django 2.2.7 on 2019-12-04 15:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('federation', '0021_auto_20191029_1257'),
]
operations = [
migrations.AlterField(
model_name='actor',
name='inbox_url',
field=models.URLField(blank=True, max_length=500, null=True),
),
migrations.AlterField(
model_name='actor',
name='outbox_url',
field=models.URLField(blank=True, max_length=500, null=True),
),
]
# Generated by Django 2.2.9 on 2020-01-22 11:01
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('common', '0007_auto_20200116_1610'),
('federation', '0022_auto_20191204_1539'),
]
operations = [
migrations.AddField(
model_name='actor',
name='summary_obj',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.Content'),
),
]
# Generated by Django 2.2.9 on 2020-01-23 13:59
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('common', '0007_auto_20200116_1610'),
('federation', '0023_actor_summary_obj'),
]
operations = [
migrations.AddField(
model_name='actor',
name='attachment_icon',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='iconed_actor', to='common.Attachment'),
),
]
# Generated by Django 3.0.4 on 2020-03-17 08:20
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('federation', '0024_actor_attachment_icon'),
]
operations = [
migrations.AlterField(
model_name='activity',
name='object_content_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objecting_activities', to='contenttypes.ContentType'),
),
migrations.AlterField(
model_name='activity',
name='object_id',
field=models.IntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='activity',
name='related_object_content_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='related_objecting_activities', to='contenttypes.ContentType'),
),
migrations.AlterField(
model_name='activity',
name='related_object_id',
field=models.IntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='activity',
name='target_content_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='targeting_activities', to='contenttypes.ContentType'),
),
migrations.AlterField(
model_name='activity',
name='target_id',
field=models.IntegerField(blank=True, null=True),
),
]
# 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 update_public_key_format(apps, schema_editor):
"""
Reserialize keys in proper format (PKCS#8 instead of #1)
https://github.com/friendica/friendica/issues/7771#issuecomment-603019826
"""
Actor = apps.get_model("federation", "Actor")
local_actors = list(
Actor.objects.exclude(private_key="")
.exclude(private_key=None)
.only("pk", "private_key", "public_key")
.order_by("id")
)
total = len(local_actors)
if total:
print("{} keys to update...".format(total))
else:
print("Skipping")
return
from cryptography.hazmat.primitives import serialization as crypto_serialization
from cryptography.hazmat.backends import default_backend
for actor in local_actors:
private_key = crypto_serialization.load_pem_private_key(
actor.private_key.encode(), password=None, backend=default_backend()
)
public_key = private_key.public_key().public_bytes(
crypto_serialization.Encoding.PEM,
crypto_serialization.PublicFormat.SubjectPublicKeyInfo,
)
actor.public_key = public_key.decode()
Actor.objects.bulk_update(local_actors, ["public_key"])
print("Done!")
def skip(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [("federation", "0025_auto_20200317_0820")]
operations = [
migrations.RunPython(update_public_key_format, skip),
]
# Generated by Django 3.2.13 on 2022-06-27 19:15
import django.core.serializers.json
from django.db import migrations, models
import funkwhale_api.federation.models
class Migration(migrations.Migration):
dependencies = [
('federation', '0026_public_key_format'),
]
operations = [
migrations.AlterField(
model_name='activity',
name='payload',
field=models.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=50000),
),
migrations.AlterField(
model_name='actor',
name='manually_approves_followers',
field=models.BooleanField(default=None, null=True),
),
migrations.AlterField(
model_name='domain',
name='nodeinfo',
field=models.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, max_length=50000),
),
migrations.AlterField(
model_name='fetch',
name='detail',
field=models.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=50000),
),
migrations.AlterField(
model_name='follow',
name='approved',
field=models.BooleanField(default=None, null=True),
),
migrations.AlterField(
model_name='libraryfollow',
name='approved',
field=models.BooleanField(default=None, null=True),
),
migrations.AlterField(
model_name='librarytrack',
name='metadata',
field=models.JSONField(blank=True, default=funkwhale_api.federation.models.empty_dict, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=10000),
),
]
# Generated by Django 3.2.16 on 2022-10-27 11:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('federation', '0027_auto_20220627_1915'),
]
operations = [
migrations.AddField(
model_name='domain',
name='last_successful_contact',
field=models.DateTimeField(default=None, null=True),
),
migrations.AddField(
model_name='domain',
name='reachable',
field=models.BooleanField(default=True),
),
]
# Generated by Django 5.1.6 on 2025-08-04 13:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("federation", "0028_auto_20221027_1141"),
("music", "0061_migrate_libraries_to_playlist"),
]
operations = [
migrations.AddField(
model_name="domain",
name="reachable_retries",
field=models.PositiveIntegerField(default=0),
),
]
import tempfile import tempfile
import urllib.parse
import uuid import uuid
from django.conf import settings from django.conf import settings
from django.contrib.postgres.fields import JSONField
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.db import models from django.db import models
from django.utils import timezone from django.db.models import JSONField
from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver
from django.urls import reverse from django.urls import reverse
from django.utils import timezone
from funkwhale_api.common import session from funkwhale_api.common import session
from funkwhale_api.common import utils as common_utils from funkwhale_api.common import utils as common_utils
...@@ -20,12 +23,17 @@ from . import utils as federation_utils ...@@ -20,12 +23,17 @@ from . import utils as federation_utils
TYPE_CHOICES = [ TYPE_CHOICES = [
("Person", "Person"), ("Person", "Person"),
("Tombstone", "Tombstone"),
("Application", "Application"), ("Application", "Application"),
("Group", "Group"), ("Group", "Group"),
("Organization", "Organization"), ("Organization", "Organization"),
("Service", "Service"), ("Service", "Service"),
] ]
MAX_LENGTHS = {
"ACTOR_NAME": 200,
}
def empty_dict(): def empty_dict():
return {} return {}
...@@ -43,6 +51,18 @@ class FederationMixin(models.Model): ...@@ -43,6 +51,18 @@ class FederationMixin(models.Model):
class Meta: class Meta:
abstract = True abstract = True
@property
def is_local(self) -> bool:
return federation_utils.is_local(self.fid)
@property
def domain_name(self):
if not self.fid:
return
parsed = urllib.parse.urlparse(self.fid)
return parsed.hostname
class ActorQuerySet(models.QuerySet): class ActorQuerySet(models.QuerySet):
def local(self, include=True): def local(self, include=True):
...@@ -52,12 +72,16 @@ class ActorQuerySet(models.QuerySet): ...@@ -52,12 +72,16 @@ class ActorQuerySet(models.QuerySet):
def with_current_usage(self): def with_current_usage(self):
qs = self qs = self
for s in ["pending", "skipped", "errored", "finished"]: for s in ["draft", "pending", "skipped", "errored", "finished"]:
uploads_query = models.Q(
libraries__uploads__import_status=s,
libraries__uploads__audio_file__isnull=False,
libraries__uploads__audio_file__ne="",
)
qs = qs.annotate( qs = qs.annotate(
**{ **{
"_usage_{}".format(s): models.Sum( f"_usage_{s}": models.Sum(
"libraries__uploads__size", "libraries__uploads__size", filter=uploads_query
filter=models.Q(libraries__uploads__import_status=s),
) )
} }
) )
...@@ -101,6 +125,11 @@ class Domain(models.Model): ...@@ -101,6 +125,11 @@ class Domain(models.Model):
null=True, null=True,
blank=True, blank=True,
) )
# are interactions with this domain allowed (only applies when allow-listing is on)
allowed = models.BooleanField(default=None, null=True)
reachable = models.BooleanField(default=True)
last_successful_contact = models.DateTimeField(default=None, null=True)
reachable_retries = models.PositiveIntegerField(default=0)
objects = DomainQuerySet.as_manager() objects = DomainQuerySet.as_manager()
def __str__(self): def __str__(self):
...@@ -119,15 +148,16 @@ class Domain(models.Model): ...@@ -119,15 +148,16 @@ class Domain(models.Model):
from funkwhale_api.music import models as music_models from funkwhale_api.music import models as music_models
data = Domain.objects.filter(pk=self.pk).aggregate( data = Domain.objects.filter(pk=self.pk).aggregate(
actors=models.Count("actors", distinct=True),
outbox_activities=models.Count("actors__outbox_activities", distinct=True), outbox_activities=models.Count("actors__outbox_activities", distinct=True),
libraries=models.Count("actors__libraries", distinct=True), libraries=models.Count("actors__libraries", distinct=True),
channels=models.Count("actors__owned_channels", distinct=True),
received_library_follows=models.Count( received_library_follows=models.Count(
"actors__libraries__received_follows", distinct=True "actors__libraries__received_follows", distinct=True
), ),
emitted_library_follows=models.Count( emitted_library_follows=models.Count(
"actors__library_follows", distinct=True "actors__library_follows", distinct=True
), ),
actors=models.Count("actors", distinct=True),
) )
data["artists"] = music_models.Artist.objects.filter( data["artists"] = music_models.Artist.objects.filter(
from_activity__actor__domain_id=self.pk from_activity__actor__domain_id=self.pk
...@@ -147,27 +177,34 @@ class Domain(models.Model): ...@@ -147,27 +177,34 @@ class Domain(models.Model):
) )
return data return data
@property
def is_local(self) -> bool:
return self.name == settings.FEDERATION_HOSTNAME
class Actor(models.Model): class Actor(models.Model):
ap_type = "Actor" ap_type = "Actor"
fid = models.URLField(unique=True, max_length=500, db_index=True) fid = models.URLField(unique=True, max_length=500, db_index=True)
url = models.URLField(max_length=500, null=True, blank=True) url = models.URLField(max_length=500, null=True, blank=True)
outbox_url = models.URLField(max_length=500) outbox_url = models.URLField(max_length=500, null=True, blank=True)
inbox_url = models.URLField(max_length=500) inbox_url = models.URLField(max_length=500, null=True, blank=True)
following_url = models.URLField(max_length=500, null=True, blank=True) following_url = models.URLField(max_length=500, null=True, blank=True)
followers_url = models.URLField(max_length=500, null=True, blank=True) followers_url = models.URLField(max_length=500, null=True, blank=True)
shared_inbox_url = models.URLField(max_length=500, null=True, blank=True) shared_inbox_url = models.URLField(max_length=500, null=True, blank=True)
type = models.CharField(choices=TYPE_CHOICES, default="Person", max_length=25) type = models.CharField(choices=TYPE_CHOICES, default="Person", max_length=25)
name = models.CharField(max_length=200, null=True, blank=True) name = models.CharField(max_length=MAX_LENGTHS["ACTOR_NAME"], null=True, blank=True)
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, related_name="actors") domain = models.ForeignKey(Domain, on_delete=models.CASCADE, related_name="actors")
summary = models.CharField(max_length=500, null=True, blank=True) summary = models.CharField(max_length=500, null=True, blank=True)
summary_obj = models.ForeignKey(
"common.Content", null=True, blank=True, on_delete=models.SET_NULL
)
preferred_username = models.CharField(max_length=200, 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) public_key = models.TextField(max_length=5000, null=True, blank=True)
private_key = models.TextField(max_length=5000, null=True, blank=True) private_key = models.TextField(max_length=5000, null=True, blank=True)
creation_date = models.DateTimeField(default=timezone.now) creation_date = models.DateTimeField(default=timezone.now)
last_fetch_date = models.DateTimeField(default=timezone.now) last_fetch_date = models.DateTimeField(default=timezone.now)
manually_approves_followers = models.NullBooleanField(default=None) manually_approves_followers = models.BooleanField(default=None, null=True)
followers = models.ManyToManyField( followers = models.ManyToManyField(
to="self", to="self",
symmetrical=False, symmetrical=False,
...@@ -175,36 +212,54 @@ class Actor(models.Model): ...@@ -175,36 +212,54 @@ class Actor(models.Model):
through_fields=("target", "actor"), through_fields=("target", "actor"),
related_name="following", related_name="following",
) )
attachment_icon = models.ForeignKey(
"common.Attachment",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="iconed_actor",
)
objects = ActorQuerySet.as_manager() objects = ActorQuerySet.as_manager()
class Meta: class Meta:
unique_together = ["domain", "preferred_username"] unique_together = ["domain", "preferred_username"]
verbose_name = "Account"
def get_moderation_url(self):
return f"/manage/moderation/accounts/{self.full_username}"
@property @property
def webfinger_subject(self): def webfinger_subject(self):
return "{}@{}".format(self.preferred_username, settings.FEDERATION_HOSTNAME) return f"{self.preferred_username}@{settings.FEDERATION_HOSTNAME}"
@property @property
def private_key_id(self): def private_key_id(self):
return "{}#main-key".format(self.fid) return f"{self.fid}#main-key"
@property @property
def full_username(self): def full_username(self) -> str:
return "{}@{}".format(self.preferred_username, self.domain_id) return f"{self.preferred_username}@{self.domain_id}"
def __str__(self): def __str__(self):
return "{}@{}".format(self.preferred_username, self.domain_id) return f"{self.preferred_username}@{self.domain_id}"
@property @property
def is_local(self): def is_local(self) -> bool:
return self.domain_id == settings.FEDERATION_HOSTNAME return self.domain_id == settings.FEDERATION_HOSTNAME
def get_approved_followers(self): def get_approved_followers(self):
follows = self.received_follows.filter(approved=True) follows = self.received_follows.filter(approved=True)
return self.followers.filter(pk__in=follows.values_list("actor", flat=True)) return self.followers.filter(pk__in=follows.values_list("actor", flat=True))
def get_approved_followings(self):
follows = self.emitted_follows.filter(approved=True)
return Actor.objects.filter(pk__in=follows.values_list("target", flat=True))
def should_autoapprove_follow(self, actor): def should_autoapprove_follow(self, actor):
if self.get_channel():
return True
if self.user.privacy_level == "public":
return True
return False return False
def get_user(self): def get_user(self):
...@@ -213,29 +268,46 @@ class Actor(models.Model): ...@@ -213,29 +268,46 @@ class Actor(models.Model):
except ObjectDoesNotExist: except ObjectDoesNotExist:
return None return None
def get_channel(self):
try:
return self.channel
except ObjectDoesNotExist:
return None
def get_absolute_url(self):
if self.is_local:
return federation_utils.full_url(f"/@{self.preferred_username}")
return self.url or self.fid
def get_current_usage(self): def get_current_usage(self):
actor = self.__class__.objects.filter(pk=self.pk).with_current_usage().get() actor = self.__class__.objects.filter(pk=self.pk).with_current_usage().get()
data = {} data = {}
for s in ["pending", "skipped", "errored", "finished"]: for s in ["draft", "pending", "skipped", "errored", "finished"]:
data[s] = getattr(actor, "_usage_{}".format(s)) or 0 data[s] = getattr(actor, f"_usage_{s}") or 0
data["total"] = sum(data.values()) data["total"] = sum(data.values())
return data return data
def get_stats(self): def get_stats(self):
from funkwhale_api.moderation import models as moderation_models
from funkwhale_api.music import models as music_models from funkwhale_api.music import models as music_models
data = Actor.objects.filter(pk=self.pk).aggregate( data = Actor.objects.filter(pk=self.pk).aggregate(
outbox_activities=models.Count("outbox_activities", distinct=True), outbox_activities=models.Count("outbox_activities", distinct=True),
libraries=models.Count("libraries", distinct=True), channels=models.Count("owned_channels", distinct=True),
received_library_follows=models.Count( received_library_follows=models.Count(
"libraries__received_follows", distinct=True "libraries__received_follows", distinct=True
), ),
emitted_library_follows=models.Count("library_follows", distinct=True), emitted_library_follows=models.Count("library_follows", distinct=True),
libraries=models.Count("libraries", distinct=True),
) )
data["artists"] = music_models.Artist.objects.filter( data["artists"] = music_models.Artist.objects.filter(
from_activity__actor=self.pk from_activity__actor=self.pk
).count() ).count()
data["reports"] = moderation_models.Report.objects.get_for_target(self).count()
data["requests"] = moderation_models.UserRequest.objects.filter(
submitter=self
).count()
data["albums"] = music_models.Album.objects.filter( data["albums"] = music_models.Album.objects.filter(
from_activity__actor=self.pk from_activity__actor=self.pk
).count() ).count()
...@@ -260,6 +332,88 @@ class Actor(models.Model): ...@@ -260,6 +332,88 @@ class Actor(models.Model):
self.private_key = v[0].decode("utf-8") self.private_key = v[0].decode("utf-8")
self.public_key = v[1].decode("utf-8") self.public_key = v[1].decode("utf-8")
def can_manage(self, obj):
attributed_to = getattr(obj, "attributed_to_id", None)
if attributed_to is not None and attributed_to == self.pk:
# easiest case, the obj is attributed to the actor
return True
if self.domain.service_actor_id != self.pk:
# actor is not system actor, so there is no way the actor can manage
# the object
return False
# actor is service actor of its domain, so if the fid domain
# matches, we consider the actor has the permission to manage
# the object
domain = self.domain_id
return obj.fid.startswith(f"http://{domain}/") or obj.fid.startswith(
f"https://{domain}/"
)
@property
def display_name(self):
return self.name or self.preferred_username
FETCH_STATUSES = [
("pending", "Pending"),
("errored", "Errored"),
("finished", "Finished"),
("skipped", "Skipped"),
]
class FetchQuerySet(models.QuerySet):
def get_for_object(self, object):
content_type = ContentType.objects.get_for_model(object)
return self.filter(object_content_type=content_type, object_id=object.pk)
class Fetch(models.Model):
url = models.URLField(max_length=500, db_index=True)
creation_date = models.DateTimeField(default=timezone.now)
fetch_date = models.DateTimeField(null=True, blank=True)
object_id = models.IntegerField(null=True)
object_content_type = models.ForeignKey(
ContentType, null=True, on_delete=models.CASCADE
)
object = GenericForeignKey("object_content_type", "object_id")
status = models.CharField(default="pending", choices=FETCH_STATUSES, max_length=20)
detail = JSONField(
default=empty_dict, max_length=50000, encoder=DjangoJSONEncoder, blank=True
)
actor = models.ForeignKey(Actor, related_name="fetches", on_delete=models.CASCADE)
objects = FetchQuerySet.as_manager()
def save(self, **kwargs):
if not self.url and self.object and hasattr(self.object, "fid"):
self.url = self.object.fid
super().save(**kwargs)
@property
def serializers(self):
from . import contexts, serializers
return {
contexts.FW.Artist: [serializers.ArtistSerializer],
contexts.FW.Album: [serializers.AlbumSerializer],
contexts.FW.Track: [serializers.TrackSerializer],
contexts.AS.Audio: [
serializers.UploadSerializer,
serializers.ChannelUploadSerializer,
],
contexts.FW.Library: [serializers.LibrarySerializer],
contexts.FW.Playlist: [serializers.PlaylistSerializer],
contexts.AS.Group: [serializers.ActorSerializer],
contexts.AS.Person: [serializers.ActorSerializer],
contexts.AS.Organization: [serializers.ActorSerializer],
contexts.AS.Service: [serializers.ActorSerializer],
contexts.AS.Application: [serializers.ActorSerializer],
}
class InboxItem(models.Model): class InboxItem(models.Model):
""" """
...@@ -301,31 +455,36 @@ class Activity(models.Model): ...@@ -301,31 +455,36 @@ class Activity(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, unique=True) uuid = models.UUIDField(default=uuid.uuid4, unique=True)
fid = models.URLField(unique=True, max_length=500, null=True, blank=True) fid = models.URLField(unique=True, max_length=500, null=True, blank=True)
url = models.URLField(max_length=500, null=True, blank=True) url = models.URLField(max_length=500, null=True, blank=True)
payload = JSONField(default=empty_dict, max_length=50000, encoder=DjangoJSONEncoder) payload = JSONField(
default=empty_dict, max_length=50000, encoder=DjangoJSONEncoder, blank=True
)
creation_date = models.DateTimeField(default=timezone.now, db_index=True) creation_date = models.DateTimeField(default=timezone.now, db_index=True)
type = models.CharField(db_index=True, null=True, max_length=100) type = models.CharField(db_index=True, null=True, max_length=100)
# generic relations # generic relations
object_id = models.IntegerField(null=True) object_id = models.IntegerField(null=True, blank=True)
object_content_type = models.ForeignKey( object_content_type = models.ForeignKey(
ContentType, ContentType,
null=True, null=True,
blank=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="objecting_activities", related_name="objecting_activities",
) )
object = GenericForeignKey("object_content_type", "object_id") object = GenericForeignKey("object_content_type", "object_id")
target_id = models.IntegerField(null=True) target_id = models.IntegerField(null=True, blank=True)
target_content_type = models.ForeignKey( target_content_type = models.ForeignKey(
ContentType, ContentType,
null=True, null=True,
blank=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="targeting_activities", related_name="targeting_activities",
) )
target = GenericForeignKey("target_content_type", "target_id") target = GenericForeignKey("target_content_type", "target_id")
related_object_id = models.IntegerField(null=True) related_object_id = models.IntegerField(null=True, blank=True)
related_object_content_type = models.ForeignKey( related_object_content_type = models.ForeignKey(
ContentType, ContentType,
null=True, null=True,
blank=True,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name="related_objecting_activities", related_name="related_objecting_activities",
) )
...@@ -340,15 +499,13 @@ class AbstractFollow(models.Model): ...@@ -340,15 +499,13 @@ class AbstractFollow(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, unique=True) uuid = models.UUIDField(default=uuid.uuid4, unique=True)
creation_date = models.DateTimeField(default=timezone.now) creation_date = models.DateTimeField(default=timezone.now)
modification_date = models.DateTimeField(auto_now=True) modification_date = models.DateTimeField(auto_now=True)
approved = models.NullBooleanField(default=None) approved = models.BooleanField(default=None, null=True)
class Meta: class Meta:
abstract = True abstract = True
def get_federation_id(self): def get_federation_id(self):
return federation_utils.full_url( return federation_utils.full_url(f"{self.actor.fid}#follows/{self.uuid}")
"{}#follows/{}".format(self.actor.fid, self.uuid)
)
class Follow(AbstractFollow): class Follow(AbstractFollow):
...@@ -417,7 +574,7 @@ class LibraryTrack(models.Model): ...@@ -417,7 +574,7 @@ class LibraryTrack(models.Model):
album_title = models.CharField(max_length=500) album_title = models.CharField(max_length=500)
title = models.CharField(max_length=500) title = models.CharField(max_length=500)
metadata = JSONField( metadata = JSONField(
default=empty_dict, max_length=10000, encoder=DjangoJSONEncoder default=empty_dict, max_length=10000, encoder=DjangoJSONEncoder, blank=True
) )
@property @property
...@@ -436,14 +593,13 @@ class LibraryTrack(models.Model): ...@@ -436,14 +593,13 @@ class LibraryTrack(models.Model):
auth=auth, auth=auth,
stream=True, stream=True,
timeout=20, timeout=20,
verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, headers={"Accept": "application/activity+json"},
headers={"Content-Type": "application/activity+json"},
) )
with remote_response as r: with remote_response as r:
remote_response.raise_for_status() remote_response.raise_for_status()
extension = music_utils.get_ext_from_type(self.audio_mimetype) extension = music_utils.get_ext_from_type(self.audio_mimetype)
title = " - ".join([self.title, self.album_title, self.artist_name]) title = " - ".join([self.title, self.album_title, self.artist_name])
filename = "{}.{}".format(title, extension) filename = f"{title}.{extension}"
tmp_file = tempfile.TemporaryFile() tmp_file = tempfile.TemporaryFile()
for chunk in r.iter_content(chunk_size=512): for chunk in r.iter_content(chunk_size=512):
tmp_file.write(chunk) tmp_file.write(chunk)
...@@ -451,3 +607,41 @@ class LibraryTrack(models.Model): ...@@ -451,3 +607,41 @@ class LibraryTrack(models.Model):
def get_metadata(self, key): def get_metadata(self, key):
return self.metadata.get(key) return self.metadata.get(key)
@receiver(pre_save, sender=LibraryFollow)
def set_approved_updated(sender, instance, update_fields, **kwargs):
if not instance.pk or not instance.actor.is_local:
return
if update_fields is not None and "approved" not in update_fields:
return
db_value = instance.__class__.objects.filter(pk=instance.pk).values_list(
"approved", flat=True
)[0]
if db_value != instance.approved:
# Needed to update denormalized permissions
setattr(instance, "_approved_updated", True)
@receiver(post_save, sender=LibraryFollow)
def update_denormalization_follow_approved(sender, instance, created, **kwargs):
from funkwhale_api.music import models as music_models
updated = getattr(instance, "_approved_updated", False)
if (created or updated) and instance.actor.is_local:
music_models.TrackActor.create_entries(
instance.target,
actor_ids=[instance.actor.pk],
delete_existing=not instance.approved,
)
@receiver(post_delete, sender=LibraryFollow)
def update_denormalization_follow_deleted(sender, instance, **kwargs):
from funkwhale_api.music import models as music_models
if instance.actor.is_local:
music_models.TrackActor.objects.filter(
actor=instance.actor, upload__in=instance.target.uploads.all()
).delete()