Пример #1
0
    def migrate(self) -> None:
        actor_cache: Dict[str, Dict[str, Any]] = {}
        for data in DB.activities.find({"type": ap.ActivityType.FOLLOW.value}):
            try:
                if data["meta"]["actor_id"] == ID:
                    # It's a "following"
                    actor = actor_cache.get(data["meta"]["object_id"])
                    if not actor:
                        actor = ap.parse_activity(ap.get_backend().fetch_iri(
                            data["meta"]["object_id"],
                            no_cache=True)).to_dict(embed=True)
                        if not actor:
                            raise ValueError(f"missing actor {data!r}")
                        actor_cache[actor["id"]] = actor
                    DB.activities.update_one({"_id": data["_id"]},
                                             {"$set": {
                                                 "meta.object": actor
                                             }})

                else:
                    # It's a "followers"
                    actor = actor_cache.get(data["meta"]["actor_id"])
                    if not actor:
                        actor = ap.parse_activity(ap.get_backend().fetch_iri(
                            data["meta"]["actor_id"],
                            no_cache=True)).to_dict(embed=True)
                        if not actor:
                            raise ValueError(f"missing actor {data!r}")
                        actor_cache[actor["id"]] = actor
                    DB.activities.update_one({"_id": data["_id"]},
                                             {"$set": {
                                                 "meta.actor": actor
                                             }})
            except Exception:
                logger.exception(f"failed to process actor {data!r}")
Пример #2
0
def outbox_item_activity(item_id):
    """
    Outbox activity
    ---
    tags:
        - ActivityPub
    responses:
        200:
            description: Returns something
    """
    be = activitypub.get_backend()
    if not be:
        abort(500)

    item = Activity.query.filter(
        Activity.box == Box.OUTBOX.value,
        Activity.url == be.activity_url(item_id)).first()
    if not item:
        abort(404)

    obj = activity_from_doc(item.payload)

    if item.meta_deleted:
        obj = activitypub.parse_activity(item.payload)
        # FIXME not sure about that /activity
        tomb = obj.get_tombstone().to_dict()
        tomb["id"] = tomb["id"] + "/activity"
        resp = jsonify(tomb)
        resp.status_code = 410
        return resp

    if obj["type"] != activitypub.ActivityType.CREATE.value:
        abort(404)

    return jsonify(**obj["object"])
Пример #3
0
def admin_new() -> _Response:
    reply_id = None
    content = ""
    thread: List[Any] = []
    print(request.args)
    if request.args.get("reply"):
        data = DB.activities.find_one(
            {"activity.object.id": request.args.get("reply")})
        if data:
            reply = ap.parse_activity(data["activity"])
        else:
            data = dict(
                meta={},
                activity=dict(object=ap.get_backend().fetch_iri(
                    request.args.get("reply"))),
            )
            reply = ap.parse_activity(data["activity"]["object"])

        reply_id = reply.id
        if reply.ACTIVITY_TYPE == ap.ActivityType.CREATE:
            reply_id = reply.get_object().id
        actor = reply.get_actor()
        domain = urlparse(actor.id).netloc
        # FIXME(tsileo): if reply of reply, fetch all participants
        content = f"@{actor.preferredUsername}@{domain} "
        thread = _build_thread(data)

    return render_template(
        "new.html",
        reply=reply_id,
        content=content,
        thread=thread,
        visibility=ap.Visibility,
        emojis=config.EMOJIS.split(" "),
    )
Пример #4
0
def user_followers(name):
    """
    User followers
    ---
    tags:
        - ActivityPub
    responses:
        200:
            description: Returns a collection of Actors
    """
    be = activitypub.get_backend()
    if not be:
        abort(500)
    # data = request.get_json(force=True)
    # if not data:
    #     abort(500)
    current_app.logger.debug(f"req_headers={request.headers}")
    # current_app.logger.debug(f"raw_data={data}")

    user = User.query.filter(User.name == name).first()
    if not user:
        abort(404)

    actor = user.actor[0]
    followers_list = actor.followers

    return jsonify(**build_ordered_collection(followers_list, actor.url,
                                              request.args.get("page")))
Пример #5
0
def outbox_item(item_id):
    be = activitypub.get_backend()
    if not be:
        abort(500)
    # data = request.get_json()
    # if not data:
    #     abort(500)
    current_app.logger.debug(f"req_headers={request.headers}")
    # current_app.logger.debug(f"raw_data={data}")

    current_app.logger.debug(f"activity url {be.activity_url(item_id)}")

    item = Activity.query.filter(Activity.box == Box.OUTBOX.value, Activity.url == be.activity_url(item_id)).first()
    if not item:
        abort(404)

    if item.meta_deleted:
        obj = activitypub.parse_activity(item.payload)
        resp = jsonify(**obj.get_tombstone().to_dict())
        resp.status_code = 410
        return resp

    current_app.logger.debug(f"item payload=={item.payload}")

    return jsonify(**activity_from_doc(item.payload))
