Frontend stuff
This commit is contained in:
parent
a12ba25f18
commit
01d4e713bb
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 %>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
||||||
|
defmodule ClacksWeb.FrontendView do
|
||||||
|
use ClacksWeb, :view
|
||||||
|
end
|
Loading…
Reference in New Issue