Frontend stuff

This commit is contained in:
Shadowfacts 2019-10-06 19:41:18 -04:00
parent a12ba25f18
commit 01d4e713bb
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
15 changed files with 298 additions and 24 deletions

View File

@ -7,6 +7,8 @@
# General application configuration # General application configuration
use Mix.Config use Mix.Config
config :clacks, :frontend, unauthenticated_homepage: :public_timeline
config :clacks, config :clacks,
ecto_repos: [Clacks.Repo] ecto_repos: [Clacks.Repo]

View File

@ -25,6 +25,19 @@ defmodule Clacks.Activity do
|> validate_required([:data, :local, :actor]) |> validate_required([:data, :local, :actor])
end end
def changeset_for_creating(activity, local \\ false) do
changeset(%__MODULE__{}, %{
data: activity,
local: local,
actor: activity["actor"]
})
end
@spec get(id :: String.t()) :: t() | nil
def get(id) do
Repo.get(__MODULE__, id)
end
@spec get_by_ap_id(ap_id :: String.t(), force_refetch :: boolean()) :: t() | nil @spec get_by_ap_id(ap_id :: String.t(), force_refetch :: boolean()) :: t() | nil
def get_by_ap_id(ap_id, force_refetch \\ false) do def get_by_ap_id(ap_id, force_refetch \\ false) do
if force_refetch do if force_refetch do

View File

@ -31,7 +31,7 @@ defmodule Clacks.ActivityPub do
@spec note( @spec note(
actor :: String.t(), actor :: String.t(),
html :: String.t(), html :: String.t(),
context :: String.t(), context :: String.t() | nil,
id :: String.t() | nil, id :: String.t() | nil,
published :: DateTime.t(), published :: DateTime.t(),
to :: [String.t()], to :: [String.t()],
@ -40,13 +40,14 @@ defmodule Clacks.ActivityPub do
def note( def note(
actor, actor,
html, html,
context, context \\ nil,
id \\ nil, id \\ nil,
published \\ DateTime.utc_now(), published \\ DateTime.utc_now(),
to \\ [@public], to \\ [@public],
cc \\ [] cc \\ []
) do ) do
id = id || object_id(Ecto.UUID.generate()) id = id || object_id(Ecto.UUID.generate())
context = context || context_id(Ecto.UUID.generate())
%{ %{
"@context" => @context, "@context" => @context,
@ -131,4 +132,9 @@ defmodule Clacks.ActivityPub do
} }
|> URI.to_string() |> URI.to_string()
end end
@spec context_id(id :: String.t()) :: String.t()
def context_id(id) do
"data:,clickityclack" <> id
end
end end

36
lib/clacks/timeline.ex Normal file
View File

@ -0,0 +1,36 @@
defmodule Clacks.Timeline do
alias Clacks.{Repo, Actor, Activity, Paginator}
import Ecto.Query
@public "https://www.w3.org/ns/activitystreams#Public"
@spec actor_timeline(actor :: Actor.t(), only_public :: boolean(), params :: map()) :: [
Activity.t()
]
def actor_timeline(actor, only_public \\ true, params) do
Activity
|> restrict_to_actor(actor.ap_id)
|> restrict_to_types(["Create", "Announce"])
|> restirct_to_public(only_public)
|> Paginator.paginate(params)
|> Repo.all()
end
defp restrict_to_actor(query, actor_id) do
where(query, [a], fragment("?->>'actor'", a.data) == ^actor_id)
end
defp restrict_to_types(query, types) do
where(query, [a], fragment("?->>'type'", a.data) in ^types)
end
defp restirct_to_public(query, true) do
where(
query,
[a],
fragment("?->'to' \\? ?", a.data, @public) or fragment("?->'cc' \\? ?", a.data, @public)
)
end
defp restirct_to_public(query, false), do: query
end

View File

@ -0,0 +1,24 @@
defmodule ClacksWeb.ActivitiesController do
use ClacksWeb, :controller
alias Clacks.{ActivityPub, Activity}
alias ClacksWeb.Router.Helpers, as: Routes
alias ClacksWeb.Endpoint
def get(conn, _params) do
ap_id = current_url(conn, %{})
case Activity.get_by_ap_id(ap_id) do
%Activity{local: true, id: id, data: data} ->
case conn.assigns[:format] do
"activity+json" ->
json(conn, data)
"html" ->
redirect(conn, to: Routes.frontend_path(Endpoint, :status, id))
end
_ ->
put_status(conn, 404)
end
end
end

View File

@ -1,36 +1,37 @@
defmodule ClacksWeb.ActorController do defmodule ClacksWeb.ActorController do
use ClacksWeb, :controller use ClacksWeb, :controller
alias Clacks.Actor alias Clacks.{Actor, User}
@context "https://www.w3.org/ns/activitystreams" @context "https://www.w3.org/ns/activitystreams"
plug :get_actor plug :get_actor
defp get_actor(%Plug.Conn{path_params: %{"nickname" => nickname}} = conn, _opts) do defp get_actor(%Plug.Conn{path_params: %{"username" => username}} = conn, _opts) do
case Actor.get_by_nickname(nickname) do case User.get_by_username(username) do
nil -> nil ->
conn conn
|> put_status(404) |> put_status(404)
|> halt() |> halt()
actor -> %User{actor: actor} ->
assign(conn, :actor, actor) assign(conn, :actor, actor)
end end
end end
defp get_actor(conn, _opts), do: conn defp get_actor(conn, _opts), do: conn
def get(conn, _params) do def get(%Plug.Conn{assigns: %{format: "activity+json"}} = conn, _params) do
case conn.assigns[:actor] do actor = conn.assigns[:actor]
%Actor{local: true, data: data} ->
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(data) |> json(actor.data)
%Actor{local: false, ap_id: ap_id} ->
conn
|> redirect(external: ap_id)
end end
def get(%Plug.Conn{assigns: %{format: "html"}} = conn, _params) do
conn
|> put_view(ClacksWeb.FrontendView)
|> ClacksWeb.FrontendController.call(:profile)
end end
def followers(conn, %{"page" => page}) do def followers(conn, %{"page" => page}) do

View File

@ -0,0 +1,102 @@
defmodule ClacksWeb.FrontendController do
use ClacksWeb, :controller
alias Clacks.{Actor, User, Timeline, Repo, ActivityPub, Activity}
alias ClacksWeb.Router.Helpers, as: Routes
alias ClacksWeb.Endpoint
def index(%Plug.Conn{assigns: %{user: user}} = conn, _params) do
user = Repo.preload(user, :actor)
render(conn, "home.html", %{
user: user,
actor: user.actor
})
end
def index(conn, params) do
Application.get_env(:clacks, :frontend, %{})
|> Keyword.get(:unauthenticated_homepage, :public_timeline)
|> index(conn, params)
end
defp index(:public_timeline, conn, params) do
# tood: show public timeline
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 public timeline
index(:public_timeline, conn)
end
end
defp actor_statuses(actor, params, only_public: only_public) do
Timeline.actor_timeline(actor, only_public, params)
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} = note
}
} <- Activity.get(id),
%Actor{} = author <- Actor.get_by_ap_id(author_id) do
render(conn, "status.html", %{
current_user: current_user,
note: note,
author: author
})
else
nil ->
put_status(conn, 404)
%Activity{local: false, data: %{"id" => ap_id}} ->
redirect(conn, external: ap_id)
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 ->
put_status(conn, 404)
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)
})
end
end
def post_status(conn, %{"content" => content} = params) do
current_user = conn.assigns[:user] |> Repo.preload(:actor)
note = ActivityPub.note(current_user.actor.ap_id, content)
create = ActivityPub.create(note)
changeset = Activity.changeset_for_creating(create, true)
{:ok, activity} = Repo.insert(changeset)
:ok = ActivityPub.Federator.federate_to_followers(activity.data, current_user.actor)
path = Map.get(params, "continue", Routes.frontend_path(Endpoint, :status, activity.id))
redirect(conn, to: path)
end
end

