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 _handle_replies(self, as_actor: ap.Person, create: ap.Create) -> None: """Do magic about replies, we don't handle that for now""" in_reply_to = create.get_object().inReplyTo if not in_reply_to: return current_app.logger.error("!!! unhandled case: _handle_replies(in_reply_to=!None) !!!") return
def _create_process_inbox(create: ap.Create, new_meta: _NewMeta) -> None: _logger.info(f"process_inbox activity={create!r}") # If it's a `Quesiion`, trigger an async task for updating it later (by fetching the remote and updating the # local copy) question = create.get_object() if question.has_type(ap.ActivityType.QUESTION): Tasks.fetch_remote_question(question) back._handle_replies(MY_PERSON, create)
def _process_question_reply(self, 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 # Check for duplicate votes if DB.activities.find_one({ "activity.object.actor": create.get_actor().id, "meta.answer_to": question.id, }): logger.info("duplicate response") return # Update the DB answer_key = _answer_key(choice) DB.activities.update_one( {"activity.object.id": question.id}, { "$inc": { "meta.question_replies": 1, f"meta.question_answers.{answer_key}": 1, } }, ) DB.activities.update_one( {"remote_id": create.id}, { "$set": { "meta.answer_to": question.id, "meta.stream": False, "meta.poll_answer": True, } }, ) return None
def send_webmentions(activity: ap.Create, links: Set[str]) -> None: if DISABLE_WEBMENTIONS: return None for link in links: p.push( { "link": link, "note_url": activity.get_object().get_url(), "remote_id": activity.id, }, "/task/send_webmention", )
def _handle_replies(self, as_actor: ap.Person, 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().inReplyTo if not in_reply_to: return new_threads = [] root_reply = in_reply_to reply = ap.fetch_remote_activity(root_reply) creply = DB.activities.find_one_and_update( {"activity.object.id": in_reply_to}, {"$inc": { "meta.count_reply": 1, "meta.count_direct_reply": 1 }}, ) if not creply: # It means the activity is not in the inbox, and not in the outbox, we want to save it self.save(Box.REPLIES, reply) new_threads.append(reply.id) while reply is not None: in_reply_to = reply.inReplyTo if not in_reply_to: break root_reply = in_reply_to reply = ap.fetch_remote_activity(root_reply) q = {"activity.object.id": root_reply} if not DB.activities.count(q): self.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 }}, )
def inbox_create(self, as_actor: ap.Person, create: ap.Create) -> None: self._handle_replies(as_actor, create) obj = create.get_object() current_app.logger.debug(f"inbox_create {obj.ACTIVITY_TYPE} {obj!r} as {as_actor!r}") if obj.ACTIVITY_TYPE == ap.ActivityType.AUDIO: # create a remote Audio and process it from tasks import create_sound_for_remote_track, upload_workflow act = Activity.query.filter(Activity.url == create.id).first() if not act: current_app.logger.error(f"cannot find activity with url == {create.id!r}") return sound_id = create_sound_for_remote_track(act) # TODO(dashie): fetch_remote_track should be done inside the upload_workflow to not have to do celery tasks dependencies # Plus it's better to do it like that, one function to do everything, locally or remotely. upload_workflow.delay(sound_id) else: current_app.logger.error(f"got an unhandled Activity Type {obj.ACTIVITY_TYPE!r} in the inbox")
def _create_set_inbox_flags(activity: ap.Create, new_meta: _NewMeta) -> None: _logger.info(f"set_inbox_flags activity={activity!r}") obj = activity.get_object() _set_flag(new_meta, MetaKey.POLL_ANSWER, False) in_reply_to = obj.get_in_reply_to() # Check if it's a local reply if in_reply_to and is_local_url(in_reply_to): # TODO(tsileo): fetch the reply to check for poll answers more precisely # reply_of = ap.fetch_remote_activity(in_reply_to) # Ensure it's not a poll answer if obj.name and not obj.content: _set_flag(new_meta, MetaKey.POLL_ANSWER) return None # 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) return None # Check for mention for mention in obj.get_mentions(): if mention.href and is_local_url(mention.href): # 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) if not in_reply_to or (REPLIES_IN_STREAM and obj.get_actor().id in ap.get_backend().following()): # A good candidate for displaying in the stream _set_flag(new_meta, MetaKey.STREAM) return None
def _handle_replies(self, as_actor: ap.Person, 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 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) # 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 self._process_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": { "meta.voted_for": create.get_object().name }}, ) return None creply = DB.activities.find_one_and_update( {"activity.object.id": in_reply_to}, {"$inc": { "meta.count_reply": 1, "meta.count_direct_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 }}, )
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())