diff --git a/blueprints/tasks.py b/blueprints/tasks.py index 4ffff69..0070e09 100644 --- a/blueprints/tasks.py +++ b/blueprints/tasks.py @@ -21,6 +21,7 @@ from core.activitypub import SIG_AUTH from core.activitypub import Box from core.activitypub import _actor_hash from core.activitypub import _add_answers_to_question +from core.activitypub import _cache_actor_icon from core.activitypub import post_to_outbox from core.activitypub import save_reply from core.activitypub import update_cached_actor @@ -30,7 +31,9 @@ from core.inbox import process_inbox from core.meta import MetaKey from core.meta import by_object_id from core.meta import by_remote_id +from core.meta import by_type from core.meta import flag +from core.meta import inc from core.meta import upsert from core.notifications import set_inbox_flags from core.outbox import process_outbox @@ -250,7 +253,10 @@ def task_cache_attachments() -> _Response: app.logger.info(f"caching attachment for activity={activity!r}") # Generates thumbnails for the actor's icon and the attachments if any - obj = activity.get_object() + if activity.has_type([ap.ActivityType.CREATE, ap.ActivityType.ANNOUNCE]): + obj = activity.get_object() + else: + obj = activity if obj.has_type(ap.ActivityType.VIDEO): if isinstance(obj.url, list): @@ -515,37 +521,67 @@ def task_process_reply() -> _Response: app.logger.info(f"activity={activity!r} is not a reply, dropping it") return "" - # new_threads = [] root_reply = in_reply_to - reply = ap.fetch_remote_activity(root_reply) + + reply = ap.fetch_remote_activity(in_reply_to) if reply.has_type(ap.ActivityType.CREATE): reply = reply.get_object() - while reply is not None: + new_replies = [activity, reply] + + while 1: in_reply_to = reply.get_in_reply_to() if not in_reply_to: break + root_reply = in_reply_to reply = ap.fetch_remote_activity(root_reply) + if reply.has_type(ap.ActivityType.CREATE): reply = reply.get_object() + new_replies.append(reply) + app.logger.info(f"root_reply={reply!r} for activity={activity!r}") # Ensure the "root reply" is present in the inbox/outbox if not find_one_activity(by_object_id(root_reply)): return "" - actor = activity.get_actor() + for new_reply in new_replies: + if find_one_activity(by_object_id(new_reply.id)) or DB.replies.find_one( + {"remote_id": root_reply} + ): + continue - save_reply( - activity, - { - "meta.thread_root_parent": root_reply, - **flag(MetaKey.ACTOR, actor.to_dict(embed=True)), - }, - ) - # FIXME(tsileo): cache actor here, spawn a task to cache attachment if needed + actor = new_reply.get_actor() + save_reply( + new_reply, + { + "meta.thread_root_parent": root_reply, + **flag(MetaKey.ACTOR, actor.to_dict(embed=True)), + **flag(MetaKey.ACTOR_HASH, _actor_hash(actor)), + }, + ) + + # Update the reply counters + if new_reply.get_in_reply_to(): + update_one_activity( + { + **by_object_id(new_reply.get_in_reply_to()), + **by_type(ap.ActivityType.CREATE), + }, + inc(MetaKey.COUNT_REPLY, 1), + ) + DB.replies.update_one( + by_remote_id(new_reply.get_in_reply_to()), + inc(MetaKey.COUNT_REPLY, 1), + ) + + # Cache the actor icon + _cache_actor_icon(actor) + # And cache the attachments + Tasks.cache_attachments(new_reply.id) except (ActivityGoneError, ActivityNotFoundError): app.logger.exception(f"dropping activity {iri}, skip processing") return "" diff --git a/core/activitypub.py b/core/activitypub.py index dc4f06a..1be68f7 100644 --- a/core/activitypub.py +++ b/core/activitypub.py @@ -34,9 +34,12 @@ from core.db import update_many_activities from core.meta import Box from core.meta import MetaKey from core.meta import by_object_id +from core.meta import by_remote_id from core.meta import flag +from core.meta import inc from core.meta import upsert from core.tasks import Tasks +from utils import now logger = logging.getLogger(__name__) @@ -187,6 +190,7 @@ def save_reply(activity: ap.BaseActivity, meta: Dict[str, Any] = {}) -> None: if visibility in [ap.Visibility.PUBLIC, ap.Visibility.UNLISTED]: is_public = True + published = activity.published if activity.published else now() DB.replies.insert_one( { "activity": activity.to_dict(), @@ -199,6 +203,7 @@ def save_reply(activity: ap.BaseActivity, meta: Dict[str, Any] = {}) -> None: "server": urlparse(activity.id).netloc, "visibility": visibility.name, "actor_id": activity.get_actor().id, + MetaKey.PUBLISHED.value: published, **meta, }, } @@ -467,10 +472,9 @@ class MicroblogPubBackend(Backend): if not in_reply_to: return - new_threads = [] - root_reply = in_reply_to - reply = ap.fetch_remote_activity(root_reply) - # FIXME(tsileo): can be a Create here (instead of a Note, Hubzilla's doing that) + reply = ap.fetch_remote_activity(in_reply_to) + if reply.has_type(ap.ActivityType.CREATE): + reply = reply.get_object() # FIXME(tsileo): can be a 403 too, in this case what to do? not error at least # Ensure the this is a local reply, of a question, with a direct "to" addressing @@ -497,35 +501,17 @@ class MicroblogPubBackend(Backend): ) return None + # It's a regular reply, try to increment the reply counter creply = DB.activities.find_one_and_update( - {"activity.object.id": in_reply_to}, - {"$inc": {"meta.count_reply": 1, "meta.count_direct_reply": 1}}, + by_object_id(in_reply_to), inc(MetaKey.COUNT_REPLY, 1) ) if not creply: - # It means the activity is not in the inbox, and not in the outbox, we want to save it - save(Box.REPLIES, reply) - new_threads.append(reply.id) - # TODO(tsileo): parses the replies collection and import the replies? - - while reply is not None: - in_reply_to = reply.get_in_reply_to() - if not in_reply_to: - break - root_reply = in_reply_to - reply = ap.fetch_remote_activity(root_reply) - # FIXME(tsileo): can be a Create here (instead of a Note, Hubzilla's doing that) - q = {"activity.object.id": root_reply} - if not DB.activities.count(q): - save(Box.REPLIES, reply) - new_threads.append(reply.id) - - DB.activities.update_one( - {"remote_id": create.id}, {"$set": {"meta.thread_root_parent": root_reply}} - ) - DB.activities.update( - {"box": Box.REPLIES.value, "remote_id": {"$in": new_threads}}, - {"$set": {"meta.thread_root_parent": root_reply}}, - ) + # Maybe it's the reply of a reply? + if not DB.replies.find_one_and_update( + by_remote_id(in_reply_to), inc(MetaKey.COUNT_REPLY, 1) + ): + # We don't have the reply stored, spawn a task to process it (and determine if it needs to be saved) + Tasks.process_reply(create.get_object().id) def embed_collection(total_items, first_page_id): diff --git a/core/meta.py b/core/meta.py index 7e9e762..0c99127 100644 --- a/core/meta.py +++ b/core/meta.py @@ -42,6 +42,7 @@ class MetaKey(Enum): COUNT_LIKE = "count_like" COUNT_BOOST = "count_boost" + COUNT_REPLY = "count_reply" def _meta(mk: MetaKey) -> str: diff --git a/templates/utils.html b/templates/utils.html index 4c4dc26..d6d298a 100644 --- a/templates/utils.html +++ b/templates/utils.html @@ -170,7 +170,7 @@ {% if request.path == url_for("admin.admin_lookup") %} {% endif %} - + {% endif %}