Пример #6
0
def user_inbox(name):
    be = activitypub.get_backend()
    if not be:
        abort(500)
    data = request.get_json(force=True)
    if not data:
        abort(500)

    current_app.logger.debug(f"req_headers={request.headers}")
    current_app.logger.debug(f"raw_data={data}")

    try:
        if not verify_request(request.method, request.path, request.headers, request.data):
            raise Exception("failed to verify request")
    except Exception:
        current_app.logger.exception("failed to verify request")
        try:
            data = be.fetch_iri(data["id"])
        except Exception:
            current_app.logger.exception(f"failed to fetch remote id " f"at {data['id']}")
            resp = {"error": "failed to verify request " "(using HTTP signatures or fetching the IRI)"}
            response = jsonify(resp)
            response.mimetype = "application/json; charset=utf-8"
            response.status = 422
            return response

    activity = activitypub.parse_activity(data)
    current_app.logger.debug(f"inbox_activity={activity}/{data}")

    post_to_inbox(activity)

    return Response(status=201)
Пример #7
0
def admin_new() -> _Response:
    reply_id = None
    content = ""
    thread: List[Any] = []
    print(request.args)
    default_visibility = None  # ap.Visibility.PUBLIC
    if request.args.get("reply"):
        data = DB.activities.find_one(
            {"activity.object.id": request.args.get("reply")})
        if data:
            reply = ap.parse_activity(data["activity"])
        else:
            obj = ap.get_backend().fetch_iri(request.args.get("reply"))
            data = dict(meta=_meta(ap.parse_activity(obj)),
                        activity=dict(object=obj))
            data["_id"] = obj["id"]
            data["remote_id"] = obj["id"]
            reply = ap.parse_activity(data["activity"]["object"])
        # Fetch the post visibility, in case it's follower only
        default_visibility = ap.get_visibility(reply)
        # If it's public, we default the reply to unlisted
        if default_visibility == ap.Visibility.PUBLIC:
            default_visibility = ap.Visibility.UNLISTED

        reply_id = reply.id
        if reply.ACTIVITY_TYPE == ap.ActivityType.CREATE:
            reply_id = reply.get_object().id

        actor = reply.get_actor()
        domain = urlparse(actor.id).netloc
        # FIXME(tsileo): if reply of reply, fetch all participants
        content = f"@{actor.preferredUsername}@{domain} "
        if reply.has_type(ap.ActivityType.CREATE):
            reply = reply.get_object()
        for mention in reply.get_mentions():
            if mention.href in [actor.id, ID]:
                continue
            m = ap.fetch_remote_activity(mention.href)
            if m.has_type(ap.ACTOR_TYPES):
                d = urlparse(m.id).netloc
                content += f"@{m.preferredUsername}@{d} "

        thread = _build_thread(data)

    return htmlify(
        render_template(
            "new.html",
            reply=reply_id,
            content=content,
            thread=thread,
            default_visibility=default_visibility,
            visibility=ap.Visibility,
            emojis=config.EMOJIS.split(" "),
            custom_emojis=sorted(
                [ap.Emoji(**dat) for name, dat in EMOJIS_BY_NAME.items()],
                key=lambda e: e.name,
            ),
        ))
Пример #8
0
def task_fetch_remote_question() -> _Response:
    """Fetch a remote question for implementation that does not send Update."""
    task = p.parse(flask.request)
    app.logger.info(f"task={task!r}")
    iri = task.payload
    try:
        app.logger.info(f"Fetching remote question {iri}")
        local_question = DB.activities.find_one(
            {
                "box": Box.INBOX.value,
                "type": ap.ActivityType.CREATE.value,
                "activity.object.id": iri,
            }
        )
        remote_question = ap.get_backend().fetch_iri(iri, no_cache=True)
        # FIXME(tsileo): compute and set `meta.object_visiblity` (also update utils.py to do it)
        if (
            local_question
            and (
                local_question["meta"].get("voted_for")
                or local_question["meta"].get("subscribed")
            )
            and not DB.notifications.find_one({"activity.id": remote_question["id"]})
        ):
            DB.notifications.insert_one(
                {
                    "type": "question_ended",
                    "datetime": datetime.now(timezone.utc).isoformat(),
                    "activity": remote_question,
                }
            )

        # Update the Create if we received it in the inbox
        if local_question:
            DB.activities.update_one(
                {"remote_id": local_question["remote_id"], "box": Box.INBOX.value},
                {"$set": {"activity.object": remote_question}},
            )

        # Also update all the cached copies (Like, Announce...)
        DB.activities.update_many(
            {"meta.object.id": remote_question["id"]},
            {"$set": {"meta.object": remote_question}},
        )

    except HTTPError as err:
        app.logger.exception("request failed")
        if 400 >= err.response.status_code >= 499:
            app.logger.info("client error, no retry")
            return ""

        raise TaskError() from err
    except Exception as err:
        app.logger.exception("task failed")
        raise TaskError() from err

    return ""
Пример #9
0
def _user_api_get_note(from_outbox: bool = False) -> ap.BaseActivity:
    oid = _user_api_arg("id")
    app.logger.info(f"fetching {oid}")
    note = ap.parse_activity(ap.get_backend().fetch_iri(oid))
    if from_outbox and not note.id.startswith(ID):
        raise NotFromOutboxError(
            f"cannot load {note.id}, id must be owned by the server")

    return note
