mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2024-11-15 03:04:28 +00:00
More tests
This commit is contained in:
parent
b727c8c9c7
commit
559c65f474
4 changed files with 179 additions and 48 deletions
150
activitypub.py
150
activitypub.py
|
@ -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
22
app.py
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ requests
|
||||||
markdown
|
markdown
|
||||||
python-u2flib-server
|
python-u2flib-server
|
||||||
Flask
|
Flask
|
||||||
|
Flask-WTF
|
||||||
Celery
|
Celery
|
||||||
pymongo
|
pymongo
|
||||||
pyld
|
pyld
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue