From 39c5245c1b2fa00963106aad32aeea1e06f0f041 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 2 Apr 2018 21:08:59 +0200 Subject: [PATCH 001/101] Fix #148: User admin now includes signup and last login dates --- api/funkwhale_api/users/admin.py | 10 ++++++++++ changes/changelog.d/148.enhancement | 1 + 2 files changed, 11 insertions(+) create mode 100644 changes/changelog.d/148.enhancement diff --git a/api/funkwhale_api/users/admin.py b/api/funkwhale_api/users/admin.py index ac3712015..89b67d3df 100644 --- a/api/funkwhale_api/users/admin.py +++ b/api/funkwhale_api/users/admin.py @@ -36,3 +36,13 @@ class MyUserCreationForm(UserCreationForm): class UserAdmin(AuthUserAdmin): form = MyUserChangeForm add_form = MyUserCreationForm + list_display = [ + 'username', + 'email', + 'date_joined', + 'last_login', + 'privacy_level', + ] + list_filter = [ + 'privacy_level', + ] diff --git a/changes/changelog.d/148.enhancement b/changes/changelog.d/148.enhancement new file mode 100644 index 000000000..074e0b0b5 --- /dev/null +++ b/changes/changelog.d/148.enhancement @@ -0,0 +1 @@ +User admin now includes signup and last login dates (#148) From 48df30dbd8ba8701aa8f3137303914d396b9aaca Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 2 Apr 2018 22:12:28 +0200 Subject: [PATCH 002/101] We now persist system accounts to database --- api/funkwhale_api/federation/actors.py | 18 ++++++++++-------- api/tests/federation/test_actors.py | 9 +++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index 69033f5ca..a4e24912b 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -49,15 +49,17 @@ class SystemActor(object): additional_attributes = {} def get_actor_instance(self): - a = models.Actor( - **self.get_instance_argument( - self.id, - name=self.name, - summary=self.summary, - **self.additional_attributes - ) + args = self.get_instance_argument( + self.id, + name=self.name, + summary=self.summary, + **self.additional_attributes + ) + url = args.pop('url') + a, created = models.Actor.objects.get_or_create( + url=url, + defaults=args, ) - a.pk = self.id return a def get_instance_argument(self, id, name, summary, **kwargs): diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index b3b0f8df0..127b3c15e 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -6,6 +6,7 @@ from django.utils import timezone from rest_framework import exceptions from funkwhale_api.federation import actors +from funkwhale_api.federation import models from funkwhale_api.federation import serializers from funkwhale_api.federation import utils @@ -188,3 +189,11 @@ def test_test_post_outbox_handles_create_note( to=[actor.url], on_behalf_of=actors.SYSTEM_ACTORS['test'].get_actor_instance() ) + + +def test_getting_actor_instance_persists_in_db(db): + test = actors.SYSTEM_ACTORS['test'].get_actor_instance() + from_db = models.Actor.objects.get(url=test.url) + + for f in test._meta.fields: + assert getattr(from_db, f.name) == getattr(test, f.name) From a81c92dbf5041729abcb2963c14938c30a474e38 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Mon, 2 Apr 2018 22:43:59 +0200 Subject: [PATCH 003/101] Additional setting to control wether music library federation needs approval --- api/config/settings/common.py | 4 +++- api/funkwhale_api/federation/actors.py | 5 +++++ api/funkwhale_api/federation/models.py | 8 ++++++++ api/tests/federation/test_actors.py | 25 +++++++++++++++++++++++++ deploy/env.prod.sample | 9 +++++++++ 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/api/config/settings/common.py b/api/config/settings/common.py index fbe3b7045..6a85a934c 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -30,7 +30,9 @@ FUNKWHALE_HOSTNAME = urlsplit(FUNKWHALE_URL).netloc FEDERATION_ENABLED = env.bool('FEDERATION_ENABLED', default=True) FEDERATION_HOSTNAME = env('FEDERATION_HOSTNAME', default=FUNKWHALE_HOSTNAME) - +FEDERATION_MUSIC_NEEDS_APPROVAL = env.bool( + 'FEDERATION_MUSIC_NEEDS_APPROVAL', default=True +) ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS') # APP CONFIGURATION diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index a4e24912b..e29125f5b 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -47,6 +47,7 @@ def get_actor(actor_url): class SystemActor(object): additional_attributes = {} + manually_approves_followers = False def get_actor_instance(self): args = self.get_instance_argument( @@ -113,6 +114,9 @@ class LibraryActor(SystemActor): additional_attributes = { 'manually_approves_followers': True } + @property + def manually_approves_followers(self): + return settings.FEDERATION_MUSIC_NEEDS_APPROVAL class TestActor(SystemActor): @@ -125,6 +129,7 @@ class TestActor(SystemActor): additional_attributes = { 'manually_approves_followers': False } + manually_approves_followers = False def get_outbox(self, data, actor=None): return { diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index d76ad173b..a2cd598f7 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -57,3 +57,11 @@ class Actor(models.Model): setattr(self, field, v.lower()) super().save(**kwargs) + + @property + def is_system(self): + from . import actors + return all([ + settings.FEDERATION_HOSTNAME == self.domain, + self.preferred_username in actors.SYSTEM_ACTORS + ]) diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index 127b3c15e..8afb94ad0 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -197,3 +197,28 @@ def test_getting_actor_instance_persists_in_db(db): for f in test._meta.fields: assert getattr(from_db, f.name) == getattr(test, f.name) + + +@pytest.mark.parametrize('username,domain,expected', [ + ('test', 'wrongdomain.com', False), + ('notsystem', '', False), + ('test', '', True), +]) +def test_actor_is_system( + username, domain, expected, nodb_factories, settings): + if not domain: + domain = settings.FEDERATION_HOSTNAME + + actor = nodb_factories['federation.Actor']( + preferred_username=username, + domain=domain, + ) + assert actor.is_system is expected + + +@pytest.mark.parametrize('value', [False, True]) +def test_library_actor_manually_approves_based_on_setting( + value, settings): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = value + library_conf = actors.SYSTEM_ACTORS['library'] + assert library_conf.manually_approves_followers is value diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample index a016b34c7..9e9938500 100644 --- a/deploy/env.prod.sample +++ b/deploy/env.prod.sample @@ -85,3 +85,12 @@ API_AUTHENTICATION_REQUIRED=True # This will help us detect and correct bugs RAVEN_ENABLED=false RAVEN_DSN=https://44332e9fdd3d42879c7d35bf8562c6a4:0062dc16a22b41679cd5765e5342f716@sentry.eliotberriot.com/5 + +# This settings enable/disable federation on the instance level +FEDERATION_ENABLED=True +# This setting decide wether music library is shared automatically +# to followers or if it requires manual approval before. +# FEDERATION_MUSIC_NEEDS_APPROVAL=False +# means anyone can subscribe to your library and import your file, +# use with caution. +FEDERATION_MUSIC_NEEDS_APPROVAL=True From 168c4e7d531e2f42088e4ba4729e280fc81328e8 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 3 Apr 2018 17:36:03 +0200 Subject: [PATCH 004/101] system_conf property on Actor instances --- api/funkwhale_api/federation/models.py | 6 ++++++ api/tests/federation/test_actors.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index a2cd598f7..35ddce961 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -65,3 +65,9 @@ class Actor(models.Model): 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] diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index 8afb94ad0..e72232fc0 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -216,6 +216,22 @@ def test_actor_is_system( assert actor.is_system is expected +@pytest.mark.parametrize('username,domain,expected', [ + ('test', 'wrongdomain.com', None), + ('notsystem', '', None), + ('test', '', actors.SYSTEM_ACTORS['test']), +]) +def test_actor_is_system( + username, domain, expected, nodb_factories, settings): + if not domain: + domain = settings.FEDERATION_HOSTNAME + actor = nodb_factories['federation.Actor']( + preferred_username=username, + domain=domain, + ) + assert actor.system_conf == expected + + @pytest.mark.parametrize('value', [False, True]) def test_library_actor_manually_approves_based_on_setting( value, settings): From 2f6d3ae18002715f0ccead21ac155adb29d2ae2a Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 3 Apr 2018 18:35:08 +0200 Subject: [PATCH 005/101] Ensure unicity on actor username and domain --- .../migrations/0002_auto_20180403_1620.py | 17 +++++++++++++++++ api/funkwhale_api/federation/models.py | 3 +++ 2 files changed, 20 insertions(+) create mode 100644 api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py diff --git a/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py b/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py new file mode 100644 index 000000000..2200424d8 --- /dev/null +++ b/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.3 on 2018-04-03 16:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('federation', '0001_initial'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='actor', + unique_together={('domain', 'preferred_username')}, + ), + ] diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 35ddce961..414bcc50a 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -32,6 +32,9 @@ class Actor(models.Model): default=timezone.now) manually_approves_followers = models.NullBooleanField(default=None) + class Meta: + unique_together = ['domain', 'preferred_username'] + @property def webfinger_subject(self): return '{}@{}'.format( From 6aa6f1d8f869e3821a40dd7a2b061bfa098eb008 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 3 Apr 2018 19:48:50 +0200 Subject: [PATCH 006/101] Test actor can now follow back --- api/funkwhale_api/federation/activity.py | 34 +++++++++ api/funkwhale_api/federation/actors.py | 81 ++++++++++++++++----- api/funkwhale_api/federation/factories.py | 17 +++++ api/funkwhale_api/federation/serializers.py | 4 +- api/tests/federation/test_actors.py | 77 +++++++++++++++++++- 5 files changed, 192 insertions(+), 21 deletions(-) diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index 4eeb193b1..3b7648f10 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -83,3 +83,37 @@ def deliver(activity, on_behalf_of, to=[]): ) response.raise_for_status() logger.debug('Remote answered with %s', response.status_code) + + +def get_follow(follow_id, follower, followed): + return { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + {} + ], + 'actor': follower.url, + 'id': follower.url + '#follows/{}'.format(follow_id), + 'object': followed.url, + 'type': 'Follow' + } + + +def get_accept_follow(accept_id, accept_actor, follow, follow_actor): + return { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "id": accept_actor.url + '#accepts/follows/{}'.format( + accept_id), + "type": "Accept", + "actor": accept_actor.url, + "object": { + "id": follow['id'], + "type": "Follow", + "actor": follow_actor.url, + "object": accept_actor.url + }, + } diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index e29125f5b..031526f8b 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -1,5 +1,6 @@ import logging import requests +import uuid import xml from django.conf import settings @@ -98,7 +99,7 @@ class SystemActor(object): raise NotImplementedError def post_inbox(self, data, actor=None): - raise NotImplementedError + return self.handle(data, actor=actor) def get_outbox(self, data, actor=None): raise NotImplementedError @@ -106,6 +107,31 @@ class SystemActor(object): def post_outbox(self, data, actor=None): raise NotImplementedError + def handle(self, data, actor=None): + """ + Main entrypoint for handling activities posted to the + actor's inbox + """ + logger.info('Received activity on %s inbox', self.id) + + if actor is None: + raise PermissionDenied('Actor not authenticated') + + serializer = serializers.ActivitySerializer( + data=data, context={'actor': actor}) + serializer.is_valid(raise_exception=True) + + ac = serializer.data + try: + handler = getattr( + self, 'handle_{}'.format(ac['type'].lower())) + except (KeyError, AttributeError): + logger.debug( + 'No handler for activity %s', ac['type']) + return + + return handler(ac, actor) + class LibraryActor(SystemActor): id = 'library' @@ -147,23 +173,6 @@ class TestActor(SystemActor): "orderedItems": [] } - def post_inbox(self, data, actor=None): - if actor is None: - raise PermissionDenied('Actor not authenticated') - - serializer = serializers.ActivitySerializer( - data=data, context={'actor': actor}) - serializer.is_valid(raise_exception=True) - - ac = serializer.validated_data - logger.info('Received activity on %s inbox', self.id) - if ac['type'] == 'Create' and ac['object']['type'] == 'Note': - # we received a toot \o/ - command = self.parse_command(ac['object']['content']) - logger.debug('Parsed command: %s', command) - if command == 'ping': - self.handle_ping(ac, actor) - def parse_command(self, message): """ Remove any links or fancy markup to extract /command from @@ -175,7 +184,16 @@ class TestActor(SystemActor): except IndexError: return - def handle_ping(self, ac, sender): + def handle_create(self, ac, sender): + if ac['object']['type'] != 'Note': + return + + # we received a toot \o/ + command = self.parse_command(ac['object']['content']) + logger.debug('Parsed command: %s', command) + if command != 'ping': + return + now = timezone.now() test_actor = self.get_actor_instance() reply_url = 'https://{}/activities/note/{}'.format( @@ -221,6 +239,31 @@ class TestActor(SystemActor): to=[ac['actor']], on_behalf_of=test_actor) + def handle_follow(self, ac, sender): + # on a follow we: + # 1. send the accept answer + # 2. follow back + test_actor = self.get_actor_instance() + accept_uuid = uuid.uuid4() + accept = activity.get_accept_follow( + accept_id=accept_uuid, + accept_actor=test_actor, + follow=ac, + follow_actor=sender) + activity.deliver( + accept, + to=[ac['actor']], + on_behalf_of=test_actor) + follow_uuid = uuid.uuid4() + follow = activity.get_follow( + follow_id=follow_uuid, + follower=test_actor, + followed=sender) + activity.deliver( + follow, + to=[ac['actor']], + on_behalf_of=test_actor) + SYSTEM_ACTORS = { 'library': LibraryActor(), 'test': TestActor(), diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py index 88c86f791..6e621c7f3 100644 --- a/api/funkwhale_api/federation/factories.py +++ b/api/funkwhale_api/federation/factories.py @@ -89,3 +89,20 @@ class NoteFactory(factory.Factory): class Meta: model = dict + + +@registry.register(name='federation.Activity') +class ActivityFactory(factory.Factory): + type = 'Create' + id = factory.Faker('url') + published = factory.LazyFunction( + lambda: timezone.now().isoformat() + ) + actor = factory.Faker('url') + object = factory.SubFactory( + NoteFactory, + actor=factory.SelfAttribute('..actor'), + published=factory.SelfAttribute('..published')) + + class Meta: + model = dict diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index 2137e8d91..7c35aead3 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -120,7 +120,9 @@ class ActivitySerializer(serializers.Serializer): type = value['type'] except KeyError: raise serializers.ValidationError('Missing object type') - + except TypeError: + # probably a URL + return value try: object_serializer = OBJECT_SERIALIZERS[type] except KeyError: diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index e72232fc0..f0d8f7840 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -1,4 +1,5 @@ import pytest +import uuid from django.urls import reverse from django.utils import timezone @@ -127,7 +128,7 @@ def test_test_post_outbox_validates_actor(nodb_factories): assert msg in exc_info.value -def test_test_post_outbox_handles_create_note( +def test_test_post_inbox_handles_create_note( settings, mocker, factories): deliver = mocker.patch( 'funkwhale_api.federation.activity.deliver') @@ -238,3 +239,77 @@ def test_library_actor_manually_approves_based_on_setting( settings.FEDERATION_MUSIC_NEEDS_APPROVAL = value library_conf = actors.SYSTEM_ACTORS['library'] assert library_conf.manually_approves_followers is value + + +def test_system_actor_handle(mocker, nodb_factories): + handler = mocker.patch( + 'funkwhale_api.federation.actors.TestActor.handle_create') + actor = nodb_factories['federation.Actor']() + activity = nodb_factories['federation.Activity']( + type='Create', actor=actor.url) + serializer = serializers.ActivitySerializer( + data=activity + ) + assert serializer.is_valid() + actors.SYSTEM_ACTORS['test'].handle(activity, actor) + handler.assert_called_once_with(serializer.data, actor) + + +def test_test_actor_handles_follow( + settings, mocker, factories): + deliver = mocker.patch( + 'funkwhale_api.federation.activity.deliver') + actor = factories['federation.Actor']() + now = timezone.now() + mocker.patch('django.utils.timezone.now', return_value=now) + test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance() + data = { + 'actor': actor.url, + 'type': 'Follow', + 'id': 'http://test.federation/user#follows/267', + 'object': test_actor.url, + } + uid = uuid.uuid4() + mocker.patch('uuid.uuid4', return_value=uid) + expected_accept = { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "id": test_actor.url + '#accepts/follows/{}'.format(uid), + "type": "Accept", + "actor": test_actor.url, + "object": { + "id": data['id'], + "type": "Follow", + "actor": actor.url, + "object": test_actor.url + }, + } + expected_follow = { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + {} + ], + 'actor': test_actor.url, + 'id': test_actor.url + '#follows/{}'.format(uid), + 'object': actor.url, + 'type': 'Follow' + } + + actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor) + expected_calls = [ + mocker.call( + expected_accept, + to=[actor.url], + on_behalf_of=test_actor, + ), + mocker.call( + expected_follow, + to=[actor.url], + on_behalf_of=test_actor, + ) + ] + deliver.assert_has_calls(expected_calls) From f19418d2c27b1167e3efea06fbeffe1a7b12c4ae Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 3 Apr 2018 21:30:15 +0200 Subject: [PATCH 007/101] Added follow model and factory --- api/funkwhale_api/federation/activity.py | 2 +- api/funkwhale_api/federation/actors.py | 7 +++++ api/funkwhale_api/federation/factories.py | 10 +++++- .../migrations/0003_auto_20180403_1921.py | 31 +++++++++++++++++++ api/funkwhale_api/federation/models.py | 22 +++++++++++++ api/tests/federation/test_actors.py | 4 +++ api/tests/federation/test_models.py | 25 +++++++++++++++ 7 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 api/funkwhale_api/federation/migrations/0003_auto_20180403_1921.py create mode 100644 api/tests/federation/test_models.py diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index 3b7648f10..5a0974011 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -52,7 +52,7 @@ OBJECT_TYPES = [ 'Relationship', 'Tombstone', 'Video', -] +] + ACTIVITY_TYPES def deliver(activity, on_behalf_of, to=[]): from . import actors diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index 031526f8b..22b231e08 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -243,6 +243,7 @@ class TestActor(SystemActor): # on a follow we: # 1. send the accept answer # 2. follow back + # test_actor = self.get_actor_instance() accept_uuid = uuid.uuid4() accept = activity.get_accept_follow( @@ -254,6 +255,12 @@ class TestActor(SystemActor): accept, to=[ac['actor']], on_behalf_of=test_actor) + # we persist the sender in database + sender.save() + models.Follow.objects.get_or_create( + actor=sender, + target=test_actor, + ) follow_uuid = uuid.uuid4() follow = activity.get_follow( follow_id=follow_uuid, diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py index 6e621c7f3..217b47218 100644 --- a/api/funkwhale_api/federation/factories.py +++ b/api/funkwhale_api/federation/factories.py @@ -53,7 +53,6 @@ class SignedRequestFactory(factory.Factory): @registry.register class ActorFactory(factory.DjangoModelFactory): - public_key = None private_key = None preferred_username = factory.Faker('user_name') @@ -77,6 +76,15 @@ class ActorFactory(factory.DjangoModelFactory): return super()._generate(create, attrs) +@registry.register +class FollowFactory(factory.DjangoModelFactory): + target = factory.SubFactory(ActorFactory) + actor = factory.SubFactory(ActorFactory) + + class Meta: + model = models.Follow + + @registry.register(name='federation.Note') class NoteFactory(factory.Factory): type = 'Note' diff --git a/api/funkwhale_api/federation/migrations/0003_auto_20180403_1921.py b/api/funkwhale_api/federation/migrations/0003_auto_20180403_1921.py new file mode 100644 index 000000000..aadf3257e --- /dev/null +++ b/api/funkwhale_api/federation/migrations/0003_auto_20180403_1921.py @@ -0,0 +1,31 @@ +# Generated by Django 2.0.3 on 2018-04-03 19:21 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('federation', '0002_auto_20180403_1620'), + ] + + operations = [ + migrations.CreateModel( + name='Follow', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)), + ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), + ('last_modification_date', models.DateTimeField(default=django.utils.timezone.now)), + ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emitted_follows', to='federation.Actor')), + ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follows', to='federation.Actor')), + ], + ), + migrations.AlterUniqueTogether( + name='follow', + unique_together={('actor', 'target')}, + ), + ] diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 414bcc50a..875268bca 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -1,3 +1,5 @@ +import uuid + from django.conf import settings from django.db import models from django.utils import timezone @@ -74,3 +76,23 @@ class Actor(models.Model): from . import actors if self.is_system: return actors.SYSTEM_ACTORS[self.preferred_username] + + +class Follow(models.Model): + uuid = models.UUIDField(default=uuid.uuid4, unique=True) + actor = models.ForeignKey( + Actor, + related_name='emitted_follows', + on_delete=models.CASCADE, + ) + target = models.ForeignKey( + Actor, + related_name='received_follows', + on_delete=models.CASCADE, + ) + creation_date = models.DateTimeField(default=timezone.now) + last_modification_date = models.DateTimeField( + default=timezone.now) + + class Meta: + unique_together = ['actor', 'target'] diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index f0d8f7840..d50b52ee6 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -313,3 +313,7 @@ def test_test_actor_handles_follow( ) ] deliver.assert_has_calls(expected_calls) + + follow = test_actor.received_follows.first() + assert follow.actor == actor + assert follow.target == test_actor diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py new file mode 100644 index 000000000..297fe2c58 --- /dev/null +++ b/api/tests/federation/test_models.py @@ -0,0 +1,25 @@ +import pytest + +from django import db + +from funkwhale_api.federation import models + + +def test_cannot_duplicate_actor(factories): + actor = factories['federation.Actor']() + + with pytest.raises(db.IntegrityError): + factories['federation.Actor']( + domain=actor.domain, + preferred_username=actor.preferred_username, + ) + + +def test_cannot_duplicate_follow(factories): + follow = factories['federation.Follow']() + + with pytest.raises(db.IntegrityError): + factories['federation.Follow']( + target=follow.target, + actor=follow.actor, + ) From 657bd4b01ab3fc92afc3bc45b0c2c311a247de0f Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 3 Apr 2018 23:24:51 +0200 Subject: [PATCH 008/101] Follow serializer --- api/funkwhale_api/federation/serializers.py | 34 ++++++++++++++++++--- api/tests/federation/test_serializers.py | 19 ++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index 7c35aead3..075e253da 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -11,6 +11,12 @@ from . import models from . import utils +AP_CONTEXT = [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + {}, +] + class ActorSerializer(serializers.ModelSerializer): # left maps to activitypub fields, right to our internal models id = serializers.URLField(source='url') @@ -43,11 +49,7 @@ class ActorSerializer(serializers.ModelSerializer): def to_representation(self, instance): ret = super().to_representation(instance) - ret['@context'] = [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - {}, - ] + ret['@context'] = AP_CONTEXT if instance.public_key: ret['publicKey'] = { 'owner': instance.url, @@ -87,6 +89,28 @@ class ActorSerializer(serializers.ModelSerializer): return value[:500] +class FollowSerializer(serializers.ModelSerializer): + # left maps to activitypub fields, right to our internal models + id = serializers.URLField(source='get_federation_url') + object = serializers.URLField(source='target.url') + actor = serializers.URLField(source='actor.url') + type = serializers.CharField(source='ap_type') + + class Meta: + model = models.Actor + fields = [ + 'id', + 'object', + 'actor', + 'type' + ] + + def to_representation(self, instance): + ret = super().to_representation(instance) + ret['@context'] = AP_CONTEXT + return ret + + class ActorWebfingerSerializer(serializers.ModelSerializer): class Meta: model = models.Actor diff --git a/api/tests/federation/test_serializers.py b/api/tests/federation/test_serializers.py index efa92b16a..77c14531c 100644 --- a/api/tests/federation/test_serializers.py +++ b/api/tests/federation/test_serializers.py @@ -144,3 +144,22 @@ def test_webfinger_serializer(): serializer = serializers.ActorWebfingerSerializer(actor) assert serializer.data == expected + + +def test_follow_serializer_to_ap(factories): + follow = factories['federation.Follow'](local=True) + serializer = serializers.FollowSerializer(follow) + + expected = { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + {}, + ], + 'id': follow.get_federation_url(), + 'type': 'Follow', + 'actor': follow.actor.url, + 'object': follow.target.url, + } + + assert serializer.data == expected From 81e7f03f7757f31cba0803cc089d555f56318a90 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 3 Apr 2018 23:25:22 +0200 Subject: [PATCH 009/101] Now persist actors in database during auth --- api/funkwhale_api/federation/actors.py | 2 -- api/funkwhale_api/federation/authentication.py | 6 +++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index 22b231e08..d70ce23e5 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -255,8 +255,6 @@ class TestActor(SystemActor): accept, to=[ac['actor']], on_behalf_of=test_actor) - # we persist the sender in database - sender.save() models.Follow.objects.get_or_create( actor=sender, target=test_actor, diff --git a/api/funkwhale_api/federation/authentication.py b/api/funkwhale_api/federation/authentication.py index e199ef134..f2926bb30 100644 --- a/api/funkwhale_api/federation/authentication.py +++ b/api/funkwhale_api/federation/authentication.py @@ -7,6 +7,7 @@ from rest_framework import exceptions from . import actors from . import keys +from . import models from . import serializers from . import signing from . import utils @@ -42,7 +43,10 @@ class SignatureAuthentication(authentication.BaseAuthentication): except cryptography.exceptions.InvalidSignature: raise exceptions.AuthenticationFailed('Invalid signature') - return serializer.build() + try: + return models.Actor.objects.get(url=actor_data['id']) + except models.Actor.DoesNotExist: + return serializer.save() def authenticate(self, request): setattr(request, 'actor', None) From 3ad1fe17d5109a04f622c94b6c64dea1e248d521 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 3 Apr 2018 23:25:44 +0200 Subject: [PATCH 010/101] Test bot can now unfollow --- api/funkwhale_api/federation/actors.py | 36 +++++++++++++++- api/funkwhale_api/federation/factories.py | 12 ++++++ api/funkwhale_api/federation/models.py | 7 +++ api/tests/federation/test_actors.py | 47 ++++++++++++++++----- api/tests/federation/test_authentication.py | 4 +- api/tests/federation/test_models.py | 7 +++ 6 files changed, 100 insertions(+), 13 deletions(-) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index d70ce23e5..89c621edc 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -130,7 +130,7 @@ class SystemActor(object): 'No handler for activity %s', ac['type']) return - return handler(ac, actor) + return handler(data, actor) class LibraryActor(SystemActor): @@ -269,6 +269,40 @@ class TestActor(SystemActor): to=[ac['actor']], on_behalf_of=test_actor) + def handle_undo(self, ac, sender): + if ac['object']['type'] != 'Follow': + return + + if ac['object']['actor'] != sender.url: + # not the same actor, permission issue + return + + test_actor = self.get_actor_instance() + models.Follow.objects.filter( + actor=sender, + target=test_actor, + ).delete() + # we also unfollow the sender, if possible + try: + follow = models.Follow.objects.get( + target=sender, + actor=test_actor, + ) + except models.Follow.DoesNotExist: + return + undo = { + '@context': serializers.AP_CONTEXT, + 'type': 'Undo', + 'id': follow.get_federation_url() + '/undo', + 'actor': test_actor.url, + 'object': serializers.FollowSerializer(follow).data, + } + follow.delete() + activity.deliver( + undo, + to=[sender.url], + on_behalf_of=test_actor) + SYSTEM_ACTORS = { 'library': LibraryActor(), 'test': TestActor(), diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py index 217b47218..16abf80dc 100644 --- a/api/funkwhale_api/federation/factories.py +++ b/api/funkwhale_api/federation/factories.py @@ -3,6 +3,7 @@ import requests import requests_http_signature from django.utils import timezone +from django.conf import settings from funkwhale_api.factories import registry @@ -65,6 +66,12 @@ class ActorFactory(factory.DjangoModelFactory): class Meta: model = models.Actor + class Params: + local = factory.Trait( + domain=factory.LazyAttribute( + lambda o: settings.FEDERATION_HOSTNAME) + ) + @classmethod def _generate(cls, create, attrs): has_public = attrs.get('public_key') is not None @@ -84,6 +91,11 @@ class FollowFactory(factory.DjangoModelFactory): class Meta: model = models.Follow + class Params: + local = factory.Trait( + actor=factory.SubFactory(ActorFactory, local=True) + ) + @registry.register(name='federation.Note') class NoteFactory(factory.Factory): diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 875268bca..a228a3803 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -14,6 +14,8 @@ TYPE_CHOICES = [ class Actor(models.Model): + ap_type = 'Actor' + url = models.URLField(unique=True, max_length=500, db_index=True) outbox_url = models.URLField(max_length=500) inbox_url = models.URLField(max_length=500) @@ -79,6 +81,8 @@ class Actor(models.Model): class Follow(models.Model): + ap_type = 'Follow' + uuid = models.UUIDField(default=uuid.uuid4, unique=True) actor = models.ForeignKey( Actor, @@ -96,3 +100,6 @@ class Follow(models.Model): class Meta: unique_together = ['actor', 'target'] + + def get_federation_url(self): + return '{}#follows/{}'.format(self.actor.url, self.uuid) diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index d50b52ee6..c1b9d8a23 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -169,11 +169,7 @@ def test_test_post_inbox_handles_create_note( }] ) expected_activity = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - {} - ], + '@context': serializers.AP_CONTEXT, 'actor': test_actor.url, 'id': 'https://{}/activities/note/{}/activity'.format( settings.FEDERATION_HOSTNAME, now.timestamp() @@ -288,11 +284,7 @@ def test_test_actor_handles_follow( }, } expected_follow = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - {} - ], + '@context': serializers.AP_CONTEXT, 'actor': test_actor.url, 'id': test_actor.url + '#follows/{}'.format(uid), 'object': actor.url, @@ -317,3 +309,38 @@ def test_test_actor_handles_follow( follow = test_actor.received_follows.first() assert follow.actor == actor assert follow.target == test_actor + + +def test_test_actor_handles_undo_follow( + settings, mocker, factories): + deliver = mocker.patch( + 'funkwhale_api.federation.activity.deliver') + test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance() + follow = factories['federation.Follow'](target=test_actor) + reverse_follow = factories['federation.Follow']( + actor=test_actor, target=follow.actor) + follow_serializer = serializers.FollowSerializer(follow) + reverse_follow_serializer = serializers.FollowSerializer( + reverse_follow) + undo = { + '@context': serializers.AP_CONTEXT, + 'type': 'Undo', + 'id': follow_serializer.data['id'] + '/undo', + 'actor': follow.actor.url, + 'object': follow_serializer.data, + } + expected_undo = { + '@context': serializers.AP_CONTEXT, + 'type': 'Undo', + 'id': reverse_follow_serializer.data['id'] + '/undo', + 'actor': reverse_follow.actor.url, + 'object': reverse_follow_serializer.data, + } + + actors.SYSTEM_ACTORS['test'].post_inbox(undo, actor=follow.actor) + deliver.assert_called_once_with( + expected_undo, + to=[follow.actor.url], + on_behalf_of=test_actor,) + + assert models.Follow.objects.count() == 0 diff --git a/api/tests/federation/test_authentication.py b/api/tests/federation/test_authentication.py index 1837b3950..c6a97a07a 100644 --- a/api/tests/federation/test_authentication.py +++ b/api/tests/federation/test_authentication.py @@ -3,7 +3,7 @@ from funkwhale_api.federation import keys from funkwhale_api.federation import signing -def test_authenticate(nodb_factories, mocker, api_request): +def test_authenticate(factories, mocker, api_request): private, public = keys.get_key_pair() actor_url = 'https://test.federation/actor' mocker.patch( @@ -18,7 +18,7 @@ def test_authenticate(nodb_factories, mocker, api_request): 'id': actor_url + '#main-key', } }) - signed_request = nodb_factories['federation.SignedRequest']( + signed_request = factories['federation.SignedRequest']( auth__key=private, auth__key_id=actor_url + '#main-key', auth__headers=[ diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py index 297fe2c58..18daf8788 100644 --- a/api/tests/federation/test_models.py +++ b/api/tests/federation/test_models.py @@ -23,3 +23,10 @@ def test_cannot_duplicate_follow(factories): target=follow.target, actor=follow.actor, ) + +def test_follow_federation_url(factories): + follow = factories['federation.Follow'](local=True) + expected = '{}#follows/{}'.format( + follow.actor.url, follow.uuid) + + assert follow.get_federation_url() == expected From b833a11fb657c41755f18002aa4daca50c43ccd6 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 4 Apr 2018 19:38:28 +0200 Subject: [PATCH 011/101] FollowRequest model --- .../migrations/0004_followrequest.py | 28 +++++++++++++++++++ api/funkwhale_api/federation/models.py | 18 ++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 api/funkwhale_api/federation/migrations/0004_followrequest.py diff --git a/api/funkwhale_api/federation/migrations/0004_followrequest.py b/api/funkwhale_api/federation/migrations/0004_followrequest.py new file mode 100644 index 000000000..6ede72747 --- /dev/null +++ b/api/funkwhale_api/federation/migrations/0004_followrequest.py @@ -0,0 +1,28 @@ +# Generated by Django 2.0.3 on 2018-04-04 17:11 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('federation', '0003_auto_20180403_1921'), + ] + + operations = [ + migrations.CreateModel( + name='FollowRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)), + ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), + ('last_modification_date', models.DateTimeField(default=django.utils.timezone.now)), + ('approved', models.NullBooleanField(default=None)), + ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emmited_follow_requests', to='federation.Actor')), + ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follow_requests', to='federation.Actor')), + ], + ), + ] diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index a228a3803..50ff9d319 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -103,3 +103,21 @@ class Follow(models.Model): def get_federation_url(self): return '{}#follows/{}'.format(self.actor.url, self.uuid) + + +class FollowRequest(models.Model): + uuid = models.UUIDField(default=uuid.uuid4, unique=True) + actor = models.ForeignKey( + Actor, + related_name='emmited_follow_requests', + on_delete=models.CASCADE, + ) + target = models.ForeignKey( + Actor, + related_name='received_follow_requests', + on_delete=models.CASCADE, + ) + creation_date = models.DateTimeField(default=timezone.now) + last_modification_date = models.DateTimeField( + default=timezone.now) + approved = models.NullBooleanField(default=None) From d8f86c4fce52b6b610ac72e22bf658ecc1cbba3c Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 4 Apr 2018 19:38:55 +0200 Subject: [PATCH 012/101] Factorized follow logic between system actors, Library can now accept follows --- api/funkwhale_api/federation/activity.py | 19 +++++ api/funkwhale_api/federation/actors.py | 61 ++++++++-------- api/tests/federation/test_activity.py | 42 +++++++++++ api/tests/federation/test_actors.py | 89 ++++++++++++++---------- 4 files changed, 145 insertions(+), 66 deletions(-) diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index 5a0974011..1b03d19f8 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -2,7 +2,9 @@ import logging import json import requests import requests_http_signature +import uuid +from . import models from . import signing logger = logging.getLogger(__name__) @@ -117,3 +119,20 @@ def get_accept_follow(accept_id, accept_actor, follow, follow_actor): "object": accept_actor.url }, } + + +def accept_follow(target, follow, actor): + accept_uuid = uuid.uuid4() + accept = get_accept_follow( + accept_id=accept_uuid, + accept_actor=target, + follow=follow, + follow_actor=actor) + deliver( + accept, + to=[actor.url], + on_behalf_of=target) + return models.Follow.objects.get_or_create( + actor=actor, + target=target, + ) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index 89c621edc..8871b1013 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -132,6 +132,20 @@ class SystemActor(object): return handler(data, actor) + def handle_follow(self, ac, sender): + system_actor = self.get_actor_instance() + if self.manually_approves_followers: + fr, created = models.FollowRequest.objects.get_or_create( + actor=sender, + target=system_actor, + approved=None, + ) + return fr + + return activity.accept_follow( + system_actor, ac, sender + ) + class LibraryActor(SystemActor): id = 'library' @@ -140,6 +154,7 @@ class LibraryActor(SystemActor): additional_attributes = { 'manually_approves_followers': True } + @property def manually_approves_followers(self): return settings.FEDERATION_MUSIC_NEEDS_APPROVAL @@ -159,18 +174,18 @@ class TestActor(SystemActor): def get_outbox(self, data, actor=None): return { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - {} - ], - "id": utils.full_url( + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "id": utils.full_url( reverse( 'federation:instance-actors-outbox', kwargs={'actor': self.id})), - "type": "OrderedCollection", - "totalItems": 0, - "orderedItems": [] + "type": "OrderedCollection", + "totalItems": 0, + "orderedItems": [] } def parse_command(self, message): @@ -204,10 +219,10 @@ class TestActor(SystemActor): ) reply_activity = { "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - {} - ], + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], 'type': 'Create', 'actor': test_actor.url, 'id': '{}/activity'.format(reply_url), @@ -240,25 +255,9 @@ class TestActor(SystemActor): on_behalf_of=test_actor) def handle_follow(self, ac, sender): - # on a follow we: - # 1. send the accept answer - # 2. follow back - # + super().handle_follow(ac, sender) + # also, we follow back test_actor = self.get_actor_instance() - accept_uuid = uuid.uuid4() - accept = activity.get_accept_follow( - accept_id=accept_uuid, - accept_actor=test_actor, - follow=ac, - follow_actor=sender) - activity.deliver( - accept, - to=[ac['actor']], - on_behalf_of=test_actor) - models.Follow.objects.get_or_create( - actor=sender, - target=test_actor, - ) follow_uuid = uuid.uuid4() follow = activity.get_follow( follow_id=follow_uuid, diff --git a/api/tests/federation/test_activity.py b/api/tests/federation/test_activity.py index a6e1d28aa..09c5e3bf7 100644 --- a/api/tests/federation/test_activity.py +++ b/api/tests/federation/test_activity.py @@ -1,5 +1,8 @@ +import uuid + from funkwhale_api.federation import activity + def test_deliver(nodb_factories, r_mock, mocker): to = nodb_factories['federation.Actor']() mocker.patch( @@ -30,3 +33,42 @@ def test_deliver(nodb_factories, r_mock, mocker): assert r_mock.call_count == 1 assert request.url == to.inbox_url assert request.headers['content-type'] == 'application/activity+json' + + +def test_accept_follow(mocker, factories): + deliver = mocker.patch( + 'funkwhale_api.federation.activity.deliver') + actor = factories['federation.Actor']() + target = factories['federation.Actor'](local=True) + follow = { + 'actor': actor.url, + 'type': 'Follow', + 'id': 'http://test.federation/user#follows/267', + 'object': target.url, + } + uid = uuid.uuid4() + mocker.patch('uuid.uuid4', return_value=uid) + expected_accept = { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "id": target.url + '#accepts/follows/{}'.format(uid), + "type": "Accept", + "actor": target.url, + "object": { + "id": follow['id'], + "type": "Follow", + "actor": actor.url, + "object": target.url + }, + } + activity.accept_follow( + target, follow, actor + ) + deliver.assert_called_once_with( + expected_accept, to=[actor.url], on_behalf_of=target + ) + follow_instance = actor.emitted_follows.first() + assert follow_instance.target == target diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index c1b9d8a23..5ade9cdc8 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -93,18 +93,18 @@ def test_get_test(settings, preferences): def test_test_get_outbox(): expected = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - {} - ], - "id": utils.full_url( + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {} + ], + "id": utils.full_url( reverse( 'federation:instance-actors-outbox', kwargs={'actor': 'test'})), - "type": "OrderedCollection", - "totalItems": 0, - "orderedItems": [] + "type": "OrderedCollection", + "totalItems": 0, + "orderedItems": [] } data = actors.SYSTEM_ACTORS['test'].get_outbox({}, actor=None) @@ -248,7 +248,7 @@ def test_system_actor_handle(mocker, nodb_factories): ) assert serializer.is_valid() actors.SYSTEM_ACTORS['test'].handle(activity, actor) - handler.assert_called_once_with(serializer.data, actor) + handler.assert_called_once_with(activity, actor) def test_test_actor_handles_follow( @@ -258,6 +258,8 @@ def test_test_actor_handles_follow( actor = factories['federation.Actor']() now = timezone.now() mocker.patch('django.utils.timezone.now', return_value=now) + accept_follow = mocker.patch( + 'funkwhale_api.federation.activity.accept_follow') test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance() data = { 'actor': actor.url, @@ -267,22 +269,6 @@ def test_test_actor_handles_follow( } uid = uuid.uuid4() mocker.patch('uuid.uuid4', return_value=uid) - expected_accept = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - {} - ], - "id": test_actor.url + '#accepts/follows/{}'.format(uid), - "type": "Accept", - "actor": test_actor.url, - "object": { - "id": data['id'], - "type": "Follow", - "actor": actor.url, - "object": test_actor.url - }, - } expected_follow = { '@context': serializers.AP_CONTEXT, 'actor': test_actor.url, @@ -292,12 +278,10 @@ def test_test_actor_handles_follow( } actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor) + accept_follow.assert_called_once_with( + test_actor, data, actor + ) expected_calls = [ - mocker.call( - expected_accept, - to=[actor.url], - on_behalf_of=test_actor, - ), mocker.call( expected_follow, to=[actor.url], @@ -306,10 +290,6 @@ def test_test_actor_handles_follow( ] deliver.assert_has_calls(expected_calls) - follow = test_actor.received_follows.first() - assert follow.actor == actor - assert follow.target == test_actor - def test_test_actor_handles_undo_follow( settings, mocker, factories): @@ -344,3 +324,42 @@ def test_test_actor_handles_undo_follow( on_behalf_of=test_actor,) assert models.Follow.objects.count() == 0 + + +def test_library_actor_handles_follow_manual_approval( + settings, mocker, factories): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True + actor = factories['federation.Actor']() + now = timezone.now() + mocker.patch('django.utils.timezone.now', return_value=now) + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + data = { + 'actor': actor.url, + 'type': 'Follow', + 'id': 'http://test.federation/user#follows/267', + 'object': library_actor.url, + } + + library_actor.system_conf.post_inbox(data, actor=actor) + fr = library_actor.received_follow_requests.first() + + assert library_actor.received_follow_requests.count() == 1 + assert fr.target == library_actor + assert fr.actor == actor + assert fr.approved is None + + +def test_library_actor_handles_follow_auto_approval( + settings, mocker, factories): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True + actor = factories['federation.Actor']() + accept_follow = mocker.patch( + 'funkwhale_api.federation.activity.accept_follow') + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + data = { + 'actor': actor.url, + 'type': 'Follow', + 'id': 'http://test.federation/user#follows/267', + 'object': library_actor.url, + } + library_actor.system_conf.post_inbox(data, actor=actor) From cb9309c29882ebce5b618d0a242f39978a3be286 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 4 Apr 2018 22:40:57 +0200 Subject: [PATCH 013/101] Factorized undo follow --- api/funkwhale_api/federation/activity.py | 14 +++++++ api/funkwhale_api/federation/actors.py | 50 +++++++++++++----------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index 1b03d19f8..b253955c8 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -101,6 +101,20 @@ def get_follow(follow_id, follower, followed): } +def get_undo(id, actor, object): + return { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + {} + ], + 'type': 'Undo', + 'id': id + '/undo', + 'actor': actor.url, + 'object': object, + } + + def get_accept_follow(accept_id, accept_actor, follow, follow_actor): return { "@context": [ diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index 8871b1013..0da78fdbe 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -146,6 +146,23 @@ class SystemActor(object): system_actor, ac, sender ) + def handle_undo_follow(self, ac, sender): + actor = self.get_actor_instance() + models.Follow.objects.filter( + actor=sender, + target=actor, + ).delete() + + def handle_undo(self, ac, sender): + if ac['object']['type'] != 'Follow': + return + + if ac['object']['actor'] != sender.url: + # not the same actor, permission issue + return + + self.handle_undo_follow(ac, sender) + class LibraryActor(SystemActor): id = 'library' @@ -268,39 +285,28 @@ class TestActor(SystemActor): to=[ac['actor']], on_behalf_of=test_actor) - def handle_undo(self, ac, sender): - if ac['object']['type'] != 'Follow': - return - - if ac['object']['actor'] != sender.url: - # not the same actor, permission issue - return - - test_actor = self.get_actor_instance() - models.Follow.objects.filter( - actor=sender, - target=test_actor, - ).delete() + def handle_undo_follow(self, ac, sender): + super().handle_undo_follow(ac, sender) + actor = self.get_actor_instance() # we also unfollow the sender, if possible try: follow = models.Follow.objects.get( target=sender, - actor=test_actor, + actor=actor, ) except models.Follow.DoesNotExist: return - undo = { - '@context': serializers.AP_CONTEXT, - 'type': 'Undo', - 'id': follow.get_federation_url() + '/undo', - 'actor': test_actor.url, - 'object': serializers.FollowSerializer(follow).data, - } + undo = activity.get_undo( + id=follow.get_federation_url(), + actor=actor, + object=serializers.FollowSerializer(follow).data, + ) follow.delete() activity.deliver( undo, to=[sender.url], - on_behalf_of=test_actor) + on_behalf_of=actor) + SYSTEM_ACTORS = { 'library': LibraryActor(), From e0dcb87f15579cf0018dc40936fdc5352b2648a9 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 4 Apr 2018 23:12:41 +0200 Subject: [PATCH 014/101] Follow request approve/refuse logic --- api/funkwhale_api/federation/factories.py | 9 +++++ api/funkwhale_api/federation/models.py | 36 ++++++++++++++++++ api/tests/federation/test_actors.py | 7 +++- api/tests/federation/test_models.py | 46 +++++++++++++++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py index 16abf80dc..277b9ce0c 100644 --- a/api/funkwhale_api/federation/factories.py +++ b/api/funkwhale_api/federation/factories.py @@ -97,6 +97,15 @@ class FollowFactory(factory.DjangoModelFactory): ) +@registry.register +class FollowRequestFactory(factory.DjangoModelFactory): + target = factory.SubFactory(ActorFactory) + actor = factory.SubFactory(ActorFactory) + + class Meta: + model = models.FollowRequest + + @registry.register(name='federation.Note') class NoteFactory(factory.Factory): type = 'Note' diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 50ff9d319..833b5d8f3 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -35,6 +35,13 @@ class Actor(models.Model): last_fetch_date = models.DateTimeField( default=timezone.now) manually_approves_followers = models.NullBooleanField(default=None) + followers = models.ManyToManyField( + to='self', + symmetrical=False, + through='Follow', + through_fields=('target', 'actor'), + related_name='following', + ) class Meta: unique_together = ['domain', 'preferred_username'] @@ -65,6 +72,10 @@ class Actor(models.Model): super().save(**kwargs) + @property + def is_local(self): + return self.domain == settings.FEDERATION_HOSTNAME + @property def is_system(self): from . import actors @@ -121,3 +132,28 @@ class FollowRequest(models.Model): last_modification_date = models.DateTimeField( default=timezone.now) approved = models.NullBooleanField(default=None) + + def approve(self): + from . import activity + from . import serializers + self.approved = True + self.save(update_fields=['approved']) + Follow.objects.get_or_create( + target=self.target, + actor=self.actor + ) + if self.target.is_local: + follow = { + '@context': serializers.AP_CONTEXT, + 'actor': self.actor.url, + 'id': self.actor.url + '#follows/{}'.format(uuid.uuid4()), + 'object': self.target.url, + 'type': 'Follow' + } + activity.accept_follow( + self.target, follow, self.actor + ) + + def refuse(self): + self.approved = False + self.save(update_fields=['approved']) diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index 5ade9cdc8..be24a5360 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -351,7 +351,7 @@ def test_library_actor_handles_follow_manual_approval( def test_library_actor_handles_follow_auto_approval( settings, mocker, factories): - settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False actor = factories['federation.Actor']() accept_follow = mocker.patch( 'funkwhale_api.federation.activity.accept_follow') @@ -363,3 +363,8 @@ def test_library_actor_handles_follow_auto_approval( 'object': library_actor.url, } library_actor.system_conf.post_inbox(data, actor=actor) + + assert library_actor.received_follow_requests.count() == 0 + accept_follow.assert_called_once_with( + library_actor, data, actor + ) diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py index 18daf8788..86e4f4a84 100644 --- a/api/tests/federation/test_models.py +++ b/api/tests/federation/test_models.py @@ -1,8 +1,10 @@ import pytest +import uuid from django import db from funkwhale_api.federation import models +from funkwhale_api.federation import serializers def test_cannot_duplicate_actor(factories): @@ -30,3 +32,47 @@ def test_follow_federation_url(factories): follow.actor.url, follow.uuid) assert follow.get_federation_url() == expected + + +def test_follow_request_approve(mocker, factories): + uid = uuid.uuid4() + mocker.patch('uuid.uuid4', return_value=uid) + accept_follow = mocker.patch( + 'funkwhale_api.federation.activity.accept_follow') + fr = factories['federation.FollowRequest'](target__local=True) + fr.approve() + + follow = { + '@context': serializers.AP_CONTEXT, + 'actor': fr.actor.url, + 'id': fr.actor.url + '#follows/{}'.format(uid), + 'object': fr.target.url, + 'type': 'Follow' + } + + assert fr.approved is True + assert list(fr.target.followers.all()) == [fr.actor] + accept_follow.assert_called_once_with( + fr.target, follow, fr.actor + ) + + +def test_follow_request_approve_non_local(mocker, factories): + uid = uuid.uuid4() + mocker.patch('uuid.uuid4', return_value=uid) + accept_follow = mocker.patch( + 'funkwhale_api.federation.activity.accept_follow') + fr = factories['federation.FollowRequest']() + fr.approve() + + assert fr.approved is True + assert list(fr.target.followers.all()) == [fr.actor] + accept_follow.assert_not_called() + + +def test_follow_request_refused(mocker, factories): + fr = factories['federation.FollowRequest']() + fr.refuse() + + assert fr.approved is False + assert fr.target.followers.count() == 0 From 1ca4505e2f22b02632a636ad5b74126153c72325 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 5 Apr 2018 16:05:45 +0000 Subject: [PATCH 015/101] Incremented version number --- api/funkwhale_api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py index 0a12b794e..ecd44e045 100644 --- a/api/funkwhale_api/__init__.py +++ b/api/funkwhale_api/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = '0.7' +__version__ = '0.8' __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')]) From 17edc4586636d7e15001b8972ab873258b8ab09c Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 5 Apr 2018 16:31:13 +0000 Subject: [PATCH 016/101] Added basic (commented) nginx proxy in deploy docker-compose --- deploy/docker-compose.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 69d5e1721..cc4f357ca 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -53,3 +53,19 @@ services: links: - postgres - redis + + # If you want to have the nginx proxy managed by docker for some reason + # (i.e. if you use apache as a proxy on your host), + # you can uncomment the following lines. + # nginx: + # image: nginx + # links: + # - api + # volumes: + # - ./nginx.conf:/etc/nginx/conf.d/funkwhale.conf:ro + # - ./funkwhale_proxy.conf:/etc/nginx/funkwhale_proxy.conf:ro + # - ./data/media:/srv/funkwhale/data/media:ro + # - ./front/dist:/srv/funkwhale/front/dist:ro + # - ./data/static:/srv/funkwhale/data/static/:ro + # ports: + # - "127.0.0.1:5001:80" From 4d6e894b6291684b97a42d0d864020137f800715 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 5 Apr 2018 23:22:28 +0200 Subject: [PATCH 017/101] AudioCollection to import job and track file creation --- api/funkwhale_api/music/serializers.py | 81 ++++++++++++++++++++ api/funkwhale_api/music/tasks.py | 48 +++++++++++- api/tests/music/test_import.py | 101 +++++++++++++++++++++++++ api/tests/music/test_serializers.py | 32 ++++++++ 4 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 api/tests/music/test_serializers.py diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 48419bbe4..9f0b7af5c 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -1,3 +1,4 @@ +from django.db import transaction from rest_framework import serializers from taggit.models import Tag @@ -150,3 +151,83 @@ class TrackActivitySerializer(activity_serializers.ModelSerializer): def get_type(self, obj): return 'Audio' + + +class AudioMetadataSerializer(serializers.Serializer): + artist = serializers.CharField(required=False) + release = serializers.CharField(required=False) + recording = serializers.CharField(required=False) + + +class AudioSerializer(serializers.Serializer): + type = serializers.CharField() + id = serializers.URLField() + url = serializers.JSONField() + metadata = AudioMetadataSerializer() + + def validate_type(self, v): + if v != 'Audio': + raise serializers.ValidationError('Invalid type for audio') + return v + + def validate_url(self, v): + try: + url = v['href'] + except (KeyError, TypeError): + raise serializers.ValidationError('Missing href') + + try: + media_type = v['mediaType'] + except (KeyError, TypeError): + raise serializers.ValidationError('Missing mediaType') + + if not media_type.startswith('audio/'): + raise serializers.ValidationError('Invalid mediaType') + + return url + + def validate_url(self, v): + try: + url = v['href'] + except (KeyError, TypeError): + raise serializers.ValidationError('Missing href') + + try: + media_type = v['mediaType'] + except (KeyError, TypeError): + raise serializers.ValidationError('Missing mediaType') + + if not media_type.startswith('audio/'): + raise serializers.ValidationError('Invalid mediaType') + + return v + + def create(self, validated_data, batch): + metadata = validated_data['metadata'].copy() + metadata['mediaType'] = validated_data['url']['mediaType'] + return models.ImportJob.objects.create( + batch=batch, + source=validated_data['url']['href'], + federation_source=validated_data['id'], + metadata=metadata, + ) + + +class AudioCollectionImportSerializer(serializers.Serializer): + id = serializers.URLField() + items = serializers.ListField( + child=AudioSerializer(), + min_length=1, + ) + + @transaction.atomic + def create(self, validated_data): + batch = models.ImportBatch.objects.create( + federation_actor=self.context['sender'], + federation_source=validated_data['id'], + source='federation', + ) + for i in validated_data['items']: + s = AudioSerializer(data=i) + job = s.create(i, batch) + return batch diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py index bf7a847d0..e4214d990 100644 --- a/api/funkwhale_api/music/tasks.py +++ b/api/funkwhale_api/music/tasks.py @@ -25,6 +25,44 @@ def set_acoustid_on_track_file(track_file): return update(result['id']) +def get_mbid(url, type): + prefix = 'https://musicbrainz.org/{}/'.format(type) + if url.startswith(prefix): + return url.replace(prefix, '') + + +def import_track_from_metadata(metadata): + raw_track = metadata['recording'] + if isinstance(raw_track, str): + track_mbid = get_mbid(raw_track, 'recording') + return models.Track.get_or_create_from_api(mbid=track_mbid) + + raw_album = metadata['release'] + if isinstance(raw_album, str): + album_mbid = get_mbid(raw_album, 'release') + album = models.Album.get_or_create_from_api(mbid=album_mbid) + return models.Track.get_or_create_from_title( + raw_track['title'], artist=album.artist, album=album) + + raw_artist = metadata['artist'] + if isinstance(raw_artist, str): + artist_mbid = get_mbid(raw_artist, 'artist') + artist = models.Artist.get_or_create_from_api(mbid=artist_mbid) + album = models.Album.get_or_create_from_title( + raw_album['title'], artist=artist) + return models.Track.get_or_create_from_title( + raw_track['title'], artist=artist, album=album) + + # worst case scenario, we have absolutely no way to link to a + # musicbrainz resource, we rely on the name/titles + artist = models.Artist.get_or_create_from_name( + raw_artist['name']) + album = models.Album.get_or_create_from_title( + raw_album['title'], artist=artist) + return models.Track.get_or_create_from_title( + raw_track['title'], artist=artist, album=album) + + def _do_import(import_job, replace, use_acoustid=True): from_file = bool(import_job.audio_file) mbid = import_job.mbid @@ -43,9 +81,14 @@ def _do_import(import_job, replace, use_acoustid=True): acoustid_track_id = match['id'] if mbid: track, _ = models.Track.get_or_create_from_api(mbid=mbid) - else: + elif import_job.audio_file: track = import_track_data_from_path(import_job.audio_file.path) + else: + # probably federation, we use metadata stored on the job itself + if not import_job.metadata: + raise ValueError('We cannot import without at least metadatas') + track = import_track_from_metadata(import_job.metadata) track_file = None if replace: track_file = track.files.first() @@ -63,6 +106,9 @@ def _do_import(import_job, replace, use_acoustid=True): track_file.audio_file = ContentFile(import_job.audio_file.read()) track_file.audio_file.name = import_job.audio_file.name track_file.duration = duration + elif import_job.federation_source: + # no downloading, we hotlink + pass else: track_file.download_file() track_file.save() diff --git a/api/tests/music/test_import.py b/api/tests/music/test_import.py index 0f709e81f..e9ad9d0f5 100644 --- a/api/tests/music/test_import.py +++ b/api/tests/music/test_import.py @@ -1,7 +1,10 @@ import json +import pytest from django.urls import reverse +from funkwhale_api.music import tasks + def test_create_import_can_bind_to_request( artists, albums, mocker, factories, superuser_api_client): @@ -33,3 +36,101 @@ def test_create_import_can_bind_to_request( batch = request.import_batches.latest('id') assert batch.import_request == request + + +@pytest.mark.parametrize('url,type,expected', [ + ('https://musicbrainz.org/artist/test', 'artist', 'test'), + ('https://musicbrainz.org/release/test', 'release', 'test'), + ('https://musicbrainz.org/recording/test', 'recording', 'test'), + ('https://musicbrainz.org/recording/test', 'artist', None), +]) +def test_get_mbid(url, type, expected): + assert tasks.get_mbid(url, type) == expected + + +def test_import_job_from_federation_no_musicbrainz(factories): + job = factories['music.ImportJob']( + federation=True, + metadata__artist={'name': 'Hello'}, + metadata__release={'title': 'World'}, + metadata__recording={'title': 'Ping'}, + mbid=None, + ) + + tasks.import_job_run(import_job_id=job.pk) + job.refresh_from_db() + + tf = job.track_file + assert tf.track.title == 'Ping' + assert tf.track.artist.name == 'Hello' + assert tf.track.album.title == 'World' + + +def test_import_job_from_federation_musicbrainz_recording(factories, mocker): + t = factories['music.Track']() + track_from_api = mocker.patch( + 'funkwhale_api.music.models.Track.get_or_create_from_api', + return_value=t) + job = factories['music.ImportJob']( + federation=True, + metadata__artist={'name': 'Hello'}, + metadata__release={'title': 'World'}, + mbid=None, + ) + + tasks.import_job_run(import_job_id=job.pk) + job.refresh_from_db() + + tf = job.track_file + assert tf.track == t + track_from_api.assert_called_once_with( + mbid=tasks.get_mbid(job.metadata['recording'], 'recording')) + + +def test_import_job_from_federation_musicbrainz_release(factories, mocker): + a = factories['music.Album']() + album_from_api = mocker.patch( + 'funkwhale_api.music.models.Album.get_or_create_from_api', + return_value=a) + job = factories['music.ImportJob']( + federation=True, + metadata__artist={'name': 'Hello'}, + metadata__recording={'title': 'Ping'}, + mbid=None, + ) + + tasks.import_job_run(import_job_id=job.pk) + job.refresh_from_db() + + tf = job.track_file + assert tf.track.title == 'Ping' + assert tf.track.artist == a.artist + assert tf.track.album == a + + album_from_api.assert_called_once_with( + mbid=tasks.get_mbid(job.metadata['release'], 'release')) + + +def test_import_job_from_federation_musicbrainz_artist(factories, mocker): + a = factories['music.Artist']() + artist_from_api = mocker.patch( + 'funkwhale_api.music.models.Artist.get_or_create_from_api', + return_value=a) + job = factories['music.ImportJob']( + federation=True, + metadata__release={'title': 'World'}, + metadata__recording={'title': 'Ping'}, + mbid=None, + ) + + tasks.import_job_run(import_job_id=job.pk) + job.refresh_from_db() + + tf = job.track_file + assert tf.track.title == 'Ping' + assert tf.track.artist == a + assert tf.track.album.artist == a + assert tf.track.album.title == 'World' + + artist_from_api.assert_called_once_with( + mbid=tasks.get_mbid(job.metadata['artist'], 'artist')) diff --git a/api/tests/music/test_serializers.py b/api/tests/music/test_serializers.py new file mode 100644 index 000000000..556ac4c0e --- /dev/null +++ b/api/tests/music/test_serializers.py @@ -0,0 +1,32 @@ +from funkwhale_api.music import serializers + + +def test_activity_pub_audio_collection_serializer(factories): + sender = factories['federation.Actor']() + + collection = { + 'id': 'https://batch.import', + 'type': 'Collection', + 'totalItems': 2, + 'items': factories['federation.Audio'].create_batch(size=2) + } + + serializer = serializers.AudioCollectionImportSerializer( + data=collection, context={'sender': sender}) + + assert serializer.is_valid(raise_exception=True) + + batch = serializer.save() + jobs = list(batch.jobs.all()) + + assert batch.source == 'federation' + assert batch.federation_source == collection['id'] + assert batch.federation_actor == sender + assert len(jobs) == 2 + + for i, a in enumerate(collection['items']): + job = jobs[i] + assert job.federation_source == a['id'] + assert job.source == a['url']['href'] + a['metadata']['mediaType'] = a['url']['mediaType'] + assert job.metadata == a['metadata'] From 363acca53db4ea164e18892b603de4724bf05648 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 5 Apr 2018 23:26:41 +0200 Subject: [PATCH 018/101] AudioCollection to import job and track file creation --- .../migrations/0005_actor_followers.py | 18 +++++++ .../migrations/0023_auto_20180405_1830.py | 47 +++++++++++++++++++ api/funkwhale_api/music/models.py | 47 ++++++++++++++++++- api/tests/music/test_import.py | 4 ++ 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 api/funkwhale_api/federation/migrations/0005_actor_followers.py create mode 100644 api/funkwhale_api/music/migrations/0023_auto_20180405_1830.py diff --git a/api/funkwhale_api/federation/migrations/0005_actor_followers.py b/api/funkwhale_api/federation/migrations/0005_actor_followers.py new file mode 100644 index 000000000..94a1c75ac --- /dev/null +++ b/api/funkwhale_api/federation/migrations/0005_actor_followers.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.3 on 2018-04-05 16:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('federation', '0004_followrequest'), + ] + + operations = [ + migrations.AddField( + model_name='actor', + name='followers', + field=models.ManyToManyField(related_name='following', through='federation.Follow', to='federation.Actor'), + ), + ] diff --git a/api/funkwhale_api/music/migrations/0023_auto_20180405_1830.py b/api/funkwhale_api/music/migrations/0023_auto_20180405_1830.py new file mode 100644 index 000000000..3cef1f42e --- /dev/null +++ b/api/funkwhale_api/music/migrations/0023_auto_20180405_1830.py @@ -0,0 +1,47 @@ +# Generated by Django 2.0.3 on 2018-04-05 18:30 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('federation', '0005_actor_followers'), + ('music', '0022_importbatch_import_request'), + ] + + operations = [ + migrations.AddField( + model_name='importbatch', + name='federation_actor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='import_batches', to='federation.Actor'), + ), + migrations.AddField( + model_name='importbatch', + name='federation_source', + field=models.URLField(blank=True, null=True), + ), + migrations.AddField( + model_name='importjob', + name='federation_source', + field=models.URLField(blank=True, null=True), + ), + migrations.AddField( + model_name='importjob', + name='metadata', + field=django.contrib.postgres.fields.jsonb.JSONField(default={}), + ), + migrations.AlterField( + model_name='importbatch', + name='source', + field=models.CharField(choices=[('api', 'api'), ('shell', 'shell'), ('federation', 'federation')], default='api', max_length=30), + ), + migrations.AlterField( + model_name='importbatch', + name='submitted_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='imports', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 7138dcdd6..cff162972 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -8,6 +8,7 @@ import markdown from django.conf import settings from django.db import models +from django.contrib.postgres.fields import JSONField from django.core.files.base import ContentFile from django.core.files import File from django.db.models.signals import post_save @@ -65,6 +66,7 @@ class APIModelMixin(models.Model): pass return cleaned_data + class Artist(APIModelMixin): name = models.CharField(max_length=255) @@ -90,10 +92,19 @@ class Artist(APIModelMixin): t.append(tag) return set(t) + @classmethod + def get_or_create_from_name(cls, name, **kwargs): + kwargs.update({'name': name}) + return cls.objects.get_or_create( + name__iexact=name, + defaults=kwargs)[0] + + def import_artist(v): a = Artist.get_or_create_from_api(mbid=v[0]['artist']['id'])[0] return a + def parse_date(v): if len(v) == 4: return datetime.date(int(v), 1, 1) @@ -108,6 +119,7 @@ def import_tracks(instance, cleaned_data, raw_data): track_cleaned_data['position'] = int(track_data['position']) track = importers.load(Track, track_cleaned_data, track_data, Track.import_hooks) + class Album(APIModelMixin): title = models.CharField(max_length=255) artist = models.ForeignKey( @@ -170,6 +182,14 @@ class Album(APIModelMixin): t.append(tag) return set(t) + @classmethod + def get_or_create_from_title(cls, title, **kwargs): + kwargs.update({'title': title}) + return cls.objects.get_or_create( + title__iexact=title, + defaults=kwargs)[0] + + def import_tags(instance, cleaned_data, raw_data): MINIMUM_COUNT = 2 tags_to_add = [] @@ -182,6 +202,7 @@ def import_tags(instance, cleaned_data, raw_data): tags_to_add.append(tag_data['name']) instance.tags.add(*tags_to_add) + def import_album(v): a = Album.get_or_create_from_api(mbid=v[0]['id'])[0] return a @@ -328,7 +349,7 @@ class Track(APIModelMixin): def save(self, **kwargs): try: self.artist - except Artist.DoesNotExist: + except Artist.DoesNotExist: self.artist = self.album.artist super().save(**kwargs) @@ -366,6 +387,13 @@ class Track(APIModelMixin): self.mbid) return settings.FUNKWHALE_URL + '/tracks/{}'.format(self.pk) + @classmethod + def get_or_create_from_title(cls, title, **kwargs): + kwargs.update({'title': title}) + return cls.objects.get_or_create( + title__iexact=title, + defaults=kwargs)[0] + class TrackFile(models.Model): track = models.ForeignKey( @@ -420,7 +448,8 @@ IMPORT_STATUS_CHOICES = ( class ImportBatch(models.Model): IMPORT_BATCH_SOURCES = [ ('api', 'api'), - ('shell', 'shell') + ('shell', 'shell'), + ('federation', 'federation'), ] source = models.CharField( max_length=30, default='api', choices=IMPORT_BATCH_SOURCES) @@ -428,6 +457,8 @@ class ImportBatch(models.Model): submitted_by = models.ForeignKey( 'users.User', related_name='imports', + null=True, + blank=True, on_delete=models.CASCADE) status = models.CharField( choices=IMPORT_STATUS_CHOICES, default='pending', max_length=30) @@ -437,6 +468,16 @@ class ImportBatch(models.Model): null=True, blank=True, on_delete=models.CASCADE) + + federation_source = models.URLField(null=True, blank=True) + federation_actor = models.ForeignKey( + 'federation.Actor', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='import_batches', + ) + class Meta: ordering = ['-creation_date'] @@ -464,6 +505,8 @@ class ImportJob(models.Model): choices=IMPORT_STATUS_CHOICES, default='pending', max_length=30) audio_file = models.FileField( upload_to='imports/%Y/%m/%d', max_length=255, null=True, blank=True) + federation_source = models.URLField(null=True, blank=True) + metadata = JSONField(default={}) class Meta: ordering = ('id', ) diff --git a/api/tests/music/test_import.py b/api/tests/music/test_import.py index e9ad9d0f5..87e1899d6 100644 --- a/api/tests/music/test_import.py +++ b/api/tests/music/test_import.py @@ -61,6 +61,7 @@ def test_import_job_from_federation_no_musicbrainz(factories): job.refresh_from_db() tf = job.track_file + assert tf.source == job.source assert tf.track.title == 'Ping' assert tf.track.artist.name == 'Hello' assert tf.track.album.title == 'World' @@ -82,6 +83,7 @@ def test_import_job_from_federation_musicbrainz_recording(factories, mocker): job.refresh_from_db() tf = job.track_file + assert tf.source == job.source assert tf.track == t track_from_api.assert_called_once_with( mbid=tasks.get_mbid(job.metadata['recording'], 'recording')) @@ -103,6 +105,7 @@ def test_import_job_from_federation_musicbrainz_release(factories, mocker): job.refresh_from_db() tf = job.track_file + assert tf.source == job.source assert tf.track.title == 'Ping' assert tf.track.artist == a.artist assert tf.track.album == a @@ -127,6 +130,7 @@ def test_import_job_from_federation_musicbrainz_artist(factories, mocker): job.refresh_from_db() tf = job.track_file + assert tf.source == job.source assert tf.track.title == 'Ping' assert tf.track.artist == a assert tf.track.album.artist == a From feab0f98ba009ef3eeeef2175fb32a020b3c8084 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 5 Apr 2018 23:27:03 +0200 Subject: [PATCH 019/101] Spaces > Tabs --- api/tests/federation/test_serializers.py | 88 ++++++++++++------------ 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/api/tests/federation/test_serializers.py b/api/tests/federation/test_serializers.py index 77c14531c..6d027ec91 100644 --- a/api/tests/federation/test_serializers.py +++ b/api/tests/federation/test_serializers.py @@ -7,25 +7,25 @@ from funkwhale_api.federation import serializers def test_actor_serializer_from_ap(db): payload = { - 'id': 'https://test.federation/user', - 'type': 'Person', - 'following': 'https://test.federation/user/following', - 'followers': 'https://test.federation/user/followers', - 'inbox': 'https://test.federation/user/inbox', - 'outbox': 'https://test.federation/user/outbox', - 'preferredUsername': 'user', - 'name': 'Real User', - 'summary': 'Hello world', - 'url': 'https://test.federation/@user', - 'manuallyApprovesFollowers': False, - 'publicKey': { - 'id': 'https://test.federation/user#main-key', - 'owner': 'https://test.federation/user', - 'publicKeyPem': 'yolo' - }, - 'endpoints': { - 'sharedInbox': 'https://test.federation/inbox' - }, + 'id': 'https://test.federation/user', + 'type': 'Person', + 'following': 'https://test.federation/user/following', + 'followers': 'https://test.federation/user/followers', + 'inbox': 'https://test.federation/user/inbox', + 'outbox': 'https://test.federation/user/outbox', + 'preferredUsername': 'user', + 'name': 'Real User', + 'summary': 'Hello world', + 'url': 'https://test.federation/@user', + 'manuallyApprovesFollowers': False, + 'publicKey': { + 'id': 'https://test.federation/user#main-key', + 'owner': 'https://test.federation/user', + 'publicKeyPem': 'yolo' + }, + 'endpoints': { + 'sharedInbox': 'https://test.federation/inbox' + }, } serializer = serializers.ActorSerializer(data=payload) @@ -50,13 +50,13 @@ def test_actor_serializer_from_ap(db): def test_actor_serializer_only_mandatory_field_from_ap(db): payload = { - 'id': 'https://test.federation/user', - 'type': 'Person', - 'following': 'https://test.federation/user/following', - 'followers': 'https://test.federation/user/followers', - 'inbox': 'https://test.federation/user/inbox', - 'outbox': 'https://test.federation/user/outbox', - 'preferredUsername': 'user', + 'id': 'https://test.federation/user', + 'type': 'Person', + 'following': 'https://test.federation/user/following', + 'followers': 'https://test.federation/user/followers', + 'inbox': 'https://test.federation/user/inbox', + 'outbox': 'https://test.federation/user/outbox', + 'preferredUsername': 'user', } serializer = serializers.ActorSerializer(data=payload) @@ -82,24 +82,24 @@ def test_actor_serializer_to_ap(): 'https://w3id.org/security/v1', {}, ], - 'id': 'https://test.federation/user', - 'type': 'Person', - 'following': 'https://test.federation/user/following', - 'followers': 'https://test.federation/user/followers', - 'inbox': 'https://test.federation/user/inbox', - 'outbox': 'https://test.federation/user/outbox', - 'preferredUsername': 'user', - 'name': 'Real User', - 'summary': 'Hello world', - 'manuallyApprovesFollowers': False, - 'publicKey': { - 'id': 'https://test.federation/user#main-key', - 'owner': 'https://test.federation/user', - 'publicKeyPem': 'yolo' - }, - 'endpoints': { - 'sharedInbox': 'https://test.federation/inbox' - }, + 'id': 'https://test.federation/user', + 'type': 'Person', + 'following': 'https://test.federation/user/following', + 'followers': 'https://test.federation/user/followers', + 'inbox': 'https://test.federation/user/inbox', + 'outbox': 'https://test.federation/user/outbox', + 'preferredUsername': 'user', + 'name': 'Real User', + 'summary': 'Hello world', + 'manuallyApprovesFollowers': False, + 'publicKey': { + 'id': 'https://test.federation/user#main-key', + 'owner': 'https://test.federation/user', + 'publicKeyPem': 'yolo' + }, + 'endpoints': { + 'sharedInbox': 'https://test.federation/inbox' + }, } ac = models.Actor( url=expected['id'], From 87daa81762d0b8fd7c4a34671a8f6173a7921e5e Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 5 Apr 2018 23:27:19 +0200 Subject: [PATCH 020/101] More factories --- api/funkwhale_api/federation/factories.py | 47 +++++++++++++++++++++++ api/funkwhale_api/music/factories.py | 18 +++++++++ 2 files changed, 65 insertions(+) diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py index 277b9ce0c..63d40aff8 100644 --- a/api/funkwhale_api/federation/factories.py +++ b/api/funkwhale_api/federation/factories.py @@ -1,6 +1,7 @@ import factory import requests import requests_http_signature +import uuid from django.utils import timezone from django.conf import settings @@ -52,6 +53,21 @@ class SignedRequestFactory(factory.Factory): self.headers.update(default_headers) +@registry.register(name='federation.Link') +class LinkFactory(factory.Factory): + type = 'Link' + href = factory.Faker('url') + mediaType = 'text/html' + + class Meta: + model = dict + + class Params: + audio = factory.Trait( + mediaType=factory.Iterator(['audio/mp3', 'audio/ogg']) + ) + + @registry.register class ActorFactory(factory.DjangoModelFactory): public_key = None @@ -135,3 +151,34 @@ class ActivityFactory(factory.Factory): class Meta: model = dict + + +@registry.register(name='federation.AudioMetadata') +class AudioMetadataFactory(factory.Factory): + recording = factory.LazyAttribute( + lambda o: 'https://musicbrainz.org/recording/{}'.format(uuid.uuid4()) + ) + artist = factory.LazyAttribute( + lambda o: 'https://musicbrainz.org/artist/{}'.format(uuid.uuid4()) + ) + release = factory.LazyAttribute( + lambda o: 'https://musicbrainz.org/release/{}'.format(uuid.uuid4()) + ) + + class Meta: + model = dict + + +@registry.register(name='federation.Audio') +class AudioFactory(factory.Factory): + type = 'Audio' + id = factory.Faker('url') + published = factory.LazyFunction( + lambda: timezone.now().isoformat() + ) + actor = factory.Faker('url') + url = factory.SubFactory(LinkFactory, audio=True) + metadata = factory.SubFactory(AudioMetadataFactory) + + class Meta: + model = dict diff --git a/api/funkwhale_api/music/factories.py b/api/funkwhale_api/music/factories.py index 303e45228..27387ca9f 100644 --- a/api/funkwhale_api/music/factories.py +++ b/api/funkwhale_api/music/factories.py @@ -2,6 +2,10 @@ import factory import os from funkwhale_api.factories import registry, ManyToManyFromList +from funkwhale_api.federation.factories import ( + AudioMetadataFactory, + ActorFactory, +) from funkwhale_api.users.factories import UserFactory SAMPLES_PATH = os.path.join( @@ -61,6 +65,13 @@ class ImportBatchFactory(factory.django.DjangoModelFactory): class Meta: model = 'music.ImportBatch' + class Params: + federation = factory.Trait( + submitted_by=None, + federation_actor=factory.SubFactory(ActorFactory), + source='federation', + ) + @registry.register class ImportJobFactory(factory.django.DjangoModelFactory): @@ -71,6 +82,13 @@ class ImportJobFactory(factory.django.DjangoModelFactory): class Meta: model = 'music.ImportJob' + class Params: + federation = factory.Trait( + batch=factory.SubFactory(ImportBatchFactory, federation=True), + federation_source=factory.Faker('url'), + metadata=factory.SubFactory(AudioMetadataFactory), + ) + @registry.register(name='music.FileImportJob') class FileImportJobFactory(ImportJobFactory): From 679adfe156dcf784b0865ca58a0709127f42a235 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Fri, 6 Apr 2018 13:17:26 +0200 Subject: [PATCH 021/101] See #126: Added uuid field to all music models --- .../migrations/0024_auto_20180406_1115.py | 54 +++++++++++++++++++ api/funkwhale_api/music/models.py | 11 ++++ 2 files changed, 65 insertions(+) create mode 100644 api/funkwhale_api/music/migrations/0024_auto_20180406_1115.py diff --git a/api/funkwhale_api/music/migrations/0024_auto_20180406_1115.py b/api/funkwhale_api/music/migrations/0024_auto_20180406_1115.py new file mode 100644 index 000000000..8b655a642 --- /dev/null +++ b/api/funkwhale_api/music/migrations/0024_auto_20180406_1115.py @@ -0,0 +1,54 @@ +# Generated by Django 2.0.3 on 2018-04-06 11:15 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('music', '0023_auto_20180405_1830'), + ] + + operations = [ + migrations.AddField( + model_name='album', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='artist', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='importbatch', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='importjob', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='lyrics', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='track', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='trackfile', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='work', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ] diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index cff162972..cf48667cc 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -5,6 +5,7 @@ import datetime import tempfile import shutil import markdown +import uuid from django.conf import settings from django.db import models @@ -27,6 +28,8 @@ from . import utils class APIModelMixin(models.Model): mbid = models.UUIDField(unique=True, db_index=True, null=True, blank=True) + uuid = models.UUIDField( + unique=True, db_index=True, default=uuid.uuid4) api_includes = [] creation_date = models.DateTimeField(default=timezone.now) import_hooks = [] @@ -269,6 +272,8 @@ class Work(APIModelMixin): class Lyrics(models.Model): + uuid = models.UUIDField( + unique=True, db_index=True, default=uuid.uuid4) work = models.ForeignKey( Work, related_name='lyrics', @@ -396,6 +401,8 @@ class Track(APIModelMixin): class TrackFile(models.Model): + uuid = models.UUIDField( + unique=True, db_index=True, default=uuid.uuid4) track = models.ForeignKey( Track, related_name='files', on_delete=models.CASCADE) audio_file = models.FileField(upload_to='tracks/%Y/%m/%d', max_length=255) @@ -446,6 +453,8 @@ IMPORT_STATUS_CHOICES = ( ) class ImportBatch(models.Model): + uuid = models.UUIDField( + unique=True, db_index=True, default=uuid.uuid4) IMPORT_BATCH_SOURCES = [ ('api', 'api'), ('shell', 'shell'), @@ -490,6 +499,8 @@ class ImportBatch(models.Model): class ImportJob(models.Model): + uuid = models.UUIDField( + unique=True, db_index=True, default=uuid.uuid4) batch = models.ForeignKey( ImportBatch, related_name='jobs', on_delete=models.CASCADE) track_file = models.ForeignKey( From 80206761a311893b96ecf0fb4e2ae2f5adf7a50f Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Fri, 6 Apr 2018 14:26:39 +0200 Subject: [PATCH 022/101] Easy and resusable Audio and AudioCollection serializer --- api/funkwhale_api/music/models.py | 12 +++++ api/funkwhale_api/music/serializers.py | 46 ++++++++++++++++++ api/tests/music/test_serializers.py | 64 +++++++++++++++++++++++++- 3 files changed, 121 insertions(+), 1 deletion(-) diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index cf48667cc..5d8035f35 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -22,6 +22,7 @@ from versatileimagefield.fields import VersatileImageField from funkwhale_api import downloader from funkwhale_api import musicbrainz +from funkwhale_api.federation import utils as federation_utils from . import importers from . import utils @@ -69,6 +70,12 @@ class APIModelMixin(models.Model): pass return cleaned_data + @property + def musicbrainz_url(self): + if self.mbid: + return 'https://musicbrainz.org/{}/{}'.format( + self.musicbrainz_model, self.mbid) + class Artist(APIModelMixin): name = models.CharField(max_length=255) @@ -426,6 +433,11 @@ class TrackFile(models.Model): shutil.rmtree(tmp_dir) return self.audio_file + def get_federation_url(self): + return federation_utils.full_url( + '/federation/music/file/{}'.format(self.uuid) + ) + @property def path(self): if settings.PROTECT_AUDIO_FILES: diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 9f0b7af5c..5cd2f2cc2 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -3,6 +3,8 @@ from rest_framework import serializers from taggit.models import Tag from funkwhale_api.activity import serializers as activity_serializers +from funkwhale_api.federation.serializers import AP_CONTEXT +from funkwhale_api.federation import utils as federation_utils from . import models @@ -212,6 +214,29 @@ class AudioSerializer(serializers.Serializer): metadata=metadata, ) + def to_representation(self, instance): + d = { + 'type': 'Audio', + 'id': instance.get_federation_url(), + 'name': instance.track.full_name, + 'metadata': { + 'artist': instance.track.artist.musicbrainz_url, + 'release': instance.track.album.musicbrainz_url, + 'track': instance.track.musicbrainz_url, + }, + 'url': { + 'href': federation_utils.full_url(instance.path), + 'type': 'Link', + 'mediaType': instance.mimetype + }, + 'attributedTo': [ + self.context['actor'].url + ] + } + if self.context.get('include_ap_context', True): + d['@context'] = AP_CONTEXT + return d + class AudioCollectionImportSerializer(serializers.Serializer): id = serializers.URLField() @@ -231,3 +256,24 @@ class AudioCollectionImportSerializer(serializers.Serializer): s = AudioSerializer(data=i) job = s.create(i, batch) return batch + + def to_representation(self, instance): + d = { + 'id': instance['id'], + 'actor': instance['actor'].url, + 'totalItems': len(instance['items']), + 'type': 'Collection', + 'items': [ + AudioSerializer( + i, + context={ + 'actor': instance['actor'], + 'include_ap_context': False + } + ).data + for i in instance['items'] + ] + } + if self.context.get('include_ap_context', True): + d['@context'] = AP_CONTEXT + return d diff --git a/api/tests/music/test_serializers.py b/api/tests/music/test_serializers.py index 556ac4c0e..1270ae765 100644 --- a/api/tests/music/test_serializers.py +++ b/api/tests/music/test_serializers.py @@ -1,7 +1,10 @@ +from funkwhale_api.federation import actors +from funkwhale_api.federation import utils as federation_utils +from funkwhale_api.federation.serializers import AP_CONTEXT from funkwhale_api.music import serializers -def test_activity_pub_audio_collection_serializer(factories): +def test_activity_pub_audio_collection_serializer_to_import(factories): sender = factories['federation.Actor']() collection = { @@ -30,3 +33,62 @@ def test_activity_pub_audio_collection_serializer(factories): assert job.source == a['url']['href'] a['metadata']['mediaType'] = a['url']['mediaType'] assert job.metadata == a['metadata'] + + +def test_activity_pub_audio_serializer_to_ap(factories): + tf = factories['music.TrackFile'](mimetype='audio/mp3') + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + expected = { + '@context': AP_CONTEXT, + 'type': 'Audio', + 'id': tf.get_federation_url(), + 'name': tf.track.full_name, + 'metadata': { + 'artist': tf.track.artist.musicbrainz_url, + 'release': tf.track.album.musicbrainz_url, + 'track': tf.track.musicbrainz_url, + }, + 'url': { + 'href': federation_utils.full_url(tf.path), + 'type': 'Link', + 'mediaType': 'audio/mp3' + }, + 'attributedTo': [ + library.url + ] + } + + serializer = serializers.AudioSerializer(tf, context={'actor': library}) + + assert serializer.data == expected + + +def test_activity_pub_audio_collection_serializer_to_ap(factories): + tf1 = factories['music.TrackFile'](mimetype='audio/mp3') + tf2 = factories['music.TrackFile'](mimetype='audio/ogg') + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + expected = { + '@context': AP_CONTEXT, + 'id': 'https://test.id', + 'actor': library.url, + 'totalItems': 2, + 'type': 'Collection', + 'items': [ + serializers.AudioSerializer( + tf1, context={'actor': library, 'include_ap_context': False} + ).data, + serializers.AudioSerializer( + tf2, context={'actor': library, 'include_ap_context': False} + ).data, + ] + } + + collection = { + 'id': expected['id'], + 'actor': library, + 'items': [tf1, tf2], + } + serializer = serializers.AudioCollectionImportSerializer( + collection, context={'actor': library, 'id': 'https://test.id'}) + + assert serializer.data == expected From 04d710e690a0ef4fc1cbc18a2af95d9cd3a7349b Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Fri, 6 Apr 2018 14:45:06 +0200 Subject: [PATCH 023/101] Library can now receive import info from followed instances --- api/funkwhale_api/federation/activity.py | 2 + api/funkwhale_api/federation/actors.py | 29 ++++++++++++ api/tests/federation/test_actors.py | 57 ++++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index b253955c8..db71bd4fb 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -44,10 +44,12 @@ ACTIVITY_TYPES = [ OBJECT_TYPES = [ 'Article', 'Audio', + 'Collection', 'Document', 'Event', 'Image', 'Note', + 'OrderedCollection', 'Page', 'Place', 'Profile', diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index 0da78fdbe..fa1b56282 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -176,6 +176,35 @@ class LibraryActor(SystemActor): def manually_approves_followers(self): return settings.FEDERATION_MUSIC_NEEDS_APPROVAL + def handle_create(self, ac, sender): + from funkwhale_api.music.serializers import ( + AudioCollectionImportSerializer) + + library = self.get_actor_instance() + if not library.following.filter(url=sender.url).exists(): + logger.info( + 'Skipping import, we\'re not following %s', sender.url) + return + + if ac['object']['type'] != 'Collection': + return + + if ac['object']['totalItems'] <= 0: + return + + items = ac['object']['items'] + + serializer = AudioCollectionImportSerializer( + data=ac['object'], + context={'sender': sender}) + + if not serializer.is_valid(): + logger.error( + 'Cannot import audio collection: %s', serializer.errors) + return + + serializer.save() + class TestActor(SystemActor): id = 'test' diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index be24a5360..7a5e0d31b 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -10,6 +10,7 @@ from funkwhale_api.federation import actors from funkwhale_api.federation import models from funkwhale_api.federation import serializers from funkwhale_api.federation import utils +from funkwhale_api.music import models as music_models def test_actor_fetching(r_mock): @@ -368,3 +369,59 @@ def test_library_actor_handles_follow_auto_approval( accept_follow.assert_called_once_with( library_actor, data, actor ) + + +def test_library_actor_handle_create_audio_not_following(mocker, factories): + # when we receive inbox create audio, we should not do anything + # if we're not actually following the sender + mocked_create = mocker.patch( + 'funkwhale_api.music.serializers.AudioCollectionImportSerializer.create' + ) + actor = factories['federation.Actor']() + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + data = { + 'actor': actor.url, + 'type': 'Create', + 'id': 'http://test.federation/audio/create', + 'object': { + 'id': 'https://batch.import', + 'type': 'Collection', + 'totalItems': 2, + 'items': factories['federation.Audio'].create_batch(size=2) + }, + } + library_actor.system_conf.post_inbox(data, actor=actor) + + mocked_create.assert_not_called() + music_models.ImportBatch.objects.count() == 0 + + +def test_library_actor_handle_create_audio(mocker, factories): + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + follow = factories['federation.Follow'](actor=library_actor) + + data = { + 'actor': follow.target.url, + 'type': 'Create', + 'id': 'http://test.federation/audio/create', + 'object': { + 'id': 'https://batch.import', + 'type': 'Collection', + 'totalItems': 2, + 'items': factories['federation.Audio'].create_batch(size=2) + }, + } + + library_actor.system_conf.post_inbox(data, actor=follow.target) + + batch = follow.target.import_batches.latest('id') + + assert batch.federation_source == data['object']['id'] + assert batch.federation_actor == follow.target + assert batch.jobs.count() == 2 + + jobs = list(batch.jobs.order_by('id')) + for i, a in enumerate(data['object']['items']): + job = jobs[i] + assert job.federation_source == a['id'] + assert job.source == a['url']['href'] From 8db832f03bb8b3430d19b03ecb97e488d17d92b9 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Fri, 6 Apr 2018 15:20:53 +0200 Subject: [PATCH 024/101] Now store source AP track file on track_file --- .../migrations/0003_auto_20180403_1921.py | 31 ------ .../migrations/0003_auto_20180406_1319.py | 48 ++++++++ .../migrations/0004_followrequest.py | 28 ----- .../migrations/0005_actor_followers.py | 18 --- api/funkwhale_api/federation/models.py | 8 +- .../migrations/0023_auto_20180405_1830.py | 47 -------- .../migrations/0023_auto_20180406_1319.py | 104 ++++++++++++++++++ .../migrations/0024_auto_20180406_1115.py | 54 --------- api/funkwhale_api/music/models.py | 6 + api/funkwhale_api/music/tasks.py | 1 + api/tests/music/test_import.py | 4 + 11 files changed, 167 insertions(+), 182 deletions(-) delete mode 100644 api/funkwhale_api/federation/migrations/0003_auto_20180403_1921.py create mode 100644 api/funkwhale_api/federation/migrations/0003_auto_20180406_1319.py delete mode 100644 api/funkwhale_api/federation/migrations/0004_followrequest.py delete mode 100644 api/funkwhale_api/federation/migrations/0005_actor_followers.py delete mode 100644 api/funkwhale_api/music/migrations/0023_auto_20180405_1830.py create mode 100644 api/funkwhale_api/music/migrations/0023_auto_20180406_1319.py delete mode 100644 api/funkwhale_api/music/migrations/0024_auto_20180406_1115.py diff --git a/api/funkwhale_api/federation/migrations/0003_auto_20180403_1921.py b/api/funkwhale_api/federation/migrations/0003_auto_20180403_1921.py deleted file mode 100644 index aadf3257e..000000000 --- a/api/funkwhale_api/federation/migrations/0003_auto_20180403_1921.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-03 19:21 - -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('federation', '0002_auto_20180403_1620'), - ] - - operations = [ - migrations.CreateModel( - name='Follow', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('last_modification_date', models.DateTimeField(default=django.utils.timezone.now)), - ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emitted_follows', to='federation.Actor')), - ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follows', to='federation.Actor')), - ], - ), - migrations.AlterUniqueTogether( - name='follow', - unique_together={('actor', 'target')}, - ), - ] diff --git a/api/funkwhale_api/federation/migrations/0003_auto_20180406_1319.py b/api/funkwhale_api/federation/migrations/0003_auto_20180406_1319.py new file mode 100644 index 000000000..cc653b1aa --- /dev/null +++ b/api/funkwhale_api/federation/migrations/0003_auto_20180406_1319.py @@ -0,0 +1,48 @@ +# Generated by Django 2.0.3 on 2018-04-06 13:19 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('federation', '0002_auto_20180403_1620'), + ] + + operations = [ + migrations.CreateModel( + name='Follow', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)), + ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), + ('modification_date', models.DateTimeField(auto_now=True)), + ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emitted_follows', to='federation.Actor')), + ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follows', to='federation.Actor')), + ], + ), + migrations.CreateModel( + name='FollowRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)), + ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), + ('modification_date', models.DateTimeField(auto_now=True)), + ('approved', models.NullBooleanField(default=None)), + ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emmited_follow_requests', to='federation.Actor')), + ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follow_requests', to='federation.Actor')), + ], + ), + migrations.AddField( + model_name='actor', + name='followers', + field=models.ManyToManyField(related_name='following', through='federation.Follow', to='federation.Actor'), + ), + migrations.AlterUniqueTogether( + name='follow', + unique_together={('actor', 'target')}, + ), + ] diff --git a/api/funkwhale_api/federation/migrations/0004_followrequest.py b/api/funkwhale_api/federation/migrations/0004_followrequest.py deleted file mode 100644 index 6ede72747..000000000 --- a/api/funkwhale_api/federation/migrations/0004_followrequest.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-04 17:11 - -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('federation', '0003_auto_20180403_1921'), - ] - - operations = [ - migrations.CreateModel( - name='FollowRequest', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('last_modification_date', models.DateTimeField(default=django.utils.timezone.now)), - ('approved', models.NullBooleanField(default=None)), - ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emmited_follow_requests', to='federation.Actor')), - ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follow_requests', to='federation.Actor')), - ], - ), - ] diff --git a/api/funkwhale_api/federation/migrations/0005_actor_followers.py b/api/funkwhale_api/federation/migrations/0005_actor_followers.py deleted file mode 100644 index 94a1c75ac..000000000 --- a/api/funkwhale_api/federation/migrations/0005_actor_followers.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-05 16:35 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('federation', '0004_followrequest'), - ] - - operations = [ - migrations.AddField( - model_name='actor', - name='followers', - field=models.ManyToManyField(related_name='following', through='federation.Follow', to='federation.Actor'), - ), - ] diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 833b5d8f3..4bf597001 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -106,8 +106,8 @@ class Follow(models.Model): on_delete=models.CASCADE, ) creation_date = models.DateTimeField(default=timezone.now) - last_modification_date = models.DateTimeField( - default=timezone.now) + modification_date = models.DateTimeField( + auto_now=True) class Meta: unique_together = ['actor', 'target'] @@ -129,8 +129,8 @@ class FollowRequest(models.Model): on_delete=models.CASCADE, ) creation_date = models.DateTimeField(default=timezone.now) - last_modification_date = models.DateTimeField( - default=timezone.now) + modification_date = models.DateTimeField( + auto_now=True) approved = models.NullBooleanField(default=None) def approve(self): diff --git a/api/funkwhale_api/music/migrations/0023_auto_20180405_1830.py b/api/funkwhale_api/music/migrations/0023_auto_20180405_1830.py deleted file mode 100644 index 3cef1f42e..000000000 --- a/api/funkwhale_api/music/migrations/0023_auto_20180405_1830.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-05 18:30 - -from django.conf import settings -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('federation', '0005_actor_followers'), - ('music', '0022_importbatch_import_request'), - ] - - operations = [ - migrations.AddField( - model_name='importbatch', - name='federation_actor', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='import_batches', to='federation.Actor'), - ), - migrations.AddField( - model_name='importbatch', - name='federation_source', - field=models.URLField(blank=True, null=True), - ), - migrations.AddField( - model_name='importjob', - name='federation_source', - field=models.URLField(blank=True, null=True), - ), - migrations.AddField( - model_name='importjob', - name='metadata', - field=django.contrib.postgres.fields.jsonb.JSONField(default={}), - ), - migrations.AlterField( - model_name='importbatch', - name='source', - field=models.CharField(choices=[('api', 'api'), ('shell', 'shell'), ('federation', 'federation')], default='api', max_length=30), - ), - migrations.AlterField( - model_name='importbatch', - name='submitted_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='imports', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/api/funkwhale_api/music/migrations/0023_auto_20180406_1319.py b/api/funkwhale_api/music/migrations/0023_auto_20180406_1319.py new file mode 100644 index 000000000..c51a7b9fa --- /dev/null +++ b/api/funkwhale_api/music/migrations/0023_auto_20180406_1319.py @@ -0,0 +1,104 @@ +# Generated by Django 2.0.3 on 2018-04-06 13:19 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('federation', '0003_auto_20180406_1319'), + ('music', '0022_importbatch_import_request'), + ] + + operations = [ + migrations.AddField( + model_name='album', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='artist', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='importbatch', + name='federation_actor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='import_batches', to='federation.Actor'), + ), + migrations.AddField( + model_name='importbatch', + name='federation_source', + field=models.URLField(blank=True, null=True), + ), + migrations.AddField( + model_name='importbatch', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='importjob', + name='federation_source', + field=models.URLField(blank=True, null=True), + ), + migrations.AddField( + model_name='importjob', + name='metadata', + field=django.contrib.postgres.fields.jsonb.JSONField(default={}), + ), + migrations.AddField( + model_name='importjob', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='lyrics', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='track', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='trackfile', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='trackfile', + name='federation_source', + field=models.URLField(blank=True, null=True), + ), + migrations.AddField( + model_name='trackfile', + name='modification_date', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='trackfile', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AddField( + model_name='work', + name='uuid', + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + migrations.AlterField( + model_name='importbatch', + name='source', + field=models.CharField(choices=[('api', 'api'), ('shell', 'shell'), ('federation', 'federation')], default='api', max_length=30), + ), + migrations.AlterField( + model_name='importbatch', + name='submitted_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='imports', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/api/funkwhale_api/music/migrations/0024_auto_20180406_1115.py b/api/funkwhale_api/music/migrations/0024_auto_20180406_1115.py deleted file mode 100644 index 8b655a642..000000000 --- a/api/funkwhale_api/music/migrations/0024_auto_20180406_1115.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 2.0.3 on 2018-04-06 11:15 - -from django.db import migrations, models -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('music', '0023_auto_20180405_1830'), - ] - - operations = [ - migrations.AddField( - model_name='album', - name='uuid', - field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - migrations.AddField( - model_name='artist', - name='uuid', - field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - migrations.AddField( - model_name='importbatch', - name='uuid', - field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - migrations.AddField( - model_name='importjob', - name='uuid', - field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - migrations.AddField( - model_name='lyrics', - name='uuid', - field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - migrations.AddField( - model_name='track', - name='uuid', - field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - migrations.AddField( - model_name='trackfile', - name='uuid', - field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - migrations.AddField( - model_name='work', - name='uuid', - field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ] diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 5d8035f35..efffb12d9 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -414,6 +414,12 @@ class TrackFile(models.Model): Track, related_name='files', on_delete=models.CASCADE) audio_file = models.FileField(upload_to='tracks/%Y/%m/%d', max_length=255) source = models.URLField(null=True, blank=True) + creation_date = models.DateTimeField(default=timezone.now) + modification_date = models.DateTimeField(auto_now=True) + + # points to the URL of the original trackfile ActivityPub Object + federation_source = models.URLField(null=True, blank=True) + duration = models.IntegerField(null=True, blank=True) acoustid_track_id = models.UUIDField(null=True, blank=True) mimetype = models.CharField(null=True, blank=True, max_length=200) diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py index e4214d990..4f85613eb 100644 --- a/api/funkwhale_api/music/tasks.py +++ b/api/funkwhale_api/music/tasks.py @@ -102,6 +102,7 @@ def _do_import(import_job, replace, use_acoustid=True): track_file = track_file or models.TrackFile( track=track, source=import_job.source) track_file.acoustid_track_id = acoustid_track_id + track_file.federation_source = import_job.federation_source if from_file: track_file.audio_file = ContentFile(import_job.audio_file.read()) track_file.audio_file.name = import_job.audio_file.name diff --git a/api/tests/music/test_import.py b/api/tests/music/test_import.py index 87e1899d6..98174891f 100644 --- a/api/tests/music/test_import.py +++ b/api/tests/music/test_import.py @@ -62,6 +62,7 @@ def test_import_job_from_federation_no_musicbrainz(factories): tf = job.track_file assert tf.source == job.source + assert tf.federation_source == job.federation_source assert tf.track.title == 'Ping' assert tf.track.artist.name == 'Hello' assert tf.track.album.title == 'World' @@ -84,6 +85,7 @@ def test_import_job_from_federation_musicbrainz_recording(factories, mocker): tf = job.track_file assert tf.source == job.source + assert tf.federation_source == job.federation_source assert tf.track == t track_from_api.assert_called_once_with( mbid=tasks.get_mbid(job.metadata['recording'], 'recording')) @@ -105,6 +107,7 @@ def test_import_job_from_federation_musicbrainz_release(factories, mocker): job.refresh_from_db() tf = job.track_file + assert tf.federation_source == job.federation_source assert tf.source == job.source assert tf.track.title == 'Ping' assert tf.track.artist == a.artist @@ -131,6 +134,7 @@ def test_import_job_from_federation_musicbrainz_artist(factories, mocker): tf = job.track_file assert tf.source == job.source + assert tf.federation_source == job.federation_source assert tf.track.title == 'Ping' assert tf.track.artist == a assert tf.track.album.artist == a From b75872866c20f198663c73f7896f2debbe7936ac Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Fri, 6 Apr 2018 17:57:50 +0200 Subject: [PATCH 025/101] Util function to manipulate url params --- api/funkwhale_api/common/utils.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/api/funkwhale_api/common/utils.py b/api/funkwhale_api/common/utils.py index c9d450e6a..2d7641bf5 100644 --- a/api/funkwhale_api/common/utils.py +++ b/api/funkwhale_api/common/utils.py @@ -1,3 +1,4 @@ +from urllib.parse import urlencode, parse_qs, urlsplit, urlunsplit import os import shutil @@ -25,3 +26,20 @@ def on_commit(f, *args, **kwargs): return transaction.on_commit( lambda: f(*args, **kwargs) ) + + +def set_query_parameter(url, **kwargs): + """Given a URL, set or replace a query parameter and return the + modified URL. + + >>> set_query_parameter('http://example.com?foo=bar&biz=baz', 'foo', 'stuff') + 'http://example.com?foo=stuff&biz=baz' + """ + scheme, netloc, path, query_string, fragment = urlsplit(url) + query_params = parse_qs(query_string) + + for param_name, param_value in kwargs.items(): + query_params[param_name] = [param_value] + new_query_string = urlencode(query_params, doseq=True) + + return urlunsplit((scheme, netloc, path, new_query_string, fragment)) From 4ce9f9bf08a59397645943df9fd8744b2c1ae5ce Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Fri, 6 Apr 2018 17:58:16 +0200 Subject: [PATCH 026/101] Dedicated permission to access library data via activity pub --- api/funkwhale_api/federation/permissions.py | 19 +++++++++ api/tests/federation/test_permissions.py | 45 +++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 api/funkwhale_api/federation/permissions.py create mode 100644 api/tests/federation/test_permissions.py diff --git a/api/funkwhale_api/federation/permissions.py b/api/funkwhale_api/federation/permissions.py new file mode 100644 index 000000000..370328eaa --- /dev/null +++ b/api/funkwhale_api/federation/permissions.py @@ -0,0 +1,19 @@ +from django.conf import settings + +from rest_framework.permissions import BasePermission + +from . import actors + + +class LibraryFollower(BasePermission): + + def has_permission(self, request, view): + if not settings.FEDERATION_MUSIC_NEEDS_APPROVAL: + return True + + actor = getattr(request, 'actor', None) + if actor is None: + return False + + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + return library.followers.filter(url=actor.url).exists() diff --git a/api/tests/federation/test_permissions.py b/api/tests/federation/test_permissions.py new file mode 100644 index 000000000..1a6977542 --- /dev/null +++ b/api/tests/federation/test_permissions.py @@ -0,0 +1,45 @@ +from rest_framework.views import APIView + +from funkwhale_api.federation import actors +from funkwhale_api.federation import permissions + + +def test_library_follower( + factories, api_request, anonymous_user, settings): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True + view = APIView.as_view() + permission = permissions.LibraryFollower() + request = api_request.get('/') + setattr(request, 'user', anonymous_user) + check = permission.has_permission(request, view) + + assert check is False + + +def test_library_follower_actor_non_follower( + factories, api_request, anonymous_user, settings): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True + actor = factories['federation.Actor']() + view = APIView.as_view() + permission = permissions.LibraryFollower() + request = api_request.get('/') + setattr(request, 'user', anonymous_user) + setattr(request, 'actor', actor) + check = permission.has_permission(request, view) + + assert check is False + + +def test_library_follower_actor_follower( + factories, api_request, anonymous_user, settings): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + follow = factories['federation.Follow'](target=library) + view = APIView.as_view() + permission = permissions.LibraryFollower() + request = api_request.get('/') + setattr(request, 'user', anonymous_user) + setattr(request, 'actor', follow.actor) + check = permission.has_permission(request, view) + + assert check is True From 393110a7f04b920bfdb882600ddfaae1298116b5 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Fri, 6 Apr 2018 17:58:43 +0200 Subject: [PATCH 027/101] Serializers for paginated collections --- api/config/settings/common.py | 3 + api/funkwhale_api/federation/serializers.py | 66 +++++++++++++++++++ api/tests/federation/test_serializers.py | 72 +++++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 6a85a934c..e45f6c256 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -30,6 +30,9 @@ FUNKWHALE_HOSTNAME = urlsplit(FUNKWHALE_URL).netloc FEDERATION_ENABLED = env.bool('FEDERATION_ENABLED', default=True) FEDERATION_HOSTNAME = env('FEDERATION_HOSTNAME', default=FUNKWHALE_HOSTNAME) +FEDERATION_COLLECTION_PAGE_SIZE = env.int( + 'FEDERATION_COLLECTION_PAGE_SIZE', default=50 +) FEDERATION_MUSIC_NEEDS_APPROVAL = env.bool( 'FEDERATION_MUSIC_NEEDS_APPROVAL', default=True ) diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index 075e253da..05e9c7c8e 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -2,10 +2,13 @@ import urllib.parse from django.urls import reverse from django.conf import settings +from django.core.paginator import Paginator from rest_framework import serializers from dynamic_preferences.registries import global_preferences_registry +from funkwhale_api.common.utils import set_query_parameter + from . import activity from . import models from . import utils @@ -199,3 +202,66 @@ OBJECT_SERIALIZERS = { t: ObjectSerializer for t in activity.OBJECT_TYPES } + + +class PaginatedCollectionSerializer(serializers.Serializer): + + def to_representation(self, conf): + paginator = Paginator( + conf['items'], + conf.get('page_size', 20) + ) + first = set_query_parameter(conf['id'], page=1) + current = first + last = set_query_parameter(conf['id'], page=paginator.num_pages) + d = { + 'id': conf['id'], + 'actor': conf['actor'].url, + 'totalItems': paginator.count, + 'type': 'Collection', + 'current': current, + 'first': first, + 'last': last, + } + if self.context.get('include_ap_context', True): + d['@context'] = AP_CONTEXT + return d + + +class CollectionPageSerializer(serializers.Serializer): + + def to_representation(self, conf): + page = conf['page'] + first = set_query_parameter(conf['id'], page=1) + last = set_query_parameter(conf['id'], page=page.paginator.num_pages) + id = set_query_parameter(conf['id'], page=page.number) + d = { + 'id': id, + 'partOf': conf['id'], + 'actor': conf['actor'].url, + 'totalItems': page.paginator.count, + 'type': 'CollectionPage', + 'first': first, + 'last': last, + 'items': [ + conf['item_serializer']( + i, + context={ + 'actor': conf['actor'], + 'include_ap_context': False} + ).data + for i in page.object_list + ] + } + + if page.has_previous(): + d['prev'] = set_query_parameter( + conf['id'], page=page.previous_page_number()) + + if page.has_previous(): + d['next'] = set_query_parameter( + conf['id'], page=page.next_page_number()) + + if self.context.get('include_ap_context', True): + d['@context'] = AP_CONTEXT + return d diff --git a/api/tests/federation/test_serializers.py b/api/tests/federation/test_serializers.py index 6d027ec91..1e580040e 100644 --- a/api/tests/federation/test_serializers.py +++ b/api/tests/federation/test_serializers.py @@ -1,8 +1,10 @@ from django.urls import reverse +from django.core.paginator import Paginator from funkwhale_api.federation import keys from funkwhale_api.federation import models from funkwhale_api.federation import serializers +from funkwhale_api.music.serializers import AudioSerializer def test_actor_serializer_from_ap(db): @@ -163,3 +165,73 @@ def test_follow_serializer_to_ap(factories): } assert serializer.data == expected + + +def test_paginated_collection_serializer(factories): + tfs = factories['music.TrackFile'].create_batch(size=5) + actor = factories['federation.Actor'](local=True) + + conf = { + 'id': 'https://test.federation/test', + 'items': tfs, + 'item_serializer': AudioSerializer, + 'actor': actor, + 'page_size': 2, + } + expected = { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + {}, + ], + 'type': 'Collection', + 'id': conf['id'], + 'actor': actor.url, + 'totalItems': len(tfs), + 'current': conf['id'] + '?page=1', + 'last': conf['id'] + '?page=3', + 'first': conf['id'] + '?page=1', + } + + serializer = serializers.PaginatedCollectionSerializer(conf) + + assert serializer.data == expected + + +def test_collection_page_serializer(factories): + tfs = factories['music.TrackFile'].create_batch(size=5) + actor = factories['federation.Actor'](local=True) + + conf = { + 'id': 'https://test.federation/test', + 'item_serializer': AudioSerializer, + 'actor': actor, + 'page': Paginator(tfs, 2).page(2), + } + expected = { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + {}, + ], + 'type': 'CollectionPage', + 'id': conf['id'] + '?page=2', + 'actor': actor.url, + 'totalItems': len(tfs), + 'partOf': conf['id'], + 'prev': conf['id'] + '?page=1', + 'next': conf['id'] + '?page=3', + 'first': conf['id'] + '?page=1', + 'last': conf['id'] + '?page=3', + 'items': [ + conf['item_serializer']( + i, + context={'actor': actor, 'include_ap_context': False} + ).data + for i in conf['page'].object_list + ] + } + + serializer = serializers.CollectionPageSerializer(conf) + + assert serializer.data == expected From a03f0ffea54432b6c2c5c36c9df7c3098dee3b8c Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Fri, 6 Apr 2018 17:59:06 +0200 Subject: [PATCH 028/101] We now have a library browsable via activitypub --- api/funkwhale_api/federation/actors.py | 16 +++++ api/funkwhale_api/federation/urls.py | 13 +++- api/funkwhale_api/federation/views.py | 55 +++++++++++++++- api/tests/federation/test_views.py | 90 +++++++++++++++++++++++++- 4 files changed, 169 insertions(+), 5 deletions(-) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index fa1b56282..6f782ced4 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -50,6 +50,11 @@ class SystemActor(object): additional_attributes = {} manually_approves_followers = False + def serialize(self): + actor = self.get_actor_instance() + serializer = serializers.ActorSerializer() + return serializer.data + def get_actor_instance(self): args = self.get_instance_argument( self.id, @@ -172,6 +177,17 @@ class LibraryActor(SystemActor): 'manually_approves_followers': True } + def serialize(self): + data = super().serialize() + urls = data.setdefault('url', []) + urls.append({ + 'type': 'Link', + 'mediaType': 'application/activity+json', + 'name': 'library', + 'href': utils.full_url(reverse('federation:music:files-list')) + }) + return data + @property def manually_approves_followers(self): return settings.FEDERATION_MUSIC_NEEDS_APPROVAL diff --git a/api/funkwhale_api/federation/urls.py b/api/funkwhale_api/federation/urls.py index f2c6f4c78..e899869a4 100644 --- a/api/funkwhale_api/federation/urls.py +++ b/api/funkwhale_api/federation/urls.py @@ -1,8 +1,10 @@ -from rest_framework import routers +from django.conf.urls import include, url +from rest_framework import routers from . import views router = routers.SimpleRouter(trailing_slash=False) +music_router = routers.SimpleRouter(trailing_slash=False) router.register( r'federation/instance/actors', views.InstanceActorViewSet, @@ -12,4 +14,11 @@ router.register( views.WellKnownViewSet, 'well-known') -urlpatterns = router.urls +music_router.register( + r'federation/files', + views.MusicFilesViewSet, + 'files', +) +urlpatterns = router.urls + [ + url('music/', include((music_router.urls, 'music'), namespace='music')) +] diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py index 2e3feb8d0..390a371bc 100644 --- a/api/funkwhale_api/federation/views.py +++ b/api/funkwhale_api/federation/views.py @@ -1,16 +1,23 @@ from django import forms from django.conf import settings +from django.core import paginator from django.http import HttpResponse +from django.urls import reverse from rest_framework import viewsets from rest_framework import views from rest_framework import response from rest_framework.decorators import list_route, detail_route +from funkwhale_api.music.models import TrackFile +from funkwhale_api.music.serializers import AudioSerializer + from . import actors from . import authentication +from . import permissions from . import renderers from . import serializers +from . import utils from . import webfinger @@ -38,8 +45,8 @@ class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet): def retrieve(self, request, *args, **kwargs): system_actor = self.get_object() actor = system_actor.get_actor_instance() - serializer = serializers.ActorSerializer(actor) - return response.Response(serializer.data, status=200) + data = actor.system_conf.serialize() + return response.Response(data, status=200) @detail_route(methods=['get', 'post']) def inbox(self, request, *args, **kwargs): @@ -101,3 +108,47 @@ class WellKnownViewSet(FederationMixin, viewsets.GenericViewSet): username, hostname = clean_result actor = actors.SYSTEM_ACTORS[username].get_actor_instance() return serializers.ActorWebfingerSerializer(actor).data + + +class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet): + authentication_classes = [ + authentication.SignatureAuthentication] + permission_classes = [permissions.LibraryFollower] + renderer_classes = [renderers.ActivityPubRenderer] + + def list(self, request, *args, **kwargs): + page = request.GET.get('page') + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + qs = TrackFile.objects.order_by('-creation_date') + if page is None: + conf = { + 'id': utils.full_url(reverse('federation:music:files-list')), + 'page_size': settings.FEDERATION_COLLECTION_PAGE_SIZE, + 'items': qs, + 'item_serializer': AudioSerializer, + 'actor': library, + } + serializer = serializers.PaginatedCollectionSerializer(conf) + data = serializer.data + else: + try: + page_number = int(page) + except: + return response.Response( + {'page': ['Invalid page number']}, status=400) + p = paginator.Paginator( + qs, settings.FEDERATION_COLLECTION_PAGE_SIZE) + try: + page = p.page(page_number) + except paginator.EmptyPage: + return response.Response(status=404) + conf = { + 'id': utils.full_url(reverse('federation:music:files-list')), + 'page': page, + 'item_serializer': AudioSerializer, + 'actor': library, + } + serializer = serializers.CollectionPageSerializer(conf) + data = serializer.data + + return response.Response(data) diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py index 0d2ac882f..6f05a16f9 100644 --- a/api/tests/federation/test_views.py +++ b/api/tests/federation/test_views.py @@ -1,11 +1,13 @@ from django.urls import reverse +from django.core.paginator import Paginator import pytest from funkwhale_api.federation import actors from funkwhale_api.federation import serializers +from funkwhale_api.federation import utils from funkwhale_api.federation import webfinger - +from funkwhale_api.music.serializers import AudioSerializer @pytest.mark.parametrize('system_actor', actors.SYSTEM_ACTORS.keys()) @@ -62,3 +64,89 @@ def test_wellknown_webfinger_system( assert response.status_code == 200 assert response['Content-Type'] == 'application/jrd+json' assert response.data == serializer.data + + +def test_audio_file_list_requires_authenticated_actor( + db, settings, api_client): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = True + url = reverse('federation:music:files-list') + response = api_client.get(url) + + assert response.status_code == 403 + + +def test_audio_file_list_actor_no_page( + db, settings, api_client, factories): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False + settings.FEDERATION_COLLECTION_PAGE_SIZE = 2 + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + tfs = factories['music.TrackFile'].create_batch(size=5) + conf = { + 'id': utils.full_url(reverse('federation:music:files-list')), + 'page_size': 2, + 'items': list(reversed(tfs)), # we order by -creation_date + 'item_serializer': AudioSerializer, + 'actor': library + } + expected = serializers.PaginatedCollectionSerializer(conf).data + url = reverse('federation:music:files-list') + response = api_client.get(url) + + assert response.status_code == 200 + assert response.data == expected + + +def test_audio_file_list_actor_page( + db, settings, api_client, factories): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False + settings.FEDERATION_COLLECTION_PAGE_SIZE = 2 + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + tfs = factories['music.TrackFile'].create_batch(size=5) + conf = { + 'id': utils.full_url(reverse('federation:music:files-list')), + 'page': Paginator(list(reversed(tfs)), 2).page(2), + 'item_serializer': AudioSerializer, + 'actor': library + } + expected = serializers.CollectionPageSerializer(conf).data + url = reverse('federation:music:files-list') + response = api_client.get(url, data={'page': 2}) + + assert response.status_code == 200 + assert response.data == expected + + +def test_audio_file_list_actor_page_error( + db, settings, api_client, factories): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False + url = reverse('federation:music:files-list') + response = api_client.get(url, data={'page': 'nope'}) + + assert response.status_code == 400 + + +def test_audio_file_list_actor_page_error_too_far( + db, settings, api_client, factories): + settings.FEDERATION_MUSIC_NEEDS_APPROVAL = False + url = reverse('federation:music:files-list') + response = api_client.get(url, data={'page': 5000}) + + assert response.status_code == 404 + + +def test_library_actor_includes_library_link(db, settings, api_client): + actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + url = reverse( + 'federation:instance-actors-detail', + kwargs={'actor': 'library'}) + response = api_client.get(url) + expected_links = [ + { + 'type': 'Link', + 'name': 'library', + 'mediaType': 'application/activity+json', + 'href': utils.full_url(reverse('federation:music:files-list')) + } + ] + assert response.status_code == 200 + assert response.data['url'] == expected_links From f273faf9de56e0d265d2093eef847451c25f3d64 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Fri, 6 Apr 2018 18:49:29 +0200 Subject: [PATCH 029/101] Added Library model to have more granular federation management --- api/funkwhale_api/federation/actors.py | 12 +++-- api/funkwhale_api/federation/factories.py | 11 +++++ ...406_1319.py => 0003_auto_20180406_1621.py} | 17 ++++++- api/funkwhale_api/federation/models.py | 18 ++++++++ api/funkwhale_api/music/factories.py | 6 ++- ...406_1319.py => 0023_auto_20180406_1621.py} | 31 +++++++------ api/funkwhale_api/music/models.py | 22 +++++---- api/funkwhale_api/music/serializers.py | 6 +-- api/funkwhale_api/music/tasks.py | 12 +++-- api/tests/federation/test_actors.py | 46 +++++++++++++++---- api/tests/federation/test_models.py | 7 +++ api/tests/federation/test_views.py | 2 + api/tests/music/test_import.py | 12 +++-- api/tests/music/test_serializers.py | 10 ++-- 14 files changed, 159 insertions(+), 53 deletions(-) rename api/funkwhale_api/federation/migrations/{0003_auto_20180406_1319.py => 0003_auto_20180406_1621.py} (69%) rename api/funkwhale_api/music/migrations/{0023_auto_20180406_1319.py => 0023_auto_20180406_1621.py} (86%) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index 6f782ced4..205c3486d 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -52,7 +52,7 @@ class SystemActor(object): def serialize(self): actor = self.get_actor_instance() - serializer = serializers.ActorSerializer() + serializer = serializers.ActorSerializer(actor) return serializer.data def get_actor_instance(self): @@ -196,8 +196,12 @@ class LibraryActor(SystemActor): from funkwhale_api.music.serializers import ( AudioCollectionImportSerializer) - library = self.get_actor_instance() - if not library.following.filter(url=sender.url).exists(): + try: + remote_library = models.Library.objects.get( + actor=sender, + federation_enabled=True, + ) + except models.Library.DoesNotExist: logger.info( 'Skipping import, we\'re not following %s', sender.url) return @@ -212,7 +216,7 @@ class LibraryActor(SystemActor): serializer = AudioCollectionImportSerializer( data=ac['object'], - context={'sender': sender}) + context={'library': remote_library}) if not serializer.is_valid(): logger.error( diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py index 63d40aff8..fc8932396 100644 --- a/api/funkwhale_api/federation/factories.py +++ b/api/funkwhale_api/federation/factories.py @@ -122,6 +122,17 @@ class FollowRequestFactory(factory.DjangoModelFactory): model = models.FollowRequest +@registry.register +class LibraryFactory(factory.DjangoModelFactory): + actor = factory.SubFactory(ActorFactory) + url = factory.Faker('url') + federation_enabled = True + download_files = False + + class Meta: + model = models.Library + + @registry.register(name='federation.Note') class NoteFactory(factory.Factory): type = 'Note' diff --git a/api/funkwhale_api/federation/migrations/0003_auto_20180406_1319.py b/api/funkwhale_api/federation/migrations/0003_auto_20180406_1621.py similarity index 69% rename from api/funkwhale_api/federation/migrations/0003_auto_20180406_1319.py rename to api/funkwhale_api/federation/migrations/0003_auto_20180406_1621.py index cc653b1aa..f8771752e 100644 --- a/api/funkwhale_api/federation/migrations/0003_auto_20180406_1319.py +++ b/api/funkwhale_api/federation/migrations/0003_auto_20180406_1621.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.3 on 2018-04-06 13:19 +# Generated by Django 2.0.3 on 2018-04-06 16:21 from django.db import migrations, models import django.db.models.deletion @@ -36,6 +36,21 @@ class Migration(migrations.Migration): ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follow_requests', to='federation.Actor')), ], ), + migrations.CreateModel( + name='Library', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), + ('modification_date', models.DateTimeField(auto_now=True)), + ('fetched_date', models.DateTimeField(blank=True, null=True)), + ('uuid', models.UUIDField(default=uuid.uuid4)), + ('url', models.URLField()), + ('federation_enabled', models.BooleanField()), + ('download_files', models.BooleanField()), + ('files_count', models.PositiveIntegerField(blank=True, null=True)), + ('actor', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='library', to='federation.Actor')), + ], + ), migrations.AddField( model_name='actor', name='followers', diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 4bf597001..8a90bdb76 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -157,3 +157,21 @@ class FollowRequest(models.Model): def refuse(self): self.approved = False self.save(update_fields=['approved']) + + +class Library(models.Model): + creation_date = models.DateTimeField(default=timezone.now) + modification_date = models.DateTimeField( + auto_now=True) + fetched_date = models.DateTimeField(null=True, blank=True) + actor = models.OneToOneField( + Actor, + on_delete=models.CASCADE, + related_name='library') + uuid = models.UUIDField(default=uuid.uuid4) + url = models.URLField() + # use this flag to disable federation with a library + federation_enabled = models.BooleanField() + # should we mirror files locally or hotlink them? + download_files = models.BooleanField() + files_count = models.PositiveIntegerField(null=True, blank=True) diff --git a/api/funkwhale_api/music/factories.py b/api/funkwhale_api/music/factories.py index 27387ca9f..8da56b2a9 100644 --- a/api/funkwhale_api/music/factories.py +++ b/api/funkwhale_api/music/factories.py @@ -5,6 +5,7 @@ from funkwhale_api.factories import registry, ManyToManyFromList from funkwhale_api.federation.factories import ( AudioMetadataFactory, ActorFactory, + LibraryFactory, ) from funkwhale_api.users.factories import UserFactory @@ -68,7 +69,8 @@ class ImportBatchFactory(factory.django.DjangoModelFactory): class Params: federation = factory.Trait( submitted_by=None, - federation_actor=factory.SubFactory(ActorFactory), + source_library=factory.SubFactory(LibraryFactory), + source_library_url=factory.Faker('url'), source='federation', ) @@ -85,7 +87,7 @@ class ImportJobFactory(factory.django.DjangoModelFactory): class Params: federation = factory.Trait( batch=factory.SubFactory(ImportBatchFactory, federation=True), - federation_source=factory.Faker('url'), + source_library_url=factory.Faker('url'), metadata=factory.SubFactory(AudioMetadataFactory), ) diff --git a/api/funkwhale_api/music/migrations/0023_auto_20180406_1319.py b/api/funkwhale_api/music/migrations/0023_auto_20180406_1621.py similarity index 86% rename from api/funkwhale_api/music/migrations/0023_auto_20180406_1319.py rename to api/funkwhale_api/music/migrations/0023_auto_20180406_1621.py index c51a7b9fa..2bc8085a0 100644 --- a/api/funkwhale_api/music/migrations/0023_auto_20180406_1319.py +++ b/api/funkwhale_api/music/migrations/0023_auto_20180406_1621.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.3 on 2018-04-06 13:19 +# Generated by Django 2.0.3 on 2018-04-06 16:21 from django.conf import settings import django.contrib.postgres.fields.jsonb @@ -11,7 +11,7 @@ import uuid class Migration(migrations.Migration): dependencies = [ - ('federation', '0003_auto_20180406_1319'), + ('federation', '0003_auto_20180406_1621'), ('music', '0022_importbatch_import_request'), ] @@ -28,12 +28,12 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='importbatch', - name='federation_actor', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='import_batches', to='federation.Actor'), + name='source_library', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='import_batches', to='federation.Library'), ), migrations.AddField( model_name='importbatch', - name='federation_source', + name='source_library_url', field=models.URLField(blank=True, null=True), ), migrations.AddField( @@ -43,13 +43,13 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='importjob', - name='federation_source', - field=models.URLField(blank=True, null=True), + name='metadata', + field=django.contrib.postgres.fields.jsonb.JSONField(default={}), ), migrations.AddField( model_name='importjob', - name='metadata', - field=django.contrib.postgres.fields.jsonb.JSONField(default={}), + name='source_library_url', + field=models.URLField(blank=True, null=True), ), migrations.AddField( model_name='importjob', @@ -73,13 +73,18 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='trackfile', - name='federation_source', - field=models.URLField(blank=True, null=True), + name='modification_date', + field=models.DateTimeField(auto_now=True), ), migrations.AddField( model_name='trackfile', - name='modification_date', - field=models.DateTimeField(auto_now=True), + name='source_library', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='track_files', to='federation.Library'), + ), + migrations.AddField( + model_name='trackfile', + name='source_library_url', + field=models.URLField(blank=True, null=True), ), migrations.AddField( model_name='trackfile', diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index efffb12d9..4c0b6a098 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -417,8 +417,14 @@ class TrackFile(models.Model): creation_date = models.DateTimeField(default=timezone.now) modification_date = models.DateTimeField(auto_now=True) + source_library = models.ForeignKey( + 'federation.Library', + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='track_files') # points to the URL of the original trackfile ActivityPub Object - federation_source = models.URLField(null=True, blank=True) + source_library_url = models.URLField(null=True, blank=True) duration = models.IntegerField(null=True, blank=True) acoustid_track_id = models.UUIDField(null=True, blank=True) @@ -470,6 +476,7 @@ IMPORT_STATUS_CHOICES = ( ('skipped', 'Skipped'), ) + class ImportBatch(models.Model): uuid = models.UUIDField( unique=True, db_index=True, default=uuid.uuid4) @@ -496,14 +503,13 @@ class ImportBatch(models.Model): blank=True, on_delete=models.CASCADE) - federation_source = models.URLField(null=True, blank=True) - federation_actor = models.ForeignKey( - 'federation.Actor', - on_delete=models.SET_NULL, + source_library = models.ForeignKey( + 'federation.Library', null=True, blank=True, - related_name='import_batches', - ) + on_delete=models.SET_NULL, + related_name='import_batches') + source_library_url = models.URLField(null=True, blank=True) class Meta: ordering = ['-creation_date'] @@ -534,7 +540,7 @@ class ImportJob(models.Model): choices=IMPORT_STATUS_CHOICES, default='pending', max_length=30) audio_file = models.FileField( upload_to='imports/%Y/%m/%d', max_length=255, null=True, blank=True) - federation_source = models.URLField(null=True, blank=True) + source_library_url = models.URLField(null=True, blank=True) metadata = JSONField(default={}) class Meta: diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 5cd2f2cc2..78b2f7c04 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -210,7 +210,7 @@ class AudioSerializer(serializers.Serializer): return models.ImportJob.objects.create( batch=batch, source=validated_data['url']['href'], - federation_source=validated_data['id'], + source_library_url=validated_data['id'], metadata=metadata, ) @@ -248,8 +248,8 @@ class AudioCollectionImportSerializer(serializers.Serializer): @transaction.atomic def create(self, validated_data): batch = models.ImportBatch.objects.create( - federation_actor=self.context['sender'], - federation_source=validated_data['id'], + source_library=self.context['library'], + source_library_url=validated_data['id'], source='federation', ) for i in validated_data['items']: diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py index 4f85613eb..67b538c1e 100644 --- a/api/funkwhale_api/music/tasks.py +++ b/api/funkwhale_api/music/tasks.py @@ -102,14 +102,18 @@ def _do_import(import_job, replace, use_acoustid=True): track_file = track_file or models.TrackFile( track=track, source=import_job.source) track_file.acoustid_track_id = acoustid_track_id - track_file.federation_source = import_job.federation_source + track_file.source_library = import_job.batch.source_library + track_file.source_library_url = import_job.source_library_url if from_file: track_file.audio_file = ContentFile(import_job.audio_file.read()) track_file.audio_file.name = import_job.audio_file.name track_file.duration = duration - elif import_job.federation_source: - # no downloading, we hotlink - pass + elif import_job.source_library_url: + if track_file.source_library.download_files: + raise NotImplementedError() + else: + # no downloading, we hotlink + pass else: track_file.download_file() track_file.save() diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index 7a5e0d31b..6f5e88663 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -371,9 +371,9 @@ def test_library_actor_handles_follow_auto_approval( ) -def test_library_actor_handle_create_audio_not_following(mocker, factories): +def test_library_actor_handle_create_audio_no_library(mocker, factories): # when we receive inbox create audio, we should not do anything - # if we're not actually following the sender + # if we don't have a configured library matching the sender mocked_create = mocker.patch( 'funkwhale_api.music.serializers.AudioCollectionImportSerializer.create' ) @@ -396,12 +396,40 @@ def test_library_actor_handle_create_audio_not_following(mocker, factories): music_models.ImportBatch.objects.count() == 0 +def test_library_actor_handle_create_audio_no_library_enabled( + mocker, factories): + # when we receive inbox create audio, we should not do anything + # if we don't have an enabled library + mocked_create = mocker.patch( + 'funkwhale_api.music.serializers.AudioCollectionImportSerializer.create' + ) + disabled_library = factories['federation.Library']( + federation_enabled=False) + actor = disabled_library.actor + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + data = { + 'actor': actor.url, + 'type': 'Create', + 'id': 'http://test.federation/audio/create', + 'object': { + 'id': 'https://batch.import', + 'type': 'Collection', + 'totalItems': 2, + 'items': factories['federation.Audio'].create_batch(size=2) + }, + } + library_actor.system_conf.post_inbox(data, actor=actor) + + mocked_create.assert_not_called() + music_models.ImportBatch.objects.count() == 0 + + def test_library_actor_handle_create_audio(mocker, factories): library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - follow = factories['federation.Follow'](actor=library_actor) + remote_library = factories['federation.Library']() data = { - 'actor': follow.target.url, + 'actor': remote_library.actor.url, 'type': 'Create', 'id': 'http://test.federation/audio/create', 'object': { @@ -412,16 +440,16 @@ def test_library_actor_handle_create_audio(mocker, factories): }, } - library_actor.system_conf.post_inbox(data, actor=follow.target) + library_actor.system_conf.post_inbox(data, actor=remote_library.actor) - batch = follow.target.import_batches.latest('id') + batch = remote_library.import_batches.latest('id') - assert batch.federation_source == data['object']['id'] - assert batch.federation_actor == follow.target + assert batch.source_library_url == data['object']['id'] + assert batch.source_library == remote_library assert batch.jobs.count() == 2 jobs = list(batch.jobs.order_by('id')) for i, a in enumerate(data['object']['items']): job = jobs[i] - assert job.federation_source == a['id'] + assert job.source_library_url == a['id'] assert job.source == a['url']['href'] diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py index 86e4f4a84..b17b6eb65 100644 --- a/api/tests/federation/test_models.py +++ b/api/tests/federation/test_models.py @@ -26,6 +26,7 @@ def test_cannot_duplicate_follow(factories): actor=follow.actor, ) + def test_follow_federation_url(factories): follow = factories['federation.Follow'](local=True) expected = '{}#follows/{}'.format( @@ -76,3 +77,9 @@ def test_follow_request_refused(mocker, factories): assert fr.approved is False assert fr.target.followers.count() == 0 + + +def test_library_model_unique_per_actor(factories): + library = factories['federation.Library']() + with pytest.raises(db.IntegrityError): + factories['federation.Library'](actor=library.actor) diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py index 6f05a16f9..c7e1fc012 100644 --- a/api/tests/federation/test_views.py +++ b/api/tests/federation/test_views.py @@ -19,6 +19,8 @@ def test_instance_actors(system_actor, db, settings, api_client): response = api_client.get(url) serializer = serializers.ActorSerializer(actor) + if system_actor == 'library': + response.data.pop('url') assert response.status_code == 200 assert response.data == serializer.data diff --git a/api/tests/music/test_import.py b/api/tests/music/test_import.py index 98174891f..7e3ebcc19 100644 --- a/api/tests/music/test_import.py +++ b/api/tests/music/test_import.py @@ -62,7 +62,8 @@ def test_import_job_from_federation_no_musicbrainz(factories): tf = job.track_file assert tf.source == job.source - assert tf.federation_source == job.federation_source + assert tf.source_library == job.batch.source_library + assert tf.source_library_url == job.source_library_url assert tf.track.title == 'Ping' assert tf.track.artist.name == 'Hello' assert tf.track.album.title == 'World' @@ -85,7 +86,8 @@ def test_import_job_from_federation_musicbrainz_recording(factories, mocker): tf = job.track_file assert tf.source == job.source - assert tf.federation_source == job.federation_source + assert tf.source_library == job.batch.source_library + assert tf.source_library_url == job.source_library_url assert tf.track == t track_from_api.assert_called_once_with( mbid=tasks.get_mbid(job.metadata['recording'], 'recording')) @@ -107,7 +109,8 @@ def test_import_job_from_federation_musicbrainz_release(factories, mocker): job.refresh_from_db() tf = job.track_file - assert tf.federation_source == job.federation_source + assert tf.source_library == job.batch.source_library + assert tf.source_library_url == job.source_library_url assert tf.source == job.source assert tf.track.title == 'Ping' assert tf.track.artist == a.artist @@ -134,7 +137,8 @@ def test_import_job_from_federation_musicbrainz_artist(factories, mocker): tf = job.track_file assert tf.source == job.source - assert tf.federation_source == job.federation_source + assert tf.source_library == job.batch.source_library + assert tf.source_library_url == job.source_library_url assert tf.track.title == 'Ping' assert tf.track.artist == a assert tf.track.album.artist == a diff --git a/api/tests/music/test_serializers.py b/api/tests/music/test_serializers.py index 1270ae765..a1f5999e7 100644 --- a/api/tests/music/test_serializers.py +++ b/api/tests/music/test_serializers.py @@ -5,7 +5,7 @@ from funkwhale_api.music import serializers def test_activity_pub_audio_collection_serializer_to_import(factories): - sender = factories['federation.Actor']() + remote_library = factories['federation.Library']() collection = { 'id': 'https://batch.import', @@ -15,7 +15,7 @@ def test_activity_pub_audio_collection_serializer_to_import(factories): } serializer = serializers.AudioCollectionImportSerializer( - data=collection, context={'sender': sender}) + data=collection, context={'library': remote_library}) assert serializer.is_valid(raise_exception=True) @@ -23,13 +23,13 @@ def test_activity_pub_audio_collection_serializer_to_import(factories): jobs = list(batch.jobs.all()) assert batch.source == 'federation' - assert batch.federation_source == collection['id'] - assert batch.federation_actor == sender + assert batch.source_library_url == collection['id'] + assert batch.source_library == remote_library assert len(jobs) == 2 for i, a in enumerate(collection['items']): job = jobs[i] - assert job.federation_source == a['id'] + assert job.source_library_url == a['id'] assert job.source == a['url']['href'] a['metadata']['mediaType'] = a['url']['mediaType'] assert job.metadata == a['metadata'] From b29ca4479729153201dbbaff755c84523330db74 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 7 Apr 2018 11:29:40 +0200 Subject: [PATCH 030/101] Now store remote library tracks in a dedicated model, this is much simpler --- api/funkwhale_api/federation/actors.py | 35 +++-- api/funkwhale_api/federation/factories.py | 64 +++++++- ...406_1621.py => 0003_auto_20180407_0852.py} | 26 +++- api/funkwhale_api/federation/models.py | 30 +++- api/funkwhale_api/federation/serializers.py | 147 ++++++++++++++++++ api/funkwhale_api/federation/urls.py | 4 +- api/funkwhale_api/federation/views.py | 24 +-- api/funkwhale_api/music/factories.py | 10 +- ...406_1621.py => 0023_auto_20180407_0852.py} | 34 +--- api/funkwhale_api/music/models.py | 29 +--- api/funkwhale_api/music/serializers.py | 124 --------------- api/funkwhale_api/music/tasks.py | 68 ++++---- api/tests/federation/test_actors.py | 35 +++-- api/tests/federation/test_serializers.py | 146 ++++++++++++++++- api/tests/federation/test_views.py | 5 +- api/tests/music/test_import.py | 74 +++++---- api/tests/music/test_serializers.py | 94 ----------- 17 files changed, 555 insertions(+), 394 deletions(-) rename api/funkwhale_api/federation/migrations/{0003_auto_20180406_1621.py => 0003_auto_20180407_0852.py} (65%) rename api/funkwhale_api/music/migrations/{0023_auto_20180406_1621.py => 0023_auto_20180407_0852.py} (68%) delete mode 100644 api/tests/music/test_serializers.py diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index 205c3486d..ffbafd8b7 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -4,6 +4,7 @@ import uuid import xml from django.conf import settings +from django.db import transaction from django.urls import reverse from django.utils import timezone @@ -192,10 +193,8 @@ class LibraryActor(SystemActor): def manually_approves_followers(self): return settings.FEDERATION_MUSIC_NEEDS_APPROVAL + @transaction.atomic def handle_create(self, ac, sender): - from funkwhale_api.music.serializers import ( - AudioCollectionImportSerializer) - try: remote_library = models.Library.objects.get( actor=sender, @@ -212,18 +211,28 @@ class LibraryActor(SystemActor): if ac['object']['totalItems'] <= 0: return - items = ac['object']['items'] - - serializer = AudioCollectionImportSerializer( - data=ac['object'], - context={'library': remote_library}) - - if not serializer.is_valid(): - logger.error( - 'Cannot import audio collection: %s', serializer.errors) + try: + items = ac['object']['items'] + except KeyError: + logger.warning('No items in collection!') return - serializer.save() + item_serializers = [ + serializers.AudioSerializer( + data=i, context={'library': remote_library}) + for i in items + ] + + valid_serializers = [] + for s in item_serializers: + if s.is_valid(): + valid_serializers.append(s) + else: + logger.debug( + 'Skipping invalid item %s, %s', s.initial_data, s.errors) + + for s in valid_serializers: + s.save() class TestActor(SystemActor): diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py index fc8932396..b3ac72039 100644 --- a/api/funkwhale_api/federation/factories.py +++ b/api/funkwhale_api/federation/factories.py @@ -128,11 +128,73 @@ class LibraryFactory(factory.DjangoModelFactory): url = factory.Faker('url') federation_enabled = True download_files = False + autoimport = False class Meta: model = models.Library +class ArtistMetadataFactory(factory.Factory): + name = factory.Faker('name') + + class Meta: + model = dict + + class Params: + musicbrainz = factory.Trait( + musicbrainz_id=factory.Faker('uuid4') + ) + + +class ReleaseMetadataFactory(factory.Factory): + title = factory.Faker('sentence') + + class Meta: + model = dict + + class Params: + musicbrainz = factory.Trait( + musicbrainz_id=factory.Faker('uuid4') + ) + + +class RecordingMetadataFactory(factory.Factory): + title = factory.Faker('sentence') + + class Meta: + model = dict + + class Params: + musicbrainz = factory.Trait( + musicbrainz_id=factory.Faker('uuid4') + ) + + +@registry.register(name='federation.LibraryTrackMetadata') +class LibraryTrackMetadataFactory(factory.Factory): + artist = factory.SubFactory(ArtistMetadataFactory) + recording = factory.SubFactory(RecordingMetadataFactory) + release = factory.SubFactory(ReleaseMetadataFactory) + + class Meta: + model = dict + + +@registry.register +class LibraryTrackFactory(factory.DjangoModelFactory): + library = factory.SubFactory(LibraryFactory) + url = factory.Faker('url') + title = factory.Faker('sentence') + artist_name = factory.Faker('sentence') + album_title = factory.Faker('sentence') + audio_url = factory.Faker('url') + audio_mimetype = 'audio/ogg' + metadata = factory.SubFactory(LibraryTrackMetadataFactory) + + class Meta: + model = models.LibraryTrack + + @registry.register(name='federation.Note') class NoteFactory(factory.Factory): type = 'Note' @@ -189,7 +251,7 @@ class AudioFactory(factory.Factory): ) actor = factory.Faker('url') url = factory.SubFactory(LinkFactory, audio=True) - metadata = factory.SubFactory(AudioMetadataFactory) + metadata = factory.SubFactory(LibraryTrackMetadataFactory) class Meta: model = dict diff --git a/api/funkwhale_api/federation/migrations/0003_auto_20180406_1621.py b/api/funkwhale_api/federation/migrations/0003_auto_20180407_0852.py similarity index 65% rename from api/funkwhale_api/federation/migrations/0003_auto_20180406_1621.py rename to api/funkwhale_api/federation/migrations/0003_auto_20180407_0852.py index f8771752e..afc7ea083 100644 --- a/api/funkwhale_api/federation/migrations/0003_auto_20180406_1621.py +++ b/api/funkwhale_api/federation/migrations/0003_auto_20180407_0852.py @@ -1,5 +1,6 @@ -# Generated by Django 2.0.3 on 2018-04-06 16:21 +# Generated by Django 2.0.3 on 2018-04-07 08:52 +import django.contrib.postgres.fields.jsonb from django.db import migrations, models import django.db.models.deletion import django.utils.timezone @@ -9,6 +10,7 @@ import uuid class Migration(migrations.Migration): dependencies = [ + ('music', '0022_importbatch_import_request'), ('federation', '0002_auto_20180403_1620'), ] @@ -47,10 +49,30 @@ class Migration(migrations.Migration): ('url', models.URLField()), ('federation_enabled', models.BooleanField()), ('download_files', models.BooleanField()), - ('files_count', models.PositiveIntegerField(blank=True, null=True)), + ('autoimport', models.BooleanField()), + ('tracks_count', models.PositiveIntegerField(blank=True, null=True)), ('actor', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='library', to='federation.Actor')), ], ), + migrations.CreateModel( + name='LibraryTrack', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('url', models.URLField(unique=True)), + ('audio_url', models.URLField()), + ('audio_mimetype', models.CharField(max_length=200)), + ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), + ('modification_date', models.DateTimeField(auto_now=True)), + ('fetched_date', models.DateTimeField(blank=True, null=True)), + ('published_date', models.DateTimeField(blank=True, null=True)), + ('artist_name', models.CharField(max_length=500)), + ('album_title', models.CharField(max_length=500)), + ('title', models.CharField(max_length=500)), + ('metadata', django.contrib.postgres.fields.jsonb.JSONField(default={}, max_length=10000)), + ('library', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracks', to='federation.Library')), + ('local_track_file', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='library_track', to='music.TrackFile')), + ], + ), migrations.AddField( model_name='actor', name='followers', diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 8a90bdb76..91f2ea973 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -1,6 +1,7 @@ import uuid from django.conf import settings +from django.contrib.postgres.fields import JSONField from django.db import models from django.utils import timezone @@ -170,8 +171,35 @@ class Library(models.Model): related_name='library') uuid = models.UUIDField(default=uuid.uuid4) url = models.URLField() + # use this flag to disable federation with a library federation_enabled = models.BooleanField() # should we mirror files locally or hotlink them? download_files = models.BooleanField() - files_count = models.PositiveIntegerField(null=True, blank=True) + # should we automatically import new files from this library? + autoimport = models.BooleanField() + tracks_count = models.PositiveIntegerField(null=True, blank=True) + + +class LibraryTrack(models.Model): + url = models.URLField(unique=True) + audio_url = models.URLField() + audio_mimetype = models.CharField(max_length=200) + creation_date = models.DateTimeField(default=timezone.now) + modification_date = models.DateTimeField( + auto_now=True) + fetched_date = models.DateTimeField(null=True, blank=True) + published_date = models.DateTimeField(null=True, blank=True) + library = models.ForeignKey( + Library, related_name='tracks', on_delete=models.CASCADE) + local_track_file = models.OneToOneField( + 'music.TrackFile', + related_name='library_track', + on_delete=models.CASCADE, + null=True, + blank=True, + ) + artist_name = models.CharField(max_length=500) + album_title = models.CharField(max_length=500) + title = models.CharField(max_length=500) + metadata = JSONField(default={}, max_length=10000) diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index 05e9c7c8e..17541c50f 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -3,6 +3,7 @@ import urllib.parse from django.urls import reverse from django.conf import settings from django.core.paginator import Paginator +from django.db import transaction from rest_framework import serializers from dynamic_preferences.registries import global_preferences_registry @@ -265,3 +266,149 @@ class CollectionPageSerializer(serializers.Serializer): if self.context.get('include_ap_context', True): d['@context'] = AP_CONTEXT return d + + +class ArtistMetadataSerializer(serializers.Serializer): + musicbrainz_id = serializers.UUIDField(required=False) + name = serializers.CharField() + + +class ReleaseMetadataSerializer(serializers.Serializer): + musicbrainz_id = serializers.UUIDField(required=False) + title = serializers.CharField() + + +class RecordingMetadataSerializer(serializers.Serializer): + musicbrainz_id = serializers.UUIDField(required=False) + title = serializers.CharField() + + +class AudioMetadataSerializer(serializers.Serializer): + artist = ArtistMetadataSerializer() + release = ReleaseMetadataSerializer() + recording = RecordingMetadataSerializer() + + +class AudioSerializer(serializers.Serializer): + type = serializers.CharField() + id = serializers.URLField() + url = serializers.JSONField() + published = serializers.DateTimeField() + updated = serializers.DateTimeField(required=False) + metadata = AudioMetadataSerializer() + + def validate_type(self, v): + if v != 'Audio': + raise serializers.ValidationError('Invalid type for audio') + return v + + def validate_url(self, v): + try: + url = v['href'] + except (KeyError, TypeError): + raise serializers.ValidationError('Missing href') + + try: + media_type = v['mediaType'] + except (KeyError, TypeError): + raise serializers.ValidationError('Missing mediaType') + + if not media_type.startswith('audio/'): + raise serializers.ValidationError('Invalid mediaType') + + return url + + def validate_url(self, v): + try: + url = v['href'] + except (KeyError, TypeError): + raise serializers.ValidationError('Missing href') + + try: + media_type = v['mediaType'] + except (KeyError, TypeError): + raise serializers.ValidationError('Missing mediaType') + + if not media_type.startswith('audio/'): + raise serializers.ValidationError('Invalid mediaType') + + return v + + def create(self, validated_data): + defaults = { + 'audio_mimetype': validated_data['url']['mediaType'], + 'audio_url': validated_data['url']['href'], + 'metadata': validated_data['metadata'], + 'artist_name': validated_data['metadata']['artist']['name'], + 'album_title': validated_data['metadata']['release']['title'], + 'title': validated_data['metadata']['recording']['title'], + 'published_date': validated_data['published'], + 'modification_date': validated_data.get('updated'), + } + return models.LibraryTrack.objects.get_or_create( + library=self.context['library'], + url=validated_data['id'], + defaults=defaults + )[0] + + def to_representation(self, instance): + track = instance.track + album = instance.track.album + artist = instance.track.artist + + d = { + 'type': 'Audio', + 'id': instance.get_federation_url(), + 'name': instance.track.full_name, + 'published': instance.creation_date.isoformat(), + 'updated': instance.modification_date.isoformat(), + 'metadata': { + 'artist': { + 'musicbrainz_id': str(artist.mbid) if artist.mbid else None, + 'name': artist.name, + }, + 'release': { + 'musicbrainz_id': str(album.mbid) if album.mbid else None, + 'title': album.title, + }, + 'recording': { + 'musicbrainz_id': str(track.mbid) if track.mbid else None, + 'title': track.title, + }, + }, + 'url': { + 'href': utils.full_url(instance.path), + 'type': 'Link', + 'mediaType': instance.mimetype + }, + 'attributedTo': [ + self.context['actor'].url + ] + } + if self.context.get('include_ap_context', True): + d['@context'] = AP_CONTEXT + return d + + +class CollectionSerializer(serializers.Serializer): + + def to_representation(self, conf): + d = { + 'id': conf['id'], + 'actor': conf['actor'].url, + 'totalItems': len(conf['items']), + 'type': 'Collection', + 'items': [ + conf['item_serializer']( + i, + context={ + 'actor': conf['actor'], + 'include_ap_context': False} + ).data + for i in conf['items'] + ] + } + + if self.context.get('include_ap_context', True): + d['@context'] = AP_CONTEXT + return d diff --git a/api/funkwhale_api/federation/urls.py b/api/funkwhale_api/federation/urls.py index e899869a4..2c24b5257 100644 --- a/api/funkwhale_api/federation/urls.py +++ b/api/funkwhale_api/federation/urls.py @@ -15,10 +15,10 @@ router.register( 'well-known') music_router.register( - r'federation/files', + r'files', views.MusicFilesViewSet, 'files', ) urlpatterns = router.urls + [ - url('music/', include((music_router.urls, 'music'), namespace='music')) + url('federation/music/', include((music_router.urls, 'music'), namespace='music')) ] diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py index 390a371bc..35d8a75a5 100644 --- a/api/funkwhale_api/federation/views.py +++ b/api/funkwhale_api/federation/views.py @@ -10,7 +10,6 @@ from rest_framework import response from rest_framework.decorators import list_route, detail_route from funkwhale_api.music.models import TrackFile -from funkwhale_api.music.serializers import AudioSerializer from . import actors from . import authentication @@ -119,13 +118,16 @@ class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet): def list(self, request, *args, **kwargs): page = request.GET.get('page') library = actors.SYSTEM_ACTORS['library'].get_actor_instance() - qs = TrackFile.objects.order_by('-creation_date') + qs = TrackFile.objects.order_by('-creation_date').select_related( + 'track__artist', + 'track__album__artist' + ) if page is None: conf = { 'id': utils.full_url(reverse('federation:music:files-list')), 'page_size': settings.FEDERATION_COLLECTION_PAGE_SIZE, 'items': qs, - 'item_serializer': AudioSerializer, + 'item_serializer': serializers.AudioSerializer, 'actor': library, } serializer = serializers.PaginatedCollectionSerializer(conf) @@ -140,15 +142,15 @@ class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet): qs, settings.FEDERATION_COLLECTION_PAGE_SIZE) try: page = p.page(page_number) + conf = { + 'id': utils.full_url(reverse('federation:music:files-list')), + 'page': page, + 'item_serializer': serializers.AudioSerializer, + 'actor': library, + } + serializer = serializers.CollectionPageSerializer(conf) + data = serializer.data except paginator.EmptyPage: return response.Response(status=404) - conf = { - 'id': utils.full_url(reverse('federation:music:files-list')), - 'page': page, - 'item_serializer': AudioSerializer, - 'actor': library, - } - serializer = serializers.CollectionPageSerializer(conf) - data = serializer.data return response.Response(data) diff --git a/api/funkwhale_api/music/factories.py b/api/funkwhale_api/music/factories.py index 8da56b2a9..83aad432a 100644 --- a/api/funkwhale_api/music/factories.py +++ b/api/funkwhale_api/music/factories.py @@ -3,9 +3,7 @@ import os from funkwhale_api.factories import registry, ManyToManyFromList from funkwhale_api.federation.factories import ( - AudioMetadataFactory, - ActorFactory, - LibraryFactory, + LibraryTrackFactory, ) from funkwhale_api.users.factories import UserFactory @@ -69,8 +67,6 @@ class ImportBatchFactory(factory.django.DjangoModelFactory): class Params: federation = factory.Trait( submitted_by=None, - source_library=factory.SubFactory(LibraryFactory), - source_library_url=factory.Faker('url'), source='federation', ) @@ -86,9 +82,9 @@ class ImportJobFactory(factory.django.DjangoModelFactory): class Params: federation = factory.Trait( + mbid=None, + library_track=factory.SubFactory(LibraryTrackFactory), batch=factory.SubFactory(ImportBatchFactory, federation=True), - source_library_url=factory.Faker('url'), - metadata=factory.SubFactory(AudioMetadataFactory), ) diff --git a/api/funkwhale_api/music/migrations/0023_auto_20180406_1621.py b/api/funkwhale_api/music/migrations/0023_auto_20180407_0852.py similarity index 68% rename from api/funkwhale_api/music/migrations/0023_auto_20180406_1621.py rename to api/funkwhale_api/music/migrations/0023_auto_20180407_0852.py index 2bc8085a0..b1bbeacef 100644 --- a/api/funkwhale_api/music/migrations/0023_auto_20180406_1621.py +++ b/api/funkwhale_api/music/migrations/0023_auto_20180407_0852.py @@ -1,7 +1,6 @@ -# Generated by Django 2.0.3 on 2018-04-06 16:21 +# Generated by Django 2.0.3 on 2018-04-07 08:52 from django.conf import settings -import django.contrib.postgres.fields.jsonb from django.db import migrations, models import django.db.models.deletion import django.utils.timezone @@ -11,7 +10,7 @@ import uuid class Migration(migrations.Migration): dependencies = [ - ('federation', '0003_auto_20180406_1621'), + ('federation', '0003_auto_20180407_0852'), ('music', '0022_importbatch_import_request'), ] @@ -26,16 +25,6 @@ class Migration(migrations.Migration): name='uuid', field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), ), - migrations.AddField( - model_name='importbatch', - name='source_library', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='import_batches', to='federation.Library'), - ), - migrations.AddField( - model_name='importbatch', - name='source_library_url', - field=models.URLField(blank=True, null=True), - ), migrations.AddField( model_name='importbatch', name='uuid', @@ -43,13 +32,8 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='importjob', - name='metadata', - field=django.contrib.postgres.fields.jsonb.JSONField(default={}), - ), - migrations.AddField( - model_name='importjob', - name='source_library_url', - field=models.URLField(blank=True, null=True), + name='library_track', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='import_jobs', to='federation.LibraryTrack'), ), migrations.AddField( model_name='importjob', @@ -76,16 +60,6 @@ class Migration(migrations.Migration): name='modification_date', field=models.DateTimeField(auto_now=True), ), - migrations.AddField( - model_name='trackfile', - name='source_library', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='track_files', to='federation.Library'), - ), - migrations.AddField( - model_name='trackfile', - name='source_library_url', - field=models.URLField(blank=True, null=True), - ), migrations.AddField( model_name='trackfile', name='uuid', diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 4c0b6a098..bcf691bcd 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -9,7 +9,6 @@ import uuid from django.conf import settings from django.db import models -from django.contrib.postgres.fields import JSONField from django.core.files.base import ContentFile from django.core.files import File from django.db.models.signals import post_save @@ -416,16 +415,6 @@ class TrackFile(models.Model): source = models.URLField(null=True, blank=True) creation_date = models.DateTimeField(default=timezone.now) modification_date = models.DateTimeField(auto_now=True) - - source_library = models.ForeignKey( - 'federation.Library', - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name='track_files') - # points to the URL of the original trackfile ActivityPub Object - source_library_url = models.URLField(null=True, blank=True) - duration = models.IntegerField(null=True, blank=True) acoustid_track_id = models.UUIDField(null=True, blank=True) mimetype = models.CharField(null=True, blank=True, max_length=200) @@ -503,14 +492,6 @@ class ImportBatch(models.Model): blank=True, on_delete=models.CASCADE) - source_library = models.ForeignKey( - 'federation.Library', - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name='import_batches') - source_library_url = models.URLField(null=True, blank=True) - class Meta: ordering = ['-creation_date'] @@ -540,8 +521,14 @@ class ImportJob(models.Model): choices=IMPORT_STATUS_CHOICES, default='pending', max_length=30) audio_file = models.FileField( upload_to='imports/%Y/%m/%d', max_length=255, null=True, blank=True) - source_library_url = models.URLField(null=True, blank=True) - metadata = JSONField(default={}) + + library_track = models.ForeignKey( + 'federation.LibraryTrack', + related_name='import_jobs', + on_delete=models.SET_NULL, + null=True, + blank=True + ) class Meta: ordering = ('id', ) diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index 78b2f7c04..42795dbea 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -153,127 +153,3 @@ class TrackActivitySerializer(activity_serializers.ModelSerializer): def get_type(self, obj): return 'Audio' - - -class AudioMetadataSerializer(serializers.Serializer): - artist = serializers.CharField(required=False) - release = serializers.CharField(required=False) - recording = serializers.CharField(required=False) - - -class AudioSerializer(serializers.Serializer): - type = serializers.CharField() - id = serializers.URLField() - url = serializers.JSONField() - metadata = AudioMetadataSerializer() - - def validate_type(self, v): - if v != 'Audio': - raise serializers.ValidationError('Invalid type for audio') - return v - - def validate_url(self, v): - try: - url = v['href'] - except (KeyError, TypeError): - raise serializers.ValidationError('Missing href') - - try: - media_type = v['mediaType'] - except (KeyError, TypeError): - raise serializers.ValidationError('Missing mediaType') - - if not media_type.startswith('audio/'): - raise serializers.ValidationError('Invalid mediaType') - - return url - - def validate_url(self, v): - try: - url = v['href'] - except (KeyError, TypeError): - raise serializers.ValidationError('Missing href') - - try: - media_type = v['mediaType'] - except (KeyError, TypeError): - raise serializers.ValidationError('Missing mediaType') - - if not media_type.startswith('audio/'): - raise serializers.ValidationError('Invalid mediaType') - - return v - - def create(self, validated_data, batch): - metadata = validated_data['metadata'].copy() - metadata['mediaType'] = validated_data['url']['mediaType'] - return models.ImportJob.objects.create( - batch=batch, - source=validated_data['url']['href'], - source_library_url=validated_data['id'], - metadata=metadata, - ) - - def to_representation(self, instance): - d = { - 'type': 'Audio', - 'id': instance.get_federation_url(), - 'name': instance.track.full_name, - 'metadata': { - 'artist': instance.track.artist.musicbrainz_url, - 'release': instance.track.album.musicbrainz_url, - 'track': instance.track.musicbrainz_url, - }, - 'url': { - 'href': federation_utils.full_url(instance.path), - 'type': 'Link', - 'mediaType': instance.mimetype - }, - 'attributedTo': [ - self.context['actor'].url - ] - } - if self.context.get('include_ap_context', True): - d['@context'] = AP_CONTEXT - return d - - -class AudioCollectionImportSerializer(serializers.Serializer): - id = serializers.URLField() - items = serializers.ListField( - child=AudioSerializer(), - min_length=1, - ) - - @transaction.atomic - def create(self, validated_data): - batch = models.ImportBatch.objects.create( - source_library=self.context['library'], - source_library_url=validated_data['id'], - source='federation', - ) - for i in validated_data['items']: - s = AudioSerializer(data=i) - job = s.create(i, batch) - return batch - - def to_representation(self, instance): - d = { - 'id': instance['id'], - 'actor': instance['actor'].url, - 'totalItems': len(instance['items']), - 'type': 'Collection', - 'items': [ - AudioSerializer( - i, - context={ - 'actor': instance['actor'], - 'include_ap_context': False - } - ).data - for i in instance['items'] - ] - } - if self.context.get('include_ap_context', True): - d['@context'] = AP_CONTEXT - return d diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py index 67b538c1e..c58eb7136 100644 --- a/api/funkwhale_api/music/tasks.py +++ b/api/funkwhale_api/music/tasks.py @@ -25,42 +25,46 @@ def set_acoustid_on_track_file(track_file): return update(result['id']) -def get_mbid(url, type): - prefix = 'https://musicbrainz.org/{}/'.format(type) - if url.startswith(prefix): - return url.replace(prefix, '') - - -def import_track_from_metadata(metadata): - raw_track = metadata['recording'] - if isinstance(raw_track, str): - track_mbid = get_mbid(raw_track, 'recording') +def import_track_from_remote(library_track): + metadata = library_track.metadata + try: + track_mbid = metadata['recording']['musicbrainz_id'] + assert track_mbid # for null/empty values + except (KeyError, AssertionError): + pass + else: return models.Track.get_or_create_from_api(mbid=track_mbid) - raw_album = metadata['release'] - if isinstance(raw_album, str): - album_mbid = get_mbid(raw_album, 'release') + try: + album_mbid = metadata['release']['musicbrainz_id'] + assert album_mbid # for null/empty values + except (KeyError, AssertionError): + pass + else: album = models.Album.get_or_create_from_api(mbid=album_mbid) return models.Track.get_or_create_from_title( - raw_track['title'], artist=album.artist, album=album) + library_track.title, artist=album.artist, album=album) - raw_artist = metadata['artist'] - if isinstance(raw_artist, str): - artist_mbid = get_mbid(raw_artist, 'artist') + try: + artist_mbid = metadata['artist']['musicbrainz_id'] + assert artist_mbid # for null/empty values + except (KeyError, AssertionError): + pass + else: artist = models.Artist.get_or_create_from_api(mbid=artist_mbid) album = models.Album.get_or_create_from_title( - raw_album['title'], artist=artist) + library_track.album_title, artist=artist) return models.Track.get_or_create_from_title( - raw_track['title'], artist=artist, album=album) + library_track.title, artist=artist, album=album) # worst case scenario, we have absolutely no way to link to a # musicbrainz resource, we rely on the name/titles artist = models.Artist.get_or_create_from_name( - raw_artist['name']) + library_track.artist_name) album = models.Album.get_or_create_from_title( - raw_album['title'], artist=artist) + library_track.album_title, artist=artist) return models.Track.get_or_create_from_title( - raw_track['title'], artist=artist, album=album) + library_track.title, artist=artist, album=album) def _do_import(import_job, replace, use_acoustid=True): @@ -83,12 +87,13 @@ def _do_import(import_job, replace, use_acoustid=True): track, _ = models.Track.get_or_create_from_api(mbid=mbid) elif import_job.audio_file: track = import_track_data_from_path(import_job.audio_file.path) + elif import_job.library_track: + track = import_track_from_remote(import_job.library_track) else: - # probably federation, we use metadata stored on the job itself - if not import_job.metadata: - raise ValueError('We cannot import without at least metadatas') + raise ValueError( + 'Not enough data to process import, ' + 'add a mbid, an audio file or a library track') - track = import_track_from_metadata(import_job.metadata) track_file = None if replace: track_file = track.files.first() @@ -102,14 +107,13 @@ def _do_import(import_job, replace, use_acoustid=True): track_file = track_file or models.TrackFile( track=track, source=import_job.source) track_file.acoustid_track_id = acoustid_track_id - track_file.source_library = import_job.batch.source_library - track_file.source_library_url = import_job.source_library_url if from_file: track_file.audio_file = ContentFile(import_job.audio_file.read()) track_file.audio_file.name = import_job.audio_file.name track_file.duration = duration - elif import_job.source_library_url: - if track_file.source_library.download_files: + elif import_job.library_track: + track_file.mimetype = import_job.library_track.audio_mimetype + if import_job.library_track.library.download_files: raise NotImplementedError() else: # no downloading, we hotlink @@ -117,6 +121,10 @@ def _do_import(import_job, replace, use_acoustid=True): else: track_file.download_file() track_file.save() + if import_job.library_track: + import_job.library_track.local_track_file = track_file + import_job.library_track.save( + update_fields=['modification_date', 'local_track_file']) import_job.status = 'finished' import_job.track_file = track_file if import_job.audio_file: diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index 6f5e88663..107047b56 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -1,3 +1,4 @@ +import arrow import pytest import uuid @@ -10,7 +11,6 @@ from funkwhale_api.federation import actors from funkwhale_api.federation import models from funkwhale_api.federation import serializers from funkwhale_api.federation import utils -from funkwhale_api.music import models as music_models def test_actor_fetching(r_mock): @@ -375,7 +375,7 @@ def test_library_actor_handle_create_audio_no_library(mocker, factories): # when we receive inbox create audio, we should not do anything # if we don't have a configured library matching the sender mocked_create = mocker.patch( - 'funkwhale_api.music.serializers.AudioCollectionImportSerializer.create' + 'funkwhale_api.federation.serializers.AudioSerializer.create' ) actor = factories['federation.Actor']() library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() @@ -393,7 +393,7 @@ def test_library_actor_handle_create_audio_no_library(mocker, factories): library_actor.system_conf.post_inbox(data, actor=actor) mocked_create.assert_not_called() - music_models.ImportBatch.objects.count() == 0 + models.LibraryTrack.objects.count() == 0 def test_library_actor_handle_create_audio_no_library_enabled( @@ -401,7 +401,7 @@ def test_library_actor_handle_create_audio_no_library_enabled( # when we receive inbox create audio, we should not do anything # if we don't have an enabled library mocked_create = mocker.patch( - 'funkwhale_api.music.serializers.AudioCollectionImportSerializer.create' + 'funkwhale_api.federation.serializers.AudioSerializer.create' ) disabled_library = factories['federation.Library']( federation_enabled=False) @@ -421,12 +421,14 @@ def test_library_actor_handle_create_audio_no_library_enabled( library_actor.system_conf.post_inbox(data, actor=actor) mocked_create.assert_not_called() - music_models.ImportBatch.objects.count() == 0 + models.LibraryTrack.objects.count() == 0 def test_library_actor_handle_create_audio(mocker, factories): library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - remote_library = factories['federation.Library']() + remote_library = factories['federation.Library']( + federation_enabled=True + ) data = { 'actor': remote_library.actor.url, @@ -442,14 +444,19 @@ def test_library_actor_handle_create_audio(mocker, factories): library_actor.system_conf.post_inbox(data, actor=remote_library.actor) - batch = remote_library.import_batches.latest('id') + lts = list(remote_library.tracks.order_by('id')) - assert batch.source_library_url == data['object']['id'] - assert batch.source_library == remote_library - assert batch.jobs.count() == 2 + assert len(lts) == 2 - jobs = list(batch.jobs.order_by('id')) for i, a in enumerate(data['object']['items']): - job = jobs[i] - assert job.source_library_url == a['id'] - assert job.source == a['url']['href'] + lt = lts[i] + assert lt.pk is not None + assert lt.url == a['id'] + assert lt.library == remote_library + assert lt.audio_url == a['url']['href'] + assert lt.audio_mimetype == a['url']['mediaType'] + assert lt.metadata == a['metadata'] + assert lt.title == a['metadata']['recording']['title'] + assert lt.artist_name == a['metadata']['artist']['name'] + assert lt.album_title == a['metadata']['release']['title'] + assert lt.published_date == arrow.get(a['published']) diff --git a/api/tests/federation/test_serializers.py b/api/tests/federation/test_serializers.py index 1e580040e..45778ed48 100644 --- a/api/tests/federation/test_serializers.py +++ b/api/tests/federation/test_serializers.py @@ -1,10 +1,13 @@ +import arrow + from django.urls import reverse from django.core.paginator import Paginator +from funkwhale_api.federation import actors from funkwhale_api.federation import keys from funkwhale_api.federation import models from funkwhale_api.federation import serializers -from funkwhale_api.music.serializers import AudioSerializer +from funkwhale_api.federation import utils def test_actor_serializer_from_ap(db): @@ -174,7 +177,7 @@ def test_paginated_collection_serializer(factories): conf = { 'id': 'https://test.federation/test', 'items': tfs, - 'item_serializer': AudioSerializer, + 'item_serializer': serializers.AudioSerializer, 'actor': actor, 'page_size': 2, } @@ -204,7 +207,7 @@ def test_collection_page_serializer(factories): conf = { 'id': 'https://test.federation/test', - 'item_serializer': AudioSerializer, + 'item_serializer': serializers.AudioSerializer, 'actor': actor, 'page': Paginator(tfs, 2).page(2), } @@ -235,3 +238,140 @@ def test_collection_page_serializer(factories): serializer = serializers.CollectionPageSerializer(conf) assert serializer.data == expected + + +def test_activity_pub_audio_serializer_to_library_track(factories): + remote_library = factories['federation.Library']() + audio = factories['federation.Audio']() + serializer = serializers.AudioSerializer( + data=audio, context={'library': remote_library}) + + assert serializer.is_valid(raise_exception=True) + + lt = serializer.save() + + assert lt.pk is not None + assert lt.url == audio['id'] + assert lt.library == remote_library + assert lt.audio_url == audio['url']['href'] + assert lt.audio_mimetype == audio['url']['mediaType'] + assert lt.metadata == audio['metadata'] + assert lt.title == audio['metadata']['recording']['title'] + assert lt.artist_name == audio['metadata']['artist']['name'] + assert lt.album_title == audio['metadata']['release']['title'] + assert lt.published_date == arrow.get(audio['published']) + + +def test_activity_pub_audio_serializer_to_ap(factories): + tf = factories['music.TrackFile'](mimetype='audio/mp3') + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + expected = { + '@context': serializers.AP_CONTEXT, + 'type': 'Audio', + 'id': tf.get_federation_url(), + 'name': tf.track.full_name, + 'published': tf.creation_date.isoformat(), + 'updated': tf.modification_date.isoformat(), + 'metadata': { + 'artist': { + 'musicbrainz_id': tf.track.artist.mbid, + 'name': tf.track.artist.name, + }, + 'release': { + 'musicbrainz_id': tf.track.album.mbid, + 'title': tf.track.album.title, + }, + 'recording': { + 'musicbrainz_id': tf.track.mbid, + 'title': tf.track.title, + }, + }, + 'url': { + 'href': utils.full_url(tf.path), + 'type': 'Link', + 'mediaType': 'audio/mp3' + }, + 'attributedTo': [ + library.url + ] + } + + serializer = serializers.AudioSerializer(tf, context={'actor': library}) + + assert serializer.data == expected + + +def test_activity_pub_audio_serializer_to_ap_no_mbid(factories): + tf = factories['music.TrackFile']( + mimetype='audio/mp3', + track__mbid=None, + track__album__mbid=None, + track__album__artist__mbid=None, + ) + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + expected = { + '@context': serializers.AP_CONTEXT, + 'type': 'Audio', + 'id': tf.get_federation_url(), + 'name': tf.track.full_name, + 'published': tf.creation_date.isoformat(), + 'updated': tf.modification_date.isoformat(), + 'metadata': { + 'artist': { + 'name': tf.track.artist.name, + 'musicbrainz_id': None, + }, + 'release': { + 'title': tf.track.album.title, + 'musicbrainz_id': None, + }, + 'recording': { + 'title': tf.track.title, + 'musicbrainz_id': None, + }, + }, + 'url': { + 'href': utils.full_url(tf.path), + 'type': 'Link', + 'mediaType': 'audio/mp3' + }, + 'attributedTo': [ + library.url + ] + } + + serializer = serializers.AudioSerializer(tf, context={'actor': library}) + + assert serializer.data == expected + + +def test_collection_serializer_to_ap(factories): + tf1 = factories['music.TrackFile'](mimetype='audio/mp3') + tf2 = factories['music.TrackFile'](mimetype='audio/ogg') + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + expected = { + '@context': serializers.AP_CONTEXT, + 'id': 'https://test.id', + 'actor': library.url, + 'totalItems': 2, + 'type': 'Collection', + 'items': [ + serializers.AudioSerializer( + tf1, context={'actor': library, 'include_ap_context': False} + ).data, + serializers.AudioSerializer( + tf2, context={'actor': library, 'include_ap_context': False} + ).data, + ] + } + + collection = { + 'id': expected['id'], + 'actor': library, + 'items': [tf1, tf2], + 'item_serializer': serializers.AudioSerializer + } + serializer = serializers.CollectionSerializer( + collection, context={'actor': library, 'id': 'https://test.id'}) + + assert serializer.data == expected diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py index c7e1fc012..c26810dad 100644 --- a/api/tests/federation/test_views.py +++ b/api/tests/federation/test_views.py @@ -7,7 +7,6 @@ from funkwhale_api.federation import actors from funkwhale_api.federation import serializers from funkwhale_api.federation import utils from funkwhale_api.federation import webfinger -from funkwhale_api.music.serializers import AudioSerializer @pytest.mark.parametrize('system_actor', actors.SYSTEM_ACTORS.keys()) @@ -87,7 +86,7 @@ def test_audio_file_list_actor_no_page( 'id': utils.full_url(reverse('federation:music:files-list')), 'page_size': 2, 'items': list(reversed(tfs)), # we order by -creation_date - 'item_serializer': AudioSerializer, + 'item_serializer': serializers.AudioSerializer, 'actor': library } expected = serializers.PaginatedCollectionSerializer(conf).data @@ -107,7 +106,7 @@ def test_audio_file_list_actor_page( conf = { 'id': utils.full_url(reverse('federation:music:files-list')), 'page': Paginator(list(reversed(tfs)), 2).page(2), - 'item_serializer': AudioSerializer, + 'item_serializer': serializers.AudioSerializer, 'actor': library } expected = serializers.CollectionPageSerializer(conf).data diff --git a/api/tests/music/test_import.py b/api/tests/music/test_import.py index 7e3ebcc19..a15f027ba 100644 --- a/api/tests/music/test_import.py +++ b/api/tests/music/test_import.py @@ -38,32 +38,23 @@ def test_create_import_can_bind_to_request( assert batch.import_request == request -@pytest.mark.parametrize('url,type,expected', [ - ('https://musicbrainz.org/artist/test', 'artist', 'test'), - ('https://musicbrainz.org/release/test', 'release', 'test'), - ('https://musicbrainz.org/recording/test', 'recording', 'test'), - ('https://musicbrainz.org/recording/test', 'artist', None), -]) -def test_get_mbid(url, type, expected): - assert tasks.get_mbid(url, type) == expected - - def test_import_job_from_federation_no_musicbrainz(factories): + lt = factories['federation.LibraryTrack']( + artist_name='Hello', + album_title='World', + title='Ping', + ) job = factories['music.ImportJob']( federation=True, - metadata__artist={'name': 'Hello'}, - metadata__release={'title': 'World'}, - metadata__recording={'title': 'Ping'}, - mbid=None, + library_track=lt, ) tasks.import_job_run(import_job_id=job.pk) job.refresh_from_db() tf = job.track_file - assert tf.source == job.source - assert tf.source_library == job.batch.source_library - assert tf.source_library_url == job.source_library_url + assert tf.mimetype == lt.audio_mimetype + assert tf.library_track == job.library_track assert tf.track.title == 'Ping' assert tf.track.artist.name == 'Hello' assert tf.track.album.title == 'World' @@ -74,23 +65,25 @@ def test_import_job_from_federation_musicbrainz_recording(factories, mocker): track_from_api = mocker.patch( 'funkwhale_api.music.models.Track.get_or_create_from_api', return_value=t) + lt = factories['federation.LibraryTrack']( + metadata__recording__musicbrainz=True, + artist_name='Hello', + album_title='World', + ) job = factories['music.ImportJob']( federation=True, - metadata__artist={'name': 'Hello'}, - metadata__release={'title': 'World'}, - mbid=None, + library_track=lt, ) tasks.import_job_run(import_job_id=job.pk) job.refresh_from_db() tf = job.track_file - assert tf.source == job.source - assert tf.source_library == job.batch.source_library - assert tf.source_library_url == job.source_library_url + assert tf.mimetype == lt.audio_mimetype + assert tf.library_track == job.library_track assert tf.track == t track_from_api.assert_called_once_with( - mbid=tasks.get_mbid(job.metadata['recording'], 'recording')) + mbid=lt.metadata['recording']['musicbrainz_id']) def test_import_job_from_federation_musicbrainz_release(factories, mocker): @@ -98,26 +91,28 @@ def test_import_job_from_federation_musicbrainz_release(factories, mocker): album_from_api = mocker.patch( 'funkwhale_api.music.models.Album.get_or_create_from_api', return_value=a) + lt = factories['federation.LibraryTrack']( + metadata__release__musicbrainz=True, + artist_name='Hello', + title='Ping', + ) job = factories['music.ImportJob']( federation=True, - metadata__artist={'name': 'Hello'}, - metadata__recording={'title': 'Ping'}, - mbid=None, + library_track=lt, ) tasks.import_job_run(import_job_id=job.pk) job.refresh_from_db() tf = job.track_file - assert tf.source_library == job.batch.source_library - assert tf.source_library_url == job.source_library_url - assert tf.source == job.source + assert tf.mimetype == lt.audio_mimetype + assert tf.library_track == job.library_track assert tf.track.title == 'Ping' assert tf.track.artist == a.artist assert tf.track.album == a album_from_api.assert_called_once_with( - mbid=tasks.get_mbid(job.metadata['release'], 'release')) + mbid=lt.metadata['release']['musicbrainz_id']) def test_import_job_from_federation_musicbrainz_artist(factories, mocker): @@ -125,24 +120,27 @@ def test_import_job_from_federation_musicbrainz_artist(factories, mocker): artist_from_api = mocker.patch( 'funkwhale_api.music.models.Artist.get_or_create_from_api', return_value=a) + lt = factories['federation.LibraryTrack']( + metadata__artist__musicbrainz=True, + album_title='World', + title='Ping', + ) job = factories['music.ImportJob']( federation=True, - metadata__release={'title': 'World'}, - metadata__recording={'title': 'Ping'}, - mbid=None, + library_track=lt, ) tasks.import_job_run(import_job_id=job.pk) job.refresh_from_db() tf = job.track_file - assert tf.source == job.source - assert tf.source_library == job.batch.source_library - assert tf.source_library_url == job.source_library_url + assert tf.mimetype == lt.audio_mimetype + assert tf.library_track == job.library_track + assert tf.track.title == 'Ping' assert tf.track.artist == a assert tf.track.album.artist == a assert tf.track.album.title == 'World' artist_from_api.assert_called_once_with( - mbid=tasks.get_mbid(job.metadata['artist'], 'artist')) + mbid=lt.metadata['artist']['musicbrainz_id']) diff --git a/api/tests/music/test_serializers.py b/api/tests/music/test_serializers.py deleted file mode 100644 index a1f5999e7..000000000 --- a/api/tests/music/test_serializers.py +++ /dev/null @@ -1,94 +0,0 @@ -from funkwhale_api.federation import actors -from funkwhale_api.federation import utils as federation_utils -from funkwhale_api.federation.serializers import AP_CONTEXT -from funkwhale_api.music import serializers - - -def test_activity_pub_audio_collection_serializer_to_import(factories): - remote_library = factories['federation.Library']() - - collection = { - 'id': 'https://batch.import', - 'type': 'Collection', - 'totalItems': 2, - 'items': factories['federation.Audio'].create_batch(size=2) - } - - serializer = serializers.AudioCollectionImportSerializer( - data=collection, context={'library': remote_library}) - - assert serializer.is_valid(raise_exception=True) - - batch = serializer.save() - jobs = list(batch.jobs.all()) - - assert batch.source == 'federation' - assert batch.source_library_url == collection['id'] - assert batch.source_library == remote_library - assert len(jobs) == 2 - - for i, a in enumerate(collection['items']): - job = jobs[i] - assert job.source_library_url == a['id'] - assert job.source == a['url']['href'] - a['metadata']['mediaType'] = a['url']['mediaType'] - assert job.metadata == a['metadata'] - - -def test_activity_pub_audio_serializer_to_ap(factories): - tf = factories['music.TrackFile'](mimetype='audio/mp3') - library = actors.SYSTEM_ACTORS['library'].get_actor_instance() - expected = { - '@context': AP_CONTEXT, - 'type': 'Audio', - 'id': tf.get_federation_url(), - 'name': tf.track.full_name, - 'metadata': { - 'artist': tf.track.artist.musicbrainz_url, - 'release': tf.track.album.musicbrainz_url, - 'track': tf.track.musicbrainz_url, - }, - 'url': { - 'href': federation_utils.full_url(tf.path), - 'type': 'Link', - 'mediaType': 'audio/mp3' - }, - 'attributedTo': [ - library.url - ] - } - - serializer = serializers.AudioSerializer(tf, context={'actor': library}) - - assert serializer.data == expected - - -def test_activity_pub_audio_collection_serializer_to_ap(factories): - tf1 = factories['music.TrackFile'](mimetype='audio/mp3') - tf2 = factories['music.TrackFile'](mimetype='audio/ogg') - library = actors.SYSTEM_ACTORS['library'].get_actor_instance() - expected = { - '@context': AP_CONTEXT, - 'id': 'https://test.id', - 'actor': library.url, - 'totalItems': 2, - 'type': 'Collection', - 'items': [ - serializers.AudioSerializer( - tf1, context={'actor': library, 'include_ap_context': False} - ).data, - serializers.AudioSerializer( - tf2, context={'actor': library, 'include_ap_context': False} - ).data, - ] - } - - collection = { - 'id': expected['id'], - 'actor': library, - 'items': [tf1, tf2], - } - serializer = serializers.AudioCollectionImportSerializer( - collection, context={'actor': library, 'id': 'https://test.id'}) - - assert serializer.data == expected From 9612b1bace4b59cafeed1304971395ffbc1ececb Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 7 Apr 2018 15:34:35 +0200 Subject: [PATCH 031/101] Can now serve track from remote library --- api/funkwhale_api/federation/activity.py | 15 +--- api/funkwhale_api/federation/actors.py | 32 ++++++--- .../federation/authentication.py | 2 + .../federation/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/generate_keys.py | 53 -------------- ...407_0852.py => 0003_auto_20180407_1010.py} | 4 +- api/funkwhale_api/federation/models.py | 7 -- api/funkwhale_api/federation/signing.py | 15 ++++ api/funkwhale_api/music/factories.py | 12 ++++ ...407_0852.py => 0023_auto_20180407_1010.py} | 9 ++- api/funkwhale_api/music/models.py | 14 ++-- api/funkwhale_api/music/permissions.py | 23 +++++++ api/funkwhale_api/music/tasks.py | 5 +- api/funkwhale_api/music/utils.py | 7 ++ api/funkwhale_api/music/views.py | 69 +++++++++++++++---- api/tests/conftest.py | 9 +++ api/tests/federation/conftest.py | 10 --- api/tests/federation/test_actors.py | 16 +++-- api/tests/federation/test_commands.py | 14 ---- api/tests/music/test_permissions.py | 56 +++++++++++++++ api/tests/music/test_views.py | 40 +++++++++++ 22 files changed, 272 insertions(+), 140 deletions(-) delete mode 100644 api/funkwhale_api/federation/management/__init__.py delete mode 100644 api/funkwhale_api/federation/management/commands/__init__.py delete mode 100644 api/funkwhale_api/federation/management/commands/generate_keys.py rename api/funkwhale_api/federation/migrations/{0003_auto_20180407_0852.py => 0003_auto_20180407_1010.py} (94%) rename api/funkwhale_api/music/migrations/{0023_auto_20180407_0852.py => 0023_auto_20180407_1010.py} (88%) create mode 100644 api/funkwhale_api/music/permissions.py delete mode 100644 api/tests/federation/conftest.py delete mode 100644 api/tests/federation/test_commands.py create mode 100644 api/tests/music/test_permissions.py diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index db71bd4fb..7502bd739 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -58,21 +58,12 @@ OBJECT_TYPES = [ 'Video', ] + ACTIVITY_TYPES + def deliver(activity, on_behalf_of, to=[]): from . import actors logger.info('Preparing activity delivery to %s', to) - auth = requests_http_signature.HTTPSignatureAuth( - use_auth_header=False, - headers=[ - '(request-target)', - 'user-agent', - 'host', - 'date', - 'content-type',], - algorithm='rsa-sha256', - key=on_behalf_of.private_key.encode('utf-8'), - key_id=on_behalf_of.private_key_id, - ) + auth = signing.get_auth( + on_behalf_of.private_key, on_behalf_of.private_key_id) for url in to: recipient_actor = actors.get_actor(url) logger.debug('delivering to %s', recipient_actor.inbox_url) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index ffbafd8b7..f640c9b12 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -13,8 +13,10 @@ from rest_framework.exceptions import PermissionDenied from dynamic_preferences.registries import global_preferences_registry from . import activity +from . import keys from . import models from . import serializers +from . import signing from . import utils logger = logging.getLogger(__name__) @@ -51,24 +53,37 @@ class SystemActor(object): additional_attributes = {} manually_approves_followers = False + def get_request_auth(self): + actor = self.get_actor_instance() + return signing.get_auth( + actor.private_key, actor.private_key_id) + def serialize(self): actor = self.get_actor_instance() serializer = serializers.ActorSerializer(actor) return serializer.data def get_actor_instance(self): + try: + return models.Actor.objects.get(url=self.get_actor_url()) + except models.Actor.DoesNotExist: + pass + private, public = keys.get_key_pair() args = self.get_instance_argument( self.id, name=self.name, summary=self.summary, **self.additional_attributes ) - url = args.pop('url') - a, created = models.Actor.objects.get_or_create( - url=url, - defaults=args, - ) - return a + args['private_key'] = private.decode('utf-8') + args['public_key'] = public.decode('utf-8') + return models.Actor.objects.create(**args) + + def get_actor_url(self): + return utils.full_url( + reverse( + 'federation:instance-actors-detail', + kwargs={'actor': self.id})) def get_instance_argument(self, id, name, summary, **kwargs): preferences = global_preferences_registry.manager() @@ -78,10 +93,7 @@ class SystemActor(object): 'type': 'Person', 'name': name.format(host=settings.FEDERATION_HOSTNAME), 'manually_approves_followers': True, - 'url': utils.full_url( - reverse( - 'federation:instance-actors-detail', - kwargs={'actor': id})), + 'url': self.get_actor_url(), 'shared_inbox_url': utils.full_url( reverse( 'federation:instance-actors-inbox', diff --git a/api/funkwhale_api/federation/authentication.py b/api/funkwhale_api/federation/authentication.py index f2926bb30..7f8ad6653 100644 --- a/api/funkwhale_api/federation/authentication.py +++ b/api/funkwhale_api/federation/authentication.py @@ -51,6 +51,8 @@ class SignatureAuthentication(authentication.BaseAuthentication): def authenticate(self, request): setattr(request, 'actor', None) actor = self.authenticate_actor(request) + if not actor: + return user = AnonymousUser() setattr(request, 'actor', actor) return (user, None) diff --git a/api/funkwhale_api/federation/management/__init__.py b/api/funkwhale_api/federation/management/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/funkwhale_api/federation/management/commands/__init__.py b/api/funkwhale_api/federation/management/commands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/funkwhale_api/federation/management/commands/generate_keys.py b/api/funkwhale_api/federation/management/commands/generate_keys.py deleted file mode 100644 index eafe9aae3..000000000 --- a/api/funkwhale_api/federation/management/commands/generate_keys.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.core.management.base import BaseCommand, CommandError -from django.db import transaction - -from dynamic_preferences.registries import global_preferences_registry - -from funkwhale_api.federation import keys - - -class Command(BaseCommand): - help = ( - 'Generate a public/private key pair for your instance,' - ' for federation purposes. If a key pair already exists, does nothing.' - ) - - def add_arguments(self, parser): - parser.add_argument( - '--replace', - action='store_true', - dest='replace', - default=False, - help='Replace existing key pair, if any', - ) - parser.add_argument( - '--noinput', '--no-input', action='store_false', dest='interactive', - help="Do NOT prompt the user for input of any kind.", - ) - - @transaction.atomic - def handle(self, *args, **options): - preferences = global_preferences_registry.manager() - existing_public = preferences['federation__public_key'] - existing_private = preferences['federation__public_key'] - - if existing_public or existing_private and not options['replace']: - raise CommandError( - 'Keys are already present! ' - 'Replace them with --replace if you know what you are doing.') - - if options['interactive']: - message = ( - 'Are you sure you want to do this?\n\n' - "Type 'yes' to continue, or 'no' to cancel: " - ) - if input(''.join(message)) != 'yes': - raise CommandError("Operation cancelled.") - private, public = keys.get_key_pair() - preferences['federation__public_key'] = public.decode('utf-8') - preferences['federation__private_key'] = private.decode('utf-8') - - self.stdout.write( - 'Your new key pair was generated.' - 'Your public key is now:\n\n{}'.format(public.decode('utf-8')) - ) diff --git a/api/funkwhale_api/federation/migrations/0003_auto_20180407_0852.py b/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py similarity index 94% rename from api/funkwhale_api/federation/migrations/0003_auto_20180407_0852.py rename to api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py index afc7ea083..38ac7cb4f 100644 --- a/api/funkwhale_api/federation/migrations/0003_auto_20180407_0852.py +++ b/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.3 on 2018-04-07 08:52 +# Generated by Django 2.0.3 on 2018-04-07 10:10 import django.contrib.postgres.fields.jsonb from django.db import migrations, models @@ -10,7 +10,6 @@ import uuid class Migration(migrations.Migration): dependencies = [ - ('music', '0022_importbatch_import_request'), ('federation', '0002_auto_20180403_1620'), ] @@ -70,7 +69,6 @@ class Migration(migrations.Migration): ('title', models.CharField(max_length=500)), ('metadata', django.contrib.postgres.fields.jsonb.JSONField(default={}, max_length=10000)), ('library', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracks', to='federation.Library')), - ('local_track_file', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='library_track', to='music.TrackFile')), ], ), migrations.AddField( diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 91f2ea973..bf1e5d830 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -192,13 +192,6 @@ class LibraryTrack(models.Model): published_date = models.DateTimeField(null=True, blank=True) library = models.ForeignKey( Library, related_name='tracks', on_delete=models.CASCADE) - local_track_file = models.OneToOneField( - 'music.TrackFile', - related_name='library_track', - on_delete=models.CASCADE, - null=True, - blank=True, - ) artist_name = models.CharField(max_length=500) album_title = models.CharField(max_length=500) title = models.CharField(max_length=500) diff --git a/api/funkwhale_api/federation/signing.py b/api/funkwhale_api/federation/signing.py index 7e4d2aa5a..8d984d3ff 100644 --- a/api/funkwhale_api/federation/signing.py +++ b/api/funkwhale_api/federation/signing.py @@ -53,3 +53,18 @@ def verify_django(django_request, public_key): request.headers[h] = str(v) prepared_request = request.prepare() return verify(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'], + algorithm='rsa-sha256', + key=private_key.encode('utf-8'), + key_id=private_key_id, + ) diff --git a/api/funkwhale_api/music/factories.py b/api/funkwhale_api/music/factories.py index 83aad432a..2bf1960ca 100644 --- a/api/funkwhale_api/music/factories.py +++ b/api/funkwhale_api/music/factories.py @@ -56,6 +56,18 @@ class TrackFileFactory(factory.django.DjangoModelFactory): class Meta: model = 'music.TrackFile' + class Params: + federation = factory.Trait( + audio_file=None, + library_track=factory.SubFactory(LibraryTrackFactory), + mimetype=factory.LazyAttribute( + lambda o: o.library_track.audio_mimetype + ), + source=factory.LazyAttribute( + lambda o: o.library_track.audio_url + ), + ) + @registry.register class ImportBatchFactory(factory.django.DjangoModelFactory): diff --git a/api/funkwhale_api/music/migrations/0023_auto_20180407_0852.py b/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py similarity index 88% rename from api/funkwhale_api/music/migrations/0023_auto_20180407_0852.py rename to api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py index b1bbeacef..0539d90f6 100644 --- a/api/funkwhale_api/music/migrations/0023_auto_20180407_0852.py +++ b/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.3 on 2018-04-07 08:52 +# Generated by Django 2.0.3 on 2018-04-07 10:10 from django.conf import settings from django.db import migrations, models @@ -10,7 +10,7 @@ import uuid class Migration(migrations.Migration): dependencies = [ - ('federation', '0003_auto_20180407_0852'), + ('federation', '0003_auto_20180407_1010'), ('music', '0022_importbatch_import_request'), ] @@ -55,6 +55,11 @@ class Migration(migrations.Migration): name='creation_date', field=models.DateTimeField(default=django.utils.timezone.now), ), + migrations.AddField( + model_name='trackfile', + name='library_track', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='local_track_file', to='federation.LibraryTrack'), + ), migrations.AddField( model_name='trackfile', name='modification_date', diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index bcf691bcd..beec551a5 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -419,6 +419,14 @@ class TrackFile(models.Model): acoustid_track_id = models.UUIDField(null=True, blank=True) mimetype = models.CharField(null=True, blank=True, max_length=200) + library_track = models.OneToOneField( + 'federation.LibraryTrack', + related_name='local_track_file', + on_delete=models.CASCADE, + null=True, + blank=True, + ) + def download_file(self): # import the track file, since there is not any # we create a tmp dir for the download @@ -441,10 +449,8 @@ class TrackFile(models.Model): @property def path(self): - if settings.PROTECT_AUDIO_FILES: - return reverse( - 'api:v1:trackfiles-serve', kwargs={'pk': self.pk}) - return self.audio_file.url + return reverse( + 'api:v1:trackfiles-serve', kwargs={'pk': self.pk}) @property def filename(self): diff --git a/api/funkwhale_api/music/permissions.py b/api/funkwhale_api/music/permissions.py new file mode 100644 index 000000000..a8e62f1e7 --- /dev/null +++ b/api/funkwhale_api/music/permissions.py @@ -0,0 +1,23 @@ +from django.conf import settings + +from rest_framework.permissions import BasePermission + +from funkwhale_api.federation import actors + + +class Listen(BasePermission): + + def has_permission(self, request, view): + if not settings.PROTECT_AUDIO_FILES: + return True + + user = getattr(request, 'user', None) + if user and user.is_authenticated: + return True + + actor = getattr(request, 'actor', None) + if actor is None: + return False + + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + return library.followers.filter(url=actor.url).exists() diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py index c58eb7136..012b72cd2 100644 --- a/api/funkwhale_api/music/tasks.py +++ b/api/funkwhale_api/music/tasks.py @@ -112,6 +112,7 @@ def _do_import(import_job, replace, use_acoustid=True): track_file.audio_file.name = import_job.audio_file.name track_file.duration = duration elif import_job.library_track: + track_file.library_track = import_job.library_track track_file.mimetype = import_job.library_track.audio_mimetype if import_job.library_track.library.download_files: raise NotImplementedError() @@ -121,10 +122,6 @@ def _do_import(import_job, replace, use_acoustid=True): else: track_file.download_file() track_file.save() - if import_job.library_track: - import_job.library_track.local_track_file = track_file - import_job.library_track.save( - update_fields=['modification_date', 'local_track_file']) import_job.status = 'finished' import_job.track_file = track_file if import_job.audio_file: diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py index df659cb80..af0e59ab4 100644 --- a/api/funkwhale_api/music/utils.py +++ b/api/funkwhale_api/music/utils.py @@ -60,3 +60,10 @@ def compute_status(jobs): if pending: return 'pending' return 'finished' + + +def get_ext_from_type(mimetype): + mapping = { + 'audio/ogg': 'ogg', + 'audio/mpeg': 'mp3', + } diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 5ac3143f9..5f8fc1736 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -1,36 +1,42 @@ import ffmpeg import os import json +import requests import subprocess import unicodedata import urllib -from django.urls import reverse +from django.contrib.auth.decorators import login_required +from django.core.exceptions import ObjectDoesNotExist +from django.conf import settings from django.db import models, transaction from django.db.models.functions import Length -from django.conf import settings from django.http import StreamingHttpResponse +from django.urls import reverse +from django.utils.decorators import method_decorator from rest_framework import viewsets, views, mixins from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response +from rest_framework import settings as rest_settings from rest_framework import permissions from musicbrainzngs import ResponseError -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator from funkwhale_api.common import utils as funkwhale_utils +from funkwhale_api.federation import actors from funkwhale_api.requests.models import ImportRequest from funkwhale_api.musicbrainz import api from funkwhale_api.common.permissions import ( ConditionalAuthentication, HasModelPermission) from taggit.models import Tag +from funkwhale_api.federation.authentication import SignatureAuthentication -from . import forms -from . import models -from . import serializers -from . import importers from . import filters +from . import forms +from . import importers +from . import models +from . import permissions as music_permissions +from . import serializers from . import tasks from . import utils @@ -45,6 +51,7 @@ class SearchMixin(object): serializer = self.serializer_class(queryset, many=True) return Response(serializer.data) + class TagViewSetMixin(object): def get_queryset(self): @@ -179,22 +186,54 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet): class TrackFileViewSet(viewsets.ReadOnlyModelViewSet): queryset = (models.TrackFile.objects.all().order_by('-id')) serializer_class = serializers.TrackFileSerializer - permission_classes = [ConditionalAuthentication] + authentication_classes = rest_settings.api_settings.DEFAULT_AUTHENTICATION_CLASSES + [ + SignatureAuthentication + ] + permission_classes = [music_permissions.Listen] @detail_route(methods=['get']) def serve(self, request, *args, **kwargs): try: - f = models.TrackFile.objects.get(pk=kwargs['pk']) + f = models.TrackFile.objects.select_related( + 'library_track', + 'track__album__artist', + 'track__artist', + ).get(pk=kwargs['pk']) except models.TrackFile.DoesNotExist: return Response(status=404) - response = Response() + mt = f.mimetype + try: + library_track = f.library_track + except ObjectDoesNotExist: + library_track = None + if library_track and not f.audio_file: + # we proxy the response to the remote library + # since we did not mirror the file locally + mt = library_track.audio_mimetype + file_extension = utils.get_ext_from_type(mt) + filename = '{}.{}'.format(f.track.full_name, file_extension) + auth = actors.SYSTEM_ACTORS['library'].get_request_auth() + remote_response = requests.get( + library_track.audio_url, + auth=auth, + stream=True, + headers={ + 'Content-Type': 'application/activity+json' + }) + response = StreamingHttpResponse(remote_response.iter_content()) + else: + response = Response() + filename = f.filename + response['X-Accel-Redirect'] = "{}{}".format( + settings.PROTECT_FILES_PATH, + f.audio_file.url) filename = "filename*=UTF-8''{}".format( - urllib.parse.quote(f.filename)) + urllib.parse.quote(filename)) response["Content-Disposition"] = "attachment; {}".format(filename) - response['X-Accel-Redirect'] = "{}{}".format( - settings.PROTECT_FILES_PATH, - f.audio_file.url) + if mt: + response["Content-Type"] = mt + return response @list_route(methods=['get']) diff --git a/api/tests/conftest.py b/api/tests/conftest.py index d5bb56565..4f1ee8962 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -162,3 +162,12 @@ def media_root(settings): def r_mock(): with requests_mock.mock() as m: yield m + + +@pytest.fixture +def authenticated_actor(factories, mocker): + actor = factories['federation.Actor']() + mocker.patch( + 'funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor', + return_value=actor) + yield actor diff --git a/api/tests/federation/conftest.py b/api/tests/federation/conftest.py deleted file mode 100644 index c5831914b..000000000 --- a/api/tests/federation/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest - - -@pytest.fixture -def authenticated_actor(nodb_factories, mocker): - actor = nodb_factories['federation.Actor']() - mocker.patch( - 'funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor', - return_value=actor) - yield actor diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index 107047b56..090d9b03f 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -26,14 +26,17 @@ def test_actor_fetching(r_mock): assert r == payload -def test_get_library(settings, preferences): - preferences['federation__public_key'] = 'public_key' +def test_get_library(db, settings, mocker): + get_key_pair = mocker.patch( + 'funkwhale_api.federation.keys.get_key_pair', + return_value=(b'private', b'public')) expected = { 'preferred_username': 'library', 'domain': settings.FEDERATION_HOSTNAME, 'type': 'Person', 'name': '{}\'s library'.format(settings.FEDERATION_HOSTNAME), 'manually_approves_followers': True, + 'public_key': 'public', 'url': utils.full_url( reverse( 'federation:instance-actors-detail', @@ -50,7 +53,6 @@ def test_get_library(settings, preferences): reverse( 'federation:instance-actors-outbox', kwargs={'actor': 'library'})), - 'public_key': 'public_key', 'summary': 'Bot account to federate with {}\'s library'.format( settings.FEDERATION_HOSTNAME), } @@ -59,14 +61,17 @@ def test_get_library(settings, preferences): assert getattr(actor, key) == value -def test_get_test(settings, preferences): - preferences['federation__public_key'] = 'public_key' +def test_get_test(db, mocker, settings): + get_key_pair = mocker.patch( + 'funkwhale_api.federation.keys.get_key_pair', + return_value=(b'private', b'public')) expected = { 'preferred_username': 'test', 'domain': settings.FEDERATION_HOSTNAME, 'type': 'Person', 'name': '{}\'s test account'.format(settings.FEDERATION_HOSTNAME), 'manually_approves_followers': False, + 'public_key': 'public', 'url': utils.full_url( reverse( 'federation:instance-actors-detail', @@ -83,7 +88,6 @@ def test_get_test(settings, preferences): reverse( 'federation:instance-actors-outbox', kwargs={'actor': 'test'})), - 'public_key': 'public_key', 'summary': 'Bot account to test federation with {}. Send me /ping and I\'ll answer you.'.format( settings.FEDERATION_HOSTNAME), } diff --git a/api/tests/federation/test_commands.py b/api/tests/federation/test_commands.py deleted file mode 100644 index 7c5333068..000000000 --- a/api/tests/federation/test_commands.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.core.management import call_command - - -def test_generate_instance_key_pair(preferences, mocker): - mocker.patch( - 'funkwhale_api.federation.keys.get_key_pair', - return_value=(b'private', b'public')) - assert preferences['federation__public_key'] == '' - assert preferences['federation__private_key'] == '' - - call_command('generate_keys', interactive=False) - - assert preferences['federation__private_key'] == 'private' - assert preferences['federation__public_key'] == 'public' diff --git a/api/tests/music/test_permissions.py b/api/tests/music/test_permissions.py new file mode 100644 index 000000000..6cce85e08 --- /dev/null +++ b/api/tests/music/test_permissions.py @@ -0,0 +1,56 @@ +from rest_framework.views import APIView + +from funkwhale_api.federation import actors +from funkwhale_api.music import permissions + + +def test_list_permission_no_protect(anonymous_user, api_request, settings): + settings.PROTECT_AUDIO_FILES = False + view = APIView.as_view() + permission = permissions.Listen() + request = api_request.get('/') + assert permission.has_permission(request, view) is True + + +def test_list_permission_protect_anonymous( + anonymous_user, api_request, settings): + settings.PROTECT_AUDIO_FILES = True + view = APIView.as_view() + permission = permissions.Listen() + request = api_request.get('/') + assert permission.has_permission(request, view) is False + + +def test_list_permission_protect_authenticated( + factories, api_request, settings): + settings.PROTECT_AUDIO_FILES = True + user = factories['users.User']() + view = APIView.as_view() + permission = permissions.Listen() + request = api_request.get('/') + setattr(request, 'user', user) + assert permission.has_permission(request, view) is True + + +def test_list_permission_protect_not_following_actor( + factories, api_request, settings): + settings.PROTECT_AUDIO_FILES = True + actor = factories['federation.Actor']() + view = APIView.as_view() + permission = permissions.Listen() + request = api_request.get('/') + setattr(request, 'actor', actor) + assert permission.has_permission(request, view) is False + + +def test_list_permission_protect_following_actor( + factories, api_request, settings): + settings.PROTECT_AUDIO_FILES = True + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + follow = factories['federation.Follow'](target=library_actor) + view = APIView.as_view() + permission = permissions.Listen() + request = api_request.get('/') + setattr(request, 'actor', follow.actor) + + assert permission.has_permission(request, view) is True diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py index 295604616..468ea77e3 100644 --- a/api/tests/music/test_views.py +++ b/api/tests/music/test_views.py @@ -1,6 +1,8 @@ +import io import pytest from funkwhale_api.music import views +from funkwhale_api.federation import actors @pytest.mark.parametrize('param,expected', [ @@ -43,3 +45,41 @@ def test_album_view_filter_listenable( queryset = view.filter_queryset(view.get_queryset()) assert list(queryset) == expected + + +def test_can_serve_track_file_as_remote_library( + factories, authenticated_actor, settings, api_client): + settings.PROTECT_AUDIO_FILES = True + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + follow = factories['federation.Follow']( + actor=authenticated_actor, target=library_actor) + + track_file = factories['music.TrackFile']() + response = api_client.get(track_file.path) + + assert response.status_code == 200 + assert response['X-Accel-Redirect'] == "{}{}".format( + settings.PROTECT_FILES_PATH, + track_file.audio_file.url) + + +def test_can_serve_track_file_as_remote_library_deny_not_following( + factories, authenticated_actor, settings, api_client): + settings.PROTECT_AUDIO_FILES = True + track_file = factories['music.TrackFile']() + response = api_client.get(track_file.path) + + assert response.status_code == 403 + + +def test_can_proxy_remote_track( + factories, settings, api_client, r_mock): + settings.PROTECT_AUDIO_FILES = False + track_file = factories['music.TrackFile'](federation=True) + + r_mock.get(track_file.library_track.audio_url, body=io.StringIO('test')) + response = api_client.get(track_file.path) + + assert response.status_code == 200 + assert list(response.streaming_content) == [b't', b'e', b's', b't'] + assert response['Content-Type'] == track_file.library_track.audio_mimetype From e10a82060dcf22332328ceef972da9f16fe7ab91 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 7 Apr 2018 15:39:17 +0200 Subject: [PATCH 032/101] Ensure we delete existing actors to reset private and public keys --- .../federation/migrations/0003_auto_20180407_1010.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py b/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py index 38ac7cb4f..12e3d73fe 100644 --- a/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py +++ b/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py @@ -7,6 +7,16 @@ import django.utils.timezone import uuid +def delete_system_actors(apps, schema_editor): + """Revert site domain and name to default.""" + Actor = apps.get_model("federation", "Actor") + Actor.objects.filter(preferred_username__in=['test', 'library']).delete() + + +def backward(apps, schema_editor): + pass + + class Migration(migrations.Migration): dependencies = [ @@ -14,6 +24,7 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RunPython(delete_system_actors, backward), migrations.CreateModel( name='Follow', fields=[ From bf70fa1f537f38774d7e97d1ab53a16d7459bbeb Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 7 Apr 2018 15:48:47 +0200 Subject: [PATCH 033/101] Removed now useless private and public keys preferences --- api/funkwhale_api/federation/actors.py | 3 -- .../dynamic_preferences_registry.py | 28 ------------------- 2 files changed, 31 deletions(-) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index f640c9b12..54d78d9ff 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -86,7 +86,6 @@ class SystemActor(object): kwargs={'actor': self.id})) def get_instance_argument(self, id, name, summary, **kwargs): - preferences = global_preferences_registry.manager() p = { 'preferred_username': id, 'domain': settings.FEDERATION_HOSTNAME, @@ -106,8 +105,6 @@ class SystemActor(object): reverse( 'federation:instance-actors-outbox', kwargs={'actor': id})), - 'public_key': preferences['federation__public_key'], - 'private_key': preferences['federation__private_key'], 'summary': summary.format(host=settings.FEDERATION_HOSTNAME) } p.update(kwargs) diff --git a/api/funkwhale_api/federation/dynamic_preferences_registry.py b/api/funkwhale_api/federation/dynamic_preferences_registry.py index 83d0285be..c7cb015a8 100644 --- a/api/funkwhale_api/federation/dynamic_preferences_registry.py +++ b/api/funkwhale_api/federation/dynamic_preferences_registry.py @@ -4,31 +4,3 @@ from dynamic_preferences import types from dynamic_preferences.registries import global_preferences_registry federation = types.Section('federation') - - -@global_preferences_registry.register -class FederationPrivateKey(types.StringPreference): - show_in_api = False - section = federation - name = 'private_key' - default = '' - help_text = ( - 'Instance private key, used for signing federation HTTP requests' - ) - verbose_name = ( - 'Instance private key (keep it secret, do not change it)' - ) - - -@global_preferences_registry.register -class FederationPublicKey(types.StringPreference): - show_in_api = False - section = federation - name = 'public_key' - default = '' - help_text = ( - 'Instance public key, used for signing federation HTTP requests' - ) - verbose_name = ( - 'Instance public key (do not change it)' - ) From b5ff339efaed8cf8a35ff30fa4a8d30def5cd0e2 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 7 Apr 2018 16:20:34 +0200 Subject: [PATCH 034/101] We now use a proper user agent including instance version and url during outgoing requests --- api/config/settings/common.py | 10 ---------- api/funkwhale_api/common/session.py | 18 ++++++++++++++++++ api/funkwhale_api/downloader/downloader.py | 1 - api/funkwhale_api/federation/activity.py | 5 +++-- api/funkwhale_api/federation/actors.py | 5 +++-- api/funkwhale_api/federation/keys.py | 1 - api/funkwhale_api/music/views.py | 4 ++-- api/funkwhale_api/musicbrainz/client.py | 2 +- api/tests/common/test_session.py | 18 ++++++++++++++++++ changes/changelog.d/user-agent.enhancement | 2 ++ 10 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 api/funkwhale_api/common/session.py create mode 100644 api/tests/common/test_session.py create mode 100644 changes/changelog.d/user-agent.enhancement diff --git a/api/config/settings/common.py b/api/config/settings/common.py index e45f6c256..7b7f6e64c 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -149,16 +149,6 @@ FIXTURE_DIRS = ( # ------------------------------------------------------------------------------ EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend') -# MANAGER CONFIGURATION -# ------------------------------------------------------------------------------ -# See: https://docs.djangoproject.com/en/dev/ref/settings/#admins -ADMINS = ( - ("""Eliot Berriot""", 'contact@eliotberriot.om'), -) - -# See: https://docs.djangoproject.com/en/dev/ref/settings/#managers -MANAGERS = ADMINS - # DATABASE CONFIGURATION # ------------------------------------------------------------------------------ # See: https://docs.djangoproject.com/en/dev/ref/settings/#databases diff --git a/api/funkwhale_api/common/session.py b/api/funkwhale_api/common/session.py new file mode 100644 index 000000000..7f5584bd1 --- /dev/null +++ b/api/funkwhale_api/common/session.py @@ -0,0 +1,18 @@ +import requests + +from django.conf import settings + +import funkwhale_api + + +def get_user_agent(): + return 'python-requests (funkwhale/{}; +{})'.format( + funkwhale_api.__version__, + settings.FUNKWHALE_URL + ) + + +def get_session(): + s = requests.Session() + s.headers['User-Agent'] = get_user_agent() + return s diff --git a/api/funkwhale_api/downloader/downloader.py b/api/funkwhale_api/downloader/downloader.py index b35ed6f7d..7fc237b08 100644 --- a/api/funkwhale_api/downloader/downloader.py +++ b/api/funkwhale_api/downloader/downloader.py @@ -1,5 +1,4 @@ import os -import requests import json from urllib.parse import quote_plus import youtube_dl diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index 7502bd739..7d20d7f9c 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -1,9 +1,10 @@ import logging import json -import requests import requests_http_signature import uuid +from funkwhale_api.common import session + from . import models from . import signing @@ -68,7 +69,7 @@ def deliver(activity, on_behalf_of, to=[]): recipient_actor = actors.get_actor(url) logger.debug('delivering to %s', recipient_actor.inbox_url) logger.debug('activity content: %s', json.dumps(activity)) - response = requests.post( + response = session.get_session().post( auth=auth, json=activity, url=recipient_actor.inbox_url, diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index 54d78d9ff..a461eb76a 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -1,5 +1,4 @@ import logging -import requests import uuid import xml @@ -12,6 +11,8 @@ from rest_framework.exceptions import PermissionDenied from dynamic_preferences.registries import global_preferences_registry +from funkwhale_api.common import session + from . import activity from . import keys from . import models @@ -28,7 +29,7 @@ def remove_tags(text): def get_actor_data(actor_url): - response = requests.get( + response = session.get_session().get( actor_url, headers={ 'Accept': 'application/activity+json', diff --git a/api/funkwhale_api/federation/keys.py b/api/funkwhale_api/federation/keys.py index 08d4034ea..7e9d316c2 100644 --- a/api/funkwhale_api/federation/keys.py +++ b/api/funkwhale_api/federation/keys.py @@ -3,7 +3,6 @@ from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.backends import default_backend as crypto_default_backend import re -import requests import urllib.parse from . import exceptions diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 5f8fc1736..0870d9816 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -1,7 +1,6 @@ import ffmpeg import os import json -import requests import subprocess import unicodedata import urllib @@ -23,6 +22,7 @@ from rest_framework import permissions from musicbrainzngs import ResponseError from funkwhale_api.common import utils as funkwhale_utils +from funkwhale_api.common import session from funkwhale_api.federation import actors from funkwhale_api.requests.models import ImportRequest from funkwhale_api.musicbrainz import api @@ -214,7 +214,7 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet): file_extension = utils.get_ext_from_type(mt) filename = '{}.{}'.format(f.track.full_name, file_extension) auth = actors.SYSTEM_ACTORS['library'].get_request_auth() - remote_response = requests.get( + remote_response = session.get_session().get( library_track.audio_url, auth=auth, stream=True, diff --git a/api/funkwhale_api/musicbrainz/client.py b/api/funkwhale_api/musicbrainz/client.py index 049ed298c..8e7076a78 100644 --- a/api/funkwhale_api/musicbrainz/client.py +++ b/api/funkwhale_api/musicbrainz/client.py @@ -5,7 +5,7 @@ from django.conf import settings from funkwhale_api import __version__ _api = musicbrainzngs -_api.set_useragent('funkwhale', str(__version__), 'contact@eliotberriot.com') +_api.set_useragent('funkwhale', str(__version__), settings.FUNKWHALE_URL) store = memoize.djangocache.Cache('default') diff --git a/api/tests/common/test_session.py b/api/tests/common/test_session.py new file mode 100644 index 000000000..7ff1e660b --- /dev/null +++ b/api/tests/common/test_session.py @@ -0,0 +1,18 @@ +import funkwhale_api + +from funkwhale_api.common import session + + +def test_get_user_agent(settings): + settings.FUNKWHALE_URL = 'https://test.com' + 'http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)' + expected = 'python-requests (funkwhale/{}; +{})'.format( + funkwhale_api.__version__, + settings.FUNKWHALE_URL + ) + assert session.get_user_agent() == expected + + +def test_get_session(): + expected = session.get_user_agent() + assert session.get_session().headers['User-Agent'] == expected diff --git a/changes/changelog.d/user-agent.enhancement b/changes/changelog.d/user-agent.enhancement new file mode 100644 index 000000000..605e1f6a7 --- /dev/null +++ b/changes/changelog.d/user-agent.enhancement @@ -0,0 +1,2 @@ +We now use a proper user agent including instance version and url during +outgoing requests From e7b06ab5099265618177ef67f61034e2ee00c159 Mon Sep 17 00:00:00 2001 From: Bat Date: Sun, 8 Apr 2018 17:38:26 +0100 Subject: [PATCH 035/101] i18n: .po compilation --- front/build/i18n.js | 31 +++++++++++++++++++++++++++++++ front/package.json | 1 + front/static/translations/en.json | 1 + front/static/translations/fr.json | 1 + po/en.po | 13 +++++++++++++ po/fr.po | 13 +++++++++++++ 6 files changed, 60 insertions(+) create mode 100644 front/build/i18n.js create mode 100644 front/static/translations/en.json create mode 100644 front/static/translations/fr.json create mode 100644 po/en.po create mode 100644 po/fr.po diff --git a/front/build/i18n.js b/front/build/i18n.js new file mode 100644 index 000000000..d568ee0ef --- /dev/null +++ b/front/build/i18n.js @@ -0,0 +1,31 @@ +const fs = require('fs'); +const path = require('path'); +const { gettextToI18next } = require('i18next-conv'); + +// Convert .po files to i18next files +fs.readdir(path.join(__dirname, '..', '..', 'po'), (err, files) => { + if (err) { + return console.log(err) + } + + for (const file of files) { + if (file.endsWith('.po')) { + const lang = file.replace(/\.po$/, '') + const output = path.join(__dirname, '..', 'static', 'translations', `${lang}.json`) + fs.readFile(path.join(__dirname, '..', '..', 'po', file), (err, content) => { + if (err) { + return console.log(err) + } + + gettextToI18next(lang, content).then(res => { + fs.writeFile(output, res, err => { + if (err) { + console.log(err) + } + }) + }) + }) + } + } +}) + diff --git a/front/package.json b/front/package.json index d67375f7e..505254dc0 100644 --- a/front/package.json +++ b/front/package.json @@ -70,6 +70,7 @@ "friendly-errors-webpack-plugin": "^1.1.3", "html-webpack-plugin": "^2.28.0", "http-proxy-middleware": "^0.17.3", + "i18next-conv": "^6.0.0", "inject-loader": "^3.0.0", "karma": "^1.4.1", "karma-coverage": "^1.1.1", diff --git a/front/static/translations/en.json b/front/static/translations/en.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/front/static/translations/en.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/front/static/translations/fr.json b/front/static/translations/fr.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/front/static/translations/fr.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/po/en.po b/po/en.po new file mode 100644 index 000000000..32fbd1735 --- /dev/null +++ b/po/en.po @@ -0,0 +1,13 @@ +msgid "" +msgstr "" +"Project-Id-Version: Funkwhale 0.8\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-03-23 15:49-0700\n" +"PO-Revision-Date: 2018-04-08 16:58+0100\n" +"Last-Translator: baptiste \n" +"Language-Team: English\n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" diff --git a/po/fr.po b/po/fr.po new file mode 100644 index 000000000..3dbbce54c --- /dev/null +++ b/po/fr.po @@ -0,0 +1,13 @@ +msgid "" +msgstr "" +"Project-Id-Version: Funkwhale 0.8\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-03-23 15:49-0700\n" +"PO-Revision-Date: 2018-04-08 16:58+0100\n" +"Last-Translator: baptiste \n" +"Language-Team: French\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" From 3be7d689002286810800d12c9a51443d842d959b Mon Sep 17 00:00:00 2001 From: Bat Date: Mon, 9 Apr 2018 19:26:31 +0100 Subject: [PATCH 036/101] Make translation compilation actually work --- .gitignore | 1 + dev.yml | 1 + front/build/dev-server.js | 2 + front/build/i18n.js | 14 +- front/package.json | 2 +- front/yarn.lock | 7631 +++++++++++++++++++++++++++++++++++++ 6 files changed, 7647 insertions(+), 4 deletions(-) create mode 100644 front/yarn.lock diff --git a/.gitignore b/.gitignore index 8b5117034..ef5007af7 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,7 @@ api/static api/.pytest_cache # Front +front/static/translations front/node_modules/ front/dist/ front/npm-debug.log* diff --git a/dev.yml b/dev.yml index 126efa683..b214ba0bd 100644 --- a/dev.yml +++ b/dev.yml @@ -13,6 +13,7 @@ services: - "${WEBPACK_DEVSERVER_PORT-8080}:${WEBPACK_DEVSERVER_PORT-8080}" volumes: - './front:/app' + - './po:/po' postgres: env_file: diff --git a/front/build/dev-server.js b/front/build/dev-server.js index 634a6d41e..f9c389e72 100644 --- a/front/build/dev-server.js +++ b/front/build/dev-server.js @@ -14,6 +14,8 @@ var webpackConfig = process.env.NODE_ENV === 'testing' ? require('./webpack.prod.conf') : require('./webpack.dev.conf') +require('./i18n') + // default port where dev server listens for incoming traffic var port = process.env.PORT || config.dev.port var host = process.env.HOST || config.dev.host diff --git a/front/build/i18n.js b/front/build/i18n.js index d568ee0ef..8739d06d2 100644 --- a/front/build/i18n.js +++ b/front/build/i18n.js @@ -2,8 +2,14 @@ const fs = require('fs'); const path = require('path'); const { gettextToI18next } = require('i18next-conv'); +const poDir = path.join(__dirname, '..', '..', 'po') +const outDir = path.join(__dirname, '..', 'static', 'translations') +if (!fs.existsSync(outDir) || !fs.statSync(outDir).isDirectory()) { + fs.mkdirSync(outDir) +} + // Convert .po files to i18next files -fs.readdir(path.join(__dirname, '..', '..', 'po'), (err, files) => { +fs.readdir(poDir, (err, files) => { if (err) { return console.log(err) } @@ -11,8 +17,8 @@ fs.readdir(path.join(__dirname, '..', '..', 'po'), (err, files) => { for (const file of files) { if (file.endsWith('.po')) { const lang = file.replace(/\.po$/, '') - const output = path.join(__dirname, '..', 'static', 'translations', `${lang}.json`) - fs.readFile(path.join(__dirname, '..', '..', 'po', file), (err, content) => { + const output = path.join(outDir, `${lang}.json`) + fs.readFile(path.join(poDir, file), (err, content) => { if (err) { return console.log(err) } @@ -21,6 +27,8 @@ fs.readdir(path.join(__dirname, '..', '..', 'po'), (err, files) => { fs.writeFile(output, res, err => { if (err) { console.log(err) + } else { + console.log(`Wrote translation file: ${output}`) } }) }) diff --git a/front/package.json b/front/package.json index 505254dc0..301eefc24 100644 --- a/front/package.json +++ b/front/package.json @@ -18,6 +18,7 @@ "axios": "^0.17.1", "dateformat": "^2.0.0", "django-channels": "^1.1.6", + "i18next-conv": "^6.0.0", "js-logger": "^1.3.0", "jwt-decode": "^2.2.0", "lodash": "^4.17.4", @@ -70,7 +71,6 @@ "friendly-errors-webpack-plugin": "^1.1.3", "html-webpack-plugin": "^2.28.0", "http-proxy-middleware": "^0.17.3", - "i18next-conv": "^6.0.0", "inject-loader": "^3.0.0", "karma": "^1.4.1", "karma-coverage": "^1.1.1", diff --git a/front/yarn.lock b/front/yarn.lock new file mode 100644 index 000000000..7c3927b20 --- /dev/null +++ b/front/yarn.lock @@ -0,0 +1,7631 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + +abbrev@1.0.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" + +accepts@1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" + dependencies: + mime-types "~2.1.11" + negotiator "0.6.1" + +accepts@~1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" + dependencies: + mime-types "~2.1.18" + negotiator "0.6.1" + +acorn-dynamic-import@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" + dependencies: + acorn "^4.0.3" + +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + dependencies: + acorn "^3.0.4" + +acorn@^3.0.4: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + +acorn@^4.0.3: + version "4.0.13" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" + +acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0: + version "5.5.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" + +after@0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" + +agent-base@2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-2.1.1.tgz#d6de10d5af6132d5bd692427d46fc538539094c7" + dependencies: + extend "~3.0.0" + semver "~5.0.1" + +ajv-keywords@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" + +ajv-keywords@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be" + +ajv@^4.7.0, ajv@^4.9.1: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +ajv@^5.0.0, ajv@^5.1.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ajv@^6.1.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.4.0.tgz#d3aff78e9277549771daf0164cff48482b754fc6" + dependencies: + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + uri-js "^3.0.2" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-escapes@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + +ansi-html@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + dependencies: + color-convert "^1.9.0" + +anymatch@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" + dependencies: + micromatch "^2.1.5" + normalize-path "^2.0.0" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +aproba@^1.0.3, aproba@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + +are-we-there-yet@~1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + +arr-flatten@^1.0.1, arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +array-slice@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + +arraybuffer.slice@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca" + +arrify@^1.0.0, arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +asn1.js@^4.0.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +assertion-error@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.0.tgz#c7f85438fdd466bc7ca16ab90c81513797a5d23b" + +assertion-error@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + +ast-types@0.x.x: + version "0.11.3" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.3.tgz#c20757fe72ee71278ea0ff3d87e5c2ca30d9edf8" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +async-foreach@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" + +async-limiter@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" + +async@1.x, async@^1.4.0: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@^2.0.0, async@^2.1.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" + dependencies: + lodash "^4.14.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +atob@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.0.tgz#ab2b150e51d7b122b9efc8d7340c06b6c41076bc" + +autoprefixer@^6.3.1, autoprefixer@^6.7.2: + version "6.7.7" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" + dependencies: + browserslist "^1.7.6" + caniuse-db "^1.0.30000634" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^5.2.16" + postcss-value-parser "^3.2.3" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.2.1, aws4@^1.6.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" + +axios@^0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d" + dependencies: + follow-redirects "^1.2.5" + is-buffer "^1.1.5" + +babel-code-frame@^6.16.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-core@^6.22.1, babel-core@^6.26.0, babel-core@~6: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" + dependencies: + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.0" + debug "^2.6.8" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.7" + slash "^1.0.0" + source-map "^0.5.6" + +babel-eslint@^7.1.1: + version "7.2.3" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-7.2.3.tgz#b2fe2d80126470f5c19442dc757253a897710827" + dependencies: + babel-code-frame "^6.22.0" + babel-traverse "^6.23.1" + babel-types "^6.23.0" + babylon "^6.17.0" + +babel-generator@^6.18.0, babel-generator@^6.26.0: + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.7" + trim-right "^1.0.1" + +babel-helper-bindify-decorators@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz#14c19e5f142d7b47f19a52431e52b1ccbc40a330" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" + dependencies: + babel-helper-explode-assignable-expression "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-call-delegate@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-define-map@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-explode-assignable-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-explode-class@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz#7dc2a3910dee007056e1e31d640ced3d54eaa9eb" + dependencies: + babel-helper-bindify-decorators "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-function-name@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" + dependencies: + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-get-function-arity@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-hoist-variables@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-optimise-call-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-regex@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" + dependencies: + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-remap-async-to-generator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-replace-supers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" + dependencies: + babel-helper-optimise-call-expression "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-loader@7: + version "7.1.4" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.4.tgz#e3463938bd4e6d55d1c174c5485d406a188ed015" + dependencies: + find-cache-dir "^1.0.0" + loader-utils "^1.0.2" + mkdirp "^0.5.1" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-check-es2015-constants@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-istanbul@^4.1.1: + version "4.1.6" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" + dependencies: + babel-plugin-syntax-object-rest-spread "^6.13.0" + find-up "^2.1.0" + istanbul-lib-instrument "^1.10.1" + test-exclude "^4.2.1" + +babel-plugin-syntax-async-functions@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" + +babel-plugin-syntax-async-generators@^6.5.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a" + +babel-plugin-syntax-class-properties@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" + +babel-plugin-syntax-decorators@^6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b" + +babel-plugin-syntax-dynamic-import@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" + +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + +babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + +babel-plugin-syntax-trailing-function-commas@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" + +babel-plugin-transform-async-generator-functions@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz#f058900145fd3e9907a6ddf28da59f215258a5db" + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-generators "^6.5.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-async-to-generator@^6.22.0, babel-plugin-transform-async-to-generator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-functions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-class-properties@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" + dependencies: + babel-helper-function-name "^6.24.1" + babel-plugin-syntax-class-properties "^6.8.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-decorators@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d" + dependencies: + babel-helper-explode-class "^6.24.1" + babel-plugin-syntax-decorators "^6.13.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-arrow-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoping@^6.23.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" + dependencies: + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-plugin-transform-es2015-classes@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" + dependencies: + babel-helper-define-map "^6.24.1" + babel-helper-function-name "^6.24.1" + babel-helper-optimise-call-expression "^6.24.1" + babel-helper-replace-supers "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-computed-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-destructuring@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-duplicate-keys@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-for-of@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-function-name@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" + dependencies: + babel-plugin-transform-es2015-modules-commonjs "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" + +babel-plugin-transform-es2015-modules-systemjs@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-umd@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" + dependencies: + babel-plugin-transform-es2015-modules-amd "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-object-super@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" + dependencies: + babel-helper-replace-supers "^6.24.1" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-parameters@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" + dependencies: + babel-helper-call-delegate "^6.24.1" + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-shorthand-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-spread@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-sticky-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-template-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-typeof-symbol@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-unicode-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + regexpu-core "^2.0.0" + +babel-plugin-transform-exponentiation-operator@^6.22.0, babel-plugin-transform-exponentiation-operator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" + dependencies: + babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" + babel-plugin-syntax-exponentiation-operator "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-object-rest-spread@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.26.0" + +babel-plugin-transform-regenerator@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" + dependencies: + regenerator-transform "^0.10.0" + +babel-plugin-transform-runtime@^6.22.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz#88490d446502ea9b8e7efb0fe09ec4d99479b1ee" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-preset-env@^1.3.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.1.tgz#a18b564cc9b9afdf4aae57ae3c1b0d99188e6f48" + dependencies: + babel-plugin-check-es2015-constants "^6.22.0" + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-to-generator "^6.22.0" + babel-plugin-transform-es2015-arrow-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoping "^6.23.0" + babel-plugin-transform-es2015-classes "^6.23.0" + babel-plugin-transform-es2015-computed-properties "^6.22.0" + babel-plugin-transform-es2015-destructuring "^6.23.0" + babel-plugin-transform-es2015-duplicate-keys "^6.22.0" + babel-plugin-transform-es2015-for-of "^6.23.0" + babel-plugin-transform-es2015-function-name "^6.22.0" + babel-plugin-transform-es2015-literals "^6.22.0" + babel-plugin-transform-es2015-modules-amd "^6.22.0" + babel-plugin-transform-es2015-modules-commonjs "^6.23.0" + babel-plugin-transform-es2015-modules-systemjs "^6.23.0" + babel-plugin-transform-es2015-modules-umd "^6.23.0" + babel-plugin-transform-es2015-object-super "^6.22.0" + babel-plugin-transform-es2015-parameters "^6.23.0" + babel-plugin-transform-es2015-shorthand-properties "^6.22.0" + babel-plugin-transform-es2015-spread "^6.22.0" + babel-plugin-transform-es2015-sticky-regex "^6.22.0" + babel-plugin-transform-es2015-template-literals "^6.22.0" + babel-plugin-transform-es2015-typeof-symbol "^6.23.0" + babel-plugin-transform-es2015-unicode-regex "^6.22.0" + babel-plugin-transform-exponentiation-operator "^6.22.0" + babel-plugin-transform-regenerator "^6.22.0" + browserslist "^2.1.2" + invariant "^2.2.2" + semver "^5.3.0" + +babel-preset-stage-2@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1" + dependencies: + babel-plugin-syntax-dynamic-import "^6.18.0" + babel-plugin-transform-class-properties "^6.24.1" + babel-plugin-transform-decorators "^6.24.1" + babel-preset-stage-3 "^6.24.1" + +babel-preset-stage-3@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz#836ada0a9e7a7fa37cb138fb9326f87934a48395" + dependencies: + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-generator-functions "^6.24.1" + babel-plugin-transform-async-to-generator "^6.24.1" + babel-plugin-transform-exponentiation-operator "^6.24.1" + babel-plugin-transform-object-rest-spread "^6.22.0" + +babel-register@^6.22.0, babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" + dependencies: + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + home-or-tmp "^2.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + source-map-support "^0.4.15" + +babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@^6.17.0, babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + +backo2@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + +balanced-match@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-arraybuffer@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" + +base64-js@^1.0.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.3.tgz#fb13668233d9614cf5fb4bce95a9ba4096cdf801" + +base64id@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6" + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +better-assert@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" + dependencies: + callsite "1.0.0" + +bfj-node4@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/bfj-node4/-/bfj-node4-5.3.1.tgz#e23d8b27057f1d0214fc561142ad9db998f26830" + dependencies: + bluebird "^3.5.1" + check-types "^7.3.0" + tryer "^1.0.0" + +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + +binary-extensions@^1.0.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" + +blob@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +bluebird@^3.0.5, bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.4.7, bluebird@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + +body-parser@1.18.2, body-parser@^1.16.1: + version "1.18.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.1" + http-errors "~1.6.2" + iconv-lite "0.4.19" + on-finished "~2.3.0" + qs "6.5.1" + raw-body "2.3.2" + type-is "~1.6.15" + +boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.x.x" + +brace-expansion@^1.0.0, brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^0.1.2: + version "0.1.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6" + dependencies: + expand-range "^0.1.0" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +braces@^2.3.0, braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browser-stdout@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + dependencies: + pako "~1.0.5" + +browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: + version "1.7.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" + dependencies: + caniuse-db "^1.0.30000639" + electron-to-chromium "^1.2.7" + +browserslist@^2.1.2: + version "2.11.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.11.3.tgz#fe36167aed1bbcde4827ebfe71347a2cc70b99b2" + dependencies: + caniuse-lite "^1.0.30000792" + electron-to-chromium "^1.3.30" + +buffer-from@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531" + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + +cacache@^10.0.4: + version "10.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" + dependencies: + bluebird "^3.5.1" + chownr "^1.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.1" + mississippi "^2.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.2" + ssri "^5.2.4" + unique-filename "^1.1.0" + y18n "^4.0.0" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + dependencies: + callsites "^0.2.0" + +callsite@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + +camel-case@3.0.x: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" + dependencies: + no-case "^2.2.0" + upper-case "^1.1.1" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +caniuse-api@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c" + dependencies: + browserslist "^1.3.6" + caniuse-db "^1.0.30000529" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: + version "1.0.30000824" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000824.tgz#bba3ff425296e04caa37fe426259206a7056551b" + +caniuse-lite@^1.0.30000792: + version "1.0.30000824" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000824.tgz#de3bc1ba0bff4937302f8cb2a8632a8cc1c07f9a" + +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chai-nightwatch@~0.1.x: + version "0.1.1" + resolved "https://registry.yarnpkg.com/chai-nightwatch/-/chai-nightwatch-0.1.1.tgz#1ca56de768d3c0868fe7fc2f4d32c2fe894e6be9" + dependencies: + assertion-error "1.0.0" + deep-eql "0.1.3" + +chai@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247" + dependencies: + assertion-error "^1.0.1" + deep-eql "^0.1.3" + type-detect "^1.0.0" + +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65" + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +check-types@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d" + +chokidar@^1.4.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +chokidar@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176" + dependencies: + anymatch "^2.0.0" + async-each "^1.0.0" + braces "^2.3.0" + glob-parent "^3.1.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^2.1.1" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + upath "^1.0.0" + optionalDependencies: + fsevents "^1.1.2" + +chownr@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + +chromedriver@^2.27.2: + version "2.37.0" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-2.37.0.tgz#e7867c8236f6bb89024737bbffc9a4b33ded658b" + dependencies: + del "^3.0.0" + extract-zip "^1.6.5" + kew "^0.7.0" + mkdirp "^0.5.1" + request "^2.83.0" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + +clap@^1.0.9: + version "1.2.3" + resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51" + dependencies: + chalk "^1.1.3" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +clean-css@4.1.x: + version "4.1.11" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.11.tgz#2ecdf145aba38f54740f26cefd0ff3e03e125d6a" + dependencies: + source-map "0.5.x" + +cli-cursor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + dependencies: + restore-cursor "^2.0.0" + +cli-spinners@^1.0.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +cliui@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.0.0.tgz#743d4650e05f36d1ed2575b59638d87322bfbbcc" + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +clone-deep@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" + dependencies: + for-own "^1.0.0" + is-plain-object "^2.0.4" + kind-of "^6.0.0" + shallow-clone "^1.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +co@~3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/co/-/co-3.0.6.tgz#1445f226c5eb956138e68c9ac30167ea7d2e6bda" + +coa@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" + dependencies: + q "^1.1.2" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.3.0, color-convert@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" + dependencies: + color-name "^1.1.1" + +color-name@^1.0.0, color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +color-string@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" + dependencies: + color-name "^1.0.0" + +color@^0.11.0: + version "0.11.4" + resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" + dependencies: + clone "^1.0.2" + color-convert "^1.3.0" + color-string "^0.3.0" + +colormin@^1.0.5: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133" + dependencies: + color "^0.11.0" + css-color-names "0.0.4" + has "^1.0.1" + +colors@^1.1.0, colors@^1.1.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.1.tgz#f4a3d302976aaf042356ba1ade3b1a2c62d9d794" + +colors@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + +combine-lists@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/combine-lists/-/combine-lists-1.0.1.tgz#458c07e09e0d900fc28b70a3fec2dacd1d2cb7f6" + dependencies: + lodash "^4.5.0" + +combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + dependencies: + delayed-stream "~1.0.0" + +commander@2.15.x, commander@^2.11.0, commander@^2.13.0, commander@^2.9.0, commander@~2.15.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + +commander@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + dependencies: + graceful-readlink ">= 1.0.0" + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + +component-bind@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" + +component-emitter@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.1.2.tgz#296594f2753daa63996d2af08d15a95116c9aec3" + +component-emitter@1.2.1, component-emitter@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + +component-inherit@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concat-stream@^1.5.0, concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +config-chain@~1.1.5: + version "1.1.11" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2" + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +connect-history-api-fallback@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a" + +connect@^3.6.0: + version "3.6.6" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.6.tgz#09eff6c55af7236e137135a72574858b6786f524" + dependencies: + debug "2.6.9" + finalhandler "1.1.0" + parseurl "~1.3.2" + utils-merge "1.0.1" + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +consolidate@^0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.5.tgz#5a25047bc76f73072667c8cb52c989888f494c63" + dependencies: + bluebird "^3.1.1" + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + +convert-source-map@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + +copy-webpack-plugin@^4.0.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.1.tgz#fc4f68f4add837cc5e13d111b20715793225d29c" + dependencies: + cacache "^10.0.4" + find-cache-dir "^1.0.0" + globby "^7.1.1" + is-glob "^4.0.0" + loader-utils "^1.1.0" + minimatch "^3.0.4" + p-limit "^1.0.0" + serialize-javascript "^1.4.0" + +core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0: + version "2.5.4" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.4.tgz#f2c8bf181f2a80b92f360121429ce63a2f0aeae0" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" + dependencies: + is-directory "^0.3.1" + js-yaml "^3.4.3" + minimist "^1.2.0" + object-assign "^4.1.0" + os-homedir "^1.0.1" + parse-json "^2.2.0" + require-from-string "^1.1.0" + +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^2.0.0" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-env@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-4.0.0.tgz#16083862d08275a4628b0b243b121bedaa55dd80" + dependencies: + cross-spawn "^5.1.0" + is-windows "^1.0.0" + +cross-spawn@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^5.0.1, cross-spawn@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +css-color-names@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + +css-loader@^0.28.0: + version "0.28.11" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.11.tgz#c3f9864a700be2711bb5a2462b2389b1a392dab7" + dependencies: + babel-code-frame "^6.26.0" + css-selector-tokenizer "^0.7.0" + cssnano "^3.10.0" + icss-utils "^2.1.0" + loader-utils "^1.0.2" + lodash.camelcase "^4.3.0" + object-assign "^4.1.1" + postcss "^5.0.6" + postcss-modules-extract-imports "^1.2.0" + postcss-modules-local-by-default "^1.2.0" + postcss-modules-scope "^1.1.0" + postcss-modules-values "^1.3.0" + postcss-value-parser "^3.3.0" + source-list-map "^2.0.0" + +css-select@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-selector-tokenizer@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" + dependencies: + cssesc "^0.1.0" + fastparse "^1.1.1" + regexpu-core "^1.0.0" + +css-what@2.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd" + +cssesc@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" + +cssnano@^3.10.0, cssnano@^3.4.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38" + dependencies: + autoprefixer "^6.3.1" + decamelize "^1.1.2" + defined "^1.0.0" + has "^1.0.1" + object-assign "^4.0.1" + postcss "^5.0.14" + postcss-calc "^5.2.0" + postcss-colormin "^2.1.8" + postcss-convert-values "^2.3.4" + postcss-discard-comments "^2.0.4" + postcss-discard-duplicates "^2.0.1" + postcss-discard-empty "^2.0.1" + postcss-discard-overridden "^0.1.1" + postcss-discard-unused "^2.2.1" + postcss-filter-plugins "^2.0.0" + postcss-merge-idents "^2.1.5" + postcss-merge-longhand "^2.0.1" + postcss-merge-rules "^2.0.3" + postcss-minify-font-values "^1.0.2" + postcss-minify-gradients "^1.0.1" + postcss-minify-params "^1.0.4" + postcss-minify-selectors "^2.0.4" + postcss-normalize-charset "^1.1.0" + postcss-normalize-url "^3.0.7" + postcss-ordered-values "^2.1.0" + postcss-reduce-idents "^2.2.2" + postcss-reduce-initial "^1.0.0" + postcss-reduce-transforms "^1.0.3" + postcss-svgo "^2.1.1" + postcss-unique-selectors "^2.0.2" + postcss-value-parser "^3.2.3" + postcss-zindex "^2.0.1" + +csso@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85" + dependencies: + clap "^1.0.9" + source-map "^0.5.3" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + +cyclist@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" + +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + dependencies: + es5-ext "^0.10.9" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +data-uri-to-buffer@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +dateformat@^1.0.6: + version "1.0.12" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" + dependencies: + get-stdin "^4.0.1" + meow "^3.3.0" + +dateformat@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" + +de-indent@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" + +debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +debug@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c" + dependencies: + ms "0.7.2" + +debug@2.6.8: + version "2.6.8" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + +deep-eql@0.1.3, deep-eql@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" + dependencies: + type-detect "0.1.1" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +deepmerge@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.0.tgz#511a54fff405fc346f0240bb270a3e9533a31102" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + +degenerator@~1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-1.0.4.tgz#fcf490a37ece266464d9cc431ab98c5819ced095" + dependencies: + ast-types "0.x.x" + escodegen "1.x.x" + esprima "3.x.x" + +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +del@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +depd@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + +depd@~1.1.1, depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +desandro-matches-selector@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/desandro-matches-selector/-/desandro-matches-selector-2.0.2.tgz#717beed4dc13e7d8f3762f707a6d58a6774218e1" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + +diff@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" + +diff@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" + +diff@^3.1.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dir-glob@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" + dependencies: + arrify "^1.0.1" + path-type "^3.0.0" + +django-channels@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/django-channels/-/django-channels-1.1.6.tgz#51cdf9d818be3d098ef49a49356f2a7a78499180" + dependencies: + reconnecting-websocket "^3.0.3" + +doctrine@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + dependencies: + esutils "^2.0.2" + +dom-converter@~0.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b" + dependencies: + utila "~0.3" + +dom-serialize@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +dom-serializer@0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" + dependencies: + domelementtype "~1.1.1" + entities "~1.1.1" + +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + +domelementtype@1, domelementtype@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" + +domelementtype@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" + +domhandler@2.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.1.0.tgz#d2646f5e57f6c3bab11cf6cb05d3c0acf7412594" + dependencies: + domelementtype "1" + +domhandler@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259" + dependencies: + domelementtype "1" + +domutils@1.1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485" + dependencies: + domelementtype "1" + +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + dependencies: + dom-serializer "0" + domelementtype "1" + +duplexer@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + +duplexify@^3.4.2, duplexify@^3.5.3: + version "3.5.4" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.4.tgz#4bb46c1796eabebeec4ca9a2e66b808cb7a3d8b4" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +editorconfig@^0.13.2: + version "0.13.3" + resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.13.3.tgz#e5219e587951d60958fd94ea9a9a008cdeff1b34" + dependencies: + bluebird "^3.0.5" + commander "^2.9.0" + lru-cache "^3.2.0" + semver "^5.1.0" + sigmund "^1.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +ejs@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-0.7.1.tgz#6dab41cbdee572986db328a2a8d22040f20fba7c" + +ejs@2.5.7: + version "2.5.7" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a" + +ejs@^2.5.7: + version "2.5.8" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.8.tgz#2ab6954619f225e6193b7ac5f7c39c48fefe4380" + +electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30: + version "1.3.42" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.42.tgz#95c33bf01d0cc405556aec899fe61fd4d76ea0f9" + +elliptic@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + +encodeurl@~1.0.1, encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + +encoding@^0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + dependencies: + once "^1.4.0" + +engine.io-client@1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.3.tgz#1798ed93451246453d4c6f635d7a201fe940d5ab" + dependencies: + component-emitter "1.2.1" + component-inherit "0.0.3" + debug "2.3.3" + engine.io-parser "1.3.2" + has-cors "1.1.0" + indexof "0.0.1" + parsejson "0.0.3" + parseqs "0.0.5" + parseuri "0.0.5" + ws "1.1.2" + xmlhttprequest-ssl "1.5.3" + yeast "0.1.2" + +engine.io-parser@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-1.3.2.tgz#937b079f0007d0893ec56d46cb220b8cb435220a" + dependencies: + after "0.8.2" + arraybuffer.slice "0.0.6" + base64-arraybuffer "0.1.5" + blob "0.0.4" + has-binary "0.1.7" + wtf-8 "1.0.0" + +engine.io@1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.3.tgz#8de7f97895d20d39b85f88eeee777b2bd42b13d4" + dependencies: + accepts "1.3.3" + base64id "1.0.0" + cookie "0.3.1" + debug "2.3.3" + engine.io-parser "1.3.2" + ws "1.1.2" + +enhanced-resolve@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + object-assign "^4.0.1" + tapable "^0.2.7" + +ent@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + +entities@^1.1.1, entities@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" + +errno@^0.1.3: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" + dependencies: + prr "~1.0.1" + +error-ex@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +error-stack-parser@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.1.tgz#a3202b8fb03114aa9b40a0e3669e48b2b65a010a" + dependencies: + stackframe "^1.0.3" + +es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.42" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.42.tgz#8c07dd33af04d5dcd1310b5cef13bea63a89ba8d" + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.1" + next-tick "1" + +es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-map@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + +es6-promise@^4.0.3, es6-promise@^4.2.2: + version "4.2.4" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" + +es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + +es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escodegen@1.8.x: + version "1.8.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" + dependencies: + esprima "^2.7.1" + estraverse "^1.9.1" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.2.0" + +escodegen@1.x.x: + version "1.9.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2" + dependencies: + esprima "^3.1.3" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-config-standard@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-6.2.1.tgz#d3a68aafc7191639e7ee441e7348739026354292" + +eslint-friendly-formatter@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/eslint-friendly-formatter/-/eslint-friendly-formatter-2.0.7.tgz#657f95a19af4989636afebb1cc9de6cebbd088ee" + dependencies: + chalk "^1.0.0" + extend "^3.0.0" + minimist "^1.2.0" + text-table "^0.2.0" + +eslint-loader@^1.7.1: + version "1.9.0" + resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-1.9.0.tgz#7e1be9feddca328d3dcfaef1ad49d5beffe83a13" + dependencies: + loader-fs-cache "^1.0.0" + loader-utils "^1.0.2" + object-assign "^4.0.1" + object-hash "^1.1.4" + rimraf "^2.6.1" + +eslint-plugin-html@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-2.0.3.tgz#7c89883ab0c85fa5d28b666a14a4e906aa90b897" + dependencies: + htmlparser2 "^3.8.2" + +eslint-plugin-promise@^3.4.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.7.0.tgz#f4bde5c2c77cdd69557a8f69a24d1ad3cfc9e67e" + +eslint-plugin-standard@^2.0.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-2.3.1.tgz#6765bd2a6d9ecdc7bdf1b145ae4bb30e2b7b86f8" + +eslint@^3.19.0: + version "3.19.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc" + dependencies: + babel-code-frame "^6.16.0" + chalk "^1.1.3" + concat-stream "^1.5.2" + debug "^2.1.1" + doctrine "^2.0.0" + escope "^3.6.0" + espree "^3.4.0" + esquery "^1.0.0" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + glob "^7.0.3" + globals "^9.14.0" + ignore "^3.2.0" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + levn "^0.3.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.7.5" + strip-bom "^3.0.0" + strip-json-comments "~2.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +espree@^3.4.0: + version "3.5.4" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" + dependencies: + acorn "^5.5.0" + acorn-jsx "^3.0.0" + +esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esprima@3.x.x, esprima@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +esquery@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + dependencies: + estraverse "^4.1.0" + +estraverse@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + +ev-emitter@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ev-emitter/-/ev-emitter-1.1.1.tgz#8f18b0ce5c76a5d18017f71c0a795c65b9138f2a" + +event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + dependencies: + d "1" + es5-ext "~0.10.14" + +eventemitter3@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + +events@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +eventsource-polyfill@^0.9.6: + version "0.9.6" + resolved "https://registry.yarnpkg.com/eventsource-polyfill/-/eventsource-polyfill-0.9.6.tgz#10e0d187f111b167f28fdab918843ce7d818f13c" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + +expand-braces@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea" + dependencies: + array-slice "^0.2.3" + array-unique "^0.2.1" + braces "^0.1.2" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expand-range@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-0.1.1.tgz#4cb8eda0993ca56fa4f41fc42f3cbb4ccadff044" + dependencies: + is-number "^0.1.1" + repeat-string "^0.2.2" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +express@^4.14.1, express@^4.16.2: + version "4.16.3" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53" + dependencies: + accepts "~1.3.5" + array-flatten "1.1.1" + body-parser "1.18.2" + content-disposition "0.5.2" + content-type "~1.0.4" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.1.1" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + proxy-addr "~2.0.3" + qs "6.5.1" + range-parser "~1.2.0" + safe-buffer "5.1.1" + send "0.16.2" + serve-static "1.13.2" + setprototypeof "1.1.0" + statuses "~1.4.0" + type-is "~1.6.16" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@3, extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extract-text-webpack-plugin@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-2.1.2.tgz#756ef4efa8155c3681833fbc34da53b941746d6c" + dependencies: + async "^2.1.2" + loader-utils "^1.0.2" + schema-utils "^0.3.0" + webpack-sources "^1.0.1" + +extract-zip@^1.6.5: + version "1.6.6" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c" + dependencies: + concat-stream "1.6.0" + debug "2.6.9" + mkdirp "0.5.0" + yauzl "2.4.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + +fastparse@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" + +fd-slicer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" + dependencies: + pend "~1.2.0" + +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +file-loader@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.11.2.tgz#4ff1df28af38719a6098093b88c82c71d1794a34" + dependencies: + loader-utils "^1.0.2" + +file-uri-to-path@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +filesize@^3.5.11: + version "3.6.1" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +finalhandler@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" + dependencies: + debug "2.6.9" + encodeurl "~1.0.1" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.3.1" + unpipe "~1.0.0" + +finalhandler@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.4.0" + unpipe "~1.0.0" + +find-cache-dir@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" + dependencies: + commondir "^1.0.1" + mkdirp "^0.5.1" + pkg-dir "^1.0.0" + +find-cache-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^2.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +fizzy-ui-utils@^2.0.0: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fizzy-ui-utils/-/fizzy-ui-utils-2.0.7.tgz#7df45dcc4eb374a08b65d39bb9a4beedf7330505" + dependencies: + desandro-matches-selector "^2.0.0" + +flat-cache@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481" + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +flatten@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" + +flush-write-stream@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.4" + +follow-redirects@^1.2.5: + version "1.4.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa" + dependencies: + debug "^3.1.0" + +for-in@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" + +for-in@^1.0.1, for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + dependencies: + for-in "^1.0.1" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +form-data@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + +formatio@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb" + dependencies: + samsam "1.x" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + +friendly-errors-webpack-plugin@^1.1.3: + version "1.7.0" + resolved "https://registry.yarnpkg.com/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.7.0.tgz#efc86cbb816224565861a1be7a9d84d0aafea136" + dependencies: + chalk "^1.1.3" + error-stack-parser "^2.0.0" + string-width "^2.0.0" + +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-extra@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0, fsevents@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" + dependencies: + nan "^2.3.0" + node-pre-gyp "^0.6.39" + +fstream-ignore@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +ftp@~0.3.10: + version "0.3.10" + resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" + dependencies: + readable-stream "1.1.x" + xregexp "2.0.0" + +function-bind@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gaze@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105" + dependencies: + globule "^1.0.0" + +generate-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + dependencies: + is-property "^1.0.0" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-size@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/get-size/-/get-size-2.0.3.tgz#54a1d0256b20ea7ac646516756202769941ad2ef" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +get-uri@2: + version "2.0.1" + resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-2.0.1.tgz#dbdcacacd8c608a38316869368117697a1631c59" + dependencies: + data-uri-to-buffer "1" + debug "2" + extend "3" + file-uri-to-path "1" + ftp "~0.3.10" + readable-stream "2" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +gettext-parser@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/gettext-parser/-/gettext-parser-1.3.1.tgz#74b7a99e4b5fa8daab11fa515e8a582480448a12" + dependencies: + encoding "^0.1.12" + safe-buffer "^5.1.1" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob@7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^5.0.15: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^9.14.0, globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globby@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" + dependencies: + array-union "^1.0.1" + dir-glob "^2.0.0" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + +globule@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" + dependencies: + glob "~7.1.1" + lodash "~4.17.4" + minimatch "~3.0.2" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + +growl@1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" + +gzip-size@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-4.1.0.tgz#8ae096257eabe7d69c45be2b67c448124ffb517c" + dependencies: + duplexer "^0.1.1" + pify "^3.0.0" + +handlebars@^4.0.1: + version "4.0.11" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" + dependencies: + async "^1.4.0" + optimist "^0.6.1" + source-map "^0.4.4" + optionalDependencies: + uglify-js "^2.6" + +har-schema@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + +har-validator@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" + dependencies: + ajv "^4.9.1" + har-schema "^1.0.5" + +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-binary@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.7.tgz#68e61eb16210c9545a0a5cce06a873912fe1e68c" + dependencies: + isarray "0.0.1" + +has-cors@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" + dependencies: + inherits "^2.0.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash-sum@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.0" + +hasha@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1" + dependencies: + is-stream "^1.0.1" + pinkie-promise "^2.0.0" + +hawk@3.1.3, hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + +he@1.1.1, he@1.1.x, he@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +hoek@4.x.x: + version "4.2.1" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +hosted-git-info@^2.1.4: + version "2.6.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" + +html-comment-regex@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" + +html-entities@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" + +html-minifier@^3.2.3: + version "3.5.14" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.14.tgz#88653b24b344274e3e3d7052f1541ebea054ac60" + dependencies: + camel-case "3.0.x" + clean-css "4.1.x" + commander "2.15.x" + he "1.1.x" + param-case "2.1.x" + relateurl "0.2.x" + uglify-js "3.3.x" + +html-webpack-plugin@^2.28.0: + version "2.30.1" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.30.1.tgz#7f9c421b7ea91ec460f56527d78df484ee7537d5" + dependencies: + bluebird "^3.4.7" + html-minifier "^3.2.3" + loader-utils "^0.2.16" + lodash "^4.17.3" + pretty-error "^2.0.2" + toposort "^1.0.0" + +htmlparser2@^3.8.2: + version "3.9.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" + dependencies: + domelementtype "^1.3.0" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^2.0.2" + +htmlparser2@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe" + dependencies: + domelementtype "1" + domhandler "2.1" + domutils "1.1" + readable-stream "1.0" + +http-errors@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" + dependencies: + depd "1.1.1" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-proxy-agent@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-1.0.0.tgz#cc1ce38e453bf984a0f7702d2dd59c73d081284a" + dependencies: + agent-base "2" + debug "2" + extend "3" + +http-proxy-middleware@^0.17.3: + version "0.17.4" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833" + dependencies: + http-proxy "^1.16.2" + is-glob "^3.1.0" + lodash "^4.17.2" + micromatch "^2.3.11" + +http-proxy@^1.13.0, http-proxy@^1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742" + dependencies: + eventemitter3 "1.x.x" + requires-port "1.x.x" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + +https-proxy-agent@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6" + dependencies: + agent-base "2" + debug "2" + extend "3" + +i18next-conv@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/i18next-conv/-/i18next-conv-6.0.0.tgz#875a27bfb069db894f7b0a1484e0052100bc9383" + dependencies: + bluebird "^3.5.1" + chalk "^2.1.0" + commander "^2.11.0" + gettext-parser "^1.3.0" + mkdirp "^0.5.1" + node-gettext "^2.0.0" + +iconv-lite@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + +iconv-lite@~0.4.13: + version "0.4.21" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.21.tgz#c47f8733d02171189ebc4a400f3218d348094798" + dependencies: + safer-buffer "^2.1.0" + +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + +icss-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962" + dependencies: + postcss "^6.0.1" + +ieee754@^1.1.4: + version "1.1.11" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455" + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + +ignore@^3.2.0, ignore@^3.3.5: + version "3.3.7" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" + +imagesloaded@latest: + version "4.1.4" + resolved "https://registry.yarnpkg.com/imagesloaded/-/imagesloaded-4.1.4.tgz#1376efcd162bb768c34c3727ac89cc04051f3cc7" + dependencies: + ev-emitter "^1.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +in-publish@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +ini@^1.3.4, ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + +inject-loader@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/inject-loader/-/inject-loader-3.0.1.tgz#844a4596150cf6b864c2f79806d40a2caee4c2c7" + dependencies: + babel-core "~6" + +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +interpret@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" + +invariant@^2.2.2: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + dependencies: + loose-envify "^1.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +ip@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.0.1.tgz#c7e356cdea225ae71b36d70f2e71a92ba4e42590" + +ip@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + +ipaddr.js@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.6.0.tgz#e3fa357b773da619f26e95f049d055c72796f86b" + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + dependencies: + kind-of "^6.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + dependencies: + is-extglob "^2.1.1" + +is-my-ip-valid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" + +is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4: + version "2.17.2" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + is-my-ip-valid "^1.0.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + +is-number@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-number@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + +is-odd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24" + dependencies: + is-number "^4.0.0" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + dependencies: + path-is-inside "^1.0.1" + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + dependencies: + isobject "^3.0.1" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-property@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + +is-resolvable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + +is-stream@^1.0.1, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-svg@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" + dependencies: + html-comment-regex "^1.1.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +is-windows@^1.0.0, is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isbinaryfile@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +istanbul-lib-coverage@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341" + +istanbul-lib-instrument@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz#724b4b6caceba8692d3f1f9d0727e279c401af7b" + dependencies: + babel-generator "^6.18.0" + babel-template "^6.16.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + babylon "^6.18.0" + istanbul-lib-coverage "^1.2.0" + semver "^5.3.0" + +istanbul@^0.4.0: + version "0.4.5" + resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.4.5.tgz#65c7d73d4c4da84d4f3ac310b918fb0b8033733b" + dependencies: + abbrev "1.0.x" + async "1.x" + escodegen "1.8.x" + esprima "2.7.x" + glob "^5.0.15" + handlebars "^4.0.1" + js-yaml "3.x" + mkdirp "0.5.x" + nopt "3.x" + once "1.x" + resolve "1.1.x" + supports-color "^3.1.0" + which "^1.1.1" + wordwrap "^1.0.0" + +jquery@x.*: + version "3.3.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca" + +js-base64@^2.1.8, js-base64@^2.1.9: + version "2.4.3" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582" + +js-beautify@^1.6.3: + version "1.7.5" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.7.5.tgz#69d9651ef60dbb649f65527b53674950138a7919" + dependencies: + config-chain "~1.1.5" + editorconfig "^0.13.2" + mkdirp "~0.5.0" + nopt "~3.0.1" + +js-logger@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/js-logger/-/js-logger-1.4.1.tgz#f0230dc5e84e120f213d6e5a6b767a913d290335" + +js-tokens@^3.0.0, js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.1: + version "3.11.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@~3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" + dependencies: + argparse "^1.0.7" + esprima "^2.6.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-loader@^0.5.4: + version "0.5.7" + resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json3@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" + +json5@^0.5.0, json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsonpointer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jwt-decode@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" + +karma-coverage@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/karma-coverage/-/karma-coverage-1.1.1.tgz#5aff8b39cf6994dc22de4c84362c76001b637cf6" + dependencies: + dateformat "^1.0.6" + istanbul "^0.4.0" + lodash "^3.8.0" + minimatch "^3.0.0" + source-map "^0.5.1" + +karma-mocha@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-1.3.0.tgz#eeaac7ffc0e201eb63c467440d2b69c7cf3778bf" + dependencies: + minimist "1.2.0" + +karma-phantomjs-launcher@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.4.tgz#d23ca34801bda9863ad318e3bb4bd4062b13acd2" + dependencies: + lodash "^4.0.1" + phantomjs-prebuilt "^2.1.7" + +karma-phantomjs-shim@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/karma-phantomjs-shim/-/karma-phantomjs-shim-1.5.0.tgz#e8db65883480f0dbd184cc961d39c64511742200" + +karma-sinon-chai@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/karma-sinon-chai/-/karma-sinon-chai-1.3.3.tgz#a597e5b4a1369fe7b3d7d76c09ed2061a38e747f" + dependencies: + lolex "^1.6.0" + +karma-sinon-stub-promise@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/karma-sinon-stub-promise/-/karma-sinon-stub-promise-1.0.0.tgz#617e9666e6fce77d63082cb94b637974c84a36bc" + +karma-sourcemap-loader@^0.3.7: + version "0.3.7" + resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.7.tgz#91322c77f8f13d46fed062b042e1009d4c4505d8" + dependencies: + graceful-fs "^4.1.2" + +karma-spec-reporter@0.0.30: + version "0.0.30" + resolved "https://registry.yarnpkg.com/karma-spec-reporter/-/karma-spec-reporter-0.0.30.tgz#d10b5c8bb441cb1c6adf56785f89d395f2e9093a" + dependencies: + colors "^1.1.2" + +karma-webpack@^2.0.2: + version "2.0.13" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.13.tgz#cf56e3056c15b7747a0bb2140fc9a6be41dd9f02" + dependencies: + async "^2.0.0" + babel-runtime "^6.0.0" + loader-utils "^1.0.0" + lodash "^4.0.0" + source-map "^0.5.6" + webpack-dev-middleware "^1.12.0" + +karma@^1.4.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/karma/-/karma-1.7.1.tgz#85cc08e9e0a22d7ce9cca37c4a1be824f6a2b1ae" + dependencies: + bluebird "^3.3.0" + body-parser "^1.16.1" + chokidar "^1.4.1" + colors "^1.1.0" + combine-lists "^1.0.0" + connect "^3.6.0" + core-js "^2.2.0" + di "^0.0.1" + dom-serialize "^2.2.0" + expand-braces "^0.1.1" + glob "^7.1.1" + graceful-fs "^4.1.2" + http-proxy "^1.13.0" + isbinaryfile "^3.0.0" + lodash "^3.8.0" + log4js "^0.6.31" + mime "^1.3.4" + minimatch "^3.0.2" + optimist "^0.6.1" + qjobs "^1.1.4" + range-parser "^1.2.0" + rimraf "^2.6.0" + safe-buffer "^5.0.1" + socket.io "1.7.3" + source-map "^0.5.3" + tmp "0.0.31" + useragent "^2.1.12" + +kew@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +loader-fs-cache@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.1.tgz#56e0bf08bd9708b26a765b68509840c8dec9fdbc" + dependencies: + find-cache-dir "^0.1.1" + mkdirp "0.5.1" + +loader-runner@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" + +loader-utils@^0.2.16: + version "0.2.17" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + object-assign "^4.0.1" + +loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash._arraycopy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" + +lodash._arrayeach@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz#bab156b2a90d3f1bbd5c653403349e5e5933ef9e" + +lodash._baseassign@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" + dependencies: + lodash._basecopy "^3.0.0" + lodash.keys "^3.0.0" + +lodash._baseclone@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz#303519bf6393fe7e42f34d8b630ef7794e3542b7" + dependencies: + lodash._arraycopy "^3.0.0" + lodash._arrayeach "^3.0.0" + lodash._baseassign "^3.0.0" + lodash._basefor "^3.0.0" + lodash.isarray "^3.0.0" + lodash.keys "^3.0.0" + +lodash._baseclone@^4.0.0: + version "4.5.7" + resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-4.5.7.tgz#ce42ade08384ef5d62fa77c30f61a46e686f8434" + +lodash._basecopy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" + +lodash._basecreate@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" + +lodash._basefor@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2" + +lodash._bindcallback@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + +lodash._isiterateecall@^3.0.0: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" + +lodash._stack@^4.0.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/lodash._stack/-/lodash._stack-4.1.3.tgz#751aa76c1b964b047e76d14fc72a093fcb5e2dd0" + +lodash.assign@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + +lodash.clone@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-3.0.3.tgz#84688c73d32b5a90ca25616963f189252a997043" + dependencies: + lodash._baseclone "^3.0.0" + lodash._bindcallback "^3.0.0" + lodash._isiterateecall "^3.0.0" + +lodash.clonedeep@^4.3.2: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + +lodash.create@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" + dependencies: + lodash._baseassign "^3.0.0" + lodash._basecreate "^3.0.0" + lodash._isiterateecall "^3.0.0" + +lodash.defaultsdeep@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.3.2.tgz#6c1a586e6c5647b0e64e2d798141b8836158be8a" + dependencies: + lodash._baseclone "^4.0.0" + lodash._stack "^4.0.0" + lodash.isplainobject "^4.0.0" + lodash.keysin "^4.0.0" + lodash.mergewith "^4.0.0" + lodash.rest "^4.0.0" + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.isplainobject@^4.0.0: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.keysin@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.keysin/-/lodash.keysin-4.2.0.tgz#8cc3fb35c2d94acc443a1863e02fa40799ea6f28" + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + +lodash.mergewith@^4.0.0, lodash.mergewith@^4.6.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" + +lodash.rest@^4.0.0: + version "4.0.5" + resolved "https://registry.yarnpkg.com/lodash.rest/-/lodash.rest-4.0.5.tgz#954ef75049262038c96d1fc98b28fdaf9f0772aa" + +lodash.tail@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + +lodash@^3.8.0: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + +lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0, lodash@~4.17.4: + version "4.17.5" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" + +log-symbols@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" + dependencies: + chalk "^2.0.1" + +log4js@^0.6.31: + version "0.6.38" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-0.6.38.tgz#2c494116695d6fb25480943d3fc872e662a522fd" + dependencies: + readable-stream "~1.0.2" + semver "~4.3.3" + +lolex@^1.5.2, lolex@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +loose-envify@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lower-case@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + +lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-3.2.0.tgz#71789b3b7f5399bec8565dda38aa30d2a097efee" + dependencies: + pseudomap "^1.0.1" + +lru-cache@~2.6.5: + version "2.6.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5" + +macaddress@^0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" + +make-dir@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b" + dependencies: + pify "^3.0.0" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + dependencies: + object-visit "^1.0.0" + +masonry-layout@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/masonry-layout/-/masonry-layout-4.1.1.tgz#e8c8c6f5a9e621a75203ac4b7000855a36f6753e" + dependencies: + get-size "^2.0.2" + outlayer "^2.1.0" + +masonry-layout@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/masonry-layout/-/masonry-layout-4.2.1.tgz#1c878b85a88866ba6b39b9bd0e162348aed7ce13" + dependencies: + get-size "^2.0.2" + outlayer "^2.1.0" + +masonry@latest: + version "0.0.2" + resolved "https://registry.yarnpkg.com/masonry/-/masonry-0.0.2.tgz#c06a0b2efc043727eadd16b35fdae93a3908f432" + dependencies: + ejs "0.7.1" + +math-expression-evaluator@^1.2.14: + version "1.2.17" + resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" + +md5.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +memory-fs@^0.4.0, memory-fs@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +meow@^3.3.0, meow@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +micromatch@^2.1.5, micromatch@^2.3.11: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +micromatch@^3.1.4, micromatch@^3.1.8: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + +mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + dependencies: + mime-db "~1.33.0" + +mime@1.3.x: + version "1.3.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0" + +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + +mime@^1.3.4, mime@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimatch@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" + dependencies: + brace-expansion "^1.0.0" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@1.2.0, minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + +mississippi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f" + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^2.0.1" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mixin-deep@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mixin-object@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" + dependencies: + for-in "^0.1.3" + is-extendable "^0.1.1" + +mkdirp@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" + dependencies: + minimist "0.0.8" + +mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mkpath@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mkpath/-/mkpath-1.0.0.tgz#ebb3a977e7af1c683ae6fda12b545a6ba6c5853d" + +mocha-nightwatch@3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/mocha-nightwatch/-/mocha-nightwatch-3.2.2.tgz#91bcb9b3bde057dd7677c78125e491e58d66647c" + dependencies: + browser-stdout "1.3.0" + commander "2.9.0" + debug "2.2.0" + diff "1.4.0" + escape-string-regexp "1.0.5" + glob "7.0.5" + growl "1.9.2" + json3 "3.3.2" + lodash.create "3.1.1" + mkdirp "0.5.1" + supports-color "3.1.2" + +mocha@^3.2.0: + version "3.5.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d" + dependencies: + browser-stdout "1.3.0" + commander "2.9.0" + debug "2.6.8" + diff "3.2.0" + escape-string-regexp "1.0.5" + glob "7.1.1" + growl "1.9.2" + he "1.1.1" + json3 "3.3.2" + lodash.create "3.1.1" + mkdirp "0.5.1" + supports-color "3.1.2" + +moment@^2.20.1: + version "2.22.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.0.tgz#7921ade01017dd45186e7fee5f424f0b8663a730" + +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + +moxios@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/moxios/-/moxios-0.4.0.tgz#fc0da2c65477d725ca6b9679d58370ed0c52f53b" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + +nan@^2.10.0, nan@^2.3.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" + +nanomatch@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-odd "^2.0.0" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +native-promise-only@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +neo-async@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.0.tgz#76b1c823130cca26acfbaccc8fbaf0a2fa33b18f" + +netmask@~1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35" + +next-tick@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + +nightwatch@^0.9.12: + version "0.9.20" + resolved "https://registry.yarnpkg.com/nightwatch/-/nightwatch-0.9.20.tgz#156d17cd058cbc31f43ab18e915f7ec297fb53e0" + dependencies: + chai-nightwatch "~0.1.x" + ejs "2.5.7" + lodash.clone "3.0.3" + lodash.defaultsdeep "4.3.2" + minimatch "3.0.3" + mkpath "1.0.0" + mocha-nightwatch "3.2.2" + optimist "0.6.1" + proxy-agent "2.0.0" + q "1.4.1" + +no-case@^2.2.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" + dependencies: + lower-case "^1.1.1" + +node-gettext@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/node-gettext/-/node-gettext-2.0.0.tgz#f1dc1237cdc546f51593da340304b8beba5b8525" + dependencies: + lodash.get "^4.4.2" + +node-gyp@^3.3.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60" + dependencies: + fstream "^1.0.0" + glob "^7.0.3" + graceful-fs "^4.1.2" + minimatch "^3.0.2" + mkdirp "^0.5.0" + nopt "2 || 3" + npmlog "0 || 1 || 2 || 3 || 4" + osenv "0" + request "2" + rimraf "2" + semver "~5.3.0" + tar "^2.0.0" + which "1" + +node-libs-browser@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^1.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.0" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.10.3" + vm-browserify "0.0.4" + +node-pre-gyp@^0.6.39: + version "0.6.39" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" + dependencies: + detect-libc "^1.0.2" + hawk "3.1.3" + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.0.2" + rc "^1.1.7" + request "2.81.0" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^2.2.1" + tar-pack "^3.4.0" + +node-sass@^4.5.3: + version "4.8.3" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.8.3.tgz#d077cc20a08ac06f661ca44fb6f19cd2ed41debb" + dependencies: + async-foreach "^0.1.3" + chalk "^1.1.1" + cross-spawn "^3.0.0" + gaze "^1.0.0" + get-stdin "^4.0.1" + glob "^7.0.3" + in-publish "^2.0.0" + lodash.assign "^4.2.0" + lodash.clonedeep "^4.3.2" + lodash.mergewith "^4.6.0" + meow "^3.7.0" + mkdirp "^0.5.1" + nan "^2.10.0" + node-gyp "^3.3.1" + npmlog "^4.0.0" + request "~2.79.0" + sass-graph "^2.2.4" + stdout-stream "^1.4.0" + "true-case-path" "^1.0.2" + +"nopt@2 || 3", nopt@3.x, nopt@~3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + dependencies: + abbrev "1" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + +normalize-url@^1.4.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +nth-check@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4" + dependencies: + boolbase "~1.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.1, oauth-sign@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" + +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object-component@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-hash@^1.1.4: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.0.tgz#76d9ba6ff113cf8efc0d996102851fe6723963e2" + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + dependencies: + isobject "^3.0.0" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + dependencies: + isobject "^3.0.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +onetime@^1.0.0: + version "1.1.0" + resolved "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + dependencies: + mimic-fn "^1.0.0" + +opener@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8" + +opn@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/opn/-/opn-4.0.2.tgz#7abc22e644dff63b0a96d5ab7f2790c0f01abc95" + dependencies: + object-assign "^4.0.1" + pinkie-promise "^2.0.0" + +optimist@0.6.1, optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optimize-css-assets-webpack-plugin@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-1.3.2.tgz#eb27456e21eefbd8080f31e8368c59684e585a2c" + dependencies: + cssnano "^3.4.0" + underscore "^1.8.3" + webpack-sources "^0.1.0" + +optionator@^0.8.1, optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +options@>=0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" + +ora@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-1.4.0.tgz#884458215b3a5d4097592285f93321bb7a79e2e5" + dependencies: + chalk "^2.1.0" + cli-cursor "^2.1.0" + cli-spinners "^1.0.1" + log-symbols "^2.1.0" + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + +os-homedir@^1.0.0, os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@0, osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +outlayer@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/outlayer/-/outlayer-2.1.1.tgz#29863b6de10ea5dadfffcadfa0d728907387e9a2" + dependencies: + ev-emitter "^1.0.0" + fizzy-ui-utils "^2.0.0" + get-size "^2.0.2" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.0.0, p-limit@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" + dependencies: + p-try "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-map@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + +pac-proxy-agent@1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-1.1.0.tgz#34a385dfdf61d2f0ecace08858c745d3e791fd4d" + dependencies: + agent-base "2" + debug "2" + extend "3" + get-uri "2" + http-proxy-agent "1" + https-proxy-agent "1" + pac-resolver "~2.0.0" + raw-body "2" + socks-proxy-agent "2" + +pac-resolver@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-2.0.0.tgz#99b88d2f193fbdeefc1c9a529c1f3260ab5277cd" + dependencies: + co "~3.0.6" + degenerator "~1.0.2" + ip "1.0.1" + netmask "~1.0.4" + thunkify "~2.1.1" + +pako@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" + +parallel-transform@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" + dependencies: + cyclist "~0.2.2" + inherits "^2.0.3" + readable-stream "^2.1.5" + +param-case@2.1.x: + version "2.1.1" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" + dependencies: + no-case "^2.2.0" + +parse-asn1@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parsejson@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/parsejson/-/parsejson-0.0.3.tgz#ab7e3759f209ece99437973f7d0f1f64ae0e64ab" + dependencies: + better-assert "~1.0.0" + +parseqs@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" + dependencies: + better-assert "~1.0.0" + +parseuri@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" + dependencies: + better-assert "~1.0.0" + +parseurl@~1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + +path-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + dependencies: + pify "^3.0.0" + +pbkdf2@^3.0.3: + version "3.0.14" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +phantomjs-prebuilt@^2.1.14, phantomjs-prebuilt@^2.1.7: + version "2.1.16" + resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef" + dependencies: + es6-promise "^4.0.3" + extract-zip "^1.6.5" + fs-extra "^1.0.0" + hasha "^2.2.0" + kew "^0.7.0" + progress "^1.1.8" + request "^2.81.0" + request-progress "^2.0.1" + which "^1.2.10" + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pkg-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" + dependencies: + find-up "^1.0.0" + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + dependencies: + find-up "^2.1.0" + +pluralize@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + +postcss-calc@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" + dependencies: + postcss "^5.0.2" + postcss-message-helpers "^2.0.0" + reduce-css-calc "^1.2.6" + +postcss-colormin@^2.1.8: + version "2.2.2" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b" + dependencies: + colormin "^1.0.5" + postcss "^5.0.13" + postcss-value-parser "^3.2.3" + +postcss-convert-values@^2.3.4: + version "2.6.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d" + dependencies: + postcss "^5.0.11" + postcss-value-parser "^3.1.2" + +postcss-discard-comments@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" + dependencies: + postcss "^5.0.14" + +postcss-discard-duplicates@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932" + dependencies: + postcss "^5.0.4" + +postcss-discard-empty@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5" + dependencies: + postcss "^5.0.14" + +postcss-discard-overridden@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58" + dependencies: + postcss "^5.0.16" + +postcss-discard-unused@^2.2.1: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433" + dependencies: + postcss "^5.0.14" + uniqs "^2.0.0" + +postcss-filter-plugins@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz#6d85862534d735ac420e4a85806e1f5d4286d84c" + dependencies: + postcss "^5.0.4" + uniqid "^4.0.0" + +postcss-load-config@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + postcss-load-options "^1.2.0" + postcss-load-plugins "^2.3.0" + +postcss-load-options@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + +postcss-load-plugins@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92" + dependencies: + cosmiconfig "^2.1.1" + object-assign "^4.1.0" + +postcss-merge-idents@^2.1.5: + version "2.1.7" + resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" + dependencies: + has "^1.0.1" + postcss "^5.0.10" + postcss-value-parser "^3.1.1" + +postcss-merge-longhand@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz#23d90cd127b0a77994915332739034a1a4f3d658" + dependencies: + postcss "^5.0.4" + +postcss-merge-rules@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721" + dependencies: + browserslist "^1.5.2" + caniuse-api "^1.5.2" + postcss "^5.0.4" + postcss-selector-parser "^2.2.2" + vendors "^1.0.0" + +postcss-message-helpers@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" + +postcss-minify-font-values@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69" + dependencies: + object-assign "^4.0.1" + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-minify-gradients@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1" + dependencies: + postcss "^5.0.12" + postcss-value-parser "^3.3.0" + +postcss-minify-params@^1.0.4: + version "1.2.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.2" + postcss-value-parser "^3.0.2" + uniqs "^2.0.0" + +postcss-minify-selectors@^2.0.4: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf" + dependencies: + alphanum-sort "^1.0.2" + has "^1.0.1" + postcss "^5.0.14" + postcss-selector-parser "^2.0.0" + +postcss-modules-extract-imports@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85" + dependencies: + postcss "^6.0.1" + +postcss-modules-local-by-default@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-scope@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-values@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" + dependencies: + icss-replace-symbols "^1.1.0" + postcss "^6.0.1" + +postcss-normalize-charset@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" + dependencies: + postcss "^5.0.5" + +postcss-normalize-url@^3.0.7: + version "3.0.8" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222" + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^1.4.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + +postcss-ordered-values@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.1" + +postcss-reduce-idents@^2.2.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-reduce-initial@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea" + dependencies: + postcss "^5.0.4" + +postcss-reduce-transforms@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1" + dependencies: + has "^1.0.1" + postcss "^5.0.8" + postcss-value-parser "^3.0.1" + +postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-svgo@^2.1.1: + version "2.1.6" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" + dependencies: + is-svg "^2.0.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + svgo "^0.7.0" + +postcss-unique-selectors@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" + +postcss-zindex@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22" + dependencies: + has "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.21, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16: + version "5.2.18" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" + dependencies: + chalk "^1.1.3" + js-base64 "^2.1.9" + source-map "^0.5.6" + supports-color "^3.2.3" + +postcss@^6.0.1: + version "6.0.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.21.tgz#8265662694eddf9e9a5960db6da33c39e4cd069d" + dependencies: + chalk "^2.3.2" + source-map "^0.6.1" + supports-color "^5.3.0" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +pretty-error@^2.0.2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" + dependencies: + renderkid "^2.0.1" + utila "~0.4" + +private@^0.1.6, private@^0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + +proxy-addr@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341" + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.6.0" + +proxy-agent@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-2.0.0.tgz#57eb5347aa805d74ec681cb25649dba39c933499" + dependencies: + agent-base "2" + debug "2" + extend "3" + http-proxy-agent "1" + https-proxy-agent "1" + lru-cache "~2.6.5" + pac-proxy-agent "1" + socks-proxy-agent "2" + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + +pseudomap@^1.0.1, pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + +pump@^2.0.0, pump@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb" + dependencies: + duplexify "^3.5.3" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +punycode@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d" + +q@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + +qjobs@^1.1.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + +qs@6.5.1, qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + +qs@~6.3.0: + version "6.3.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" + +qs@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +query-string@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0, querystring@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +randomatic@^1.1.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +range-parser@^1.0.3, range-parser@^1.2.0, range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +raven-js@^3.22.3: + version "3.24.0" + resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.24.0.tgz#59464d8bc4b3812ae87a282e9bb98ecad5b4b047" + +raw-body@2, raw-body@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" + dependencies: + bytes "3.0.0" + http-errors "1.6.2" + iconv-lite "0.4.19" + unpipe "1.0.0" + +rc@^1.1.7: + version "1.2.6" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.6.tgz#eb18989c6d4f4f162c399f79ddd29f3835568092" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@1.0, readable-stream@~1.0.2: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + dependencies: + resolve "^1.1.6" + +reconnecting-websocket@^3.0.3: + version "3.2.2" + resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-3.2.2.tgz#8097514e926e9855e03c39e76efa2e3d1f371bee" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +reduce-css-calc@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" + dependencies: + balanced-match "^0.4.2" + math-expression-evaluator "^1.2.14" + reduce-function-call "^1.0.1" + +reduce-function-call@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99" + dependencies: + balanced-match "^0.4.2" + +regenerate@^1.2.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + +regenerator-transform@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" + dependencies: + babel-runtime "^6.18.0" + babel-types "^6.19.0" + private "^0.1.6" + +regex-cache@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" + dependencies: + is-equal-shallow "^0.1.3" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexpu-core@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regexpu-core@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +relateurl@0.2.x: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +renderkid@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.1.tgz#898cabfc8bede4b7b91135a3ffd323e58c0db319" + dependencies: + css-select "^1.1.0" + dom-converter "~0.1" + htmlparser2 "~3.3.0" + strip-ansi "^3.0.0" + utila "~0.3" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae" + +repeat-string@^1.5.2, repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +request-progress@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08" + dependencies: + throttleit "^1.0.0" + +request@2, request@^2.81.0, request@^2.83.0: + version "2.85.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + +request@2.81.0: + version "2.81.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~4.2.1" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + performance-now "^0.2.0" + qs "~6.4.0" + safe-buffer "^5.0.1" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "^0.6.0" + uuid "^3.0.0" + +request@~2.79.0: + version "2.79.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + uuid "^3.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-from-string@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +require-uncached@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +requires-port@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + +resolve@1.1.x: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + +resolve@^1.1.6, resolve@^1.3.3: + version "1.7.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.0.tgz#2bdf5374811207285df0df652b78f118ab8f3c5e" + dependencies: + path-parse "^1.0.5" + +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" + +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + dependencies: + once "^1.3.0" + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + dependencies: + aproba "^1.1.1" + +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + +safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + dependencies: + ret "~0.1.10" + +safer-buffer@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + +samsam@1.x, samsam@^1.1.3: + version "1.3.0" + resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" + +sass-graph@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" + dependencies: + glob "^7.0.0" + lodash "^4.0.0" + scss-tokenizer "^0.2.3" + yargs "^7.0.0" + +sass-loader@^6.0.5: + version "6.0.7" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.7.tgz#dd2fdb3e7eeff4a53f35ba6ac408715488353d00" + dependencies: + clone-deep "^2.0.1" + loader-utils "^1.0.1" + lodash.tail "^4.1.1" + neo-async "^2.5.0" + pify "^3.0.0" + +sax@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +schema-utils@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" + dependencies: + ajv "^5.0.0" + +scss-tokenizer@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" + dependencies: + js-base64 "^2.1.8" + source-map "^0.4.2" + +selenium-server@^3.0.1: + version "3.11.0" + resolved "https://registry.yarnpkg.com/selenium-server/-/selenium-server-3.11.0.tgz#fb20b049805d6a663ab4a609dd505fd9eddd7d15" + +semantic-ui-css@^2.2.10: + version "2.3.1" + resolved "https://registry.yarnpkg.com/semantic-ui-css/-/semantic-ui-css-2.3.1.tgz#a5485c640c98cce29d8ddde3eff3434566a068e0" + dependencies: + jquery x.* + +"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + +semver@~4.3.3: + version "4.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" + +semver@~5.0.1: + version "5.0.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" + +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +send@0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.4.0" + +serialize-javascript@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.4.0.tgz#7c958514db6ac2443a8abc062dc9f7886a7f6005" + +serve-static@1.13.2: + version "1.13.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.16.2" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +set-value@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallow-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571" + dependencies: + is-extendable "^0.1.1" + kind-of "^5.0.0" + mixin-object "^2.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +shelljs@^0.7.5, shelljs@^0.7.6: + version "0.7.8" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +showdown@^1.8.6: + version "1.8.6" + resolved "https://registry.yarnpkg.com/showdown/-/showdown-1.8.6.tgz#91ea4ee3b7a5448aaca6820a4e27e690c6ad771c" + dependencies: + yargs "^10.0.3" + +shvl@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shvl/-/shvl-1.2.1.tgz#7ea96257475b20cb69d2a58897c0640c93973593" + +sigmund@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +sinon-chai@^2.8.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-2.14.0.tgz#da7dd4cc83cd6a260b67cca0f7a9fdae26a1205d" + +sinon-stub-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/sinon-stub-promise/-/sinon-stub-promise-4.0.0.tgz#6d498ba1198557cd01e3466af92dc7df725192c2" + +sinon@^2.1.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.4.1.tgz#021fd64b54cb77d9d2fb0d43cdedfae7629c3a36" + dependencies: + diff "^3.1.0" + formatio "1.2.0" + lolex "^1.6.0" + native-promise-only "^0.8.1" + path-to-regexp "^1.7.0" + samsam "^1.1.3" + text-encoding "0.6.4" + type-detect "^4.0.0" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + +smart-buffer@^1.0.13: + version "1.1.15" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16" + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +sntp@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" + dependencies: + hoek "4.x.x" + +socket.io-adapter@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz#cb6d4bb8bec81e1078b99677f9ced0046066bb8b" + dependencies: + debug "2.3.3" + socket.io-parser "2.3.1" + +socket.io-client@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.3.tgz#b30e86aa10d5ef3546601c09cde4765e381da377" + dependencies: + backo2 "1.0.2" + component-bind "1.0.0" + component-emitter "1.2.1" + debug "2.3.3" + engine.io-client "1.8.3" + has-binary "0.1.7" + indexof "0.0.1" + object-component "0.0.3" + parseuri "0.0.5" + socket.io-parser "2.3.1" + to-array "0.1.4" + +socket.io-parser@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-2.3.1.tgz#dd532025103ce429697326befd64005fcfe5b4a0" + dependencies: + component-emitter "1.1.2" + debug "2.2.0" + isarray "0.0.1" + json3 "3.3.2" + +socket.io@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.3.tgz#b8af9caba00949e568e369f1327ea9be9ea2461b" + dependencies: + debug "2.3.3" + engine.io "1.8.3" + has-binary "0.1.7" + object-assign "4.1.0" + socket.io-adapter "0.5.0" + socket.io-client "1.7.3" + socket.io-parser "2.3.1" + +socks-proxy-agent@2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-2.1.1.tgz#86ebb07193258637870e13b7bd99f26c663df3d3" + dependencies: + agent-base "2" + extend "3" + socks "~1.1.5" + +socks@~1.1.5: + version "1.1.10" + resolved "https://registry.yarnpkg.com/socks/-/socks-1.1.10.tgz#5b8b7fc7c8f341c53ed056e929b7bf4de8ba7b5a" + dependencies: + ip "^1.1.4" + smart-buffer "^1.0.13" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + dependencies: + is-plain-obj "^1.0.0" + +sortablejs@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.7.0.tgz#80a2b2370abd568e1cec8c271131ef30a904fa28" + +source-list-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" + +source-list-map@~0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" + +source-map-resolve@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a" + dependencies: + atob "^2.0.0" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.4.15: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + dependencies: + source-map "^0.5.6" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + +source-map@0.5.x, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.4.2, source-map@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +source-map@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" + dependencies: + amdefine ">=0.0.4" + +spdx-correct@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +ssri@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06" + dependencies: + safe-buffer "^5.1.1" + +stackframe@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + +statuses@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +statuses@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + +stdout-stream@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" + dependencies: + readable-stream "^2.0.1" + +stream-browserify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-each@^1.1.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd" + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-http@^2.7.2: + version "2.8.1" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.1.tgz#d0441be1a457a73a733a8a7b53570bebd9ef66a4" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.3" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@^1.0.0, string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +stringstream@~0.0.4, stringstream@~0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +supports-color@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" + dependencies: + has-flag "^1.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^3.1.0, supports-color@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + dependencies: + has-flag "^1.0.0" + +supports-color@^4.2.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + dependencies: + has-flag "^2.0.0" + +supports-color@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0" + dependencies: + has-flag "^3.0.0" + +svgo@^0.7.0: + version "0.7.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" + dependencies: + coa "~1.0.1" + colors "~1.1.2" + csso "~2.3.1" + js-yaml "~3.7.0" + mkdirp "~0.5.1" + sax "~1.2.1" + whet.extend "~0.9.9" + +table@^3.7.8: + version "3.8.3" + resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + +tapable@^0.2.7: + version "0.2.8" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" + +tar-pack@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" + dependencies: + debug "^2.2.0" + fstream "^1.0.10" + fstream-ignore "^1.0.5" + once "^1.3.3" + readable-stream "^2.1.4" + rimraf "^2.5.1" + tar "^2.2.1" + uid-number "^0.0.6" + +tar@^2.0.0, tar@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +test-exclude@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.1.tgz#dfa222f03480bca69207ca728b37d74b45f724fa" + dependencies: + arrify "^1.0.1" + micromatch "^3.1.8" + object-assign "^4.1.0" + read-pkg-up "^1.0.1" + require-main-filename "^1.0.1" + +text-encoding@0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" + +text-table@^0.2.0, text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + +through2@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +thunkify@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/thunkify/-/thunkify-2.1.2.tgz#faa0e9d230c51acc95ca13a361ac05ca7e04553d" + +time-stamp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357" + +timers-browserify@^2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.6.tgz#241e76927d9ca05f4d959819022f5b3664b64bae" + dependencies: + setimmediate "^1.0.4" + +tmp@0.0.31: + version "0.0.31" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" + dependencies: + os-tmpdir "~1.0.1" + +tmp@0.0.x: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + dependencies: + os-tmpdir "~1.0.2" + +to-array@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toposort@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec" + +tough-cookie@~2.3.0, tough-cookie@~2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" + dependencies: + punycode "^1.4.1" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +"true-case-path@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62" + dependencies: + glob "^6.0.4" + +tryer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.0.tgz#027b69fa823225e551cace3ef03b11f6ab37c1d7" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +type-detect@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-0.1.1.tgz#0ba5ec2a885640e470ea4e8505971900dac58822" + +type-detect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-1.0.0.tgz#762217cc06db258ec48908a1298e8b95121e8ea2" + +type-detect@^4.0.0: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + +type-is@~1.6.15, type-is@~1.6.16: + version "1.6.16" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.18" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +uglify-js@3.3.x: + version "3.3.20" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.20.tgz#dc8bdee7d454c7d31dddc36f922d170bfcee3a0a" + dependencies: + commander "~2.15.0" + source-map "~0.6.1" + +uglify-js@^2.6, uglify-js@^2.8.29: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uglifyjs-webpack-plugin@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" + dependencies: + source-map "^0.5.6" + uglify-js "^2.8.29" + webpack-sources "^1.0.1" + +uid-number@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +ultron@1.0.x: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa" + +underscore@^1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + +union-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^0.4.3" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + +uniqid@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-4.1.1.tgz#89220ddf6b751ae52b5f72484863528596bb84c1" + dependencies: + macaddress "^0.2.8" + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + +unique-filename@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3" + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab" + dependencies: + imurmurhash "^0.1.4" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.4.tgz#ee2321ba0a786c50973db043a50b7bcba822361d" + +upper-case@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" + +uri-js@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-3.0.2.tgz#f90b858507f81dea4dcfbb3c4c3dbfa2b557faaa" + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + +url-loader@^0.5.8: + version "0.5.9" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.5.9.tgz#cc8fea82c7b906e7777019250869e569e995c295" + dependencies: + loader-utils "^1.0.2" + mime "1.3.x" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544" + dependencies: + kind-of "^6.0.2" + +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + dependencies: + os-homedir "^1.0.0" + +useragent@^2.1.12: + version "2.3.0" + resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972" + dependencies: + lru-cache "4.1.x" + tmp "0.0.x" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util@0.10.3, util@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +utila@~0.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.3.3.tgz#d7e8e7d7e309107092b05f8d9688824d633a4226" + +utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + +uuid@^3.0.0, uuid@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + +validate-npm-package-license@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + +vendors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vm-browserify@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + +vue-hot-reload-api@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz#97976142405d13d8efae154749e88c4e358cf926" + +vue-lazyload@^1.1.4: + version "1.2.2" + resolved "https://registry.yarnpkg.com/vue-lazyload/-/vue-lazyload-1.2.2.tgz#73335ed32db25264f5957df1a21d277823423743" + +vue-loader@^12.1.0: + version "12.2.2" + resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-12.2.2.tgz#2b3a764f27018f975bc78cb8b1f55137548ee2d7" + dependencies: + consolidate "^0.14.0" + hash-sum "^1.0.2" + js-beautify "^1.6.3" + loader-utils "^1.1.0" + lru-cache "^4.0.1" + postcss "^5.0.21" + postcss-load-config "^1.1.0" + postcss-selector-parser "^2.0.0" + resolve "^1.3.3" + source-map "^0.5.6" + vue-hot-reload-api "^2.1.0" + vue-style-loader "^3.0.0" + vue-template-es2015-compiler "^1.2.2" + +vue-masonry@^0.10.16: + version "0.10.17" + resolved "https://registry.yarnpkg.com/vue-masonry/-/vue-masonry-0.10.17.tgz#39ae7f412ff8492a713b39ad2b3d41c34fb76b91" + dependencies: + imagesloaded latest + masonry latest + masonry-layout "4.1.1" + vue "^2.0.0" + +vue-router@^2.3.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-2.8.1.tgz#9833c9ee57ac83beb0269056fefee71713f20695" + +vue-style-loader@^3.0.0, vue-style-loader@^3.0.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-3.1.2.tgz#6b66ad34998fc9520c2f1e4d5fa4091641c1597a" + dependencies: + hash-sum "^1.0.2" + loader-utils "^1.0.2" + +vue-template-compiler@^2.3.3: + version "2.5.16" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.16.tgz#93b48570e56c720cdf3f051cc15287c26fbd04cb" + dependencies: + de-indent "^1.0.2" + he "^1.1.0" + +vue-template-es2015-compiler@^1.2.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18" + +vue-upload-component@^2.7.4: + version "2.8.5" + resolved "https://registry.yarnpkg.com/vue-upload-component/-/vue-upload-component-2.8.5.tgz#680de2934f55c6a38da7b382d0f37fd79995dd42" + +vue@^2.0.0, vue@^2.3.3: + version "2.5.16" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.16.tgz#07edb75e8412aaeed871ebafa99f4672584a0085" + +vuedraggable@^2.14.1: + version "2.16.0" + resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.16.0.tgz#52127081a2adb3de5fabd214d404ff3eee63575a" + dependencies: + sortablejs "^1.7.0" + +vuex-persistedstate@^2.4.2: + version "2.5.1" + resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-2.5.1.tgz#129274514e2e46b49e8ca8066cf85b345c7b4ce2" + dependencies: + deepmerge "^2.1.0" + shvl "^1.2.0" + +vuex-router-sync@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/vuex-router-sync/-/vuex-router-sync-5.0.0.tgz#1a225c17a1dd9e2f74af0a1b2c62072e9492b305" + +vuex@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2" + +watchpack@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.5.0.tgz#231e783af830a22f8966f65c4c4bacc814072eed" + dependencies: + chokidar "^2.0.2" + graceful-fs "^4.1.2" + neo-async "^2.5.0" + +webpack-bundle-analyzer@^2.2.1: + version "2.11.1" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.11.1.tgz#b9fbfb6a32c0a8c1c3237223e90890796b950ab9" + dependencies: + acorn "^5.3.0" + bfj-node4 "^5.2.0" + chalk "^2.3.0" + commander "^2.13.0" + ejs "^2.5.7" + express "^4.16.2" + filesize "^3.5.11" + gzip-size "^4.1.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + opener "^1.4.3" + ws "^4.0.0" + +webpack-dev-middleware@^1.10.0, webpack-dev-middleware@^1.12.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz#f8fc1120ce3b4fc5680ceecb43d777966b21105e" + dependencies: + memory-fs "~0.4.1" + mime "^1.5.0" + path-is-absolute "^1.0.0" + range-parser "^1.0.3" + time-stamp "^2.0.0" + +webpack-hot-middleware@^2.18.0: + version "2.22.0" + resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.22.0.tgz#db58c9dd2bd78e7f3868cccb42a20d24b00a7ade" + dependencies: + ansi-html "0.0.7" + html-entities "^1.2.0" + querystring "^0.2.0" + strip-ansi "^3.0.0" + +webpack-merge@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.1.2.tgz#5d372dddd3e1e5f8874f5bf5a8e929db09feb216" + dependencies: + lodash "^4.17.5" + +webpack-sources@^0.1.0: + version "0.1.5" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.1.5.tgz#aa1f3abf0f0d74db7111c40e500b84f966640750" + dependencies: + source-list-map "~0.1.7" + source-map "~0.5.3" + +webpack-sources@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54" + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack@3: + version "3.11.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.11.0.tgz#77da451b1d7b4b117adaf41a1a93b5742f24d894" + dependencies: + acorn "^5.0.0" + acorn-dynamic-import "^2.0.0" + ajv "^6.1.0" + ajv-keywords "^3.1.0" + async "^2.1.2" + enhanced-resolve "^3.4.0" + escope "^3.6.0" + interpret "^1.0.0" + json-loader "^0.5.4" + json5 "^0.5.1" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + mkdirp "~0.5.0" + node-libs-browser "^2.0.0" + source-map "^0.5.3" + supports-color "^4.2.1" + tapable "^0.2.7" + uglifyjs-webpack-plugin "^0.4.6" + watchpack "^1.4.0" + webpack-sources "^1.0.1" + yargs "^8.0.2" + +whet.extend@~0.9.9: + version "0.9.9" + resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@1, which@^1.1.1, which@^1.2.10, which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" + dependencies: + string-width "^1.0.2" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +wordwrap@^1.0.0, wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + dependencies: + mkdirp "^0.5.1" + +ws@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.2.tgz#8a244fa052401e08c9886cf44a85189e1fd4067f" + dependencies: + options ">=0.0.5" + ultron "1.0.x" + +ws@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-4.1.0.tgz#a979b5d7d4da68bf54efe0408967c324869a7289" + dependencies: + async-limiter "~1.0.0" + safe-buffer "~5.1.0" + +wtf-8@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a" + +xmlhttprequest-ssl@1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d" + +xregexp@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" + +xtend@^4.0.0, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" + dependencies: + camelcase "^3.0.0" + +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + +yargs-parser@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950" + dependencies: + camelcase "^4.1.0" + +yargs@^10.0.3: + version "10.1.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.2.tgz#454d074c2b16a51a43e2fb7807e4f9de69ccb5c5" + dependencies: + cliui "^4.0.0" + decamelize "^1.1.1" + find-up "^2.1.0" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^8.1.0" + +yargs@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^5.0.0" + +yargs@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" + +yauzl@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" + dependencies: + fd-slicer "~1.0.1" + +yeast@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" From a4418bdcfe5703ed225cf6ab9132928558cb1d82 Mon Sep 17 00:00:00 2001 From: Bat Date: Mon, 9 Apr 2018 19:33:17 +0100 Subject: [PATCH 037/101] Add a few useful packages --- front/package.json | 4 ++++ front/yarn.lock | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/front/package.json b/front/package.json index 301eefc24..6e1033851 100644 --- a/front/package.json +++ b/front/package.json @@ -15,10 +15,14 @@ "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" }, "dependencies": { + "@panter/vue-i18next": "^0.9.1", "axios": "^0.17.1", "dateformat": "^2.0.0", "django-channels": "^1.1.6", + "i18next": "^11.1.1", + "i18next-browser-languagedetector": "^2.2.0", "i18next-conv": "^6.0.0", + "i18next-fetch-backend": "^0.1.0", "js-logger": "^1.3.0", "jwt-decode": "^2.2.0", "lodash": "^4.17.4", diff --git a/front/yarn.lock b/front/yarn.lock index 7c3927b20..fe92caab2 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2,6 +2,12 @@ # yarn lockfile v1 +"@panter/vue-i18next@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@panter/vue-i18next/-/vue-i18next-0.9.1.tgz#a24694d37b7e2f54f67ac8a485f634203660e98d" + dependencies: + deepmerge "^2.0.0" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1947,7 +1953,7 @@ deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" -deepmerge@^2.1.0: +deepmerge@^2.0.0, deepmerge@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.0.tgz#511a54fff405fc346f0240bb270a3e9533a31102" @@ -3517,6 +3523,10 @@ https-proxy-agent@1: debug "2" extend "3" +i18next-browser-languagedetector@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-2.2.0.tgz#5f41abe61964a56dce70102ab31c3ed5d5866edc" + i18next-conv@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/i18next-conv/-/i18next-conv-6.0.0.tgz#875a27bfb069db894f7b0a1484e0052100bc9383" @@ -3528,6 +3538,20 @@ i18next-conv@^6.0.0: mkdirp "^0.5.1" node-gettext "^2.0.0" +i18next-fetch-backend@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/i18next-fetch-backend/-/i18next-fetch-backend-0.1.0.tgz#18b67920d0e605e616f93bbdf897e59adf9c9c05" + dependencies: + i18next-xhr-backend "^1.4.3" + +i18next-xhr-backend@^1.4.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/i18next-xhr-backend/-/i18next-xhr-backend-1.5.1.tgz#50282610780c6a696d880dfa7f4ac1d01e8c3ad5" + +i18next@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-11.1.1.tgz#df3a683542d7756a8aa8d6b884b61141239c394a" + iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" From de753502c9c7e3b173b1ffd698294c9ce3232486 Mon Sep 17 00:00:00 2001 From: Bat Date: Wed, 11 Apr 2018 13:32:29 +0100 Subject: [PATCH 038/101] Setup translations with Vue --- .gitignore | 1 - front/package.json | 2 +- front/src/App.vue | 1 + front/src/main.js | 24 ++++++++++++++++++++++++ po/funkwhale.pot | 13 +++++++++++++ 5 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 po/funkwhale.pot diff --git a/.gitignore b/.gitignore index ef5007af7..ee2ed95e2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,6 @@ htmlcov # Translations *.mo -*.pot # Pycharm .idea diff --git a/front/package.json b/front/package.json index 6e1033851..01d396d01 100644 --- a/front/package.json +++ b/front/package.json @@ -39,7 +39,7 @@ "vue-upload-component": "^2.7.4", "vuedraggable": "^2.14.1", "vuex": "^3.0.1", - "vuex-persistedstate": "^2.4.2", + "vuex-persistedstate": "^2.5.2", "vuex-router-sync": "^5.0.0" }, "devDependencies": { diff --git a/front/src/App.vue b/front/src/App.vue index e8ab18694..babe359b0 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -49,6 +49,7 @@ export default { PlaylistModal }, created () { + console.log(this.$t('hello')) this.$store.dispatch('instance/fetchSettings') let self = this setInterval(() => { diff --git a/front/src/main.js b/front/src/main.js index 5030e5c9c..aa63944fc 100644 --- a/front/src/main.js +++ b/front/src/main.js @@ -11,6 +11,9 @@ import router from './router' import axios from 'axios' import {VueMasonryPlugin} from 'vue-masonry' import VueLazyload from 'vue-lazyload' +import i18next from 'i18next' +// import i18nextFetch from 'i18next-fetch-backend' +import VueI18Next from '@panter/vue-i18next' import store from './store' import config from './config' import { sync } from 'vuex-router-sync' @@ -27,6 +30,7 @@ window.$ = window.jQuery = require('jquery') require('semantic-ui-css/semantic.js') require('masonry-layout') +Vue.use(VueI18Next) Vue.use(VueMasonryPlugin) Vue.use(VueLazyload) Vue.config.productionTip = false @@ -77,11 +81,31 @@ axios.interceptors.response.use(function (response) { return Promise.reject(error) }) store.dispatch('auth/check') + +// i18n +i18next.init({ + lng: 'en', + resources: { + en: { + translation: { + 'hello': 'Hello' + } + }, + fr: { + translation: { + 'hello': 'Bonjour' + } + } + } +}) +const i18n = new VueI18Next(i18next) + /* eslint-disable no-new */ new Vue({ el: '#app', router, store, + i18n, template: '', components: { App } }) diff --git a/po/funkwhale.pot b/po/funkwhale.pot new file mode 100644 index 000000000..b5480d509 --- /dev/null +++ b/po/funkwhale.pot @@ -0,0 +1,13 @@ +msgid "" +msgstr "" +"Project-Id-Version: Funkwhale 0.8\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-03-23 15:49-0700\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" From bddb950beb3c73f3424e96d1043134bff6a5fc35 Mon Sep 17 00:00:00 2001 From: Bat Date: Wed, 11 Apr 2018 14:24:22 +0100 Subject: [PATCH 039/101] i18n: lazy-loading --- front/src/App.vue | 4 +++- front/src/main.js | 24 +++++++++--------------- front/static/translations/en.json | 4 +++- front/static/translations/fr.json | 4 +++- po/en.po | 3 +++ po/fr.po | 3 +++ po/funkwhale.pot | 3 +++ 7 files changed, 27 insertions(+), 18 deletions(-) diff --git a/front/src/App.vue b/front/src/App.vue index babe359b0..705ed9cf3 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -9,6 +9,7 @@