Пример #10
0
def user_outbox(name):
    be = activitypub.get_backend()
    if not be:
        abort(500)
    data = request.get_json(force=True)
    if not data:
        abort(500)
    current_app.logger.debug(f"req_headers={request.headers}")
    current_app.logger.debug(f"raw_data={data}")
Пример #11
0
def get_actor(url):
    if not url:
        return None
    if isinstance(url, list):
        url = url[0]
    if isinstance(url, dict):
        url = url.get("id")
    try:
        return ap.get_backend().fetch_iri(url)
    except (ActivityNotFoundError, ActivityGoneError):
        return f"Deleted<{url}>"
    except Exception as exc:
        return f"Error<{url}/{exc!r}>"
Пример #12
0
def post_to_remote_inbox(self, payload: str, to: str) -> None:
    if not current_app.config["AP_ENABLED"]:
        return  # not federating if not enabled

    current_app.logger.debug(f"post_to_remote_inbox {payload}")

    ap_actor = json.loads(payload)["actor"]
    actor = Actor.query.filter(Actor.url == ap_actor).first()
    if not actor:
        current_app.logger.exception("no actor found")
        return

    key = Key(owner=actor.url)
    key.load(actor.private_key)

    signature_auth = HTTPSigAuth(key)

    # current_app.logger.debug(f"key=={key.__dict__}")

    try:
        current_app.logger.info("payload=%s", payload)
        current_app.logger.info("generating sig")
        signed_payload = json.loads(payload)

        backend = ap.get_backend()

        # Don't overwrite the signature if we're forwarding an activity
        if "signature" not in signed_payload:
            generate_signature(signed_payload, key)

        current_app.logger.info("to=%s", to)
        resp = requests.post(
            to,
            data=json.dumps(signed_payload),
            auth=signature_auth,
            headers={
                "Content-Type": HEADERS[1],
                "Accept": HEADERS[1],
                "User-Agent": backend.user_agent()
            },
        )
        current_app.logger.info("resp=%s", resp)
        current_app.logger.info("resp_body=%s", resp.text)
        resp.raise_for_status()
    except HTTPError as err:
        current_app.logger.exception("request failed")
        if 400 >= err.response.status_code >= 499:
            current_app.logger.info("client error, no retry")
    return
Пример #13
0
def post_to_inbox(activity: ap.BaseActivity) -> None:
    # actor = activity.get_actor()
    backend = ap.get_backend()

    # TODO: drop if emitter is blocked
    # backend.outbox_is_blocked(target actor, actor.id)

    # TODO: drop if duplicate
    # backend.inbox_check_duplicate(actor, activity.id)

    backend.save(Box.INBOX, activity)

    process_new_activity.delay(activity.id)

    finish_inbox_processing.delay(activity.id)
Пример #14
0
def post_to_outbox(activity: ap.BaseActivity) -> str:
    current_app.logger.debug(f"post_to_outbox {activity}")

    if activity.has_type(ap.CREATE_TYPES):
        activity = activity.build_create()

    backend = ap.get_backend()

    # Assign a random ID
    obj_id = backend.random_object_id()
    activity.set_id(backend.activity_url(obj_id), obj_id)

    backend.save(Box.OUTBOX, activity)

    finish_post_to_outbox.delay(activity.id)
    return activity.id
Пример #15
0
def build_inbox_json_feed(path: str,
                          request_cursor: Optional[str] = None
                          ) -> Dict[str, Any]:
    """Build a JSON feed from the inbox activities."""
    data = []
    cursor = None

    q: Dict[str, Any] = {
        "type": "Create",
        "meta.deleted": False,
        "box": Box.INBOX.value,
    }
    if request_cursor:
        q["_id"] = {"$lt": request_cursor}

    for item in DB.activities.find(q, limit=50).sort("_id", -1):
        actor = ap.get_backend().fetch_iri(item["activity"]["actor"])
        data.append({
            "id":
            item["activity"]["id"],
            "url":
            item["activity"]["object"].get("url"),
            "content_html":
            item["activity"]["object"]["content"],
            "content_text":
            html2text(item["activity"]["object"]["content"]),
            "date_published":
            item["activity"]["object"].get("published"),
            "author": {
                "name": actor.get("name", actor.get("preferredUsername")),
                "url": actor.get("url"),
                "avatar": actor.get("icon", {}).get("url"),
            },
        })
        cursor = str(item["_id"])

    resp = {
        "version": "https://jsonfeed.org/version/1",
        "title": f"{USERNAME}'s stream",
        "home_page_url": ID,
        "feed_url": ID + path,
        "items": data,
    }
    if cursor and len(data) == 50:
        resp["next_url"] = ID + path + "?cursor=" + cursor

    return resp
