Пример #1
0
def authorize_follow():
    if request.method == "GET":
        return htmlify(
            render_template("authorize_remote_follow.html",
                            profile=request.args.get("profile")))

    csrf.protect()
    actor = get_actor_url(request.form.get("profile"))
    if not actor:
        abort(500)

    q = {
        "box": Box.OUTBOX.value,
        "type": ap.ActivityType.FOLLOW.value,
        "meta.undo": False,
        "activity.object": actor,
    }
    if DB.activities.count(q) > 0:
        return redirect("/following")

    follow = ap.Follow(actor=MY_PERSON.id,
                       object=actor,
                       to=[actor],
                       cc=[ap.AS_PUBLIC],
                       published=now())
    post_to_outbox(follow)

    return redirect("/following")
Пример #2
0
def test_webfinger(_):
    # FIXME(tsileo): it should try https first
    httpretty.register_uri(
        httpretty.GET,
        "http://microblog.pub/.well-known/webfinger",
        body=json.dumps(_WEBFINGER_RESP),
    )
    data = webfinger.webfinger("@[email protected]")
    assert data == _WEBFINGER_RESP

    assert webfinger.get_actor_url(
        "@[email protected]") == "https://microblog.pub"
    assert (webfinger.get_remote_follow_template("@[email protected]") ==
            "https://microblog.pub/authorize_follow?profile={uri}")
Пример #3
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)
Пример #4
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})
Пример #5
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)
Пример #6
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))
Пример #7
0
def follow():
    user = request.args.get("user")

    actor_me = current_user.actor[0]

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

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

        # 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.7 Check if we already have a relation
        rel = Follower.query.filter(
            Follower.actor_id == actor_me.id,
            Follower.target_id == actor_target.id).first()

        if not rel:
            # 3. Initiate a Follow request from actor_me to actor_target
            follow = ap.Follow(actor=actor_me.url, object=actor_target.url)
            post_to_outbox(follow)
            flash(gettext("Follow request have been transmitted"), "success")
        else:
            flash(gettext("You already follow this user", "info"))

    return redirect(url_for("bp_users.profile", name=current_user.name))