From a6da10be41e17fb6c3ab26d2ea4513b886fc94cc Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Thu, 1 Mar 2018 23:41:51 +0100
Subject: [PATCH] API refinements for activity stream

---
 api/funkwhale_api/activity/serializers.py  |  1 +
 api/funkwhale_api/favorites/serializers.py | 10 +--
 api/funkwhale_api/history/activities.py    | 19 ++++++
 api/funkwhale_api/history/models.py        |  5 ++
 api/funkwhale_api/history/serializers.py   | 28 ++++++++
 api/funkwhale_api/history/views.py         |  9 ++-
 api/funkwhale_api/music/serializers.py     | 23 +++++++
 api/funkwhale_api/users/serializers.py     |  2 +
 api/tests/favorites/test_activity.py       | 10 ++-
 api/tests/history/test_activity.py         | 75 ++++++++++++++++++++++
 api/tests/{ => history}/test_history.py    | 17 ++++-
 api/tests/users/test_activity.py           |  1 +
 12 files changed, 188 insertions(+), 12 deletions(-)
 create mode 100644 api/funkwhale_api/history/activities.py
 create mode 100644 api/tests/history/test_activity.py
 rename api/tests/{ => history}/test_history.py (70%)

diff --git a/api/funkwhale_api/activity/serializers.py b/api/funkwhale_api/activity/serializers.py
index 4b40bb0d..325d1e82 100644
--- a/api/funkwhale_api/activity/serializers.py
+++ b/api/funkwhale_api/activity/serializers.py
@@ -3,6 +3,7 @@ from rest_framework import serializers
 
 class ModelSerializer(serializers.ModelSerializer):
     id = serializers.CharField(source='get_activity_url')
+    local_id = serializers.IntegerField(source='id')
     # url = serializers.SerializerMethodField()
 
     def get_url(self, obj):
diff --git a/api/funkwhale_api/favorites/serializers.py b/api/funkwhale_api/favorites/serializers.py
index 01ad2e47..276b0f6b 100644
--- a/api/funkwhale_api/favorites/serializers.py
+++ b/api/funkwhale_api/favorites/serializers.py
@@ -4,17 +4,15 @@ from rest_framework import serializers
 
 from funkwhale_api.activity import serializers as activity_serializers
 from funkwhale_api.music.serializers import TrackSerializerNested
+from funkwhale_api.music.serializers import TrackActivitySerializer
 from funkwhale_api.users.serializers import UserActivitySerializer
 
 from . import models
 
 
-
-
-
 class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer):
     type = serializers.SerializerMethodField()
-    object = serializers.CharField(source='track.get_activity_url')
+    object = TrackActivitySerializer(source='track')
     actor = UserActivitySerializer(source='user')
     published = serializers.DateTimeField(source='creation_date')
 
@@ -22,6 +20,7 @@ class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer):
         model = models.TrackFavorite
         fields = [
             'id',
+            'local_id',
             'object',
             'type',
             'actor',
@@ -34,9 +33,6 @@ class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer):
     def get_type(self, obj):
         return 'Like'
 
-    def get_object(self, obj):
-        return obj.track.get_activity_url()
-
 
 class UserTrackFavoriteSerializer(serializers.ModelSerializer):
     # track = TrackSerializerNested(read_only=True)
diff --git a/api/funkwhale_api/history/activities.py b/api/funkwhale_api/history/activities.py
new file mode 100644
index 00000000..e478f9b7
--- /dev/null
+++ b/api/funkwhale_api/history/activities.py
@@ -0,0 +1,19 @@
+from funkwhale_api.common import channels
+from funkwhale_api.activity import record
+
+from . import serializers
+
+record.registry.register_serializer(
+    serializers.ListeningActivitySerializer)
+
+
+@record.registry.register_consumer('history.Listening')
+def broadcast_listening_to_instance_activity(data, obj):
+    if obj.user.privacy_level not in ['instance', 'everyone']:
+        return
+
+    channels.group_send('instance_activity', {
+        'type': 'event.send',
+        'text': '',
+        'data': data
+    })
diff --git a/api/funkwhale_api/history/models.py b/api/funkwhale_api/history/models.py
index f7f62de6..56310ddc 100644
--- a/api/funkwhale_api/history/models.py
+++ b/api/funkwhale_api/history/models.py
@@ -25,3 +25,8 @@ class Listening(models.Model):
             raise ValidationError('Cannot have both session_key and user empty for listening')
 
         super().save(**kwargs)
+
+
+    def get_activity_url(self):
+        return '{}/listenings/tracks/{}'.format(
+            self.user.get_activity_url(), self.pk)
diff --git a/api/funkwhale_api/history/serializers.py b/api/funkwhale_api/history/serializers.py
index 64bdf41c..7a2280ce 100644
--- a/api/funkwhale_api/history/serializers.py
+++ b/api/funkwhale_api/history/serializers.py
@@ -1,9 +1,37 @@
 from rest_framework import serializers
 