Пример #16
0
def lookup(url: str) -> ap.BaseActivity:
    """Try to find an AP object related to the given URL."""
    try:
        if url.startswith("@"):
            actor_url = get_actor_url(url)
            if actor_url:
                return ap.fetch_remote_activity(actor_url)
    except NotAnActivityError:
        pass
    except requests.HTTPError:
        # Some websites may returns 404, 503 or others when they don't support webfinger, and we're just taking a guess
        # when performing the lookup.
        pass
    except requests.RequestException as err:
        raise RemoteServerUnavailableError(f"failed to fetch {url}: {err!r}")

    backend = ap.get_backend()
    try:
        resp = requests.head(
            url,
            timeout=10,
            allow_redirects=True,
            headers={"User-Agent": backend.user_agent()},
        )
    except requests.RequestException as err:
        raise RemoteServerUnavailableError(f"failed to GET {url}: {err!r}")

    try:
        resp.raise_for_status()
    except Exception:
        return ap.fetch_remote_activity(url)

    # If the page is HTML, maybe it contains an alternate link pointing to an AP object
    for alternate in mf2py.parse(resp.text).get("alternates", []):
        if alternate.get("type") == "application/activity+json":
            return ap.fetch_remote_activity(alternate["url"])

    try:
        # Maybe the page was JSON-LD?
        data = resp.json()
        return ap.parse_activity(data)
    except Exception:
        pass

    # Try content negotiation (retry with the AP Accept header)
    return ap.fetch_remote_activity(url)
Пример #17
0
def forward_activity(self, iri: str) -> None:
    if not current_app.config["AP_ENABLED"]:
        return  # not federating if not enabled

    try:
        activity = ap.fetch_remote_activity(iri)
        backend = ap.get_backend()
        recipients = backend.followers_as_recipients()
        current_app.logger.debug(f"Forwarding {activity!r} to {recipients}")
        activity = ap.clean_activity(activity.to_dict())
        for recp in recipients:
            current_app.logger.debug(f"forwarding {activity!r} to {recp}")
            payload = json.dumps(activity)
            post_to_remote_inbox.delay(payload, recp)

    except Exception as err:  # noqa: F841
        current_app.logger.exception(f"failed to cache attachments for {iri}")
Пример #18
0
def finish_inbox_processing(self, iri: str) -> None:
    try:
        backend = ap.get_backend()

        activity = ap.fetch_remote_activity(iri)
        current_app.logger.info(f"activity={activity!r}")

        actor = activity.get_actor()
        id = activity.get_object_id()
        current_app.logger.debug(f"finish_inbox_processing actor {actor}")

        if activity.has_type(ap.ActivityType.DELETE):
            backend.inbox_delete(actor, activity)
        elif activity.has_type(ap.ActivityType.UPDATE):
            backend.inbox_update(actor, activity)
        elif activity.has_type(ap.ActivityType.CREATE):
            backend.inbox_create(actor, activity)
        elif activity.has_type(ap.ActivityType.ANNOUNCE):
            backend.inbox_announce(actor, activity)
        elif activity.has_type(ap.ActivityType.LIKE):
            backend.inbox_like(actor, activity)
        elif activity.has_type(ap.ActivityType.FOLLOW):
            # Reply to a Follow with an Accept
            accept = ap.Accept(actor=id, object=activity.to_dict(embed=True))
            post_to_outbox(accept)
            backend.new_follower(activity, activity.get_actor(),
                                 activity.get_object())
        elif activity.has_type(ap.ActivityType.ACCEPT):
            obj = activity.get_object()
            # FIXME: probably other types to ACCEPT the Activity
            if obj.has_type(ap.ActivityType.FOLLOW):
                # Accept new follower
                backend.new_following(activity, obj)
        elif activity.has_type(ap.ActivityType.UNDO):
            obj = activity.get_object()
            if obj.has_type(ap.ActivityType.LIKE):
                backend.inbox_undo_like(actor, obj)
            elif obj.has_type(ap.ActivityType.ANNOUNCE):
                backend.inbox_undo_announce(actor, obj)
            elif obj.has_type(ap.ActivityType.FOLLOW):
                backend.undo_new_follower(actor, obj)
    except (ActivityGoneError, ActivityNotFoundError, NotAnActivityError):
        current_app.logger.exception(f"no retry")
    except Exception as err:  # noqa: F841
        current_app.logger.exception(f"failed to cache attachments for"
                                     f" {iri}")
Пример #19
0
def user_followings(name):
    be = activitypub.get_backend()
    if not be:
        abort(500)
    # data = request.get_json(force=True)
    # if not data:
    #     abort(500)
    current_app.logger.debug(f"req_headers={request.headers}")
    # current_app.logger.debug(f"raw_data={data}")

    user = User.query.filter(User.name == name).first()
    if not user:
        abort(404)

    actor = user.actor[0]
    followings_list = actor.followings

    return jsonify(**build_ordered_collection(followings_list, actor.url, request.args.get("page"), switch_side=True))
Пример #20
0
def outbox():
    """
    Global outbox
    ---
    tags:
        - ActivityPub
    responses:
        200:
            description: Returns something
    """
    be = activitypub.get_backend()
    if not be:
        abort(500)
    data = request.get_json(force=True)
    if not data:
        abort(500)
    current_app.logger.debug(f"req_headers={request.headers}")
    current_app.logger.debug(f"raw_data={data}")
