More tests

This commit is contained in:
Thomas Sileo 2018-05-29 18:59:37 +02:00
parent b727c8c9c7
commit 559c65f474
4 changed files with 179 additions and 48 deletions

View file

@ -107,7 +107,7 @@ class BaseActivity(object):
# Initialize the object # Initialize the object
self._data: Dict[str, Any] = {'type': self.ACTIVITY_TYPE.value} self._data: Dict[str, Any] = {'type': self.ACTIVITY_TYPE.value}
logger.debug(f'initializing a {self.ACTIVITY_TYPE.value} activity') logger.debug(f'initializing a {self.ACTIVITY_TYPE.value} activity: {kwargs}')
if 'id' in kwargs: if 'id' in kwargs:
self._data['id'] = kwargs.pop('id') self._data['id'] = kwargs.pop('id')
@ -687,15 +687,22 @@ class Delete(BaseActivity):
ACTIVITY_TYPE = ActivityType.DELETE ACTIVITY_TYPE = ActivityType.DELETE
ALLOWED_OBJECT_TYPES = [ActivityType.NOTE, ActivityType.TOMBSTONE] ALLOWED_OBJECT_TYPES = [ActivityType.NOTE, ActivityType.TOMBSTONE]
def _recipients(self) -> List[str]: def _get_actual_object(self) -> BaseActivity:
obj = self.get_object() obj = self.get_object()
if obj.type_enum == ActivityType.TOMBSTONE: if obj.type_enum == ActivityType.TOMBSTONE:
obj = parse_activity(OBJECT_SERVICE.get(obj.id)) obj = parse_activity(OBJECT_SERVICE.get(obj.id))
return obj
def _recipients(self) -> List[str]:
obj = self._get_actual_object()
return obj._recipients() return obj._recipients()
def _process_from_inbox(self) -> None: def _process_from_inbox(self) -> None:
DB.inbox.update_one({'activity.object.id': self.get_object().id}, {'$set': {'meta.deleted': True}}) DB.inbox.update_one({'activity.object.id': self.get_object().id}, {'$set': {'meta.deleted': True}})
# TODO(tsileo): also delete copies stored in parents' `meta.replies` obj = self._get_actual_object()
if obj.type_enum == ActivityType.NOTE:
obj._delete_from_threads()
# TODO(tsileo): also purge the cache if it's a reply of a published activity # TODO(tsileo): also purge the cache if it's a reply of a published activity
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:
@ -773,36 +780,60 @@ class Create(BaseActivity):
return recipients return recipients
def _process_from_inbox(self): def _update_threads(self) -> None:
logger.debug('_update_threads hook')
obj = self.get_object() obj = self.get_object()
tasks.fetch_og.delay('INBOX', self.id) # TODO(tsileo): re-enable me
# tasks.fetch_og.delay('INBOX', self.id)
in_reply_to = obj.inReplyTo threads = []
if in_reply_to: reply = obj.get_local_reply()
parent = DB.inbox.find_one({'activity.type': 'Create', 'activity.object.id': in_reply_to}) logger.debug(f'initial_reply={reply}')
if not parent: reply_id = None
DB.outbox.update_one( direct_reply = 1
{'activity.object.id': in_reply_to}, while reply is not None:
{'$inc': {'meta.count_reply': 1}}, if not DB.inbox.find_one_and_update({'activity.object.id': reply.id}, {
) '$inc': {
return 'meta.count_reply': 1,
'meta.count_direct_reply': direct_reply,
},
}):
DB.outbox.update_one({'activity.object.id': reply.id}, {
'$inc': {
'meta.count_reply': 1,
'meta.count_direct_reply': direct_reply,
},
})
# If the note is a "reply of a reply" update the parent message direct_reply = 0
# TODO(tsileo): review this code reply_id = reply.id
while parent: reply = reply.get_local_reply()
DB.inbox.update_one({'_id': parent['_id']}, {'$push': {'meta.replies': self.to_dict()}}) logger.debug(f'next_reply={reply}')
in_reply_to = parent.get('activity', {}).get('object', {}).get('inReplyTo') if reply:
if in_reply_to: # Only append to threads if it's not the root
parent = DB.inbox.find_one({'activity.type': 'Create', 'activity.object.id': in_reply_to}) threads.append(reply_id)
if parent is None:
# The reply is a note from the outbox if reply_id:
DB.outbox.update_one( if not DB.inbox.find_one_and_update({'activity.object.id': obj.id}, {
{'activity.object.id': in_reply_to}, '$set': {
{'$inc': {'meta.count_reply': 1}}, 'meta.thread_parents': threads,
) 'meta.thread_root_parent': reply_id,
else: },
parent = None }):
DB.outbox.update_one({'activity.object.id': obj.id}, {
'$set': {
'meta.thread_parents': threads,
'meta.thread_root_parent': reply_id,
},
})
logger.debug('_update_threads done')
def _process_from_inbox(self) -> None:
self._update_threads()
def _post_to_outbox(self, obj_id: str, activity: ObjectType, recipients: List[str]) -> None:
self._update_threads()
def _should_purge_cache(self) -> bool: def _should_purge_cache(self) -> bool:
# TODO(tsileo): handle reply of a reply... # TODO(tsileo): handle reply of a reply...
@ -828,17 +859,9 @@ class Note(BaseActivity):
# Remove the `actor` field as `attributedTo` is used for `Note` instead # Remove the `actor` field as `attributedTo` is used for `Note` instead
if 'actor' in self._data: if 'actor' in self._data:
del(self._data['actor']) del(self._data['actor'])
# FIXME(tsileo): use kwarg
# TODO(tsileo): support mention tag
# TODO(tisleo): implement the tag endpoint
if 'sensitive' not in kwargs: if 'sensitive' not in kwargs:
self._data['sensitive'] = False self._data['sensitive'] = False
# FIXME(tsileo): add the tag in CC
# for t in kwargs.get('tag', []):
# if t['type'] == 'Mention':
# cc -> c['href']
def _recipients(self) -> List[str]: def _recipients(self) -> List[str]:
# TODO(tsileo): audience support? # TODO(tsileo): audience support?
recipients: List[str] = [] recipients: List[str] = []
@ -855,6 +878,51 @@ class Note(BaseActivity):
return recipients return recipients
def _delete_from_threads(self) -> None:
logger.debug('_delete_from_threads hook')
reply = self.get_local_reply()
logger.debug(f'initial_reply={reply}')
direct_reply = -1
while reply is not None:
if not DB.inbox.find_one_and_update({'activity.object.id': reply.id}, {
'$inc': {
'meta.count_reply': -1,
'meta.count_direct_reply': direct_reply,
},
}):
DB.outbox.update_one({'activity.object.id': reply.id}, {
'$inc': {
'meta.count_reply': 1,
'meta.count_direct_reply': direct_reply,
},
})
direct_reply = 0
reply = reply.get_local_reply()
logger.debug(f'next_reply={reply}')
logger.debug('_delete_from_threads done')
return None
def get_local_reply(self) -> Optional[BaseActivity]:
"Find the note reply if any."""
in_reply_to = self.inReplyTo
if not in_reply_to:
# This is the root comment
return None
inbox_parent = DB.inbox.find_one({'activity.type': 'Create', 'activity.object.id': in_reply_to})
if inbox_parent:
return parse_activity(inbox_parent['activity']['object'])
outbox_parent = DB.outbox.find_one({'activity.type': 'Create', 'activity.object.id': in_reply_to})
if outbox_parent:
return parse_activity(outbox_parent['activity']['object'])
# The parent is no stored on this instance
return None
def build_create(self) -> BaseActivity: def build_create(self) -> BaseActivity:
"""Wraps an activity in a Create activity.""" """Wraps an activity in a Create activity."""
create_payload = { create_payload = {
@ -872,10 +940,10 @@ class Note(BaseActivity):
def build_announce(self) -> BaseActivity: def build_announce(self) -> BaseActivity:
return Announce( return Announce(
object=self.id, object=self.id,
to=[AS_PUBLIC], to=[AS_PUBLIC],
cc=[ID+'/followers', self.attributedTo], cc=[ID+'/followers', self.attributedTo],
published=datetime.utcnow().replace(microsecond=0).isoformat() + 'Z', published=datetime.utcnow().replace(microsecond=0).isoformat() + 'Z',
) )
def build_delete(self) -> BaseActivity: def build_delete(self) -> BaseActivity:

22
app.py
View file

@ -30,6 +30,7 @@ from passlib.hash import bcrypt
from u2flib_server import u2f from u2flib_server import u2f
from urllib.parse import urlparse, urlencode from urllib.parse import urlparse, urlencode
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from flask_wtf.csrf import CSRFProtect
import activitypub import activitypub
import config import config
@ -57,10 +58,17 @@ from utils.key import get_secret_key
from utils.webfinger import get_remote_follow_template from utils.webfinger import get_remote_follow_template
from utils.webfinger import get_actor_url from utils.webfinger import get_actor_url
from typing import Dict, Any from typing import Dict, Any
app = Flask(__name__) app = Flask(__name__)
app.secret_key = get_secret_key('flask') app.secret_key = get_secret_key('flask')
app.config.update(
WTF_CSRF_CHECK_DEFAULT=False,
)
csrf = CSRFProtect(app)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -441,15 +449,21 @@ def webfinger():
def add_extra_collection(raw_doc: Dict[str, Any]) -> Dict[str, Any]: def add_extra_collection(raw_doc: Dict[str, Any]) -> Dict[str, Any]:
if raw_doc['activity']['type'] != ActivityType.CREATE.value:
return raw_doc
if 'col_likes' in raw_doc.get('meta', {}): if 'col_likes' in raw_doc.get('meta', {}):
col_likes = raw_doc['meta']['col_likes'] col_likes = raw_doc['meta']['col_likes']
if raw_doc['activity']['type'] == ActivityType.CREATE.value: raw_doc['activity']['object']['likes'] = embed_collection(col_likes)
raw_doc['activity']['object']['likes'] = embed_collection(col_likes)
if 'col_shares' in raw_doc.get('meta', {}): if 'col_shares' in raw_doc.get('meta', {}):
col_shares = raw_doc['meta']['col_shares'] col_shares = raw_doc['meta']['col_shares']
if raw_doc['activity']['type'] == ActivityType.CREATE.value: raw_doc['activity']['object']['shares'] = embed_collection(col_shares)
raw_doc['activity']['object']['shares'] = embed_collection(col_shares)
if 'count_direct_reply' in raw_doc.get('meta', {}):
# FIXME(tsileo): implements the collection handler
raw_doc['activity']['object']['replies'] = {'type': 'Collection', 'totalItems': raw_doc['meta']['count_direct_reply']}
return raw_doc return raw_doc

View file

@ -5,6 +5,7 @@ requests
markdown markdown
python-u2flib-server python-u2flib-server
Flask Flask
Flask-WTF
Celery Celery
pymongo pymongo
pyld pyld

View file

@ -297,6 +297,7 @@ def test_post_content_and_like():
assert len(note['likes']['items']) == 1 assert len(note['likes']['items']) == 1
assert note['likes']['items'][0]['id'] == like_id assert note['likes']['items'][0]['id'] == like_id
def test_post_content_and_like_unlike(): def test_post_content_and_like_unlike():
instance1, instance2 = _instances() instance1, instance2 = _instances()
# Instance1 follows instance2 # Instance1 follows instance2
@ -332,6 +333,7 @@ def test_post_content_and_like_unlike():
assert 'likes' in note assert 'likes' in note
assert len(note['likes']['items']) == 0 assert len(note['likes']['items']) == 0
def test_post_content_and_boost(): def test_post_content_and_boost():
instance1, instance2 = _instances() instance1, instance2 = _instances()
# Instance1 follows instance2 # Instance1 follows instance2
@ -426,8 +428,54 @@ def test_post_content_and_post_reply():
assert len(instance1_inbox_stream['items']) == 1 assert len(instance1_inbox_stream['items']) == 1
assert instance1_inbox_stream['items'][0]['id'] == instance2_create_id assert instance1_inbox_stream['items'][0]['id'] == instance2_create_id
# TODO(tsileo): find the activity and check the `replies` collection instance1_note = instance1.outbox_get(f'{instance1_create_id}/activity')
assert 'replies' in instance1_note
assert instance1_note['replies']['totalItems'] == 1
# TODO(tsileo): inspect the `replies` collection
# TODO(tsileo): def test_post_content_and_post_reply_and_delete():
# def test_post_content_and_post_reply_and_delete(): instance1, instance2 = _instances()
# Instance1 follows instance2
instance1.follow(instance2)
instance2.follow(instance1)
inbox_stream = instance2.stream_jsonfeed()
assert len(inbox_stream['items']) == 0
instance1_create_id = instance1.new_note('hello')
instance2_debug = instance2.debug()
assert instance2_debug['inbox'] == 3 # An Follow, Accept and Create activity should be there
assert instance2_debug['outbox'] == 2 # We've sent a Accept and a Follow activity
# Ensure the post is visible in instance2's stream
instance2_inbox_stream = instance2.stream_jsonfeed()
assert len(instance2_inbox_stream['items']) == 1
assert instance2_inbox_stream['items'][0]['id'] == instance1_create_id
instance2_create_id = instance2.new_note(f'hey @instance1@{instance1.docker_url}', reply=f'{instance1_create_id}/activity')
instance2_debug = instance2.debug()
assert instance2_debug['inbox'] == 3 # An Follow, Accept and Create activity should be there
assert instance2_debug['outbox'] == 3 # We've sent a Accept and a Follow and a Create activity
instance1_debug = instance1.debug()
assert instance1_debug['inbox'] == 3 # An Follow, Accept and Create activity should be there
assert instance1_debug['outbox'] == 3 # We've sent a Accept and a Follow and a Create activity
instance1_inbox_stream = instance1.stream_jsonfeed()
assert len(instance1_inbox_stream['items']) == 1
assert instance1_inbox_stream['items'][0]['id'] == instance2_create_id
instance1_note = instance1.outbox_get(f'{instance1_create_id}/activity')
assert 'replies' in instance1_note
assert instance1_note['replies']['totalItems'] == 1
instance2.delete(f'{instance2_create_id}/activity')
instance1_debug = instance1.debug()
assert instance1_debug['inbox'] == 4 # An Follow, Accept and Create and Delete activity should be there
assert instance1_debug['outbox'] == 3 # We've sent a Accept and a Follow and a Create activity
instance1_note = instance1.outbox_get(f'{instance1_create_id}/activity')
assert 'replies' in instance1_note
assert instance1_note['replies']['totalItems'] == 0