+from funkwhale_api.activity import serializers as activity_serializers
 from funkwhale_api.music.serializers import TrackSerializerNested
+from funkwhale_api.music.serializers import TrackActivitySerializer
+from funkwhale_api.users.serializers import UserActivitySerializer
+
 from . import models
 
 
+class ListeningActivitySerializer(activity_serializers.ModelSerializer):
+    type = serializers.SerializerMethodField()
+    object = TrackActivitySerializer(source='track')
+    actor = UserActivitySerializer(source='user')
+    published = serializers.DateTimeField(source='end_date')
+
+    class Meta:
+        model = models.Listening
+        fields = [
+            'id',
+            'local_id',
+            'object',
+            'type',
+            'actor',
+            'published'
+        ]
+
+    def get_actor(self, obj):
+        return UserActivitySerializer(obj.user).data
+
+    def get_type(self, obj):
+        return 'Listen'
+
+
 class ListeningSerializer(serializers.ModelSerializer):
 
     class Meta:
diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py
index 59dcbd26..d5cbe316 100644
--- a/api/funkwhale_api/history/views.py
+++ b/api/funkwhale_api/history/views.py
@@ -3,8 +3,9 @@ from rest_framework import status
 from rest_framework.response import Response
 from rest_framework.decorators import detail_route
 
-from funkwhale_api.music.serializers import TrackSerializerNested
+from funkwhale_api.activity import record
 from funkwhale_api.common.permissions import ConditionalAuthentication
+from funkwhale_api.music.serializers import TrackSerializerNested
 
 from . import models
 from . import serializers