View File

@ -0,0 +1,10 @@
defmodule ClacksWeb.Plug.Format do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
format = Phoenix.Controller.get_format(conn)
assign(conn, :format, format)
end
end

View File

@ -4,13 +4,15 @@ defmodule ClacksWeb.Plug.WebAuthenticate do
alias ClacksWeb.Router.Helpers, as: Routes alias ClacksWeb.Router.Helpers, as: Routes
alias ClacksWeb.Endpoint alias ClacksWeb.Endpoint
def init(%{on_failure: on_failure_action} = opts) def init([on_failure: on_failure_action] = opts)
when on_failure_action in [:redirect_to_login, :pass], when on_failure_action in [:redirect_to_login, :pass],
do: opts do: opts
def init(_), do: %{on_failure: :redirect_to_login} def init(opts) do
[on_failure: :redirect_to_login]
end
def call(conn, %{on_failure: on_failure_action}) do def call(%Plug.Conn{assigns: %{format: "html"}} = conn, on_failure: on_failure_action) do
user_token = get_session(conn, :user_token) user_token = get_session(conn, :user_token)
case Phoenix.Token.verify(Endpoint, "user token", user_token, max_age: 7 * 24 * 60 * 60) do case Phoenix.Token.verify(Endpoint, "user token", user_token, max_age: 7 * 24 * 60 * 60) do
@ -29,6 +31,8 @@ defmodule ClacksWeb.Plug.WebAuthenticate do
end end
end end
def call(conn, _opts), do: conn
defp on_failure(conn, :redirect_to_login) do defp on_failure(conn, :redirect_to_login) do
conn conn
|> Phoenix.Controller.redirect(to: Routes.login_path(Endpoint, :login)) |> Phoenix.Controller.redirect(to: Routes.login_path(Endpoint, :login))

