Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
jovuit
funkwhale
Commits
a2520513
Verified
Commit
a2520513
authored
Apr 02, 2018
by
Eliot Berriot
Browse files
PoC with receiving /ping from Mastodon and replying pong
parent
3cf1a170
Changes
6
Hide whitespace changes
Inline
Side-by-side
api/funkwhale_api/federation/activity.py
View file @
a2520513
import
logging
import
json
import
requests
import
requests_http_signature
from
.
import
signing
logger
=
logging
.
getLogger
(
__name__
)
ACTIVITY_TYPES
=
[
'Accept'
,
...
...
@@ -47,5 +54,32 @@ OBJECT_TYPES = [
'Video'
,
]
def
deliver
(
content
,
on_behalf_of
,
to
=
[]):
pass
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
,
)
for
url
in
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
(
auth
=
auth
,
json
=
activity
,
url
=
recipient_actor
.
inbox_url
,
headers
=
{
'Content-Type'
:
'application/activity+json'
}
)
response
.
raise_for_status
()
logger
.
debug
(
'Remote answered with %s'
,
response
.
status_code
)
api/funkwhale_api/federation/actors.py
View file @
a2520513
import
logging
import
requests
import
xml
from
django.urls
import
reverse
from
django.conf
import
settings
from
django.urls
import
reverse
from
django.utils
import
timezone
from
rest_framework.exceptions
import
PermissionDenied
from
dynamic_preferences.registries
import
global_preferences_registry
from
.
import
activity
from
.
import
factories
from
.
import
models
from
.
import
serializers
from
.
import
utils
logger
=
logging
.
getLogger
(
__name__
)
def
remove_tags
(
text
):
return
''
.
join
(
xml
.
etree
.
ElementTree
.
fromstring
(
text
).
itertext
())
logger
.
debug
(
'Removing tags from %s'
,
text
)
return
''
.
join
(
xml
.
etree
.
ElementTree
.
fromstring
(
'<div>{}</div>'
.
format
(
text
)).
itertext
())
def
get_actor_data
(
actor_url
):
...
...
@@ -32,6 +38,13 @@ def get_actor_data(actor_url):
raise
ValueError
(
'Invalid actor payload: {}'
.
format
(
response
.
text
))
def
get_actor
(
actor_url
):
data
=
get_actor_data
(
actor_url
)
serializer
=
serializers
.
ActorSerializer
(
data
=
data
)
serializer
.
is_valid
(
raise_exception
=
True
)
return
serializer
.
build
()
class
SystemActor
(
object
):
additional_attributes
=
{}
...
...
@@ -73,6 +86,7 @@ class SystemActor(object):
'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
)
...
...
@@ -136,14 +150,13 @@ class TestActor(SystemActor):
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'
:
activity
.
deliver
(
content
=
'Pong!'
,
to
=
[
ac
[
'actor'
]],
on_behalf_of
=
self
.
get_actor_instance
())
self
.
handle_ping
(
ac
,
actor
)
def
parse_command
(
self
,
message
):
"""
...
...
@@ -156,6 +169,67 @@ class TestActor(SystemActor):
except
IndexError
:
return
def
handle_ping
(
self
,
ac
,
sender
):
now
=
timezone
.
now
()
test_actor
=
self
.
get_actor_instance
()
reply_url
=
'https://{}/activities/note/{}'
.
format
(
settings
.
FEDERATION_HOSTNAME
,
now
.
timestamp
()
)
mention
=
'@{}@{}'
.
format
(
sender
.
preferred_username
,
sender
.
domain
)
reply_content
=
'{} Pong!'
.
format
(
mention
)
reply_activity
=
{
"@context"
:
[
"https://www.w3.org/ns/activitystreams"
,
"https://w3id.org/security/v1"
,
{
"manuallyApprovesFollowers"
:
"as:manuallyApprovesFollowers"
,
"sensitive"
:
"as:sensitive"
,
"movedTo"
:
"as:movedTo"
,
"Hashtag"
:
"as:Hashtag"
,
"ostatus"
:
"http://ostatus.org#"
,
"atomUri"
:
"ostatus:atomUri"
,
"inReplyToAtomUri"
:
"ostatus:inReplyToAtomUri"
,
"conversation"
:
"ostatus:conversation"
,
"toot"
:
"http://joinmastodon.org/ns#"
,
"Emoji"
:
"toot:Emoji"
}
],
'type'
:
'Create'
,
'actor'
:
test_actor
.
url
,
'id'
:
'{}/activity'
.
format
(
reply_url
),
'published'
:
now
.
isoformat
(),
'to'
:
ac
[
'actor'
],
'cc'
:
[],
'object'
:
factories
.
NoteFactory
(
content
=
'Pong!'
,
summary
=
None
,
published
=
now
.
isoformat
(),
id
=
reply_url
,
inReplyTo
=
ac
[
'object'
][
'id'
],
sensitive
=
False
,
url
=
reply_url
,
to
=
[
ac
[
'actor'
]],
attributedTo
=
test_actor
.
url
,
cc
=
[],
attachment
=
[],
tag
=
[
{
"type"
:
"Mention"
,
"href"
:
ac
[
'actor'
],
"name"
:
mention
}
]
)
}
activity
.
deliver
(
reply_activity
,
to
=
[
ac
[
'actor'
]],
on_behalf_of
=
test_actor
)
SYSTEM_ACTORS
=
{
'library'
:
LibraryActor
(),
...
...
api/funkwhale_api/federation/factories.py
View file @
a2520513
...
...
@@ -2,6 +2,8 @@ import factory
import
requests
import
requests_http_signature
from
django.utils
import
timezone
from
funkwhale_api.factories
import
registry
from
.
import
keys
...
...
@@ -64,8 +66,24 @@ class ActorFactory(factory.DjangoModelFactory):
@
classmethod
def
_generate
(
cls
,
create
,
attrs
):
has_public
=
attrs
.
get
(
'public_key'
)
is
None
has_private
=
attrs
.
get
(
'private_key'
)
is
None
has_public
=
attrs
.
get
(
'public_key'
)
is
not
None
has_private
=
attrs
.
get
(
'private_key'
)
is
not
None
if
not
has_public
and
not
has_private
:
attrs
[
'private_key'
],
attrs
[
'public'
]
=
keys
.
get_key_pair
()
private
,
public
=
keys
.
get_key_pair
()
attrs
[
'private_key'
]
=
private
.
decode
(
'utf-8'
)
attrs
[
'public_key'
]
=
public
.
decode
(
'utf-8'
)
return
super
().
_generate
(
create
,
attrs
)
@
registry
.
register
(
name
=
'federation.Note'
)
class
NoteFactory
(
factory
.
Factory
):
type
=
'Note'
id
=
factory
.
Faker
(
'url'
)
published
=
factory
.
LazyFunction
(
lambda
:
timezone
.
now
().
isoformat
()
)
inReplyTo
=
None
content
=
factory
.
Faker
(
'sentence'
)
class
Meta
:
model
=
dict
api/funkwhale_api/federation/models.py
View file @
a2520513
...
...
@@ -38,3 +38,7 @@ class Actor(models.Model):
self
.
preferred_username
,
settings
.
FEDERATION_HOSTNAME
,
)
@
property
def
private_key_id
(
self
):
return
'{}#main-key'
.
format
(
self
.
url
)
api/tests/federation/test_activity.py
View file @
a2520513
from
funkwhale_api.federation
import
activity
def
test_deliver
(
nodb_factories
,
r_mock
,
mocker
):
to
=
nodb_factories
[
'federation.Actor'
]()
mocker
.
patch
(
'funkwhale_api.federation.actors.get_actor'
,
return_value
=
to
)
sender
=
nodb_factories
[
'federation.Actor'
]()
ac
=
{
'id'
:
'http://test.federation/activity'
,
'type'
:
'Create'
,
'actor'
:
sender
.
url
,
'object'
:
{
'id'
:
'http://test.federation/note'
,
'type'
:
'Note'
,
'content'
:
'Hello'
,
}
}
r_mock
.
post
(
to
.
inbox_url
)
activity
.
deliver
(
ac
,
to
=
[
to
.
url
],
on_behalf_of
=
sender
,
)
request
=
r_mock
.
request_history
[
0
]
assert
r_mock
.
called
is
True
assert
r_mock
.
call_count
==
1
assert
request
.
url
==
to
.
inbox_url
assert
request
.
headers
[
'content-type'
]
==
'application/activity+json'
api/tests/federation/test_actors.py
View file @
a2520513
import
pytest
from
django.urls
import
reverse
from
django.utils
import
timezone
from
rest_framework
import
exceptions
from
funkwhale_api.federation
import
actors
...
...
@@ -128,6 +130,8 @@ def test_test_post_outbox_handles_create_note(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
)
data
=
{
'actor'
:
actor
.
url
,
'type'
:
'Create'
,
...
...
@@ -138,9 +142,27 @@ def test_test_post_outbox_handles_create_note(mocker, factories):
'content'
:
'<p><a>@mention</a> /ping</p>'
}
}
expected_note
=
factories
[
'federation.Note'
](
id
=
'https://test.federation/activities/note/{}'
.
format
(
now
.
timestamp
()
),
content
=
'Pong!'
,
published
=
now
.
isoformat
(),
inReplyTo
=
data
[
'object'
][
'id'
],
)
test_actor
=
actors
.
SYSTEM_ACTORS
[
'test'
].
get_actor_instance
()
expected_activity
=
{
'actor'
:
test_actor
.
url
,
'id'
:
'https://test.federation/activities/note/{}/activity'
.
format
(
now
.
timestamp
()
),
'type'
:
'Create'
,
'published'
:
now
.
isoformat
(),
'object'
:
expected_note
}
actors
.
SYSTEM_ACTORS
[
'test'
].
post_inbox
(
data
,
actor
=
actor
)
deliver
.
assert_called_once_with
(
content
=
'Pong!'
,
expected_activity
,
to
=
[
actor
.
url
],
on_behalf_of
=
actors
.
SYSTEM_ACTORS
[
'test'
].
get_actor_instance
()
)
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment