Working the test suite, and fixing formatting
This commit is contained in:
parent
3496aee23f
commit
c9ba124bdd
5 changed files with 62 additions and 39 deletions
|
@ -13,7 +13,7 @@ install:
|
||||||
- pip install -r dev-requirements.txt
|
- pip install -r dev-requirements.txt
|
||||||
script:
|
script:
|
||||||
- mypy --ignore-missing-imports .
|
- mypy --ignore-missing-imports .
|
||||||
# - flake8
|
- flake8 activitypub.py
|
||||||
- cp -r tests/me.yml config/me.yml
|
- cp -r tests/me.yml config/me.yml
|
||||||
- docker-compose up -d
|
- docker-compose up -d
|
||||||
- docker-compose ps
|
- docker-compose ps
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import typing
|
|
||||||
import re
|
|
||||||
import json
|
import json
|
||||||
import binascii
|
import binascii
|
||||||
import os
|
import os
|
||||||
|
@ -7,16 +5,12 @@ from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from bleach.linkifier import Linker
|
|
||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
from html2text import html2text
|
from html2text import html2text
|
||||||
from feedgen.feed import FeedGenerator
|
from feedgen.feed import FeedGenerator
|
||||||
from markdown import markdown
|
|
||||||
|
|
||||||
from utils.linked_data_sig import generate_signature
|
from utils.linked_data_sig import generate_signature
|
||||||
from utils.actor_service import NotAnActorError
|
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 USERNAME, BASE_URL, ID
|
||||||
from config import CTX_AS, CTX_SECURITY, AS_PUBLIC
|
from config import CTX_AS, CTX_SECURITY, AS_PUBLIC
|
||||||
from config import KEY, DB, ME, ACTOR_SERVICE
|
from config import KEY, DB, ME, ACTOR_SERVICE
|
||||||
|
@ -24,7 +18,7 @@ from config import OBJECT_SERVICE
|
||||||
from config import PUBLIC_INSTANCES
|
from config import PUBLIC_INSTANCES
|
||||||
import tasks
|
import tasks
|
||||||
|
|
||||||
from typing import List, Optional, Tuple, Dict, Any, Union, Type
|
from typing import List, Optional, Dict, Any, Union
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
A = TypeVar('A', bound='BaseActivity')
|
A = TypeVar('A', bound='BaseActivity')
|
||||||
|
@ -32,10 +26,6 @@ ObjectType = Dict[str, Any]
|
||||||
ObjectOrIDType = Union[str, ObjectType]
|
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': '<span><a href=\'https://cybre.space/users/vantablack\'>@<span>vantablack</span></a></span><br><a href="https://soc.freedombone.net/media/e1a3ca6f-df73-4f2d-a931-c389a221b008/stallmanlemote.jpg" class=\'attachment\'>stallmanlemote.jpg</a>', '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):
|
class ActivityTypes(Enum):
|
||||||
ANNOUNCE = 'Announce'
|
ANNOUNCE = 'Announce'
|
||||||
BLOCK = 'Block'
|
BLOCK = 'Block'
|
||||||
|
@ -142,7 +132,7 @@ class BaseActivity(object):
|
||||||
if not self.NO_CONTEXT:
|
if not self.NO_CONTEXT:
|
||||||
if not isinstance(self._data['@context'], list):
|
if not isinstance(self._data['@context'], list):
|
||||||
self._data['@context'] = [self._data['@context']]
|
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)
|
self._data['@context'].append(CTX_SECURITY)
|
||||||
if isinstance(self._data['@context'][-1], dict):
|
if isinstance(self._data['@context'][-1], dict):
|
||||||
self._data['@context'][-1]['Hashtag'] = 'as:Hashtag'
|
self._data['@context'][-1]['Hashtag'] = 'as:Hashtag'
|
||||||
|
@ -326,7 +316,6 @@ class BaseActivity(object):
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
#return
|
|
||||||
generate_signature(activity, KEY.privkey)
|
generate_signature(activity, KEY.privkey)
|
||||||
payload = json.dumps(activity)
|
payload = json.dumps(activity)
|
||||||
print('will post')
|
print('will post')
|
||||||
|
@ -399,17 +388,16 @@ class Person(BaseActivity):
|
||||||
ACTIVITY_TYPE = ActivityTypes.PERSON
|
ACTIVITY_TYPE = ActivityTypes.PERSON
|
||||||
|
|
||||||
def _init(self, **kwargs):
|
def _init(self, **kwargs):
|
||||||
#if 'icon' in kwargs:
|
# if 'icon' in kwargs:
|
||||||
# self._data['icon'] = Image(**kwargs.pop('icon'))
|
# self._data['icon'] = Image(**kwargs.pop('icon'))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _verify(self) -> None:
|
def _verify(self) -> None:
|
||||||
ACTOR_SERVICE.get(self._data['id'])
|
ACTOR_SERVICE.get(self._data['id'])
|
||||||
|
|
||||||
def _to_dict(self, data):
|
def _to_dict(self, data):
|
||||||
#if 'icon' in data:
|
# if 'icon' in data:
|
||||||
# data['icon'] = data['icon'].to_dict()
|
# data['icon'] = data['icon'].to_dict()
|
||||||
#
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@ -512,6 +500,7 @@ class Undo(BaseActivity):
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Like(BaseActivity):
|
class Like(BaseActivity):
|
||||||
ACTIVITY_TYPE = ActivityTypes.LIKE
|
ACTIVITY_TYPE = ActivityTypes.LIKE
|
||||||
ALLOWED_OBJECT_TYPES = [ActivityTypes.NOTE]
|
ALLOWED_OBJECT_TYPES = [ActivityTypes.NOTE]
|
||||||
|
@ -567,7 +556,12 @@ class Announce(BaseActivity):
|
||||||
return
|
return
|
||||||
# Save/cache the object, and make it part of the stream so we can fetch it
|
# Save/cache the object, and make it part of the stream so we can fetch it
|
||||||
if isinstance(self._data['object'], str):
|
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)
|
obj = parse_activity(raw_obj)
|
||||||
else:
|
else:
|
||||||
obj = self.get_object()
|
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:
|
def _post_to_outbox(self, obj_id: str, activity: ObjectType, recipients: List[str]) -> None:
|
||||||
if isinstance(self._data['object'], str):
|
if isinstance(self._data['object'], str):
|
||||||
# Put the object in the cache
|
# 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()
|
obj = self.get_object()
|
||||||
DB.inbox.update_one({'activity.object.id': obj.id}, {'$set': {'meta.boosted': obj_id}})
|
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})
|
parent = DB.inbox.find_one({'activity.type': 'Create', 'activity.object.id': in_reply_to})
|
||||||
if parent is None:
|
if parent is None:
|
||||||
# The reply is a note from the outbox
|
# The reply is a note from the outbox
|
||||||
data = DB.outbox.update_one(
|
DB.outbox.update_one(
|
||||||
{'activity.object.id': in_reply_to},
|
{'activity.object.id': in_reply_to},
|
||||||
{'$inc': {'meta.count_reply': 1}},
|
{'$inc': {'meta.count_reply': 1}},
|
||||||
)
|
)
|
||||||
|
@ -732,7 +731,7 @@ class Note(BaseActivity):
|
||||||
# for t in kwargs.get('tag', []):
|
# for t in kwargs.get('tag', []):
|
||||||
# if t['type'] == 'Mention':
|
# if t['type'] == 'Mention':
|
||||||
# cc -> c['href']
|
# cc -> c['href']
|
||||||
|
|
||||||
def _recipients(self) -> List[str]:
|
def _recipients(self) -> List[str]:
|
||||||
# TODO(tsileo): audience support?
|
# TODO(tsileo): audience support?
|
||||||
recipients = [] # type: List[str]
|
recipients = [] # type: List[str]
|
||||||
|
@ -772,6 +771,7 @@ class Note(BaseActivity):
|
||||||
published=datetime.utcnow().replace(microsecond=0).isoformat() + 'Z',
|
published=datetime.utcnow().replace(microsecond=0).isoformat() + 'Z',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
_ACTIVITY_TYPE_TO_CLS = {
|
_ACTIVITY_TYPE_TO_CLS = {
|
||||||
ActivityTypes.IMAGE: Image,
|
ActivityTypes.IMAGE: Image,
|
||||||
ActivityTypes.PERSON: Person,
|
ActivityTypes.PERSON: Person,
|
||||||
|
@ -789,6 +789,7 @@ _ACTIVITY_TYPE_TO_CLS = {
|
||||||
ActivityTypes.TOMBSTONE: Tombstone,
|
ActivityTypes.TOMBSTONE: Tombstone,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def parse_activity(payload: ObjectType) -> BaseActivity:
|
def parse_activity(payload: ObjectType) -> BaseActivity:
|
||||||
t = ActivityTypes(payload['type'])
|
t = ActivityTypes(payload['type'])
|
||||||
if t not in _ACTIVITY_TYPE_TO_CLS:
|
if t not in _ACTIVITY_TYPE_TO_CLS:
|
||||||
|
@ -801,11 +802,10 @@ def gen_feed():
|
||||||
fg = FeedGenerator()
|
fg = FeedGenerator()
|
||||||
fg.id(f'{ID}')
|
fg.id(f'{ID}')
|
||||||
fg.title(f'{USERNAME} notes')
|
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.link(href=ID, rel='alternate')
|
||||||
fg.description(f'{USERNAME} notes')
|
fg.description(f'{USERNAME} notes')
|
||||||
fg.logo(ME.get('icon', {}).get('url'))
|
fg.logo(ME.get('icon', {}).get('url'))
|
||||||
#fg.link( href='http://larskiesow.de/test.atom', rel='self' )
|
|
||||||
fg.language('en')
|
fg.language('en')
|
||||||
for item in DB.outbox.find({'type': 'Create'}, limit=50):
|
for item in DB.outbox.find({'type': 'Create'}, limit=50):
|
||||||
fe = fg.add_entry()
|
fe = fg.add_entry()
|
||||||
|
@ -829,7 +829,8 @@ def json_feed(path: str) -> Dict[str, Any]:
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
"version": "https://jsonfeed.org/version/1",
|
"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,
|
"title": USERNAME,
|
||||||
"home_page_url": ID,
|
"home_page_url": ID,
|
||||||
"feed_url": ID + path,
|
"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
|
# No cursor, this is the first page and we return an OrderedCollection
|
||||||
if not cursor:
|
if not cursor:
|
||||||
resp = {
|
resp = {
|
||||||
'@context': CTX_AS,
|
'@context': CTX_AS,
|
||||||
'first': {
|
'id': f'{BASE_URL}/{col_name}',
|
||||||
'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,
|
|
||||||
'totalItems': total_items,
|
'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:
|
if len(data) == limit:
|
||||||
|
|
2
app.py
2
app.py
|
@ -34,7 +34,7 @@ import activitypub
|
||||||
import config
|
import config
|
||||||
from activitypub import ActivityTypes
|
from activitypub import ActivityTypes
|
||||||
from activitypub import clean_activity
|
from activitypub import clean_activity
|
||||||
from activitypub import parse_markdown
|
from utils.content_helper import parse_markdown
|
||||||
from config import KEY
|
from config import KEY
|
||||||
from config import DB
|
from config import DB
|
||||||
from config import ME
|
from config import ME
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pytest
|
pytest
|
||||||
requests
|
requests
|
||||||
|
html2text
|
||||||
flake8
|
flake8
|
||||||
mypy
|
mypy
|
||||||
|
|
|
@ -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."""
|
"""Ensure the homepage is accessible."""
|
||||||
resp = requests.get('http://localhost:5005')
|
resp = requests.get('http://localhost:5005')
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
body = resp2plaintext(resp)
|
||||||
|
assert config['name'] in body
|
||||||
|
assert f"@{config['username']}@{config['domain']}" in body
|
||||||
|
|
Loading…
Reference in a new issue