View File

@ -3,10 +3,14 @@ defmodule ClacksWeb.Router do
pipeline :browser do pipeline :browser do
plug :accepts, ["html"] plug :accepts, ["html"]
plug ClacksWeb.Plug.Format
plug :fetch_session plug :fetch_session
plug :fetch_flash plug :fetch_flash
plug :protect_from_forgery plug :protect_from_forgery
plug :put_secure_browser_headers plug :put_secure_browser_headers
end
pipeline :browser_maybe_authenticated do
plug ClacksWeb.Plug.WebAuthenticate, on_failure: :pass plug ClacksWeb.Plug.WebAuthenticate, on_failure: :pass
end end
@ -18,6 +22,22 @@ defmodule ClacksWeb.Router do
plug :accepts, ["activity+json", "html"] plug :accepts, ["activity+json", "html"]
end end
pipeline :browser_or_activitypub do
plug :accepts, ["html", "activity+json"]
plug ClacksWeb.Plug.Format
plug :browser_if_html
end
defp browser_if_html(%Plug.Conn{assigns: %{format: "html"}} = conn, _opts) do
conn
|> fetch_session()
|> fetch_flash()
|> protect_from_forgery()
|> put_secure_browser_headers()
end
defp browser_if_html(conn, _opts), do: conn
scope "/", ClacksWeb do scope "/", ClacksWeb do
pipe_through :browser pipe_through :browser
@ -26,6 +46,15 @@ defmodule ClacksWeb.Router do
post "/logout", LoginController, :logout_post post "/logout", LoginController, :logout_post
end end
scope "/", ClacksWeb do
pipe_through :browser
pipe_through :browser_maybe_authenticated
get "/", FrontendController, :index
get "/status/:id", FrontendController, :status
post "/post", FrontendController, :post_status
end
scope "/", ClacksWeb do scope "/", ClacksWeb do
pipe_through :browser pipe_through :browser
pipe_through :browser_authenticated pipe_through :browser_authenticated
@ -36,17 +65,24 @@ defmodule ClacksWeb.Router do
get "/objects/:id", ObjectsController, :get get "/objects/:id", ObjectsController, :get
get "/users/:nickname", ActorController, :get get "/users/:username/followers", ActorController, :followers
get "/users/:nickname/followers", ActorController, :followers get "/users/:username/following", ActorController, :following
get "/users/:nickname/following", ActorController, :following get "/users/:username/outbox", OutboxController, :outbox
get "/users/:nickname/outbox", OutboxController, :outbox
post "/inbox", InboxController, :shared post "/inbox", InboxController, :shared
post "/users/:nickname/inbox", InboxController, :user_specific post "/users/:username/inbox", InboxController, :user_specific
get "/.well-known/webfinger", WebFingerController, :get get "/.well-known/webfinger", WebFingerController, :get
end end
scope "/", ClacksWeb do
pipe_through :browser_or_activitypub
pipe_through :browser_maybe_authenticated
get "/users/:username", ActorController, :get
get "/activities/:id", ActivitiesController, :get
end
# Other scopes may use custom stacks. # Other scopes may use custom stacks.
# scope "/api", ClacksWeb do # scope "/api", ClacksWeb do
# pipe_through :api # pipe_through :api

View File

@ -0,0 +1,7 @@
<h1>Home</h1>
<p>Logged in as <%= @user.username %></p>
<%= form_tag Routes.frontend_path(@conn, :post_status), method: :post do %>
<textarea id="content" name="content" cols="30" rows="10"></textarea>
<%= submit "Post" %>
<% end %>

View File

@ -0,0 +1,15 @@
<h1><%= @actor.data["preferredUsername"] %></h1>
<h2>@<%= @actor.data["name"] %></h2>
<p><%= @actor.data["summary"] %></p>
<ul>
<%= for status <- @statuses do %>
<li>
<div class="status">
<div class="status-content">
<%= status.data["object"]["content"] %>
</div>
</div>
</li>
<% end %>
</ul>

View File

@ -0,0 +1,15 @@
<div class="status">
<h2>
<a href="<%= @author.ap_id %>">
<%= @author.data["preferredUsername"] %>
</a>
</h2>
<h3>
<a href="<%= @author.ap_id %>">
<%= @author.data["name"] %>
</a>
</h3>
<div class="status-content">
<%= @note["content"] %>
</div>
</div>

View File

@ -0,0 +1,3 @@
defmodule ClacksWeb.FrontendView do
use ClacksWeb, :view
end