Links

@@ -50,8 +52,6 @@ export default { PlaylistModal }, created () { - // this.$i18n.i18next.changeLanguage('fr') - console.log(this.$t('Hello, world!')) this.$store.dispatch('instance/fetchSettings') let self = this setInterval(() => { diff --git a/front/src/components/activity/Like.vue b/front/src/components/activity/Like.vue index ffb831278..deda121cc 100644 --- a/front/src/components/activity/Like.vue +++ b/front/src/components/activity/Like.vue @@ -5,17 +5,20 @@
- - favorited a track - + + + +
{{ event.object.name }} - - - + + {{ event.object.album }} + {{ event.object.artist }} + + + {{ event.object.artist }} +
diff --git a/front/src/components/activity/Listen.vue b/front/src/components/activity/Listen.vue index 7c8ee8a69..d207c280d 100644 --- a/front/src/components/activity/Listen.vue +++ b/front/src/components/activity/Listen.vue @@ -5,17 +5,20 @@
- - listened to a track - + + + +
{{ event.object.name }} - - - + + {{ event.object.album }} + {{ event.object.artist }} + + + {{ event.object.artist }} +
diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue index f2a389862..14d381ca1 100644 --- a/front/src/components/audio/PlayButton.vue +++ b/front/src/components/audio/PlayButton.vue @@ -1,18 +1,18 @@ diff --git a/front/src/components/audio/track/Table.vue b/front/src/components/audio/track/Table.vue index 512ba1b49..7045cf9bd 100644 --- a/front/src/components/audio/track/Table.vue +++ b/front/src/components/audio/track/Table.vue @@ -4,9 +4,9 @@ - Title - Artist - Album + + + @@ -20,20 +20,18 @@ - + -
- Download tracks -
+
-

There is currently no way to download directly multiple tracks from funkwhale as a ZIP archive. - However, you can use a command line tools such as cURL to easily download a list of tracks. -

-

Simply copy paste the snippet below into a terminal to launch the download.

-
- Keep your PRIVATE_TOKEN secret as it gives access to your account. -
+ + cURL + + +
 export PRIVATE_TOKEN="{{ $store.state.auth.token }}"