327 lines
9.2 KiB
Elixir
327 lines
9.2 KiB
Elixir
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
|