def migrate(self) -> None: for data in find_activities({ **by_type(ap.ActivityType.CREATE), **not_deleted() }): try: in_reply_to = data["activity"]["object"].get("inReplyTo") if in_reply_to: update_one_activity( by_remote_id(data["remote_id"]), upsert({MetaKey.IN_REPLY_TO: in_reply_to}), ) except Exception: logger.exception(f"failed to process activity {data!r}") for data in DB.replies.find({**not_deleted()}): try: in_reply_to = data["activity"].get("inReplyTo") if in_reply_to: DB.replies.update_one( by_remote_id(data["remote_id"]), upsert({MetaKey.IN_REPLY_TO: in_reply_to}), ) except Exception: logger.exception(f"failed to process activity {data!r}")
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 _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 migrate(self) -> None: for data in find_activities({ **by_type(ap.ActivityType.CREATE), **not_deleted() }): try: activity = ap.parse_activity(data["activity"]) mentions = [] obj = activity.get_object() for m in obj.get_mentions(): mentions.append(m.href) hashtags = [] for h in obj.get_hashtags(): hashtags.append(h.name[1:]) # Strip the # update_one_activity( by_remote_id(data["remote_id"]), upsert({ MetaKey.MENTIONS: mentions, MetaKey.HASHTAGS: hashtags }), ) except Exception: logger.exception(f"failed to process activity {data!r}")
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 task_cache_actor() -> _Response: task = p.parse(flask.request) app.logger.info(f"task={task!r}") iri = task.payload["iri"] try: activity = ap.fetch_remote_activity(iri) app.logger.info(f"activity={activity!r}") # Reload the actor without caching (in case it got upated) actor = ap.fetch_remote_activity(activity.get_actor().id, no_cache=True) # Fetch the Open Grah metadata if it's a `Create` if activity.has_type(ap.ActivityType.CREATE): obj = activity.get_object() links = opengraph.links_from_note(obj.to_dict()) if links: Tasks.fetch_og_meta(iri) # Send Webmentions only if it's from the outbox, and public if (is_from_outbox(obj) and ap.get_visibility(obj) == ap.Visibility.PUBLIC): Tasks.send_webmentions(activity, links) if activity.has_type(ap.ActivityType.FOLLOW): if actor.id == config.ID: # It's a new following, cache the "object" (which is the actor we follow) DB.activities.update_one( by_remote_id(iri), upsert({ MetaKey.OBJECT: activity.get_object().to_dict(embed=True) }), ) # Cache the actor info update_cached_actor(actor) app.logger.info(f"actor cached for {iri}") if not activity.has_type( [ap.ActivityType.CREATE, ap.ActivityType.ANNOUNCE]): return "" if activity.get_object()._data.get( "attachment", []) or activity.get_object().has_type( ap.ActivityType.VIDEO): Tasks.cache_attachments(iri) except (ActivityGoneError, ActivityNotFoundError): DB.activities.update_one({"remote_id": iri}, {"$set": { "meta.deleted": True }}) app.logger.exception( f"flagging activity {iri} as deleted, no actor caching") except Exception as err: app.logger.exception(f"failed to cache actor for {iri}") raise TaskError() from err return ""
def task_cache_object() -> _Response: 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"activity={activity!r}") obj = activity.get_object() # Refetch the object actor (without cache) with no_cache(): obj_actor = ap.fetch_remote_activity(obj.get_actor().id) cache = {MetaKey.OBJECT: obj.to_dict(embed=True)} if activity.get_actor().id != obj_actor.id: # Cache the object actor obj_actor_hash = _actor_hash(obj_actor) cache[MetaKey.OBJECT_ACTOR] = obj_actor.to_dict(embed=True) cache[MetaKey.OBJECT_ACTOR_ID] = obj_actor.id cache[MetaKey.OBJECT_ACTOR_HASH] = obj_actor_hash # Update the actor cache for the other activities update_cached_actor(obj_actor) update_one_activity(by_remote_id(activity.id), upsert(cache)) except (ActivityGoneError, ActivityNotFoundError, NotAnActivityError): DB.activities.update_one({"remote_id": iri}, {"$set": {"meta.deleted": True}}) app.logger.exception(f"flagging activity {iri} as deleted, no object caching") except Exception as err: app.logger.exception(f"failed to cache object for {iri}") raise TaskError() from err return ""
def outbox_activity_replies(item_id): if not is_api_request(): abort(404) _log_sig() data = DB.activities.find_one({ **in_outbox(), **by_remote_id(activity_url(item_id)), **not_deleted(), **is_public(), }) if not data: abort(404) obj = ap.parse_activity(data["activity"]) if obj.ACTIVITY_TYPE != ActivityType.CREATE: abort(404) q = { **is_public(), **not_deleted(), **by_type(ActivityType.CREATE), "activity.object.inReplyTo": obj.get_object().id, } return activitypubify(**activitypub.build_ordered_collection( DB.activities, q=q, cursor=request.args.get("cursor"), map_func=lambda doc: doc["activity"]["object"], col_name=f"outbox/{item_id}/replies", first_page=request.args.get("page") == "first", ))
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_actor_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.REJECT), **in_inbox()}): try: update_one_activity( { **by_type(ap.ActivityType.FOLLOW), **by_remote_id(data["meta"]["object_id"]), }, upsert({MetaKey.FOLLOW_STATUS: FollowStatus.REJECTED.value}), ) except Exception: logger.exception(f"failed to process activity {data!r}") DB.activities.update_many( { **by_type(ap.ActivityType.FOLLOW), **in_inbox(), "meta.follow_status": {"$exists": False}, }, {"$set": {"meta.follow_status": "waiting"}}, )
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 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 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 _follow_process_inbox(activity: ap.Follow, new_meta: _NewMeta) -> None: _logger.info(f"process_inbox activity={activity!r}") # Reply to a Follow with an Accept if we're not manully approving them if not config.MANUALLY_APPROVES_FOLLOWERS: accept_follow(activity) else: update_one_activity( by_remote_id(activity.id), upsert({MetaKey.FOLLOW_STATUS: FollowStatus.WAITING.value}), )
def outbox_activity(item_id): data = find_one_activity({ **in_outbox(), **by_remote_id(activity_url(item_id)), **is_public() }) if not data: abort(404) _log_sig() obj = activity_from_doc(data) if data["meta"].get("deleted", False): abort(404) if obj["type"] != ActivityType.CREATE.value: abort(404) return jsonify(**obj["object"])
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 outbox_detail(item_id): if "text/html" in request.headers.get("Accept", ""): return redirect(url_for("note_by_id", note_id=item_id)) doc = DB.activities.find_one({ **in_outbox(), **by_remote_id(activity_url(item_id)), **not_deleted(), **is_public(), }) if not doc: abort(404) _log_sig() if doc["meta"].get("deleted", False): abort(404) return activitypubify(**activity_from_doc(doc))
def accept_follow(activity: ap.BaseActivity) -> str: actor_id = activity.get_actor().id accept = ap.Accept( actor=ID, context=new_context(activity), object={ "type": "Follow", "id": activity.id, "object": activity.get_object_id(), "actor": actor_id, }, to=[actor_id], published=now(), ) update_one_activity( by_remote_id(activity.id), upsert({MetaKey.FOLLOW_STATUS: FollowStatus.ACCEPTED.value}), ) return post_to_outbox(accept)
def outbox_activity(item_id): if "text/html" in request.headers.get("Accept", ""): return redirect(url_for("note_by_id", note_id=item_id)) data = find_one_activity({ **in_outbox(), **by_remote_id(activity_url(item_id)), **is_public() }) if not data: abort(404) _log_sig() obj = activity_from_doc(data) if data["meta"].get("deleted", False): abort(404) if obj["type"] != ActivityType.CREATE.value: abort(404) return activitypubify(**obj["object"])
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): post_query = { **by_object_id(obj_id), **by_type(ap.ActivityType.CREATE) } in_reply_to = ap._get_id( DB.activities.find_one(post_query)["activity"]["object"].get( "inReplyTo")) if in_reply_to: DB.activities.update_one( { **by_object_id(in_reply_to), **by_type(ap.ActivityType.CREATE) }, inc(MetaKey.COUNT_REPLY, -1), ) DB.replies.update_one(by_remote_id(in_reply_to), inc(MetaKey.COUNT_REPLY, -1)) 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 _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 _fetch_iri(self, iri: str) -> ap.ObjectType: # noqa: C901 # Shortcut if the instance actor is fetched if iri == ME["id"]: return ME # Internal collecitons handling # Followers if iri == MY_PERSON.followers: followers = [] for data in DB.activities.find( { "box": Box.INBOX.value, "type": ap.ActivityType.FOLLOW.value, "meta.undo": False, } ): followers.append(data["meta"]["actor_id"]) return {"type": "Collection", "items": followers} # Following if iri == MY_PERSON.following: following = [] for data in DB.activities.find( { "box": Box.OUTBOX.value, "type": ap.ActivityType.FOLLOW.value, "meta.undo": False, } ): following.append(data["meta"]["object_id"]) return {"type": "Collection", "items": following} # TODO(tsileo): handle the liked collection too # Check if the activity is owned by this server if iri.startswith(BASE_URL): is_a_note = False if iri.endswith("/activity"): iri = iri.replace("/activity", "") is_a_note = True data = DB.activities.find_one({"box": Box.OUTBOX.value, "remote_id": iri}) if data and data["meta"]["deleted"]: raise ActivityGoneError(f"{iri} is gone") if data and is_a_note: return data["activity"]["object"] elif data: return data["activity"] else: # Check if the activity is stored in the inbox data = DB.activities.find_one({"remote_id": iri}) if data: if data["meta"]["deleted"]: raise ActivityGoneError(f"{iri} is gone") return data["activity"] # Check if we're looking for an object wrapped in a Create obj = DB.activities.find_one({"meta.object_id": iri, "type": "Create"}) if obj: if obj["meta"]["deleted"]: raise ActivityGoneError(f"{iri} is gone") cached_object = obj["meta"].get("object") if cached_object: return cached_object embedded_object = obj["activity"]["object"] if isinstance(embedded_object, dict): return embedded_object # TODO(tsileo): also check the REPLIES box # Check if it's cached because it's a follower # Remove extra info (like the key hash if any) cleaned_iri = iri.split("#")[0] actor = DB.activities.find_one( {"meta.actor_id": cleaned_iri, "meta.actor": {"$exists": True}} ) # "type" check is here to skip old metadata for "old/buggy" followers if ( actor and actor["meta"].get("actor") and "type" in actor["meta"]["actor"] ): return actor["meta"]["actor"] # Check if it's cached because it's a following actor2 = DB.activities.find_one( { "meta.object_id": cleaned_iri, "type": ap.ActivityType.FOLLOW.value, "meta.undo": False, } ) if ( actor2 and actor2["meta"].get("object") and "type" in actor2["meta"]["object"] ): return actor2["meta"]["object"] reply = DB.replies.find_one(by_remote_id(iri)) if reply: return reply["activity"] # Fetch the URL via HTTP logger.info(f"dereference {iri} via HTTP") return super().fetch_iri(iri)
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())
def _update_follow_status(follow_id: str, status: FollowStatus) -> None: _logger.info(f"{follow_id} is {status}") update_one_activity(by_remote_id(follow_id), upsert({MetaKey.FOLLOW_STATUS: status.value}))