def _delete_process_inbox(delete: ap.Delete, new_meta: _NewMeta) -> None: _logger.info(f"process_inbox activity={delete!r}") obj_id = delete.get_object_id() _logger.debug(f"delete object={obj_id}") try: # FIXME(tsileo): call the DB here instead? like for the oubox obj = ap.fetch_remote_activity(obj_id) _logger.info(f"inbox_delete handle_replies obj={obj!r}") in_reply_to = obj.get_in_reply_to() if obj.inReplyTo else None if obj.has_type(ap.CREATE_TYPES): in_reply_to = ap._get_id( DB.activities.find_one({ "meta.object_id": obj_id, "type": ap.ActivityType.CREATE.value })["activity"]["object"].get("inReplyTo")) if in_reply_to: back._handle_replies_delete(MY_PERSON, in_reply_to) except Exception: _logger.exception(f"failed to handle delete replies for {obj_id}") update_one_activity( { **by_object_id(obj_id), **by_type(ap.ActivityType.CREATE) }, upsert({MetaKey.DELETED: True}), ) # Foce undo other related activities DB.activities.update(by_object_id(obj_id), upsert({MetaKey.UNDO: True}))
def _undo_process_inbox(activity: ap.Undo, new_meta: _NewMeta) -> None: _logger.info(f"process_inbox activity={activity!r}") # Fetch the object that's been undo'ed obj = activity.get_object() # Set the undo flag on the mentionned activity update_one_activity(by_remote_id(obj.id), upsert({MetaKey.UNDO: True})) # Handle cached counters if obj.has_type(ap.ActivityType.LIKE): # Update the meta counter if the object is published by the server update_one_activity( { **by_object_id(obj.get_object_id()), **by_type(ap.ActivityType.CREATE) }, inc(MetaKey.COUNT_LIKE, -1), ) elif obj.has_type(ap.ActivityType.ANNOUNCE): announced = obj.get_object() # Update the meta counter if the object is published by the server update_one_activity( { **by_type(ap.ActivityType.CREATE), **by_object_id(announced.id) }, inc(MetaKey.COUNT_BOOST, -1), )
def _emoji_reaction_process_inbox(emoji_reaction: ap.EmojiReaction, new_meta: _NewMeta) -> None: _logger.info(f"process_inbox activity={emoji_reaction!r}") obj = emoji_reaction.get_object() # Try to update an existing emoji reaction counter entry for the activity emoji if not update_one_activity( { **by_type(ap.ActivityType.CREATE), **by_object_id(obj.id), "meta.emoji_reactions.emoji": emoji_reaction.content, }, {"$inc": { "meta.emoji_reactions.$.count": 1 }}, ): # Bootstrap the current emoji counter update_one_activity( { **by_type(ap.ActivityType.CREATE), **by_object_id(obj.id) }, { "$push": { "meta.emoji_reactions": { "emoji": emoji_reaction.content, "count": 1, } } }, )
def _update_process_inbox(update: ap.Update, new_meta: _NewMeta) -> None: _logger.info(f"process_inbox activity={update!r}") obj = update.get_object() if obj.ACTIVITY_TYPE == ap.ActivityType.NOTE: update_one_activity({"activity.object.id": obj.id}, {"$set": { "activity.object": obj.to_dict() }}) elif obj.has_type(ap.ActivityType.QUESTION): choices = obj._data.get("oneOf", obj.anyOf) total_replies = 0 _set = {} for choice in choices: answer_key = _answer_key(choice["name"]) cnt = choice["replies"]["totalItems"] total_replies += cnt _set[f"meta.question_answers.{answer_key}"] = cnt _set["meta.question_replies"] = total_replies update_one_activity({ **in_inbox(), **by_object_id(obj.id) }, {"$set": _set}) # Also update the cached copies of the question (like Announce and Like) DB.activities.update_many(by_object_id(obj.id), upsert({MetaKey.OBJECT: obj.to_dict()})) elif obj.has_type(ap.ACTOR_TYPES): actor = ap.fetch_remote_activity(obj.id, no_cache=True) update_cached_actor(actor) else: raise ValueError(f"don't know how to update {obj!r}")
def _undo_process_outbox(undo: ap.Undo, new_meta: _NewMeta) -> None: _logger.info(f"process_outbox activity={undo!r}") obj = undo.get_object() update_one_activity({"remote_id": obj.id}, {"$set": {"meta.undo": True}}) # Undo Like if obj.has_type(ap.ActivityType.LIKE): liked = obj.get_object_id() update_one_activity( { **by_object_id(liked), **by_type(ap.ActivityType.CREATE) }, { **inc(MetaKey.COUNT_LIKE, -1), **upsert({MetaKey.LIKED: False}) }, ) elif obj.has_type(ap.ActivityType.ANNOUNCE): announced = obj.get_object_id() update_one_activity( { **by_object_id(announced), **by_type(ap.ActivityType.CREATE) }, upsert({MetaKey.BOOSTED: False}), ) # Undo Follow (undo new following) elif obj.has_type(ap.ActivityType.FOLLOW): pass
def note_by_id(note_id): if is_api_request(): return redirect(url_for("outbox_activity", item_id=note_id)) query = {} # Prevent displaying direct messages on the public frontend if not session.get("logged_in", False): query = is_public() data = DB.activities.find_one({ **in_outbox(), **by_remote_id(activity_url(note_id)), **query }) if not data: abort(404) if data["meta"].get("deleted", False): abort(410) thread = _build_thread(data, query=query) app.logger.info(f"thread={thread!r}") raw_likes = list( DB.activities.find({ **not_undo(), **not_deleted(), **by_type(ActivityType.LIKE), **by_object_id(data["activity"]["object"]["id"]), })) likes = [] for doc in raw_likes: try: likes.append(doc["meta"]["actor"]) except Exception: app.logger.exception(f"invalid doc: {doc!r}") app.logger.info(f"likes={likes!r}") raw_shares = list( DB.activities.find({ **not_undo(), **not_deleted(), **by_type(ActivityType.ANNOUNCE), **by_object_id(data["activity"]["object"]["id"]), })) shares = [] for doc in raw_shares: try: shares.append(doc["meta"]["actor"]) except Exception: app.logger.exception(f"invalid doc: {doc!r}") app.logger.info(f"shares={shares!r}") return htmlify( render_template("note.html", likes=likes, shares=shares, thread=thread, note=data))
def _announce_process_inbox(announce: ap.Announce, new_meta: _NewMeta) -> None: _logger.info(f"process_inbox activity={announce!r}") # TODO(tsileo): actually drop it without storing it and better logging, also move the check somewhere else # or remove it? try: obj = announce.get_object() except NotAnActivityError: _logger.exception( f'received an Annouce referencing an OStatus notice ({announce._data["object"]}), dropping the message' ) return if obj.has_type(ap.ActivityType.QUESTION): Tasks.fetch_remote_question(obj) update_one_activity( by_remote_id(announce.id), upsert({ MetaKey.OBJECT: obj.to_dict(embed=True), MetaKey.OBJECT_ACTOR: obj.get_actor().to_dict(embed=True), }), ) update_one_activity( { **by_type(ap.ActivityType.CREATE), **by_object_id(obj.id) }, inc(MetaKey.COUNT_BOOST, 1), )
def api_ack_reply() -> _Response: reply_iri = _user_api_arg("reply_iri") obj = ap.fetch_remote_activity(reply_iri) if obj.has_type(ap.ActivityType.CREATE): obj = obj.get_object() # TODO(tsileo): tweak the adressing? update_one_activity( { **by_type(ap.ActivityType.CREATE), **by_object_id(obj.id) }, {"$set": { "meta.reply_acked": True }}, ) read = ap.Read( actor=MY_PERSON.id, object=obj.id, to=[MY_PERSON.followers], cc=[ap.AS_PUBLIC, obj.get_actor().id], published=now(), context=new_context(obj), ) read_id = post_to_outbox(read) return _user_api_response(activity=read_id)
def admin_thread() -> _Response: oid = request.args.get("oid") if not oid: abort(404) data = find_one_activity({ **by_type(ap.ActivityType.CREATE), **by_object_id(oid) }) if not data: dat = DB.replies.find_one({**by_remote_id(oid)}) data = { "activity": { "object": dat["activity"] }, "meta": dat["meta"], "_id": dat["_id"], } if not data: abort(404) if data["meta"].get("deleted", False): abort(410) thread = _build_thread(data) tpl = "note.html" if request.args.get("debug"): tpl = "note_debug.html" return htmlify(render_template(tpl, thread=thread, note=data))
def _announce_set_inbox_flags(activity: ap.Announce, new_meta: _NewMeta) -> None: _logger.info(f"set_inbox_flags activity={activity!r}") obj = activity.get_object() # Is it a Annnounce/boost of local acitivty/from the outbox if is_from_outbox(obj): # Flag it as a notification _flag_as_notification(activity, new_meta) # Also set the "keep mark" for the GC (as we want to keep it forever) _set_flag(new_meta, MetaKey.GC_KEEP) # Dedup boosts (it's annoying to see the same note multipe times on the same page) if not find_one_activity({ **in_inbox(), **by_type([ap.ActivityType.CREATE, ap.ActivityType.ANNOUNCE]), **by_object_id(obj.id), **flag(MetaKey.STREAM, True), **published_after( datetime.now(timezone.utc) - timedelta(hours=12)), }): # Display it in the stream only it not there already (only looking at the last 12 hours) _set_flag(new_meta, MetaKey.STREAM) return None
def post_to_inbox(activity: ap.BaseActivity) -> None: # Check for Block activity actor = activity.get_actor() if outbox_is_blocked(actor.id): logger.info( f"actor {actor!r} is blocked, dropping the received activity {activity!r}" ) return # If the message is coming from a Pleroma relay, we process it as a possible reply for a stream activity if ( actor.has_type(ap.ActivityType.APPLICATION) and actor.id.endswith("/relay") and activity.has_type(ap.ActivityType.ANNOUNCE) and not find_one_activity( { **by_object_id(activity.get_object_id()), **by_type(ap.ActivityType.CREATE), } ) and not DB.replies.find_one(by_remote_id(activity.get_object_id())) ): Tasks.process_reply(activity.get_object_id()) return # Hubzilla sends Update with the same ID as the actor, and it poisons the cache if ( activity.has_type(ap.ActivityType.UPDATE) and activity.id == activity.get_object_id() ): # Start a task to update the cached actor Tasks.cache_actor(activity.id) return # Honk forwards activities in a Read, process them as replies if activity.has_type(ap.ActivityType.READ): Tasks.process_reply(activity.get_object_id()) return # TODO(tsileo): support ignore from Honk # Hubzilla forwards activities in a Create, process them as possible replies if activity.has_type(ap.ActivityType.CREATE) and server(activity.id) != server( activity.get_object_id() ): Tasks.process_reply(activity.get_object_id()) return if DB.activities.find_one({"box": Box.INBOX.value, "remote_id": activity.id}): # The activity is already in the inbox logger.info(f"received duplicate activity {activity!r}, dropping it") return save(Box.INBOX, activity) logger.info(f"spawning tasks for {activity!r}") if not activity.has_type([ap.ActivityType.DELETE, ap.ActivityType.UPDATE]): Tasks.cache_actor(activity.id) Tasks.process_new_activity(activity.id) Tasks.finish_post_to_inbox(activity.id)
def handle_question_reply(create: ap.Create, question: ap.Question) -> None: choice = create.get_object().name # Ensure it's a valid choice if choice not in [ c["name"] for c in question._data.get("oneOf", question.anyOf) ]: logger.info("invalid choice") return # Hash the choice/answer (so we can use it as a key) answer_key = _answer_key(choice) is_single_choice = bool(question._data.get("oneOf", [])) dup_query = { "activity.object.actor": create.get_actor().id, "meta.answer_to": question.id, **({} if is_single_choice else { "meta.poll_answer_choice": choice }), } print(f"dup_q={dup_query}") # Check for duplicate votes if DB.activities.find_one(dup_query): logger.info("duplicate response") return # Update the DB DB.activities.update_one( { **by_object_id(question.id), **by_type(ap.ActivityType.CREATE) }, { "$inc": { "meta.question_replies": 1, f"meta.question_answers.{answer_key}": 1, } }, ) DB.activities.update_one( by_remote_id(create.id), { "$set": { "meta.poll_answer_to": question.id, "meta.poll_answer_choice": choice, "meta.stream": False, "meta.poll_answer": True, } }, ) return None
def _like_process_inbox(like: ap.Like, new_meta: _NewMeta) -> None: _logger.info(f"process_inbox activity={like!r}") obj = like.get_object() # Update the meta counter if the object is published by the server update_one_activity( { **by_type(ap.ActivityType.CREATE), **by_object_id(obj.id) }, inc(MetaKey.COUNT_LIKE, 1), )
def _delete_process_outbox(delete: ap.Delete, new_meta: _NewMeta) -> None: _logger.info(f"process_outbox activity={delete!r}") obj_id = delete.get_object_id() # Flag everything referencing the deleted object as deleted (except the Delete activity itself) update_many_activities( { **by_object_id(obj_id), "remote_id": { "$ne": delete.id } }, upsert({ MetaKey.DELETED: True, MetaKey.UNDO: True }), ) # If the deleted activity was in DB, decrease some threads-related counter data = find_one_activity({ **by_object_id(obj_id), **by_type(ap.ActivityType.CREATE) }) _logger.info(f"found local copy of deleted activity: {data}") if data: obj = ap.parse_activity(data["activity"]).get_object() _logger.info(f"obj={obj!r}") in_reply_to = obj.get_in_reply_to() if in_reply_to: update_one_activity( { **by_type(ap.ActivityType.CREATE), **by_object_id(in_reply_to) }, { "$inc": { "meta.count_reply": -1, "meta.count_direct_reply": -1 } }, )
def _announce_process_outbox(announce: ap.Announce, new_meta: _NewMeta) -> None: _logger.info(f"process_outbox activity={announce!r}") obj = announce.get_object() if obj.has_type(ap.ActivityType.QUESTION): Tasks.fetch_remote_question(obj) Tasks.cache_object(announce.id) update_one_activity( {**by_object_id(obj.id), **by_type(ap.ActivityType.CREATE)}, upsert({MetaKey.BOOSTED: announce.id}), )
def _like_process_outbox(like: ap.Like, new_meta: _NewMeta) -> None: _logger.info(f"process_outbox activity={like!r}") obj = like.get_object() if obj.has_type(ap.ActivityType.QUESTION): Tasks.fetch_remote_question(obj) # Cache the object for display on the "Liked" public page Tasks.cache_object(like.id) update_one_activity( {**by_object_id(obj.id), **by_type(ap.ActivityType.CREATE)}, {**inc(MetaKey.COUNT_LIKE, 1), **upsert({MetaKey.LIKED: like.id})}, )
def _announce_process_inbox(announce: ap.Announce, new_meta: _NewMeta) -> None: _logger.info(f"process_inbox activity={announce!r}") # TODO(tsileo): actually drop it without storing it and better logging, also move the check somewhere else # or remove it? try: obj = announce.get_object() except NotAnActivityError: _logger.exception( f'received an Annouce referencing an OStatus notice ({announce._data["object"]}), dropping the message' ) return if obj.has_type(ap.ActivityType.QUESTION): Tasks.fetch_remote_question(obj) # Cache the announced object Tasks.cache_object(announce.id) # Process the reply of the announced object if any in_reply_to = obj.get_in_reply_to() if in_reply_to: reply = ap.fetch_remote_activity(in_reply_to) if reply.has_type(ap.ActivityType.CREATE): reply = reply.get_object() in_reply_to_data = {MetaKey.IN_REPLY_TO: in_reply_to} # Update the activity to save some data about the reply if reply.get_actor().id == obj.get_actor().id: in_reply_to_data.update({MetaKey.IN_REPLY_TO_SELF: True}) else: in_reply_to_data.update({ MetaKey.IN_REPLY_TO_ACTOR: reply.get_actor().to_dict(embed=True) }) update_one_activity(by_remote_id(announce.id), upsert(in_reply_to_data)) # Spawn a task to process it (and determine if it needs to be saved) Tasks.process_reply(reply.id) update_one_activity( { **by_type(ap.ActivityType.CREATE), **by_object_id(obj.id) }, inc(MetaKey.COUNT_BOOST, 1), )
def migrate(self) -> None: for data in find_activities({**by_type(ap.ActivityType.ACCEPT), **in_inbox()}): try: update_one_activity( { **by_type(ap.ActivityType.FOLLOW), **by_remote_id(data["meta"]["object_id"]), }, upsert({MetaKey.FOLLOW_STATUS: FollowStatus.ACCEPTED.value}), ) # Check if we are following this actor follow_query = { **in_inbox(), **by_type(ap.ActivityType.FOLLOW), **by_object_id(data["meta"]["actor_id"]), **not_undo(), } raw_follow = DB.activities.find_one(follow_query) if raw_follow: DB.activities.update_many( follow_query, {"$set": {_meta(MetaKey.NOTIFICATION_FOLLOWS_BACK): True}}, ) except Exception: logger.exception(f"failed to process activity {data!r}") for data in find_activities({**by_type(ap.ActivityType.FOLLOW), **in_outbox()}): try: print(data) follow_query = { **in_inbox(), **by_type(ap.ActivityType.FOLLOW), **by_actor_id(data["meta"]["object_id"]), **not_undo(), } raw_accept = DB.activities.find_one(follow_query) print(raw_accept) if raw_accept: DB.activities.update_many( by_remote_id(data["remote_id"]), {"$set": {_meta(MetaKey.NOTIFICATION_FOLLOWS_BACK): True}}, ) except Exception: logger.exception(f"failed to process activity {data!r}")
def admin_profile() -> _Response: if not request.args.get("actor_id"): abort(404) actor_id = request.args.get("actor_id") actor = ap.fetch_remote_activity(actor_id) q = { "meta.actor_id": actor_id, "box": "inbox", **not_deleted(), "type": { "$in": [ap.ActivityType.CREATE.value, ap.ActivityType.ANNOUNCE.value] }, } inbox_data, older_than, newer_than = paginated_query( DB.activities, q, limit=int(request.args.get("limit", 25))) follower = find_one_activity({ "box": "inbox", "type": ap.ActivityType.FOLLOW.value, "meta.actor_id": actor.id, "meta.undo": False, }) following = find_one_activity({ **by_type(ap.ActivityType.FOLLOW), **by_object_id(actor.id), **not_undo(), **in_outbox(), **follow_request_accepted(), }) return htmlify( render_template( "stream.html", actor_id=actor_id, actor=actor.to_dict(), inbox_data=inbox_data, older_than=older_than, newer_than=newer_than, follower=follower, following=following, lists=list(DB.lists.find()), ))
def admin_lookup() -> _Response: data = None meta = None follower = None following = None if request.args.get("url"): data = lookup(request.args.get("url")) # type: ignore if data: if not data.has_type(ap.ACTOR_TYPES): meta = _meta(data) else: follower = find_one_activity({ "box": "inbox", "type": ap.ActivityType.FOLLOW.value, "meta.actor_id": data.id, "meta.undo": False, }) following = find_one_activity({ **by_type(ap.ActivityType.FOLLOW), **by_object_id(data.id), **not_undo(), **in_outbox(), **follow_request_accepted(), }) if data.has_type(ap.ActivityType.QUESTION): p.push(data.id, "/task/fetch_remote_question") print(data) app.logger.debug(data.to_dict()) return htmlify( render_template( "lookup.html", data=data, meta=meta, follower=follower, following=following, url=request.args.get("url"), ))
def _announce_process_outbox(announce: ap.Announce, new_meta: _NewMeta) -> None: _logger.info(f"process_outbox activity={announce!r}") obj = announce.get_object() if obj.has_type(ap.ActivityType.QUESTION): Tasks.fetch_remote_question(obj) update_one_activity( by_remote_id(announce.id), upsert({ MetaKey.OBJECT: obj.to_dict(embed=True), MetaKey.OBJECT_ACTOR: obj.get_actor().to_dict(embed=True), }), ) update_one_activity( { **by_object_id(obj.id), **by_type(ap.ActivityType.CREATE) }, upsert({MetaKey.BOOSTED: announce.id}), )
def task_process_reply() -> _Response: """Process `Announce`d posts from Pleroma relays in order to process replies of activities that are in the inbox.""" task = p.parse(flask.request) app.logger.info(f"task={task!r}") iri = task.payload try: activity = ap.fetch_remote_activity(iri) app.logger.info(f"checking for reply activity={activity!r}") # Some AP server always return Create when requesting an object if activity.has_type(ap.ActivityType.CREATE): activity = activity.get_object() in_reply_to = activity.get_in_reply_to() if not in_reply_to: # If it's not reply, we can drop it app.logger.info( f"activity={activity!r} is not a reply, dropping it") return "" root_reply = in_reply_to # Fetch the activity reply reply = ap.fetch_remote_activity(in_reply_to) if reply.has_type(ap.ActivityType.CREATE): reply = reply.get_object() 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}") # In case the activity was from the inbox update_one_activity( { **by_object_id(activity.id), **by_type(ap.ActivityType.CREATE) }, upsert({MetaKey.THREAD_ROOT_PARENT: root_reply}), ) for (new_reply_idx, new_reply) in enumerate(new_replies): if find_one_activity({ **by_object_id(new_reply.id), **by_type(ap.ActivityType.CREATE) }) or DB.replies.find_one(by_remote_id(new_reply.id)): continue actor = new_reply.get_actor() is_root_reply = new_reply_idx == len(new_replies) - 1 if is_root_reply: reply_flags: Dict[str, Any] = {} else: reply_actor = new_replies[new_reply_idx + 1].get_actor() is_in_reply_to_self = actor.id == reply_actor.id reply_flags = { MetaKey.IN_REPLY_TO_SELF.value: is_in_reply_to_self, MetaKey.IN_REPLY_TO.value: new_reply.get_in_reply_to(), } if not is_in_reply_to_self: reply_flags[MetaKey.IN_REPLY_TO_ACTOR. value] = reply_actor.to_dict(embed=True) # Save the reply with the cached actor and the thread flag/ID save_reply( new_reply, { **reply_flags, MetaKey.THREAD_ROOT_PARENT.value: root_reply, MetaKey.ACTOR.value: actor.to_dict(embed=True), MetaKey.ACTOR_HASH.value: _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 "" except Exception as err: app.logger.exception(f"failed to process new activity {iri}") raise TaskError() from err return ""
def _build_thread(data, include_children=True, query=None): # noqa: C901 if query is None: query = {} data["_requested"] = True app.logger.info(f"_build_thread({data!r})") root_id = (data["meta"].get(MetaKey.THREAD_ROOT_PARENT.value) or data["meta"].get(MetaKey.OBJECT_ID.value) or data["remote_id"]) replies = [data] for dat in find_activities({ **by_object_id(root_id), **not_deleted(), **by_type(ap.ActivityType.CREATE), **query, }): replies.append(dat) for dat in find_activities({ **flag(MetaKey.THREAD_ROOT_PARENT, root_id), **not_deleted(), **by_type(ap.ActivityType.CREATE), **query, }): replies.append(dat) for dat in DB.replies.find({ **flag(MetaKey.THREAD_ROOT_PARENT, root_id), **not_deleted(), **query }): # Make a Note/Question/... looks like a Create dat["meta"].update({ MetaKey.OBJECT_VISIBILITY.value: dat["meta"][MetaKey.VISIBILITY.value] }) dat = { "activity": { "object": dat["activity"] }, "meta": dat["meta"], "_id": dat["_id"], } replies.append(dat) replies = sorted(replies, key=lambda d: d["meta"]["published"]) # Index all the IDs in order to build a tree idx = {} replies2 = [] for rep in replies: rep_id = rep["activity"]["object"]["id"] if rep_id in idx: continue idx[rep_id] = rep.copy() idx[rep_id]["_nodes"] = [] replies2.append(rep) # Build the tree for rep in replies2: rep_id = rep["activity"]["object"]["id"] if rep_id == root_id: continue reply_of = ap._get_id(rep["activity"]["object"].get("inReplyTo")) try: idx[reply_of]["_nodes"].append(rep) except KeyError: app.logger.info(f"{reply_of} is not there! skipping {rep}") # Flatten the tree thread = [] def _flatten(node, level=0): node["_level"] = level thread.append(node) for snode in sorted( idx[node["activity"]["object"]["id"]]["_nodes"], key=lambda d: d["activity"]["object"]["published"], ): _flatten(snode, level=level + 1) try: _flatten(idx[root_id]) except KeyError: app.logger.info(f"{root_id} is not there! skipping") return thread
def handle_replies(create: ap.Create) -> None: """Go up to the root reply, store unknown replies in the `threads` DB and set the "meta.thread_root_parent" key to make it easy to query a whole thread.""" in_reply_to = create.get_object().get_in_reply_to() if not in_reply_to: return 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 if ( reply.id.startswith(BASE_URL) and reply.has_type(ap.ActivityType.QUESTION.value) and _is_local_reply(create) and not create.is_public() ): return handle_question_reply(create, reply) elif ( create.id.startswith(BASE_URL) and reply.has_type(ap.ActivityType.QUESTION.value) and not create.is_public() ): # Keep track of our own votes DB.activities.update_one( {"activity.object.id": reply.id, "box": "inbox"}, { "$set": { f"meta.poll_answers_sent.{_answer_key(create.get_object().name)}": True } }, ) # Mark our reply as a poll answers, to "hide" it from the UI update_one_activity( by_remote_id(create.id), upsert({MetaKey.POLL_ANSWER: True, MetaKey.POLL_ANSWER_TO: reply.id}), ) return None in_reply_to_data = {MetaKey.IN_REPLY_TO: in_reply_to} # Update the activity to save some data about the reply if reply.get_actor().id == create.get_actor().id: in_reply_to_data.update({MetaKey.IN_REPLY_TO_SELF: True}) else: in_reply_to_data.update( {MetaKey.IN_REPLY_TO_ACTOR: reply.get_actor().to_dict(embed=True)} ) update_one_activity(by_remote_id(create.id), upsert(in_reply_to_data)) # It's a regular reply, try to increment the reply counter creply = DB.activities.find_one_and_update( {**by_object_id(in_reply_to), **by_type(ap.ActivityType.CREATE)}, inc(MetaKey.COUNT_REPLY, 1), ) if not creply: # Maybe it's the reply of a reply? DB.replies.find_one_and_update( by_remote_id(in_reply_to), inc(MetaKey.COUNT_REPLY, 1) ) # Spawn a task to process it (and determine if it needs to be saved) Tasks.process_reply(create.get_object_id())