diff --git a/activitypub.py b/activitypub.py index 1534a78..ec14cc3 100644 --- a/activitypub.py +++ b/activitypub.py @@ -380,6 +380,16 @@ class BaseActivity(object): else: try: actor = Person(**ACTOR_SERVICE.get(recipient)) + + if actor.endpoints: + shared_inbox = actor.endpoints.get('sharedInbox') + if shared_inbox not in out: + out.append(shared_inbox) + continue + + if actor.inbox and actor.inbox not in out: + out.append(actor.inbox) + except NotAnActorError as error: # Is the activity a `Collection`/`OrderedCollection`? if error.activity and error.activity['type'] in [ActivityType.COLLECTION.value, @@ -400,17 +410,6 @@ class BaseActivity(object): if col_actor.inbox and col_actor.inbox not in out: out.append(col_actor.inbox) - continue - - if actor.endpoints: - shared_inbox = actor.endpoints.get('sharedInbox') - if shared_inbox not in out: - out.append(shared_inbox) - continue - - if actor.inbox and actor.inbox not in out: - out.append(actor.inbox) - return out def build_undo(self) -> 'BaseActivity': @@ -1076,11 +1075,11 @@ def parse_collection(payload: Optional[Dict[str, Any]] = None, url: Optional[str return activitypub_utils.parse_collection(payload, url) -def embed_collection(data): +def embed_collection(total_items, first_page_id): return { - "type": "Collection", - "totalItems": len(data), - "items": data, + "type": ActivityType.ORDERED_COLLECTION.value, + "totalItems": total_items, + "first": first_page_id, } @@ -1097,7 +1096,7 @@ def build_ordered_collection(col, q=None, cursor=None, map_func=None, limit=50, return { 'id': BASE_URL + '/' + col_name, 'totalItems': 0, - 'type': 'OrderedCollection', + 'type': ActivityType.ORDERED_COLLECTION.value, 'orederedItems': [], } @@ -1115,13 +1114,13 @@ def build_ordered_collection(col, q=None, cursor=None, map_func=None, limit=50, '@context': CTX_AS, 'id': f'{BASE_URL}/{col_name}', 'totalItems': total_items, - 'type': 'OrderedCollection', + 'type': ActivityType.ORDERED_COLLECTION.value, 'first': { 'id': f'{BASE_URL}/{col_name}?cursor={start_cursor}', 'orderedItems': data, 'partOf': f'{BASE_URL}/{col_name}', 'totalItems': total_items, - 'type': 'OrderedCollectionPage' + 'type': ActivityType.ORDERED_COLLECTION_PAGE.value, }, } @@ -1130,10 +1129,11 @@ def build_ordered_collection(col, q=None, cursor=None, map_func=None, limit=50, return resp + # If there's a cursor, then we return an OrderedCollectionPage resp = { '@context': CTX_AS, - 'type': 'OrderedCollectionPage', + 'type': ActivityType.ORDERED_COLLECTION_PAGE.value, 'id': BASE_URL + '/' + col_name + '?cursor=' + start_cursor, 'totalItems': total_items, 'partOf': BASE_URL + '/' + col_name, @@ -1142,4 +1142,6 @@ def build_ordered_collection(col, q=None, cursor=None, map_func=None, limit=50, if len(data) == limit: resp['next'] = BASE_URL + '/' + col_name + '?cursor=' + next_page_cursor + # TODO(tsileo): implements prev with prev= + return resp diff --git a/app.py b/app.py index 1b10749..127c0f5 100644 --- a/app.py +++ b/app.py @@ -471,18 +471,21 @@ 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', {}): - col_likes = raw_doc['meta']['col_likes'] - raw_doc['activity']['object']['likes'] = embed_collection(col_likes) + raw_doc['activity']['object']['replies'] = embed_collection( + raw_doc.get('meta', {}).get('count_direct_reply', 0), + f'{ID}/outbox/{raw_doc["id"]}/replies', + ) - if 'col_shares' in raw_doc.get('meta', {}): - col_shares = raw_doc['meta']['col_shares'] - raw_doc['activity']['object']['shares'] = embed_collection(col_shares) + raw_doc['activity']['object']['likes'] = embed_collection( + raw_doc.get('meta', {}).get('count_like', 0), + f'{ID}/outbox/{raw_doc["id"]}/likes', + ) + + raw_doc['activity']['object']['shares'] = embed_collection( + raw_doc.get('meta', {}).get('count_boost', 0), + f'{ID}/outbox/{raw_doc["id"]}/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 @@ -547,6 +550,86 @@ def outbox_activity(item_id): return jsonify(**obj['object']) +@app.route('/outbox//replies') +def outbox_activity_replies(item_id): + if not is_api_request(): + abort(404) + data = DB.outbox.find_one({'id': item_id, 'meta.deleted': False}) + if not data: + abort(404) + obj = activitypub.parse_activity(data) + if obj.type_enum != ActivityType.CREATE: + abort(404) + + q = { + 'meta.deleted': False, + 'type': ActivityType.CREATE.value, + 'activity.object.inReplyTo': obj.get_object().id, + } + + return jsonify(**activitypub.build_ordered_collection( + DB.inbox, + q=q, + cursor=request.args.get('cursor'), + map_func=lambda doc: doc['activity'], + col_name=f'outbox/{item_id}/replies', + )) + + +@app.route('/outbox//likes') +def outbox_activity_likes(item_id): + if not is_api_request(): + abort(404) + data = DB.outbox.find_one({'id': item_id, 'meta.deleted': False}) + if not data: + abort(404) + obj = activitypub.parse_activity(data) + if obj.type_enum != ActivityType.CREATE: + abort(404) + + q = { + 'meta.undo': False, + 'type': ActivityType.LIKE.value, + '$or': [{'activity.object.id': obj.get_object().id}, + {'activity.object': obj.get_object().id}], + } + + return jsonify(**activitypub.build_ordered_collection( + DB.inbox, + q=q, + cursor=request.args.get('cursor'), + map_func=lambda doc: doc['activity'], + col_name=f'outbox/{item_id}/likes', + )) + + +@app.route('/outbox//shares') +def outbox_activity_shares(item_id): + if not is_api_request(): + abort(404) + data = DB.outbox.find_one({'id': item_id, 'meta.deleted': False}) + if not data: + abort(404) + obj = activitypub.parse_activity(data) + if obj.type_enum != ActivityType.CREATE: + abort(404) + + q = { + 'meta.undo': False, + 'type': ActivityType.ANNOUNCE.value, + '$or': [{'activity.object.id': obj.get_object().id}, + {'activity.object': obj.get_object().id}], + } + + return jsonify(**activitypub.build_ordered_collection( + DB.inbox, + q=q, + cursor=request.args.get('cursor'), + map_func=lambda doc: doc['activity'], + col_name=f'outbox/{item_id}/shares', + )) + + @app.route('/admin', methods=['GET']) @login_required def admin(): diff --git a/tests/federation_test.py b/tests/federation_test.py index c681dc1..9a6c5a1 100644 --- a/tests/federation_test.py +++ b/tests/federation_test.py @@ -301,8 +301,8 @@ def test_post_content_and_like(): note = instance1.outbox_get(f'{create_id}/activity') assert 'likes' in note - assert len(note['likes']['items']) == 1 - assert note['likes']['items'][0]['id'] == like_id + assert note['likes']['totalItems'] == 1 + # assert note['likes']['items'][0]['id'] == like_id def test_post_content_and_like_unlike(): @@ -327,8 +327,9 @@ def test_post_content_and_like_unlike(): note = instance1.outbox_get(f'{create_id}/activity') assert 'likes' in note - assert len(note['likes']['items']) == 1 - assert note['likes']['items'][0]['id'] == like_id + assert note['likes']['totalItems'] == 1 + # FIXME(tsileo): parse the collection + # assert note['likes']['items'][0]['id'] == like_id instance2.undo(like_id) @@ -338,7 +339,7 @@ def test_post_content_and_like_unlike(): note = instance1.outbox_get(f'{create_id}/activity') assert 'likes' in note - assert len(note['likes']['items']) == 0 + assert note['likes']['totalItems'] == 0 def test_post_content_and_boost(): @@ -363,8 +364,9 @@ def test_post_content_and_boost(): note = instance1.outbox_get(f'{create_id}/activity') assert 'shares' in note - assert len(note['shares']['items']) == 1 - assert note['shares']['items'][0]['id'] == boost_id + assert note['shares']['totalItems'] == 1 + # FIXME(tsileo): parse the collection + # assert note['shares']['items'][0]['id'] == boost_id def test_post_content_and_boost_unboost(): @@ -389,8 +391,9 @@ def test_post_content_and_boost_unboost(): note = instance1.outbox_get(f'{create_id}/activity') assert 'shares' in note - assert len(note['shares']['items']) == 1 - assert note['shares']['items'][0]['id'] == boost_id + assert note['shares']['totalItems'] == 1 + # FIXME(tsileo): parse the collection + # assert note['shares']['items'][0]['id'] == boost_id instance2.undo(boost_id) @@ -400,7 +403,7 @@ def test_post_content_and_boost_unboost(): note = instance1.outbox_get(f'{create_id}/activity') assert 'shares' in note - assert len(note['shares']['items']) == 0 + assert note['shares']['totalItems'] == 0 def test_post_content_and_post_reply():