clacks/lib/clacks_web/controllers/frontend_controller.ex

324 lines
9.1 KiB
Elixir

defmodule ClacksWeb.FrontendController do
use ClacksWeb, :controller
alias Clacks.{Actor, User, Timeline, Repo, ActivityPub, Activity, Object}
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
%Actor{local: true} = actor ->
# only local profiles are shown
render(conn, "profile.html", %{
actor: actor,
statuses: actor_statuses(actor, params, only_public: true)
})
_ ->
# 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)
render(conn, "profile.html", %{
current_user: current_user,
actor: user.actor,
statuses: actor_statuses(user.actor, params, only_public: true),
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 ->
render(conn, "profile.html", %{
current_user: current_user,
actor: actor,
statuses: actor_statuses(actor, params, only_public: true),
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 post_status(conn, %{"content" => _content} = params) do
current_user = conn.assigns[:user] |> Repo.preload(:actor)
note = note_for_posting(current_user, params)
note_changeset = Object.changeset_for_creating(note)
{:ok, _object} = Repo.insert(note_changeset)
create = ActivityPub.create(note)
{:ok, activity} = ActivityPub.Helper.save_and_federate(create, current_user.actor)
path = Map.get(params, "continue", Routes.frontend_path(Endpoint, :status, activity.id))
redirect(conn, to: path)
end
defp note_for_posting(current_user, %{"content" => content, "in_reply_to" => in_reply_to_ap_id}) do
with %Activity{data: %{"context" => context, "actor" => in_reply_to_actor}} <-
Activity.get_by_ap_id(in_reply_to_ap_id) do
to = [in_reply_to_actor, @public]
# todo: followers
cc = []
ActivityPub.note(
current_user.actor.ap_id,
content,
context,
in_reply_to_ap_id,
nil,
DateTime.utc_now(),
to,
cc
)
else
_ ->
ActivityPub.note(current_user.actor.ap_id, content)
end
end
defp note_for_posting(current_user, %{"content" => content}) do
ActivityPub.note(current_user.actor.ap_id, content)
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