@@ -17,6 +18,12 @@ class ListeningViewSet(mixins.CreateModelMixin,
     queryset = models.Listening.objects.all()
     permission_classes = [ConditionalAuthentication]
 
+    def perform_create(self, serializer):
+        r = super().perform_create(serializer)
+        if self.request.user.is_authenticated:
+            record.send(serializer.instance)
+        return r
+
     def get_queryset(self):
         queryset = super().get_queryset()
         if self.request.user.is_authenticated:
diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index db6298a9..48419bbe 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -1,6 +1,8 @@
 from rest_framework import serializers
 from taggit.models import Tag
 
+from funkwhale_api.activity import serializers as activity_serializers
+
 from . import models
 
 
@@ -127,3 +129,24 @@ class ImportBatchSerializer(serializers.ModelSerializer):
         model = models.ImportBatch
         fields = ('id', 'jobs', 'status', 'creation_date', 'import_request')
         read_only_fields = ('creation_date',)
+
+
+class TrackActivitySerializer(activity_serializers.ModelSerializer):
+    type = serializers.SerializerMethodField()
+    name = serializers.CharField(source='title')
+    artist = serializers.CharField(source='artist.name')
+    album = serializers.CharField(source='album.title')
+
+    class Meta:
+        model = models.Track
+        fields = [
+            'id',
+            'local_id',
+            'name',
+            'type',
+            'artist',
+            'album',
+        ]
+
+    def get_type(self, obj):
+        return 'Audio'
diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py
index 2e873d94..e8adf9ed 100644
--- a/api/funkwhale_api/users/serializers.py
+++ b/api/funkwhale_api/users/serializers.py
@@ -8,11 +8,13 @@ from . import models
 class UserActivitySerializer(activity_serializers.ModelSerializer):
     type = serializers.SerializerMethodField()
     name = serializers.CharField(source='username')
+    local_id = serializers.CharField(source='username')
 
     class Meta:
         model = models.User
         fields = [
             'id',
+            'local_id',
             'name',
             'type'
         ]
diff --git a/api/tests/favorites/test_activity.py b/api/tests/favorites/test_activity.py
index 74695ed8..63174f9e 100644
--- a/api/tests/favorites/test_activity.py
+++ b/api/tests/favorites/test_activity.py
@@ -1,4 +1,5 @@
 from funkwhale_api.users.serializers import UserActivitySerializer
+from funkwhale_api.music.serializers import TrackActivitySerializer
 from funkwhale_api.favorites import serializers
 from funkwhale_api.favorites import activities
 
@@ -18,9 +19,10 @@ def test_activity_favorite_serializer(factories):
     field = serializers.serializers.DateTimeField()
     expected = {
         "type": "Like",
+        "local_id": favorite.pk,
         "id": favorite.get_activity_url(),
         "actor": actor,
-        "object": favorite.track.get_activity_url(),
+        "object": TrackActivitySerializer(favorite.track).data,
         "published": field.to_representation(favorite.creation_date),
     }
 
@@ -48,7 +50,8 @@ def test_broadcast_track_favorite_to_instance_activity(
     data = serializers.TrackFavoriteActivitySerializer(favorite).data
     consumer = activities.broadcast_track_favorite_to_instance_activity
     message = {
-        "type": 'event',
+        "type": 'event.send',
+        "text": '',
         "data": data
     }
     consumer(data=data, obj=favorite)
@@ -64,7 +67,8 @@ def test_broadcast_track_favorite_to_instance_activity_private(
     data = serializers.TrackFavoriteActivitySerializer(favorite).data
     consumer = activities.broadcast_track_favorite_to_instance_activity
     message = {
-        "type": 'event',
+        "type": 'event.send',
+        "text": '',
         "data": data
     }
     consumer(data=data, obj=favorite)
diff --git a/api/tests/history/test_activity.py b/api/tests/history/test_activity.py
new file mode 100644
index 00000000..b5ab07b8
--- /dev/null
+++ b/api/tests/history/test_activity.py
@@ -0,0 +1,75 @@
+from funkwhale_api.users.serializers import UserActivitySerializer
+from funkwhale_api.music.serializers import TrackActivitySerializer
+from funkwhale_api.history import serializers
+from funkwhale_api.history import activities
+
+
+def test_get_listening_activity_url(settings, factories):
+    listening = factories['history.Listening']()
+    user_url = listening.user.get_activity_url()
+    expected = '{}/listenings/tracks/{}'.format(
+        user_url, listening.pk)
+    assert listening.get_activity_url() == expected
+
+
+def test_activity_listening_serializer(factories):
+    listening = factories['history.Listening']()
+
+    actor = UserActivitySerializer(listening.user).data
+    field = serializers.serializers.DateTimeField()
+    expected = {
+        "type": "Listen",
+        "local_id": listening.pk,
+        "id": listening.get_activity_url(),
+        "actor": actor,
+        "object": TrackActivitySerializer(listening.track).data,
+        "published": field.to_representation(listening.end_date),
+    }
+
+    data = serializers.ListeningActivitySerializer(listening).data
+
+    assert data == expected
+
+
+def test_track_listening_serializer_is_connected(activity_registry):
+    conf = activity_registry['history.Listening']
+    assert conf['serializer'] == serializers.ListeningActivitySerializer
+
+
+def test_track_listening_serializer_instance_activity_consumer(
+        activity_registry):
+    conf = activity_registry['history.Listening']
+    consumer = activities.broadcast_listening_to_instance_activity
+    assert consumer in conf['consumers']
+
+
+def test_broadcast_listening_to_instance_activity(
+        factories, mocker):
+    p = mocker.patch('funkwhale_api.common.channels.group_send')
+    listening = factories['history.Listening']()
+    data = serializers.ListeningActivitySerializer(listening).data
+    consumer = activities.broadcast_listening_to_instance_activity
+    message = {
+        "type": 'event.send',
+        "text": '',
+        "data": data
+    }
+    consumer(data=data, obj=listening)
+    p.assert_called_once_with('instance_activity', message)
+
+
+def test_broadcast_listening_to_instance_activity_private(
+        factories, mocker):
+    p = mocker.patch('funkwhale_api.common.channels.group_send')
+    listening = factories['history.Listening'](
+        user__privacy_level='me'
+    )
+    data = serializers.ListeningActivitySerializer(listening).data
+    consumer = activities.broadcast_listening_to_instance_activity
+    message = {
+        "type": 'event.send',
+        "text": '',
+        "data": data
+    }
+    consumer(data=data, obj=listening)
+    p.assert_not_called()
diff --git a/api/tests/test_history.py b/api/tests/history/test_history.py
similarity index 70%
rename from api/tests/test_history.py
rename to api/tests/history/test_history.py
index 113e5ff6..ec8689e9 100644
--- a/api/tests/test_history.py
+++ b/api/tests/history/test_history.py
@@ -28,7 +28,8 @@ def test_anonymous_user_can_create_listening_via_api(client, factories, settings
     assert listening.session_key == client.session.session_key
 
 
-def test_logged_in_user_can_create_listening_via_api(logged_in_client, factories):
+def test_logged_in_user_can_create_listening_via_api(
+        logged_in_client, factories, activity_muted):
     track = factories['music.Track']()
 
     url = reverse('api:v1:history:listenings-list')
@@ -40,3 +41,17 @@ def test_logged_in_user_can_create_listening_via_api(logged_in_client, factories
 
     assert listening.track == track
     assert listening.user == logged_in_client.user
+
+
+def test_adding_listening_calls_activity_record(
+        factories, logged_in_client, activity_muted):
+    track = factories['music.Track']()
+
+    url = reverse('api:v1:history:listenings-list')
+    response = logged_in_client.post(url, {
+        'track': track.pk,
+    })
+
+    listening = models.Listening.objects.latest('id')
+
+    activity_muted.assert_called_once_with(listening)
diff --git a/api/tests/users/test_activity.py b/api/tests/users/test_activity.py
index 3cee4fb4..26d0b11f 100644
--- a/api/tests/users/test_activity.py
+++ b/api/tests/users/test_activity.py
@@ -13,6 +13,7 @@ def test_activity_user_serializer(factories):
     expected = {
         "type": "Person",
         "id": user.get_activity_url(),
+        "local_id": user.username,
         "name": user.username,
     }
 
-- 
GitLab