Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
Von
funkwhale
Commits
49769819
Verified
Commit
49769819
authored
Nov 09, 2018
by
Eliot Berriot
Browse files
Broadcast library updates (name, description, visibility) over federation
parent
51457aa8
Changes
8
Hide whitespace changes
Inline
Side-by-side
api/funkwhale_api/common/serializers.py
View file @
49769819
...
...
@@ -159,3 +159,34 @@ class ActionSerializer(serializers.Serializer):
"result"
:
result
,
}
return
payload
def
track_fields_for_update
(
*
fields
):
"""
Apply this decorator to serializer to call function when specific values
are updated on an object:
.. code-block:: python
@track_fields_for_update('privacy_level')
class LibrarySerializer(serializers.ModelSerializer):
def on_updated_privacy_level(self, obj, old_value, new_value):
print('Do someting')
"""
def
decorator
(
serializer_class
):
original_update
=
serializer_class
.
update
def
new_update
(
self
,
obj
,
validated_data
):
tracked_fields_before
=
{
f
:
getattr
(
obj
,
f
)
for
f
in
fields
}
obj
=
original_update
(
self
,
obj
,
validated_data
)
tracked_fields_after
=
{
f
:
getattr
(
obj
,
f
)
for
f
in
fields
}
if
tracked_fields_before
!=
tracked_fields_after
:
self
.
on_updated_fields
(
obj
,
tracked_fields_before
,
tracked_fields_after
)
return
obj
serializer_class
.
update
=
new_update
return
serializer_class
return
decorator
api/funkwhale_api/federation/routes.py
View file @
49769819
...
...
@@ -195,6 +195,45 @@ def outbox_delete_library(context):
}
@
outbox
.
register
({
"type"
:
"Update"
,
"object.type"
:
"Library"
})
def
outbox_update_library
(
context
):
library
=
context
[
"library"
]
serializer
=
serializers
.
ActivitySerializer
(
{
"type"
:
"Update"
,
"object"
:
serializers
.
LibrarySerializer
(
library
).
data
}
)
yield
{
"type"
:
"Update"
,
"actor"
:
library
.
actor
,
"payload"
:
with_recipients
(
serializer
.
data
,
to
=
[{
"type"
:
"followers"
,
"target"
:
library
}]
),
}
@
inbox
.
register
({
"type"
:
"Update"
,
"object.type"
:
"Library"
})
def
inbox_update_library
(
payload
,
context
):
actor
=
context
[
"actor"
]
library_id
=
payload
[
"object"
].
get
(
"id"
)
if
not
library_id
:
logger
.
debug
(
"Discarding deletion of empty library"
)
return
if
not
actor
.
libraries
.
filter
(
fid
=
library_id
).
exists
():
logger
.
debug
(
"Discarding deletion of unkwnown library %s"
,
library_id
)
return
serializer
=
serializers
.
LibrarySerializer
(
data
=
payload
[
"object"
])
if
serializer
.
is_valid
():
serializer
.
save
()
else
:
logger
.
debug
(
"Discarding update of library %s because of payload errors: %s"
,
library_id
,
serializer
.
errors
,
)
@
inbox
.
register
({
"type"
:
"Delete"
,
"object.type"
:
"Audio"
})
def
inbox_delete_audio
(
payload
,
context
):
actor
=
context
[
"actor"
]
...
...
api/funkwhale_api/federation/signing.py
View file @
49769819
...
...
@@ -85,7 +85,7 @@ def verify_django(django_request, public_key):
def
get_auth
(
private_key
,
private_key_id
):
return
requests_http_signature
.
HTTPSignatureAuth
(
use_auth_header
=
False
,
headers
=
[
"(request-target)"
,
"user-agent"
,
"host"
,
"date"
,
"content-type"
],
headers
=
[
"(request-target)"
,
"user-agent"
,
"host"
,
"date"
],
algorithm
=
"rsa-sha256"
,
key
=
private_key
.
encode
(
"utf-8"
),
key_id
=
private_key_id
,
...
...
api/funkwhale_api/music/serializers.py
View file @
49769819
...
...
@@ -192,6 +192,7 @@ class TrackSerializer(serializers.ModelSerializer):
return
TrackUploadSerializer
(
uploads
,
many
=
True
).
data
@
common_serializers
.
track_fields_for_update
(
"name"
,
"description"
,
"privacy_level"
)
class
LibraryForOwnerSerializer
(
serializers
.
ModelSerializer
):
uploads_count
=
serializers
.
SerializerMethodField
()
size
=
serializers
.
SerializerMethodField
()
...
...
@@ -216,6 +217,11 @@ class LibraryForOwnerSerializer(serializers.ModelSerializer):
def
get_size
(
self
,
o
):
return
getattr
(
o
,
"_size"
,
0
)
def
on_updated_fields
(
self
,
obj
,
before
,
after
):
routes
.
outbox
.
dispatch
(
{
"type"
:
"Update"
,
"object"
:
{
"type"
:
"Library"
}},
context
=
{
"library"
:
obj
}
)
class
UploadSerializer
(
serializers
.
ModelSerializer
):
track
=
TrackSerializer
(
required
=
False
,
allow_null
=
True
)
...
...
api/tests/common/test_serializers.py
View file @
49769819
...
...
@@ -134,3 +134,32 @@ def test_action_serializers_can_require_filter(factories):
assert
serializer
.
is_valid
(
raise_exception
=
True
)
is
True
assert
list
(
serializer
.
validated_data
[
"objects"
])
==
[
user1
]
def
test_track_fields_for_update
(
mocker
):
@
serializers
.
track_fields_for_update
(
"field1"
,
"field2"
)
class
S
(
serializers
.
serializers
.
Serializer
):
field1
=
serializers
.
serializers
.
CharField
()
field2
=
serializers
.
serializers
.
CharField
()
def
update
(
self
,
obj
,
validated_data
):
for
key
,
value
in
validated_data
.
items
():
setattr
(
obj
,
key
,
value
)
return
obj
on_updated_fields
=
mocker
.
stub
()
class
Obj
(
object
):
field1
=
"value1"
field2
=
"value2"
obj
=
Obj
()
serializer
=
S
(
obj
,
data
=
{
"field1"
:
"newvalue1"
,
"field2"
:
"newvalue2"
})
assert
serializer
.
is_valid
(
raise_exception
=
True
)
serializer
.
save
()
serializer
.
on_updated_fields
.
assert_called_once_with
(
obj
,
{
"field1"
:
"value1"
,
"field2"
:
"value2"
},
{
"field1"
:
"newvalue1"
,
"field2"
:
"newvalue2"
},
)
api/tests/federation/test_routes.py
View file @
49769819
...
...
@@ -9,6 +9,7 @@ from funkwhale_api.federation import routes, serializers
({
"type"
:
"Follow"
},
routes
.
inbox_follow
),
({
"type"
:
"Accept"
},
routes
.
inbox_accept
),
({
"type"
:
"Create"
,
"object.type"
:
"Audio"
},
routes
.
inbox_create_audio
),
({
"type"
:
"Update"
,
"object.type"
:
"Library"
},
routes
.
inbox_update_library
),
({
"type"
:
"Delete"
,
"object.type"
:
"Library"
},
routes
.
inbox_delete_library
),
({
"type"
:
"Delete"
,
"object.type"
:
"Audio"
},
routes
.
inbox_delete_audio
),
({
"type"
:
"Undo"
,
"object.type"
:
"Follow"
},
routes
.
inbox_undo_follow
),
...
...
@@ -29,6 +30,7 @@ def test_inbox_routes(route, handler):
({
"type"
:
"Accept"
},
routes
.
outbox_accept
),
({
"type"
:
"Follow"
},
routes
.
outbox_follow
),
({
"type"
:
"Create"
,
"object.type"
:
"Audio"
},
routes
.
outbox_create_audio
),
({
"type"
:
"Update"
,
"object.type"
:
"Library"
},
routes
.
outbox_update_library
),
({
"type"
:
"Delete"
,
"object.type"
:
"Library"
},
routes
.
outbox_delete_library
),
({
"type"
:
"Delete"
,
"object.type"
:
"Audio"
},
routes
.
outbox_delete_audio
),
({
"type"
:
"Undo"
,
"object.type"
:
"Follow"
},
routes
.
outbox_undo_follow
),
...
...
@@ -262,6 +264,55 @@ def test_outbox_delete_library(factories):
assert
activity
[
"actor"
]
==
library
.
actor
def
test_outbox_update_library
(
factories
):
library
=
factories
[
"music.Library"
]()
activity
=
list
(
routes
.
outbox_update_library
({
"library"
:
library
}))[
0
]
expected
=
serializers
.
ActivitySerializer
(
{
"type"
:
"Update"
,
"object"
:
serializers
.
LibrarySerializer
(
library
).
data
}
).
data
expected
[
"to"
]
=
[{
"type"
:
"followers"
,
"target"
:
library
}]
assert
dict
(
activity
[
"payload"
])
==
dict
(
expected
)
assert
activity
[
"actor"
]
==
library
.
actor
def
test_inbox_update_library
(
factories
):
activity
=
factories
[
"federation.Activity"
]()
library
=
factories
[
"music.Library"
]()
data
=
serializers
.
LibrarySerializer
(
library
).
data
data
[
"name"
]
=
"New name"
payload
=
{
"type"
:
"Update"
,
"actor"
:
library
.
actor
.
fid
,
"object"
:
data
}
routes
.
inbox_update_library
(
payload
,
context
=
{
"actor"
:
library
.
actor
,
"raise_exception"
:
True
,
"activity"
:
activity
},
)
library
.
refresh_from_db
()
assert
library
.
name
==
"New name"
# def test_inbox_update_library_impostor(factories):
# activity = factories["federation.Activity"]()
# impostor = factories["federation.Actor"]()
# library = factories["music.Library"]()
# payload = {
# "type": "Delete",
# "actor": library.actor.fid,
# "object": {"type": "Library", "id": library.fid},
# }
# routes.inbox_update_library(
# payload,
# context={"actor": impostor, "raise_exception": True, "activity": activity},
# )
# # not deleted, should still be here
# library.refresh_from_db()
def
test_inbox_delete_audio
(
factories
):
activity
=
factories
[
"federation.Activity"
]()
...
...
api/tests/music/test_serializers.py
View file @
49769819
import
pytest
from
funkwhale_api.music
import
models
from
funkwhale_api.music
import
serializers
from
funkwhale_api.music
import
tasks
...
...
@@ -274,3 +276,28 @@ def test_track_upload_serializer(factories):
serializer
=
serializers
.
TrackUploadSerializer
(
upload
)
assert
serializer
.
data
==
expected
@
pytest
.
mark
.
parametrize
(
"field,before,after"
,
[
(
"privacy_level"
,
"me"
,
"everyone"
),
(
"name"
,
"Before"
,
"After"
),
(
"description"
,
"Before"
,
"After"
),
],
)
def
test_update_library_privacy_level_broadcasts_to_followers
(
factories
,
field
,
before
,
after
,
mocker
):
dispatch
=
mocker
.
patch
(
"funkwhale_api.federation.routes.outbox.dispatch"
)
library
=
factories
[
"music.Library"
](
**
{
field
:
before
})
serializer
=
serializers
.
LibraryForOwnerSerializer
(
library
,
data
=
{
field
:
after
},
partial
=
True
)
assert
serializer
.
is_valid
(
raise_exception
=
True
)
serializer
.
save
()
dispatch
.
assert_called_once_with
(
{
"type"
:
"Update"
,
"object"
:
{
"type"
:
"Library"
}},
context
=
{
"library"
:
library
}
)
changes/changelog.d/library-update-federation.enhancement
0 → 100644
View file @
49769819
Broadcast library updates (name, description, visibility) over federation
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment