diff --git a/.travis.yml b/.travis.yml
index e41bf99..c2bc717 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,7 +13,7 @@ install:
- pip install -r dev-requirements.txt
script:
- mypy --ignore-missing-imports .
-# - flake8
+ - flake8 activitypub.py
- cp -r tests/me.yml config/me.yml
- docker-compose up -d
- docker-compose ps
diff --git a/activitypub.py b/activitypub.py
index 31eab5b..1b51789 100644
--- a/activitypub.py
+++ b/activitypub.py
@@ -1,5 +1,3 @@
-import typing
-import re
import json
import binascii
import os
@@ -7,16 +5,12 @@ from datetime import datetime
from enum import Enum
import requests
-from bleach.linkifier import Linker
from bson.objectid import ObjectId
from html2text import html2text
from feedgen.feed import FeedGenerator
-from markdown import markdown
from utils.linked_data_sig import generate_signature
from utils.actor_service import NotAnActorError
-from utils.webfinger import get_actor_url
-from utils.content_helper import parse_markdown
from config import USERNAME, BASE_URL, ID
from config import CTX_AS, CTX_SECURITY, AS_PUBLIC
from config import KEY, DB, ME, ACTOR_SERVICE
@@ -24,7 +18,7 @@ from config import OBJECT_SERVICE
from config import PUBLIC_INSTANCES
import tasks
-from typing import List, Optional, Tuple, Dict, Any, Union, Type
+from typing import List, Optional, Dict, Any, Union
from typing import TypeVar
A = TypeVar('A', bound='BaseActivity')
@@ -32,10 +26,6 @@ ObjectType = Dict[str, Any]
ObjectOrIDType = Union[str, ObjectType]
-# Pleroma sample
-# {'@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', {'Emoji': 'toot:Emoji', 'Hashtag': 'as:Hashtag', 'atomUri': 'ostatus:atomUri', 'conversation': 'ostatus:conversation', 'inReplyToAtomUri': 'ostatus:inReplyToAtomUri', 'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers', 'ostatus': 'http://ostatus.org#', 'sensitive': 'as:sensitive', 'toot': 'http://joinmastodon.org/ns#'}], 'actor': 'https://soc.freedombone.net/users/bob', 'attachment': [{'mediaType': 'image/jpeg', 'name': 'stallmanlemote.jpg', 'type': 'Document', 'url': 'https://soc.freedombone.net/media/e1a3ca6f-df73-4f2d-a931-c389a221b008/stallmanlemote.jpg'}], 'attributedTo': 'https://soc.freedombone.net/users/bob', 'cc': ['https://cybre.space/users/vantablack', 'https://soc.freedombone.net/users/bob/followers'], 'content': '@vantablack
stallmanlemote.jpg', 'context': 'tag:cybre.space,2018-04-05:objectId=5756519:objectType=Conversation', 'conversation': 'tag:cybre.space,2018-04-05:objectId=5756519:objectType=Conversation', 'emoji': {}, 'id': 'https://soc.freedombone.net/objects/3f0faeca-4d37-4acf-b990-6a50146d23cc', 'inReplyTo': 'https://cybre.space/users/vantablack/statuses/99808953472969467', 'inReplyToStatusId': 300713, 'like_count': 1, 'likes': ['https://cybre.space/users/vantablack'], 'published': '2018-04-05T21:30:52.658817Z', 'sensitive': False, 'summary': None, 'tag': [{'href': 'https://cybre.space/users/vantablack', 'name': '@vantablack@cybre.space', 'type': 'Mention'}], 'to': ['https://www.w3.org/ns/activitystreams#Public'], 'type': 'Note'}
-
-
class ActivityTypes(Enum):
ANNOUNCE = 'Announce'
BLOCK = 'Block'
@@ -142,7 +132,7 @@ class BaseActivity(object):
if not self.NO_CONTEXT:
if not isinstance(self._data['@context'], list):
self._data['@context'] = [self._data['@context']]
- if not CTX_SECURITY in self._data['@context']:
+ if CTX_SECURITY not in self._data['@context']:
self._data['@context'].append(CTX_SECURITY)
if isinstance(self._data['@context'][-1], dict):
self._data['@context'][-1]['Hashtag'] = 'as:Hashtag'
@@ -326,7 +316,6 @@ class BaseActivity(object):
except NotImplementedError:
pass
- #return
generate_signature(activity, KEY.privkey)
payload = json.dumps(activity)
print('will post')
@@ -399,17 +388,16 @@ class Person(BaseActivity):
ACTIVITY_TYPE = ActivityTypes.PERSON
def _init(self, **kwargs):
- #if 'icon' in kwargs:
- # self._data['icon'] = Image(**kwargs.pop('icon'))
+ # if 'icon' in kwargs:
+ # self._data['icon'] = Image(**kwargs.pop('icon'))
pass
def _verify(self) -> None:
ACTOR_SERVICE.get(self._data['id'])
def _to_dict(self, data):
- #if 'icon' in data:
- # data['icon'] = data['icon'].to_dict()
- #
+ # if 'icon' in data:
+ # data['icon'] = data['icon'].to_dict()
return data
@@ -512,6 +500,7 @@ class Undo(BaseActivity):
except NotImplementedError:
pass
+
class Like(BaseActivity):
ACTIVITY_TYPE = ActivityTypes.LIKE
ALLOWED_OBJECT_TYPES = [ActivityTypes.NOTE]
@@ -567,7 +556,12 @@ class Announce(BaseActivity):
return
# Save/cache the object, and make it part of the stream so we can fetch it
if isinstance(self._data['object'], str):
- raw_obj = OBJECT_SERVICE.get(self._data['object'], reload_cache=True, part_of_stream=True, announce_published=self._data['published'])
+ raw_obj = OBJECT_SERVICE.get(
+ self._data['object'],
+ reload_cache=True,
+ part_of_stream=True,
+ announce_published=self._data['published'],
+ )
obj = parse_activity(raw_obj)
else:
obj = self.get_object()
@@ -581,7 +575,12 @@ class Announce(BaseActivity):
def _post_to_outbox(self, obj_id: str, activity: ObjectType, recipients: List[str]) -> None:
if isinstance(self._data['object'], str):
# Put the object in the cache
- OBJECT_SERVICE.get(self._data['object'], reload_cache=True, part_of_stream=True, announce_published=self._data['published'])
+ OBJECT_SERVICE.get(
+ self._data['object'],
+ reload_cache=True,
+ part_of_stream=True,
+ announce_published=self._data['published'],
+ )
obj = self.get_object()
DB.inbox.update_one({'activity.object.id': obj.id}, {'$set': {'meta.boosted': obj_id}})
@@ -702,7 +701,7 @@ class Create(BaseActivity):
parent = DB.inbox.find_one({'activity.type': 'Create', 'activity.object.id': in_reply_to})
if parent is None:
# The reply is a note from the outbox
- data = DB.outbox.update_one(
+ DB.outbox.update_one(
{'activity.object.id': in_reply_to},
{'$inc': {'meta.count_reply': 1}},
)
@@ -732,7 +731,7 @@ class Note(BaseActivity):
# for t in kwargs.get('tag', []):
# if t['type'] == 'Mention':
# cc -> c['href']
-
+
def _recipients(self) -> List[str]:
# TODO(tsileo): audience support?
recipients = [] # type: List[str]
@@ -772,6 +771,7 @@ class Note(BaseActivity):
published=datetime.utcnow().replace(microsecond=0).isoformat() + 'Z',
)
+
_ACTIVITY_TYPE_TO_CLS = {
ActivityTypes.IMAGE: Image,
ActivityTypes.PERSON: Person,
@@ -789,6 +789,7 @@ _ACTIVITY_TYPE_TO_CLS = {
ActivityTypes.TOMBSTONE: Tombstone,
}
+
def parse_activity(payload: ObjectType) -> BaseActivity:
t = ActivityTypes(payload['type'])
if t not in _ACTIVITY_TYPE_TO_CLS:
@@ -801,11 +802,10 @@ def gen_feed():
fg = FeedGenerator()
fg.id(f'{ID}')
fg.title(f'{USERNAME} notes')
- fg.author( {'name': USERNAME,'email':'t@a4.io'} )
+ fg.author({'name': USERNAME, 'email': 't@a4.io'})
fg.link(href=ID, rel='alternate')
fg.description(f'{USERNAME} notes')
fg.logo(ME.get('icon', {}).get('url'))
- #fg.link( href='http://larskiesow.de/test.atom', rel='self' )
fg.language('en')
for item in DB.outbox.find({'type': 'Create'}, limit=50):
fe = fg.add_entry()
@@ -829,7 +829,8 @@ def json_feed(path: str) -> Dict[str, Any]:
})
return {
"version": "https://jsonfeed.org/version/1",
- "user_comment": "This is a microblog feed. You can add this to your feed reader using the following URL: " + ID + path,
+ "user_comment": ("This is a microblog feed. You can add this to your feed reader using the following URL: "
+ + ID + path),
"title": USERNAME,
"home_page_url": ID,
"feed_url": ID + path,
@@ -958,17 +959,17 @@ def build_ordered_collection(col, q=None, cursor=None, map_func=None, limit=50,
# No cursor, this is the first page and we return an OrderedCollection
if not cursor:
resp = {
- '@context': CTX_AS,
- 'first': {
- 'id': BASE_URL + '/' + col_name + '?cursor=' + start_cursor,
- 'orderedItems': data,
- 'partOf': BASE_URL + '/' + col_name,
- 'totalItems': total_items,
- 'type': 'OrderedCollectionPage'
- },
- 'id': BASE_URL + '/' + col_name,
+ '@context': CTX_AS,
+ 'id': f'{BASE_URL}/{col_name}',
'totalItems': total_items,
- 'type': 'OrderedCollection'
+ 'type': 'OrderedCollection',
+ 'first': {
+ 'id': f'{BASE_URL}/{col_name}?cursor={start_cursor}',
+ 'orderedItems': data,
+ 'partOf': f'{BASE_URL}/{col_name}',
+ 'totalItems': total_items,
+ 'type': 'OrderedCollectionPage'
+ },
}
if len(data) == limit:
diff --git a/app.py b/app.py
index c766a3d..55e2d47 100644
--- a/app.py
+++ b/app.py
@@ -34,7 +34,7 @@ import activitypub
import config
from activitypub import ActivityTypes
from activitypub import clean_activity
-from activitypub import parse_markdown
+from utils.content_helper import parse_markdown
from config import KEY
from config import DB
from config import ME
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 9eba2d1..dc80f66 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,4 +1,5 @@
pytest
requests
+html2text
flake8
mypy
diff --git a/tests/integration_test.py b/tests/integration_test.py
index 7b8510f..4270b4b 100644
--- a/tests/integration_test.py
+++ b/tests/integration_test.py
@@ -1,7 +1,28 @@
-import requests
+import os
-def test_ping_homepage():
+import pytest
+import requests
+from html2text import html2text
+
+
+@pytest.fixture
+def config():
+ """Return the current config as a dict."""
+ import yaml
+ with open(os.path.join(os.path.dirname(__file__), '..', 'config/me.yml'), 'rb') as f:
+ yield yaml.load(f)
+
+
+def resp2plaintext(resp):
+ """Convert the body of a requests reponse to plain text in order to make basic assertions."""
+ return html2text(resp.text)
+
+
+def test_ping_homepage(config):
"""Ensure the homepage is accessible."""
resp = requests.get('http://localhost:5005')
resp.raise_for_status()
assert resp.status_code == 200
+ body = resp2plaintext(resp)
+ assert config['name'] in body
+ assert f"@{config['username']}@{config['domain']}" in body