Example #1
0
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
Example #2
0
    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
Example #3
0
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)
Example #4
0
    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
Example #5
0
    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",
            )
Example #6
0
    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
            }},
        )
Example #7
0
    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")
Example #8
0
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
Example #9
0
    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
            }},
        )
Example #10
0
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())