Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
interfect
funkwhale
Commits
3cf1a170
Verified
Commit
3cf1a170
authored
Apr 01, 2018
by
Eliot Berriot
Browse files
We are now able to receive a toot and react to it
parent
6fbf8fa4
Changes
12
Hide whitespace changes
Inline
Side-by-side
api/config/settings/common.py
View file @
3cf1a170
...
...
@@ -344,7 +344,12 @@ REST_FRAMEWORK = {
),
'DEFAULT_PAGINATION_CLASS'
:
'funkwhale_api.common.pagination.FunkwhalePagination'
,
'PAGE_SIZE'
:
25
,
'DEFAULT_PARSER_CLASSES'
:
(
'rest_framework.parsers.JSONParser'
,
'rest_framework.parsers.FormParser'
,
'rest_framework.parsers.MultiPartParser'
,
'funkwhale_api.federation.parsers.ActivityParser'
,
),
'DEFAULT_AUTHENTICATION_CLASSES'
:
(
'funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS'
,
'rest_framework_jwt.authentication.JSONWebTokenAuthentication'
,
...
...
api/funkwhale_api/federation/activity.py
0 → 100644
View file @
3cf1a170
ACTIVITY_TYPES
=
[
'Accept'
,
'Add'
,
'Announce'
,
'Arrive'
,
'Block'
,
'Create'
,
'Delete'
,
'Dislike'
,
'Flag'
,
'Follow'
,
'Ignore'
,
'Invite'
,
'Join'
,
'Leave'
,
'Like'
,
'Listen'
,
'Move'
,
'Offer'
,
'Question'
,
'Reject'
,
'Read'
,
'Remove'
,
'TentativeReject'
,
'TentativeAccept'
,
'Travel'
,
'Undo'
,
'Update'
,
'View'
,
]
OBJECT_TYPES
=
[
'Article'
,
'Audio'
,
'Document'
,
'Event'
,
'Image'
,
'Note'
,
'Page'
,
'Place'
,
'Profile'
,
'Relationship'
,
'Tombstone'
,
'Video'
,
]
def
deliver
(
content
,
on_behalf_of
,
to
=
[]):
pass
api/funkwhale_api/federation/actors.py
View file @
3cf1a170
import
requests
import
xml
from
django.urls
import
reverse
from
django.conf
import
settings
from
rest_framework.exceptions
import
PermissionDenied
from
dynamic_preferences.registries
import
global_preferences_registry
from
.
import
activity
from
.
import
models
from
.
import
serializers
from
.
import
utils
def
remove_tags
(
text
):
return
''
.
join
(
xml
.
etree
.
ElementTree
.
fromstring
(
text
).
itertext
())
def
get_actor_data
(
actor_url
):
response
=
requests
.
get
(
actor_url
,
...
...
@@ -23,39 +32,132 @@ def get_actor_data(actor_url):
raise
ValueError
(
'Invalid actor payload: {}'
.
format
(
response
.
text
))
SYSTEM_ACTORS
=
{
'library'
:
{
'get_actor'
:
lambda
:
models
.
Actor
(
**
get_base_system_actor_arguments
(
'library'
)),
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
)
)
a
.
pk
=
self
.
id
return
a
def
get_instance_argument
(
self
,
id
,
name
,
summary
,
**
kwargs
):
preferences
=
global_preferences_registry
.
manager
()
p
=
{
'preferred_username'
:
id
,
'domain'
:
settings
.
FEDERATION_HOSTNAME
,
'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
})),
'shared_inbox_url'
:
utils
.
full_url
(
reverse
(
'federation:instance-actors-inbox'
,
kwargs
=
{
'actor'
:
id
})),
'inbox_url'
:
utils
.
full_url
(
reverse
(
'federation:instance-actors-inbox'
,
kwargs
=
{
'actor'
:
id
})),
'outbox_url'
:
utils
.
full_url
(
reverse
(
'federation:instance-actors-outbox'
,
kwargs
=
{
'actor'
:
id
})),
'public_key'
:
preferences
[
'federation__public_key'
],
'summary'
:
summary
.
format
(
host
=
settings
.
FEDERATION_HOSTNAME
)
}
p
.
update
(
kwargs
)
return
p
def
get_inbox
(
self
,
data
,
actor
=
None
):
raise
NotImplementedError
def
post_inbox
(
self
,
data
,
actor
=
None
):
raise
NotImplementedError
def
get_outbox
(
self
,
data
,
actor
=
None
):
raise
NotImplementedError
def
post_outbox
(
self
,
data
,
actor
=
None
):
raise
NotImplementedError
class
LibraryActor
(
SystemActor
):
id
=
'library'
name
=
'{host}
\'
s library'
summary
=
'Bot account to federate with {host}
\'
s library'
additional_attributes
=
{
'manually_approves_followers'
:
True
}
}
def
get_base_system_actor_arguments
(
name
):
preferences
=
global_preferences_registry
.
manager
()
return
{
'preferred_username'
:
name
,
'domain'
:
settings
.
FEDERATION_HOSTNAME
,
'type'
:
'Person'
,
'name'
:
'{}
\'
s library'
.
format
(
settings
.
FEDERATION_HOSTNAME
),
'manually_approves_followers'
:
True
,
'url'
:
utils
.
full_url
(
reverse
(
'federation:instance-actors-detail'
,
kwargs
=
{
'actor'
:
name
})),
'shared_inbox_url'
:
utils
.
full_url
(
reverse
(
'federation:instance-actors-inbox'
,
kwargs
=
{
'actor'
:
name
})),
'inbox_url'
:
utils
.
full_url
(
reverse
(
'federation:instance-actors-inbox'
,
kwargs
=
{
'actor'
:
name
})),
'outbox_url'
:
utils
.
full_url
(
reverse
(
'federation:instance-actors-outbox'
,
kwargs
=
{
'actor'
:
name
})),
'public_key'
:
preferences
[
'federation__public_key'
],
'summary'
:
'Bot account to federate with {}
\'
s library'
.
format
(
settings
.
FEDERATION_HOSTNAME
),
class
TestActor
(
SystemActor
):
id
=
'test'
name
=
'{host}
\'
s test account'
summary
=
(
'Bot account to test federation with {host}. '
'Send me /ping and I
\'
ll answer you.'
)
additional_attributes
=
{
'manually_approves_followers'
:
False
}
def
get_outbox
(
self
,
data
,
actor
=
None
):
return
{
"@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"
:
[]
}
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
if
ac
[
'type'
]
==
'Create'
and
ac
[
'object'
][
'type'
]
==
'Note'
:
# we received a toot \o/
command
=
self
.
parse_command
(
ac
[
'object'
][
'content'
])
if
command
==
'ping'
:
activity
.
deliver
(
content
=
'Pong!'
,
to
=
[
ac
[
'actor'
]],
on_behalf_of
=
self
.
get_actor_instance
())
def
parse_command
(
self
,
message
):
"""
Remove any links or fancy markup to extract /command from
a note message.
"""
raw
=
remove_tags
(
message
)
try
:
return
raw
.
split
(
'/'
)[
1
]
except
IndexError
:
return
SYSTEM_ACTORS
=
{
'library'
:
LibraryActor
(),
'test'
:
TestActor
(),
}
api/funkwhale_api/federation/authentication.py
View file @
3cf1a170
...
...
@@ -45,6 +45,7 @@ class SignatureAuthentication(authentication.BaseAuthentication):
return
serializer
.
build
()
def
authenticate
(
self
,
request
):
setattr
(
request
,
'actor'
,
None
)
actor
=
self
.
authenticate_actor
(
request
)
user
=
AnonymousUser
()
setattr
(
request
,
'actor'
,
actor
)
...
...
api/funkwhale_api/federation/parsers.py
0 → 100644
View file @
3cf1a170
from
rest_framework
import
parsers
class
ActivityParser
(
parsers
.
JSONParser
):
media_type
=
'application/activity+json'
api/funkwhale_api/federation/serializers.py
View file @
3cf1a170
...
...
@@ -6,6 +6,7 @@ from django.conf import settings
from
rest_framework
import
serializers
from
dynamic_preferences.registries
import
global_preferences_registry
from
.
import
activity
from
.
import
models
from
.
import
utils
...
...
@@ -105,3 +106,70 @@ class ActorWebfingerSerializer(serializers.ModelSerializer):
instance
.
url
]
return
data
class
ActivitySerializer
(
serializers
.
Serializer
):
actor
=
serializers
.
URLField
()
id
=
serializers
.
URLField
()
type
=
serializers
.
ChoiceField
(
choices
=
[(
c
,
c
)
for
c
in
activity
.
ACTIVITY_TYPES
])
object
=
serializers
.
JSONField
()
def
validate_object
(
self
,
value
):
try
:
type
=
value
[
'type'
]
except
KeyError
:
raise
serializers
.
ValidationError
(
'Missing object type'
)
try
:
object_serializer
=
OBJECT_SERIALIZERS
[
type
]
except
KeyError
:
raise
serializers
.
ValidationError
(
'Unsupported type {}'
.
format
(
type
))
serializer
=
object_serializer
(
data
=
value
)
serializer
.
is_valid
(
raise_exception
=
True
)
return
serializer
.
data
def
validate_actor
(
self
,
value
):
request_actor
=
self
.
context
.
get
(
'actor'
)
if
request_actor
and
request_actor
.
url
!=
value
:
raise
serializers
.
ValidationError
(
'The actor making the request do not match'
' the activity actor'
)
return
value
class
ObjectSerializer
(
serializers
.
Serializer
):
id
=
serializers
.
URLField
()
url
=
serializers
.
URLField
(
required
=
False
,
allow_null
=
True
)
type
=
serializers
.
ChoiceField
(
choices
=
[(
c
,
c
)
for
c
in
activity
.
OBJECT_TYPES
])
content
=
serializers
.
CharField
(
required
=
False
,
allow_null
=
True
)
summary
=
serializers
.
CharField
(
required
=
False
,
allow_null
=
True
)
name
=
serializers
.
CharField
(
required
=
False
,
allow_null
=
True
)
published
=
serializers
.
DateTimeField
(
required
=
False
,
allow_null
=
True
)
updated
=
serializers
.
DateTimeField
(
required
=
False
,
allow_null
=
True
)
to
=
serializers
.
ListField
(
child
=
serializers
.
URLField
(),
required
=
False
,
allow_null
=
True
)
cc
=
serializers
.
ListField
(
child
=
serializers
.
URLField
(),
required
=
False
,
allow_null
=
True
)
bto
=
serializers
.
ListField
(
child
=
serializers
.
URLField
(),
required
=
False
,
allow_null
=
True
)
bcc
=
serializers
.
ListField
(
child
=
serializers
.
URLField
(),
required
=
False
,
allow_null
=
True
)
OBJECT_SERIALIZERS
=
{
t
:
ObjectSerializer
for
t
in
activity
.
OBJECT_TYPES
}
api/funkwhale_api/federation/views.py
View file @
3cf1a170
...
...
@@ -36,18 +36,35 @@ class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
raise
Http404
def
retrieve
(
self
,
request
,
*
args
,
**
kwargs
):
actor
_conf
=
self
.
get_object
()
actor
=
actor_conf
[
'get_actor'
]
()
system_
actor
=
self
.
get_object
()
actor
=
system_actor
.
get_actor_instance
()
serializer
=
serializers
.
ActorSerializer
(
actor
)
return
response
.
Response
(
serializer
.
data
,
status
=
200
)
@
detail_route
(
methods
=
[
'get'
])
@
detail_route
(
methods
=
[
'get'
,
'post'
])
def
inbox
(
self
,
request
,
*
args
,
**
kwargs
):
raise
NotImplementedError
()
system_actor
=
self
.
get_object
()
handler
=
getattr
(
system_actor
,
'{}_inbox'
.
format
(
request
.
method
.
lower
()
))
@
detail_route
(
methods
=
[
'get'
])
try
:
data
=
handler
(
request
.
data
,
actor
=
request
.
actor
)
except
NotImplementedError
:
return
response
.
Response
(
status
=
405
)
return
response
.
Response
(
data
,
status
=
200
)
@
detail_route
(
methods
=
[
'get'
,
'post'
])
def
outbox
(
self
,
request
,
*
args
,
**
kwargs
):
raise
NotImplementedError
()
system_actor
=
self
.
get_object
()
handler
=
getattr
(
system_actor
,
'{}_outbox'
.
format
(
request
.
method
.
lower
()
))
try
:
data
=
handler
(
request
.
data
,
actor
=
request
.
actor
)
except
NotImplementedError
:
return
response
.
Response
(
status
=
405
)
return
response
.
Response
(
data
,
status
=
200
)
class
WellKnownViewSet
(
FederationMixin
,
viewsets
.
GenericViewSet
):
...
...
@@ -82,5 +99,5 @@ class WellKnownViewSet(FederationMixin, viewsets.GenericViewSet):
def
handler_acct
(
self
,
clean_result
):
username
,
hostname
=
clean_result
actor
=
actors
.
SYSTEM_ACTORS
[
username
]
[
'
get_actor
'
]
()
actor
=
actors
.
SYSTEM_ACTORS
[
username
]
.
get_actor
_instance
()
return
serializers
.
ActorWebfingerSerializer
(
actor
).
data
api/funkwhale_api/federation/webfinger.py
View file @
3cf1a170
...
...
@@ -30,7 +30,8 @@ def clean_acct(acct_string):
raise
forms
.
ValidationError
(
'Invalid format'
)
if
hostname
!=
settings
.
FEDERATION_HOSTNAME
:
raise
forms
.
ValidationError
(
'Invalid hostname'
)
raise
forms
.
ValidationError
(
'Invalid hostname {}'
.
format
(
hostname
))
if
username
not
in
actors
.
SYSTEM_ACTORS
:
raise
forms
.
ValidationError
(
'Invalid username'
)
...
...
api/tests/federation/test_activity.py
0 → 100644
View file @
3cf1a170
api/tests/federation/test_actors.py
View file @
3cf1a170
import
pytest
from
django.urls
import
reverse
from
rest_framework
import
exceptions
from
funkwhale_api.federation
import
actors
from
funkwhale_api.federation
import
serializers
from
funkwhale_api.federation
import
utils
...
...
@@ -37,10 +41,106 @@ def test_get_library(settings, preferences):
reverse
(
'federation:instance-actors-inbox'
,
kwargs
=
{
'actor'
:
'library'
})),
'outbox_url'
:
utils
.
full_url
(
reverse
(
'federation:instance-actors-outbox'
,
kwargs
=
{
'actor'
:
'library'
})),
'public_key'
:
'public_key'
,
'summary'
:
'Bot account to federate with {}
\'
s library'
.
format
(
settings
.
FEDERATION_HOSTNAME
),
}
actor
=
actors
.
SYSTEM_ACTORS
[
'library'
]
[
'
get_actor
'
]
()
actor
=
actors
.
SYSTEM_ACTORS
[
'library'
]
.
get_actor
_instance
()
for
key
,
value
in
expected
.
items
():
assert
getattr
(
actor
,
key
)
==
value
def
test_get_test
(
settings
,
preferences
):
preferences
[
'federation__public_key'
]
=
'public_key'
expected
=
{
'preferred_username'
:
'test'
,
'domain'
:
settings
.
FEDERATION_HOSTNAME
,
'type'
:
'Person'
,
'name'
:
'{}
\'
s test account'
.
format
(
settings
.
FEDERATION_HOSTNAME
),
'manually_approves_followers'
:
False
,
'url'
:
utils
.
full_url
(
reverse
(
'federation:instance-actors-detail'
,
kwargs
=
{
'actor'
:
'test'
})),
'shared_inbox_url'
:
utils
.
full_url
(
reverse
(
'federation:instance-actors-inbox'
,
kwargs
=
{
'actor'
:
'test'
})),
'inbox_url'
:
utils
.
full_url
(
reverse
(
'federation:instance-actors-inbox'
,
kwargs
=
{
'actor'
:
'test'
})),
'outbox_url'
:
utils
.
full_url
(
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
),
}
actor
=
actors
.
SYSTEM_ACTORS
[
'test'
].
get_actor_instance
()
for
key
,
value
in
expected
.
items
():
assert
getattr
(
actor
,
key
)
==
value
def
test_test_get_outbox
():
expected
=
{
"@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"
:
[]
}
data
=
actors
.
SYSTEM_ACTORS
[
'test'
].
get_outbox
({},
actor
=
None
)
assert
data
==
expected
def
test_test_post_inbox_requires_authenticated_actor
():
with
pytest
.
raises
(
exceptions
.
PermissionDenied
):
actors
.
SYSTEM_ACTORS
[
'test'
].
post_inbox
({},
actor
=
None
)
def
test_test_post_outbox_validates_actor
(
nodb_factories
):
actor
=
nodb_factories
[
'federation.Actor'
]()
data
=
{
'actor'
:
'noop'
}
with
pytest
.
raises
(
exceptions
.
ValidationError
)
as
exc_info
:
actors
.
SYSTEM_ACTORS
[
'test'
].
post_inbox
(
data
,
actor
=
actor
)
msg
=
'The actor making the request do not match'
assert
msg
in
exc_info
.
value
def
test_test_post_outbox_handles_create_note
(
mocker
,
factories
):
deliver
=
mocker
.
patch
(
'funkwhale_api.federation.activity.deliver'
)
actor
=
factories
[
'federation.Actor'
]()
data
=
{
'actor'
:
actor
.
url
,
'type'
:
'Create'
,
'id'
:
'http://test.federation/activity'
,
'object'
:
{
'type'
:
'Note'
,
'id'
:
'http://test.federation/object'
,
'content'
:
'<p><a>@mention</a> /ping</p>'
}
}
actors
.
SYSTEM_ACTORS
[
'test'
].
post_inbox
(
data
,
actor
=
actor
)
deliver
.
assert_called_once_with
(
content
=
'Pong!'
,
to
=
[
actor
.
url
],
on_behalf_of
=
actors
.
SYSTEM_ACTORS
[
'test'
].
get_actor_instance
()
)
api/tests/federation/test_views.py
View file @
3cf1a170
...
...
@@ -10,7 +10,7 @@ from funkwhale_api.federation import webfinger
@
pytest
.
mark
.
parametrize
(
'system_actor'
,
actors
.
SYSTEM_ACTORS
.
keys
())
def
test_instance_actors
(
system_actor
,
db
,
settings
,
api_client
):
actor
=
actors
.
SYSTEM_ACTORS
[
system_actor
]
[
'
get_actor
'
]
()
actor
=
actors
.
SYSTEM_ACTORS
[
system_actor
]
.
get_actor
_instance
()
url
=
reverse
(
'federation:instance-actors-detail'
,
kwargs
=
{
'actor'
:
system_actor
})
...
...
@@ -27,7 +27,7 @@ def test_instance_actors(system_actor, db, settings, api_client):
(
'instance-actors-detail'
,
{
'actor'
:
'library'
}),
(
'well-known-webfinger'
,
{}),
])
def
test_instance_
inbox
_405_if_federation_disabled
(
def
test_instance_
endpoints
_405_if_federation_disabled
(
authenticated_actor
,
db
,
settings
,
api_client
,
route
,
kwargs
):
settings
.
FEDERATION_ENABLED
=
False
url
=
reverse
(
'federation:{}'
.
format
(
route
),
kwargs
=
kwargs
)
...
...
@@ -53,7 +53,7 @@ def test_wellknown_webfinger_validates_resource(
@
pytest
.
mark
.
parametrize
(
'system_actor'
,
actors
.
SYSTEM_ACTORS
.
keys
())
def
test_wellknown_webfinger_system
(
system_actor
,
db
,
api_client
,
settings
,
mocker
):
actor
=
actors
.
SYSTEM_ACTORS
[
system_actor
]
[
'
get_actor
'
]
()
actor
=
actors
.
SYSTEM_ACTORS
[
system_actor
]
.
get_actor
_instance
()
url
=
reverse
(
'federation:well-known-webfinger'
)
response
=
api_client
.
get
(
url
,
data
=
{
'resource'
:
'acct:{}'
.
format
(
actor
.
webfinger_subject
)})
...
...
api/tests/federation/test_webfinger.py
View file @
3cf1a170
...
...
@@ -32,7 +32,7 @@ def test_webfinger_clean_acct(settings):
@
pytest
.
mark
.
parametrize
(
'resource,message'
,
[
(
'service'
,
'Invalid format'
),
(
'service@test.com'
,
'Invalid hostname'
),
(
'service@test.com'
,
'Invalid hostname
test.com
'
),
(
'noop@test.federation'
,
'Invalid account'
),
])
def
test_webfinger_clean_acct_errors
(
resource
,
message
,
settings
):
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment