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")
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}")
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)
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})
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)
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))
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))