Пример #21
0
def finish_post_to_outbox(self, iri: str) -> None:
    try:
        activity = ap.fetch_remote_activity(iri)
        backend = ap.get_backend()

        current_app.logger.info(f"finish_post_to_outbox {activity}")

        recipients = activity.recipients()

        actor = activity.get_actor()
        current_app.logger.debug(f"finish_post_to_outbox actor {actor!r}")

        if activity.has_type(ap.ActivityType.DELETE):
            backend.outbox_delete(actor, activity)
        elif activity.has_type(ap.ActivityType.UPDATE):
            backend.outbox_update(actor, activity)
        elif activity.has_type(ap.ActivityType.CREATE):
            backend.outbox_create(actor, activity)
        elif activity.has_type(ap.ActivityType.ANNOUNCE):
            backend.outbox_announce(actor, activity)
        elif activity.has_type(ap.ActivityType.LIKE):
            backend.outbox_like(actor, activity)
        elif activity.has_type(ap.ActivityType.UNDO):
            obj = activity.get_object()
            if obj.has_type(ap.ActivityType.LIKE):
                backend.outbox_undo_like(actor, obj)
            elif obj.has_type(ap.ActivityType.ANNOUNCE):
                backend.outbox_undo_announce(actor, obj)
            elif obj.has_type(ap.ActivityType.FOLLOW):
                backend.undo_new_following(actor, obj)

        current_app.logger.info(f"recipients={recipients}")
        activity = ap.clean_activity(activity.to_dict())

        payload = json.dumps(activity)
        for recp in recipients:
            current_app.logger.debug(f"posting to {recp}")
            post_to_remote_inbox.delay(payload, recp)
    except (ActivityGoneError, ActivityNotFoundError):
        current_app.logger.exception(f"no retry")
    except Exception as err:  # noqa: F841
        current_app.logger.exception(f"failed to post "
                                     f"to remote inbox for {iri}")
Пример #22
0
def post_to_outbox(activity: ap.BaseActivity) -> str:
    current_app.logger.debug(f"post_to_outbox {activity!r}")

    if activity.has_type(ap.CREATE_TYPES):
        print("BUILD CREATE POST TO OUTBOX")
        activity = activity.build_create()

    backend = ap.get_backend()

    # Assign a random ID
    obj_id = backend.random_object_id()
    activity.set_id(backend.activity_url(obj_id), obj_id)

    backend.save(Box.OUTBOX, activity)

    # Broadcast only if AP is enabled
    if current_app.config["AP_ENABLED"]:
        finish_post_to_outbox.delay(activity.id)
    return activity.id
Пример #23
0
def _get_nodeinfo_url(server: str) -> Optional[str]:
    backend = ap.get_backend()
    for scheme in {"https", "http"}:
        try:
            resp = requests.get(
                f"{scheme}://{server}/.well-known/nodeinfo",
                timeout=10,
                allow_redirects=True,
                headers={"User-Agent": backend.user_agent()},
            )
            resp.raise_for_status()
            data = resp.json()
            for link in data.get("links", []):
                return link["href"]
        except requests.HTTPError:
            return None
        except requests.RequestException:
            continue

    return None
Пример #24
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
Пример #25
0
def api_vote() -> _Response:
    oid = _user_api_arg("id")
    app.logger.info(f"fetching {oid}")
    note = ap.parse_activity(ap.get_backend().fetch_iri(oid))
    choice = _user_api_arg("choice")

    raw_note = dict(
        attributedTo=MY_PERSON.id,
        cc=[],
        to=note.get_actor().id,
        name=choice,
        tag=[],
        inReplyTo=note.id,
    )
    raw_note["@context"] = config.DEFAULT_CTX

    note = ap.Note(**raw_note)
    create = note.build_create()
    create_id = post_to_outbox(create)

    return _user_api_response(activity=create_id)
Пример #26
0
def get_software_name(server: str) -> str:
    backend = ap.get_backend()
    nodeinfo_endpoint = _get_nodeinfo_url(server)
    if nodeinfo_endpoint:
        try:
            resp = requests.get(
                nodeinfo_endpoint,
                timeout=10,
                headers={"User-Agent": backend.user_agent()},
            )
            resp.raise_for_status()
            software_name = resp.json().get("software", {}).get("name")
            if software_name:
                return software_name

            return SoftwareName.UNKNOWN.value
        except requests.RequestException:
            return SoftwareName.UNKNOWN.value

    if _try_mastodon_api(server):
        return SoftwareName.MASTODON.value

    return SoftwareName.UNKNOWN.value
Пример #27
0
def search():
    s = request.args.get("what")
    pcfg = {"title": gettext("Search user")}

    results = {"accounts": [], "sounds": [], "mode": None, "from": None}

    if current_user.is_authenticated:
        results["from"] = current_user.name

    # Search for sounds
    # TODO: Implement FTS to get sounds search

    # Search for accounts
    accounts = []
    is_user_at_account = RE_ACCOUNT.match(s)

    if s.startswith("https://"):
        results["mode"] = "uri"
        if current_user.is_authenticated:
            users = (
                db.session.query(Actor, Follower)
                .outerjoin(
                    Follower, and_(Actor.id == Follower.target_id, Follower.actor_id == current_user.actor[0].id)
                )
                .filter(Actor.url == s)
                .filter(not_(Actor.id == current_user.actor[0].id))
                .all()
            )
        else:
            users = db.session.query(Actor).filter(Actor.url == s).all()
    elif is_user_at_account:
        results["mode"] = "acct"
        user = is_user_at_account.group("user")
        instance = is_user_at_account.group("instance")
        if current_user.is_authenticated:
            users = (
                db.session.query(Actor, Follower)
                .outerjoin(
                    Follower, and_(Actor.id == Follower.target_id, Follower.actor_id == current_user.actor[0].id)
                )
                .filter(Actor.preferred_username == user, Actor.domain == instance)
                .filter(not_(Actor.id == current_user.actor[0].id))
                .all()
            )
        else:
            users = db.session.query(Actor).filter(Actor.preferred_username == user, Actor.domain == instance).all()
    else:
        results["mode"] = "username"
        # Match actor username in database
        if current_user.is_authenticated:
            users = (
                db.session.query(Actor, Follower)
                .outerjoin(
                    Follower, and_(Actor.id == Follower.target_id, Follower.actor_id == current_user.actor[0].id)
                )
                .filter(or_(Actor.preferred_username.contains(s), Actor.name.contains(s)))
                .filter(not_(Actor.id == current_user.actor[0].id))
                .all()
            )
        else:
            users = (
                db.session.query(Actor).filter(or_(Actor.preferred_username.contains(s), Actor.name.contains(s))).all()
            )

    # Handle the results
    if len(users) > 0:
        for user in users:
            if current_user.is_authenticated:
                if user[1]:
                    follows = user[1].actor_id == current_user.actor[0].id
                else:
                    follows = False
            else:
                follows = None

            if type(user) is Actor:
                # Unauthenticated results
                accounts.append(
                    {
                        "username": user.name,
                        "name": user.preferred_username,
                        "summary": user.summary,
                        "instance": user.domain,
                        "url": user.url,
                        "remote": not user.is_local(),
                        "follow": follows,
                    }
                )
            else:
                accounts.append(
                    {
                        "username": user[0].name,
                        "name": user[0].preferred_username,
                        "summary": user[0].summary,
                        "instance": user[0].domain,
                        "url": user[0].url,
                        "remote": not user[0].is_local(),
                        "follow": follows,
                    }
                )

    if len(accounts) <= 0:
        # Do a webfinger
        current_app.logger.debug(f"webfinger for {s}")
        try:
            remote_actor_url = get_actor_url(s, debug=current_app.debug)
            # We need to get the remote Actor
            backend = ap.get_backend()
            iri = backend.fetch_iri(remote_actor_url)
            if iri:
                current_app.logger.debug(f"got remote actor URL {remote_actor_url}")
                # Fixme handle unauthenticated users plus duplicates follows
                follow_rel = (
                    db.session.query(Actor.id, Follower.id)
                    .outerjoin(Follower, Actor.id == Follower.target_id)
                    .filter(Actor.url == remote_actor_url)
                    .first()
                )
                if follow_rel:
                    follow_status = follow_rel[1] is not None
                else:
                    follow_status = False

                domain = urlparse(iri["url"])
                user = {
                    "username": iri["name"],
                    "name": iri["preferredUsername"],
                    "instance": domain.netloc,
                    "url": iri["url"],
                    "remote": True,
                    "summary": iri["summary"],
                    "follow": follow_status,
                }
                accounts.append(user)
                results["mode"] = "webfinger"
                # Use iri to populate results["accounts"]
        except (InvalidURLError, ValueError):
            current_app.logger.exception(f"Invalid webfinger URL: {s}")

    # Finally fill the results dict
    results["accounts"] = accounts

    return render_template("search/results.jinja2", pcfg=pcfg, who=s, results=results)
Пример #28
0
def inbox():
    # GET /inbox
    if request.method == "GET":
        if not is_api_request():
            abort(404)
        try:
            _api_required()
        except BadSignature:
            abort(404)

        return activitypubify(**activitypub.build_ordered_collection(
            DB.activities,
            q={
                "meta.deleted": False,
                "box": Box.INBOX.value
            },
            cursor=request.args.get("cursor"),
            map_func=lambda doc: remove_context(doc["activity"]),
            col_name="inbox",
        ))

    # POST/ inbox
    try:
        data = request.get_json(force=True)
        if not isinstance(data, dict):
            raise ValueError("not a dict")
    except Exception:
        return Response(
            status=422,
            headers={"Content-Type": "application/json"},
            response=json.dumps({
                "error": "failed to decode request body as JSON",
                "request_id": g.request_id,
            }),
        )

    # Check the blacklist now to see if we can return super early
    if is_blacklisted(data):
        logger.info(f"dropping activity from blacklisted host: {data['id']}")
        return Response(status=201)

    logger.info(f"request_id={g.request_id} req_headers={request.headers!r}")
    logger.info(f"request_id={g.request_id} raw_data={data}")
    try:
        req_verified, actor_id = verify_request(request.method, request.path,
                                                request.headers, request.data)
        if not req_verified:
            raise Exception("failed to verify request")
        logger.info(f"request_id={g.request_id} signed by {actor_id}")
    except Exception:
        logger.exception(
            f"failed to verify request {g.request_id}, trying to verify the payload by fetching the remote"
        )
        try:
            remote_data = get_backend().fetch_iri(data["id"])
        except ActivityGoneError:
            # XXX Mastodon sends Delete activities that are not dereferencable, it's the actor url with #delete
            # appended, so an `ActivityGoneError` kind of ensure it's "legit"
            if data["type"] == ActivityType.DELETE.value and data[
                    "id"].startswith(data["object"]):
                # If we're here, this means the key is not saved, so we cannot verify the object
                logger.info(
                    f"received a Delete for an unknown actor {data!r}, drop it"
                )

                return Response(status=201)
        except Exception:
            logger.exception(f"failed to fetch remote for payload {data!r}")

            if "type" in data:
                # Friendica does not returns a 410, but a 302 that redirect to an HTML page
                if ap._has_type(data["type"], ActivityType.DELETE):
                    logger.info(
                        f"received a Delete for an unknown actor {data!r}, drop it"
                    )
                    return Response(status=201)

            if "id" in data:
                if DB.trash.find_one({"activity.id": data["id"]}):
                    # It's already stored in trash, returns early
                    return Response(
                        status=422,
                        headers={"Content-Type": "application/json"},
                        response=json.dumps({
                            "error":
                            "failed to verify request (using HTTP signatures or fetching the IRI)",
                            "request_id": g.request_id,
                        }),
                    )

            # Now we can store this activity in the trash for later analysis

            # Track/store the payload for analysis
            ip, geoip = _get_ip()

            DB.trash.insert({
                "activity": data,
                "meta": {
                    "ts": datetime.now().timestamp(),
                    "ip_address": ip,
                    "geoip": geoip,
                    "tb": traceback.format_exc(),
                    "headers": dict(request.headers),
                    "request_id": g.request_id,
                },
            })

            return Response(
                status=422,
                headers={"Content-Type": "application/json"},
                response=json.dumps({
                    "error":
                    "failed to verify request (using HTTP signatures or fetching the IRI)",
                    "request_id": g.request_id,
                }),
            )

        # We fetched the remote data successfully
        data = remote_data
    try:
        activity = ap.parse_activity(data)
    except ValueError:
        logger.exception(
            "failed to parse activity for req {g.request_id}: {data!r}")

        # Track/store the payload for analysis
        ip, geoip = _get_ip()

        DB.trash.insert({
            "activity": data,
            "meta": {
                "ts": datetime.now().timestamp(),
                "ip_address": ip,
                "geoip": geoip,
                "tb": traceback.format_exc(),
                "headers": dict(request.headers),
                "request_id": g.request_id,
            },
        })

        return Response(status=201)

    logger.debug(f"inbox activity={g.request_id}/{activity}/{data}")

    post_to_inbox(activity)

    return Response(status=201)
Пример #29
0
def unfollow():
    user = request.args.get("user")

    actor_me = current_user.actor[0]

    if user.startswith("https://"):
        actor = Actor.query.filter(Actor.url == user).first()
        local_user = actor.user
    else:
        local_user = User.query.filter(User.name == user).first()

    if local_user:
        # Process local unfollow
        actor_me.unfollow(local_user.actor[0])
        flash(gettext("Unfollow successful"), "success")
    else:
        # Might be a remote unfollow

        # 1. Webfinger the user
        try:
            remote_actor_url = get_actor_url(user, debug=current_app.debug)
        except InvalidURLError:
            current_app.logger.exception(f"Invalid webfinger URL: {user}")
            remote_actor_url = None
        except requests.exceptions.HTTPError:
            current_app.logger.exception(f"Invali webfinger URL: {user}")
            remote_actor_url = None

        if not remote_actor_url:
            flash(gettext("User not found"), "error")
            return redirect(url_for("bp_users.profile",
                                    name=current_user.name))

        # 2. Check if we have a local user
        actor_target = Actor.query.filter(
            Actor.url == remote_actor_url).first()

        if not actor_target:
            # 2.5 Fetch and save remote actor
            backend = ap.get_backend()
            iri = backend.fetch_iri(remote_actor_url)
            if not iri:
                flash(gettext("User not found"), "error")
                return redirect(url_for("bp_main.home"))
            act = ap.parse_activity(iri)
            actor_target, user_target = create_remote_actor(act)
            db.session.add(user_target)
            db.session.add(actor_target)

        # 2.5 Get the relation of the follow
        follow_relation = Follower.query.filter(
            Follower.actor_id == actor_me.id,
            Follower.target_id == actor_target.id).first()
        if not follow_relation:
            flash(gettext("You don't follow this user"), "error")
            return redirect(url_for("bp_users.profile",
                                    name=current_user.name))

        # 3. Fetch the Activity of the Follow
        accept_activity = Activity.query.filter(
            Activity.url == follow_relation.activity_url).first()
        if not accept_activity:
            current_app.logger.error(
                f"cannot find accept activity {follow_relation.activity_url}")
            flash(gettext("Whoops, something went wrong"))
            return redirect(url_for("bp_users.profile",
                                    name=current_user.name))
        # Then the Activity ID of the Accept will be the object id
        activity = ap.parse_activity(payload=accept_activity.payload)

        # Get the final activity (the Follow one)
        follow_activity = Activity.query.filter(
            Activity.url == activity.get_object_id()).first()
        if not follow_activity:
            current_app.logger.error(
                f"cannot find follow activity {activity.get_object_id()}")
            flash(gettext("Whoops, something went wrong"))
            return redirect(url_for("bp_users.profile",
                                    name=current_user.name))

        ap_follow_activity = ap.parse_activity(payload=follow_activity.payload)

        # 4. Initiate a Follow request from actor_me to actor_target
        unfollow = ap_follow_activity.build_undo()
        post_to_outbox(unfollow)
        flash(gettext("Unfollow request have been transmitted"), "success")

    return redirect(url_for("bp_users.profile", name=current_user.name))
