Beispiel #1
0
    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}")
Beispiel #2
0
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))
Beispiel #3
0
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),
        )
Beispiel #4
0
    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}")
Beispiel #5
0
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),
    )
Beispiel #6
0
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 ""
Beispiel #7
0
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 ""
Beispiel #8
0
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",
    ))
Beispiel #9
0
    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"}},
        )
Beispiel #10
0
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)
Beispiel #11
0
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))
Beispiel #12
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
Beispiel #13
0
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}),
        )
Beispiel #14
0
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"])
Beispiel #15
0
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),
    )
Beispiel #16
0
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))
Beispiel #17
0
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)
Beispiel #18
0
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"])
Beispiel #19
0
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}))
Beispiel #20
0
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}),
    )
Beispiel #21
0
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 ""
Beispiel #22
0
    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)
Beispiel #23
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())
Beispiel #24
0
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}))