defmodule ClacksWeb.FrontendController do use ClacksWeb, :controller alias Clacks.{Actor, User, Timeline, Repo, ActivityPub, Activity, Object, UserActionsHelper} alias ClacksWeb.Router.Helpers, as: Routes alias ClacksWeb.Endpoint import Ecto.Query @public "https://www.w3.org/ns/activitystreams#Public" def index(%Plug.Conn{assigns: %{user: user}} = conn, params) do user = Repo.preload(user, :actor) render(conn, "home.html", %{ user: user, actor: user.actor, statuses_with_authors: Timeline.home_timeline(user, params) }) end def index(conn, params) do Application.get_env(:clacks, :frontend, %{}) |> Keyword.get(:unauthenticated_homepage, :local_timeline) |> index(conn, params) end defp index(:local_timeline, conn, params) do render(conn, "local_timeline.html", %{ statuses_with_authors: Timeline.local_timeline(params) }) end defp index({:profile, nickname}, conn, params) do case Actor.get_by_nickname(nickname) do # only local profiles are shown %Actor{local: true} = actor -> statuses_with_actor = actor_statuses(actor, params, only_public: true) |> Enum.map(fn {activity, announce} -> {activity, actor, announce} end) render(conn, "profile.html", %{ actor: actor, statuses_with_actor: statuses_with_actor }) _ -> # otherwise show local timeline index(:local_timeline, conn, params) end end defp actor_statuses(actor, params, only_public: only_public) do Timeline.actor_timeline(actor, params, only_public) end def status(conn, %{"id" => id}) do current_user = conn.assigns[:user] |> Repo.preload(:actor) with %Activity{ local: true, data: %{ "type" => "Create", "object" => %{"type" => "Note", "attributedTo" => author_id} } = data } = activity <- Activity.get(id), %Actor{} = author <- Actor.get_by_ap_id(author_id) do case conn.assigns[:format] do "activity+json" -> json(conn, data) "html" -> render(conn, "status.html", %{ current_user: current_user, status: activity, author: author }) end else nil -> case conn.assigns[:format] do "activity+json" -> conn |> put_status(404) |> json(%{error: "Not Found"}) "html" -> resp(conn, 404, "Not Found") end %Activity{local: false, data: %{"id" => ap_id}} -> case conn.assigns[:format] do "activity+json" -> conn |> put_status(404) |> json(%{error: "Not Found"}) "html" -> redirect(conn, external: ap_id) end end end def reply(conn, %{"id" => id}) do current_user = conn.assigns[:user] with %Activity{ data: %{ "type" => "Create", "object" => %{"type" => "Note", "attributedTo" => author_id} } } = activity <- Activity.get(id), %Actor{} = author <- Actor.get_by_ap_id(author_id) do render(conn, "status.html", %{ current_user: current_user, status: activity, author: author }) else _ -> resp(conn, 404, "Not Found") end end def profile(conn, %{"username" => username} = params) do current_user = conn.assigns[:user] |> Repo.preload(:actor) case User.get_by_username(username) do nil -> resp(conn, 404, "Not Found") user -> user = Repo.preload(user, :actor) statuses_with_actor = actor_statuses(user.actor, params, only_public: true) |> Enum.map(fn {activity, announce} -> {activity, user.actor, announce} end) render(conn, "profile.html", %{ current_user: current_user, actor: user.actor, statuses_with_actor: statuses_with_actor, following_state: following_state(current_user.actor, user.actor) }) end end def actor(conn, %{"id" => id} = params) do current_user = conn.assigns[:user] |> Repo.preload(:actor) case Repo.get(Actor, id) do nil -> resp(conn, 404, "Not Found") actor -> statuses_with_actor = actor_statuses(actor, params, only_public: true) |> Enum.map(fn {activity, announce} -> {activity, actor, announce} end) render(conn, "profile.html", %{ current_user: current_user, actor: actor, statuses_with_actor: statuses_with_actor, following_state: following_state(current_user.actor, actor) }) end end def follow(conn, %{"id" => id}) do current_user = conn.assigns[:user] |> Repo.preload(:actor) case Repo.get(Actor, id) do nil -> resp(conn, 404, "Not Found") followee -> if current_user.actor.ap_id in followee.followers do redirect(conn, to: ClacksWeb.FrontendView.local_actor_link(followee)) else follow = ActivityPub.follow(current_user.actor.ap_id, followee.ap_id) ActivityPub.Helper.save_and_federate(follow, current_user.actor) conn |> put_flash(:info, "Follow request sent") |> redirect(to: ClacksWeb.FrontendView.local_actor_link(followee)) end end end def unfollow(conn, %{"id" => id}) do current_user = conn.assigns[:user] |> Repo.preload(:actor) case Repo.get(Actor, id) do nil -> resp(conn, 404, "Not Found") followee -> unless current_user.actor.ap_id in followee.followers do redirect(conn, to: ClacksWeb.FrontendView.local_actor_link(followee)) else new_followers = List.delete(followee.followers, current_user.actor.ap_id) changeset = Actor.changeset(followee, %{followers: new_followers}) {:ok, followee} = Repo.update(changeset) follow_activity = follow_activity(current_user.actor, followee) changeset = Activity.changeset(follow_activity, %{ data: %{follow_activity.data | "state" => "unfollowed"} }) {:ok, follow_activity} = Repo.update(changeset) undo_follow = ActivityPub.undo_follow(current_user.actor.ap_id, follow_activity.data) ActivityPub.Helper.save_and_federate(undo_follow, current_user.actor) conn |> put_flash(:info, "Unfollowed") |> redirect(to: ClacksWeb.FrontendView.local_actor_link(followee)) end end end def search(conn, %{"q" => q}) when is_binary(q) do current_user = conn.assigns[:user] actor_results = case Actor.fetch(q) do %Actor{} = actor -> [actor] _ -> [] end status_results = with %Activity{ actor: actor_id, data: %{"type" => "Create", "object" => %{"type" => "Note"}} } = activity <- Object.fetch(q, true, :activity), actor <- Actor.get_by_ap_id(actor_id) do [{activity, actor}] else _ -> [] end render(conn, "search.html", %{ current_user: current_user, q: q, status_results: status_results, actor_results: actor_results }) end def search(conn, _params) do current_user = conn.assigns[:user] render(conn, "search.html", %{ current_user: current_user, q: "", status_results: [], actor_results: [] }) end def notifications(conn, params) do current_user = conn.assigns[:user] |> Repo.preload(:actor) notifications = Timeline.notifications(current_user.actor, params) render(conn, "notifications.html", %{ current_user: current_user, notifications: notifications }) end def post_status(conn, %{"content" => content} = params) do current_user = conn.assigns[:user] |> Repo.preload(:actor) UserActionsHelper.post_status( current_user, content, "text/plain", Map.get(params, "in_reply_to") ) |> case do {:ok, activity} -> path = Map.get(params, "continue", Routes.frontend_path(Endpoint, :status, activity.id)) redirect(conn, to: path) {:error, reason} -> conn |> put_flash(:error, "Unable to post status: #{inspect(reason)}") |> redirect(to: Map.get(params, "continue", Routes.frontend_path(Endpoint, :index))) end end @spec follow_activity(follower :: Actor.t(), followee :: Actor.t()) :: map() def follow_activity(follower, followee) do # todo: get latest query = Activity |> where([a], fragment("?->>'type'", a.data) == "Follow") |> where([a], fragment("?->>'actor'", a.data) == ^follower.ap_id) |> where([a], fragment("?->>'object'", a.data) == ^followee.ap_id) |> order_by(desc: :inserted_at) |> limit(1) Repo.one(query) end @spec following_state(follower :: Actor.t(), followee :: Actor.t()) :: :following | :not_following | :pending defp following_state(follower, followee) do case follow_activity(follower, followee) do %Activity{data: %{"state" => "pending"}} -> :pending %Activity{data: %{"state" => "accepted"}} -> :following _ -> :not_following end end end