Пример #30
0
def search():
    """
    Search.
    ---
    tags:
        - Global
    parameters:
        - name: q
          in: query
          type: string
          required: true
          description: search string
    responses:
        200:
            description: fixme.
    """
    # Get logged in user from bearer token, or None if not logged in
    if current_token:
        current_user = current_token.user
    else:
        current_user = None

    s = request.args.get("q", None)
    if not s:
        return jsonify({"error": "No search string provided"}), 400

    # This is the old search endpoint and needs to be improved
    # Especially tracks and accounts needs to be returned in the right format, with the data helpers
    # Users should be searched from known Actors or fetched
    # URI should be searched from known activities or fetched
    # FTS, well, FTS needs to be implemented

    results = {"accounts": [], "sounds": [], "mode": None, "from": None}

    if current_user:
        results["from"] = current_user.name

    # Search for sounds
    # TODO: Implement FTS to get sounds search
    sounds = []

    # Search for accounts
    accounts = []
    is_user_at_account = RE_ACCOUNT.match(s)

    if s.startswith("https://"):
        # Try to match the URI from Activities in database
        results["mode"] = "uri"
        users = Actor.query.filter(Actor.meta_deleted.is_(False),
                                   Actor.url == s).all()
    elif is_user_at_account:
        # It matches [email protected], try to match it locally
        results["mode"] = "acct"
        user = is_user_at_account.group("user")
        instance = is_user_at_account.group("instance")
        users = Actor.query.filter(Actor.meta_deleted.is_(False),
                                   Actor.preferred_username == user,
                                   Actor.domain == instance).all()
    else:
        # It's a FTS search
        results["mode"] = "username"
        # Match actor username in database
        if current_user:
            users = (db.session.query(Actor, Follower).outerjoin(
                Follower,
                and_(Actor.id == Follower.target_id,
                     Follower.actor_id == current_user.actor[0].id)).filter(
                         or_(Actor.preferred_username.contains(s),
                             Actor.name.contains(s))).filter(
                                 not_(Actor.id ==
                                      current_user.actor[0].id)).all())
        else:
            users = (db.session.query(Actor).filter(
                or_(Actor.preferred_username.contains(s),
                    Actor.name.contains(s))).all())

    # Handle the found users
    if len(users) > 0:
        for actor in users:
            relationship = False
            if current_user:
                relationship = to_json_relationship(current_user, actor.user)
            accounts.append(to_json_account(actor.user, relationship))

    if len(accounts) <= 0:
        # Do a webfinger
        # TODO FIXME: We should do this only if https:// or user@account submitted
        # And rework it slightly differently since we needs to backend.fetch_iri() for https:// who
        # can match a Sound and not only an Actor
        current_app.logger.debug(f"webfinger for {s}")
        try:
            remote_actor_url = get_actor_url(s, debug=current_app.debug)
            # We need to get the remote Actor
            backend = ap.get_backend()
            iri = backend.fetch_iri(remote_actor_url)
            if iri:
                # We have fetched an unknown Actor
                # Save it in database and return it properly
                current_app.logger.debug(
                    f"got remote actor URL {remote_actor_url}")

                act = ap.parse_activity(iri)

                fetched_actor, fetched_user = create_remote_actor(act)
                db.session.add(fetched_user)
                db.session.add(fetched_actor)
                db.session.commit()

                relationship = False
                if current_user:
                    relationship = to_json_relationship(
                        current_user, fetched_user)
                accounts.append(to_json_account(fetched_user, relationship))
                results["mode"] = "webfinger"

        except (InvalidURLError, ValueError):
            current_app.logger.exception(f"Invalid AP URL: {s}")
            # Then test fetching as a "normal" Activity ?
    # Finally fill the results dict
    results["accounts"] = accounts

    # FIXME: handle exceptions
    if results["mode"] == "uri" and len(sounds) <= 0:
        backend = ap.get_backend()
        iri = backend.fetch_iri(s)
        if iri:
            # FIXME: Is INBOX the right choice here ?
            backend.save(Box.INBOX, iri)
        # Fetch again, but get it from database
        activity = Activity.query.filter(Activity.url == iri).first()
        if not activity:
            current_app.logger.exception("WTF Activity is not saved")
        else:
            from tasks import create_sound_for_remote_track, upload_workflow

            sound_id = create_sound_for_remote_track(activity)
            sound = Sound.query.filter(Sound.id == sound_id).one()
            upload_workflow.delay(sound.id)

            relationship = False
            if current_user:
                relationship = to_json_relationship(current_user, sound.user)
            acct = to_json_account(sound.user, relationship)
            sounds.append(to_json_track(sound, acct))

    return jsonify({"who": s, "results": results})