Compare commits
14 Commits
ace33f3d06
...
04692e50c2
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 04692e50c2 | |
Shadowfacts | e194f0fc07 | |
Shadowfacts | 8becb6b174 | |
Shadowfacts | 803fc4c36e | |
Shadowfacts | 1a1a58cb82 | |
Shadowfacts | fcd66f3d51 | |
Shadowfacts | e0c8f8e142 | |
Shadowfacts | e7dcbdc6a4 | |
Shadowfacts | e7c19940b0 | |
Shadowfacts | 00c51e4ad9 | |
Shadowfacts | a0e290197f | |
Shadowfacts | b034d159a9 | |
Shadowfacts | f51d4a6be4 | |
Shadowfacts | 8178fc3ba6 |
|
@ -22,6 +22,7 @@ button.btn-link {
|
|||
border: none;
|
||||
color: $link-color;
|
||||
text-decoration: none;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
@ -29,9 +30,14 @@ button.btn-link {
|
|||
}
|
||||
}
|
||||
|
||||
button[type=submit]:not(.btn-link) {
|
||||
input, button:not(.btn-link) {
|
||||
box-sizing: border-box;
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
button:not(.btn-link) {
|
||||
padding: 0 2rem;
|
||||
font-family: $sans-serif;
|
||||
padding: 0.25rem 2rem;
|
||||
background-color: $tint-color;
|
||||
border: 1px solid darken($tint-color, 20%);
|
||||
color: white;
|
||||
|
@ -56,6 +62,19 @@ h1, h2, h3 {
|
|||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.flash-info {
|
||||
padding: 0.5rem;
|
||||
background-color: lighten(blue, 35%);
|
||||
border: 1px solid darken(blue, 20%);
|
||||
color: darken(blue, 35%);
|
||||
}
|
||||
.flash-error {
|
||||
padding: 0.5rem;
|
||||
background-color: lighten(red, 30%);
|
||||
border: 1px solid darken(red, 20%);
|
||||
color: darken(red, 35%);
|
||||
}
|
||||
|
||||
header {
|
||||
nav {
|
||||
display: flex;
|
||||
|
@ -69,6 +88,10 @@ header {
|
|||
li {
|
||||
display: inline;
|
||||
list-style: none;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -141,3 +164,29 @@ ul.status-list {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.search-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
input[type=text] {
|
||||
flex-grow: 1;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
label {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actor-actions {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
|
|
@ -110,6 +110,41 @@ defmodule Clacks.ActivityPub do
|
|||
}
|
||||
end
|
||||
|
||||
@spec follow(
|
||||
actor :: String.t(),
|
||||
followee :: String.t(),
|
||||
published :: NaiveDateTime.t(),
|
||||
state :: String.t()
|
||||
) ::
|
||||
map()
|
||||
def follow(actor, followee, published \\ DateTime.utc_now(), state \\ "pending") do
|
||||
%{
|
||||
"@context" => @context,
|
||||
"type" => "Follow",
|
||||
"id" => activity_id(Ecto.UUID.generate()),
|
||||
"actor" => actor,
|
||||
"object" => followee,
|
||||
"context" => context_id(Ecto.UUID.generate()),
|
||||
"to" => [followee],
|
||||
"cc" => [@public],
|
||||
"published" => published |> DateTime.to_iso8601(),
|
||||
"state" => state
|
||||
}
|
||||
end
|
||||
|
||||
@spec undo_follow(actor :: String.t(), follow_activity :: map()) :: map()
|
||||
def undo_follow(actor, %{"object" => followee} = follow_activity) do
|
||||
%{
|
||||
"@context" => @context,
|
||||
"type" => "Undo",
|
||||
"id" => activity_id(Ecto.UUID.generate()),
|
||||
"actor" => actor,
|
||||
"object" => follow_activity,
|
||||
"to" => [followee],
|
||||
"cc" => []
|
||||
}
|
||||
end
|
||||
|
||||
@spec object_id(id :: String.t()) :: String.t()
|
||||
def object_id(id) do
|
||||
url = Application.get_env(:clacks, ClacksWeb.Endpoint)[:url]
|
||||
|
|
|
@ -67,7 +67,7 @@ defmodule Clacks.ActivityPub.Federator do
|
|||
@public in activity["to"] or @public in activity["cc"] ->
|
||||
shared_inbox_for(actor)
|
||||
|
||||
actor.data["followers"] in activity["to"] or actor.data["followers"] in activity["cc"] ->
|
||||
actor["followers"] in activity["to"] or actor["followers"] in activity["cc"] ->
|
||||
shared_inbox_for(actor)
|
||||
|
||||
true ->
|
||||
|
|
|
@ -7,7 +7,7 @@ defmodule Clacks.ActivityPub.Fetcher do
|
|||
|
||||
with %{"type" => type, "id" => remote_id} = actor <- fetch(id),
|
||||
"person" <- String.downcase(type),
|
||||
%{host: actor_host} when actor_host == id_host <- URI.parse(remote_id) do
|
||||
%{host: ^id_host} <- URI.parse(remote_id) do
|
||||
actor
|
||||
else
|
||||
_ ->
|
||||
|
@ -20,7 +20,7 @@ defmodule Clacks.ActivityPub.Fetcher do
|
|||
%{host: id_host} = URI.parse(id)
|
||||
|
||||
with %{"actor" => remote_actor} = object <- fetch(id),
|
||||
%{host: actor_host} when actor_host == id_host <- URI.parse(remote_actor) do
|
||||
%{host: ^id_host} <- URI.parse(remote_actor) do
|
||||
object
|
||||
else
|
||||
_ ->
|
||||
|
@ -40,15 +40,10 @@ defmodule Clacks.ActivityPub.Fetcher do
|
|||
headers = [Accept: "application/activity+json, application/ld+json"]
|
||||
opts = [hackney: Application.get_env(:clacks, :hackney_opts, [])]
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: status_code, body: body}}
|
||||
when status_code in 200..299 <-
|
||||
HTTPoison.get(uri, headers, opts),
|
||||
with {:ok, %HTTPoison.Response{body: body}} <- Clacks.HTTP.get(uri, headers, opts),
|
||||
{:ok, data} <- Jason.decode(body) do
|
||||
data
|
||||
else
|
||||
{:ok, %HTTPoison.Response{}} ->
|
||||
nil
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warn("Couldn't fetch AP object at #{uri}: #{inspect(reason)}")
|
||||
nil
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
defmodule Clacks.ActivityPub.Helper do
|
||||
alias Clacks.{Activity, Actor, Repo}
|
||||
require Logger
|
||||
|
||||
@spec save_and_federate(activity_data :: map(), actor :: Actor.t()) ::
|
||||
{:ok, Activity.t()} | :error
|
||||
def save_and_federate(activity_data, actor) do
|
||||
changeset = Activity.changeset_for_creating(activity_data, true)
|
||||
|
||||
case Repo.insert(changeset) do
|
||||
{:error, changeset} ->
|
||||
Logger.error("Couldn't save activity: #{inspect(changeset)}")
|
||||
:error
|
||||
|
||||
{:ok, activity} ->
|
||||
worker =
|
||||
%{id: activity.id, actor_id: actor.id}
|
||||
|> Clacks.Worker.Federate.new()
|
||||
|
||||
case Oban.insert(worker) do
|
||||
{:ok, _} ->
|
||||
{:ok, activity}
|
||||
|
||||
{:error, changeset} ->
|
||||
Logger.error("Couldn't save federate job: #{inspect(changeset)}")
|
||||
:error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,46 @@
|
|||
defmodule Clacks.HTTP do
|
||||
require Logger
|
||||
|
||||
def get(url, headers \\ [], options \\ []) do
|
||||
case HTTPoison.get(url, headers, options) do
|
||||
{:ok, %HTTPoison.Response{status_code: status_code} = response}
|
||||
when status_code in 200..299 ->
|
||||
{:ok, response}
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: status_code, headers: resp_headers}}
|
||||
when status_code in [301, 302] ->
|
||||
resp_headers
|
||||
|> Enum.find(fn {name, _value} -> String.downcase(name) == "location" end)
|
||||
|> case do
|
||||
{_, new_url} ->
|
||||
new_url =
|
||||
case URI.parse(new_url) do
|
||||
%URI{host: nil, path: path} ->
|
||||
# relative path
|
||||
%URI{URI.parse(url) | path: path} |> URI.to_string()
|
||||
|
||||
uri ->
|
||||
uri
|
||||
end
|
||||
|
||||
Logger.debug("Got 301 redirect from #{url} to #{new_url}")
|
||||
get(new_url, headers, options)
|
||||
|
||||
_ ->
|
||||
{:error, "Missing Location header for redirect"}
|
||||
end
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: 403}} ->
|
||||
{:error, "403 Forbidden"}
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: 404}} ->
|
||||
{:error, "404 Not Found"}
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: status_code}} ->
|
||||
{:error, "HTTP #{status_code}"}
|
||||
|
||||
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,15 +2,16 @@ defmodule Clacks.Inbox do
|
|||
require Logger
|
||||
alias Clacks.{Repo, Activity, Object, Actor, ActivityPub}
|
||||
|
||||
defp store_activity(%{"actor" => actor} = activity, local \\ false) when is_binary(actor) do
|
||||
defp store_activity(%{"actor" => actor, "id" => ap_id} = activity, local \\ false)
|
||||
when is_binary(actor) do
|
||||
changeset =
|
||||
Activity.changeset(%Activity{}, %{
|
||||
Activity.changeset(Activity.get_cached_by_ap_id(ap_id) || %Activity{}, %{
|
||||
data: activity,
|
||||
local: local,
|
||||
actor: actor
|
||||
})
|
||||
|
||||
Repo.insert(changeset)
|
||||
Repo.insert_or_update(changeset)
|
||||
end
|
||||
|
||||
@spec handle(activity :: map()) :: :ok | {:error, reason :: any()}
|
||||
|
@ -42,12 +43,13 @@ defmodule Clacks.Inbox do
|
|||
|
||||
store_activity(activity)
|
||||
|
||||
changeset = Actor.changeset(followed, %{followers: [follower_id | followed.followers]})
|
||||
new_followers = [follower_id | followed.followers] |> Enum.uniq()
|
||||
changeset = Actor.changeset(followed, %{followers: new_followers})
|
||||
|
||||
case Repo.update(changeset) do
|
||||
{:error, changeset} ->
|
||||
Logger.error("Couldn't store Follow activity: #{inspect(changeset)}")
|
||||
{:error, "Couldn't store Follow activity"}
|
||||
Logger.error("Couldn't store updated followers: #{inspect(changeset)}")
|
||||
{:error, "Couldn't store updated followers"}
|
||||
|
||||
{:ok, _followed} ->
|
||||
accept = ActivityPub.accept_follow(activity)
|
||||
|
@ -63,8 +65,49 @@ defmodule Clacks.Inbox do
|
|||
end
|
||||
end
|
||||
|
||||
def handle(
|
||||
%{
|
||||
"type" => "Accept",
|
||||
"actor" => followee_id,
|
||||
"object" => %{"type" => "Follow", "id" => follow_activity_id, "actor" => follower_id}
|
||||
} = activity
|
||||
) do
|
||||
followee = Actor.get_by_ap_id(followee_id)
|
||||
follower = Actor.get_by_ap_id(follower_id)
|
||||
|
||||
store_activity(activity)
|
||||
|
||||
follow_activity = Activity.get_cached_by_ap_id(follow_activity_id)
|
||||
|
||||
changeset =
|
||||
Activity.changeset(follow_activity, %{
|
||||
data: %{follow_activity.data | "state" => "accepted"}
|
||||
})
|
||||
|
||||
case Repo.update(changeset) do
|
||||
{:error, changeset} ->
|
||||
Logger.error("Couldn't store updated Follow activity: #{inspect(changeset)}")
|
||||
{:error, "Couldn't store updated Follow activity"}
|
||||
|
||||
{:ok, _follow_activity} ->
|
||||
new_followers = [follower_id | followee.followers] |> Enum.uniq()
|
||||
changeset = Actor.changeset(followee, %{followers: new_followers})
|
||||
|
||||
case Repo.update(changeset) do
|
||||
{:error, changeset} ->
|
||||
Logger.error("Couldn't store updated followers: #{inspect(changeset)}")
|
||||
{:error, "Couldn't store updated followers"}
|
||||
|
||||
{:ok, _followee} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# as a fallback, just store the activity
|
||||
def handle(activity) do
|
||||
Logger.debug("Unhandled activity: #{inspect(activity)}")
|
||||
|
||||
case store_activity(activity) do
|
||||
{:error, changeset} ->
|
||||
Logger.error("Could not store activity: #{inspect(changeset)}")
|
||||
|
|
|
@ -24,6 +24,17 @@ defmodule Clacks.Object do
|
|||
changeset(%__MODULE__{}, %{data: data})
|
||||
end
|
||||
|
||||
@spec get_actor(object :: t()) :: Clacks.Actor.t() | nil
|
||||
def get_actor(object) do
|
||||
case object.data["actor"] || object.data["attributedTo"] do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
id when is_binary(id) ->
|
||||
Clacks.Actor.get_by_ap_id(id)
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_by_ap_id(
|
||||
ap_id :: String.t(),
|
||||
force_refetch :: boolean(),
|
||||
|
@ -42,8 +53,9 @@ defmodule Clacks.Object do
|
|||
Repo.one(from o in __MODULE__, where: fragment("?->>'id'", o.data) == ^ap_id)
|
||||
end
|
||||
|
||||
@spec fetch(ap_id :: String.t(), synthesize_create :: boolean()) :: t() | nil
|
||||
def fetch(ap_id, synthesize_create \\ true) do
|
||||
@spec fetch(ap_id :: String.t(), synthesize_create :: boolean(), return :: :object | :activity) ::
|
||||
t() | Activity.t() | nil
|
||||
def fetch(ap_id, synthesize_create \\ true, return \\ :object) do
|
||||
case Clacks.ActivityPub.Fetcher.fetch_object(ap_id) do
|
||||
nil ->
|
||||
nil
|
||||
|
@ -61,21 +73,34 @@ defmodule Clacks.Object do
|
|||
actor = data["actor"] || data["attributedTo"]
|
||||
_ = Clacks.Actor.get_by_ap_id(actor)
|
||||
|
||||
if synthesize_create do
|
||||
create = Clacks.ActivityPub.synthesized_create(data)
|
||||
activity =
|
||||
case Clacks.Activity.get_by_object_ap_id(ap_id) do
|
||||
nil ->
|
||||
if synthesize_create do
|
||||
create = Clacks.ActivityPub.synthesized_create(data)
|
||||
|
||||
changeset =
|
||||
Clacks.Activity.changeset(%Clacks.Activity{}, %{
|
||||
data: create,
|
||||
local: false,
|
||||
actor: actor
|
||||
})
|
||||
changeset =
|
||||
Clacks.Activity.changeset(%Clacks.Activity{}, %{
|
||||
data: create,
|
||||
local: false,
|
||||
actor: actor
|
||||
})
|
||||
|
||||
{:ok, _create} = Repo.insert_or_update(changeset)
|
||||
{:ok, create} = Repo.insert_or_update(changeset)
|
||||
create
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
%Clacks.Activity{} = activity ->
|
||||
activity
|
||||
end
|
||||
|
||||
case return do
|
||||
:object -> object
|
||||
:activity -> activity
|
||||
end
|
||||
|
||||
object
|
||||
|
||||
{:error, changeset} ->
|
||||
Logger.error("Couldn't store remote object #{ap_id}: #{inspect(changeset)}")
|
||||
nil
|
||||
|
|
|
@ -9,26 +9,24 @@ defmodule Clacks.Timeline do
|
|||
@spec actor_timeline(
|
||||
actor :: Actor.t(),
|
||||
params :: map(),
|
||||
only_public :: boolean(),
|
||||
actors :: boolean()
|
||||
only_public :: boolean()
|
||||
) :: [
|
||||
Activity.t()
|
||||
]
|
||||
def actor_timeline(actor, params, only_public \\ true, actors \\ false) do
|
||||
def actor_timeline(actor, params, only_public \\ true) do
|
||||
Activity
|
||||
|> restrict_to_actor(actor.ap_id)
|
||||
|> restrict_to_types(@timeline_types)
|
||||
|> restrict_to_public(only_public)
|
||||
|> paginate(params)
|
||||
|> limit(^Map.get(params, "limit", 20))
|
||||
|> join_with_actors(actors)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec home_timeline(user :: User.t(), params :: map(), actors :: boolean()) :: [
|
||||
Activity.t()
|
||||
@spec home_timeline(user :: User.t(), params :: map()) :: [
|
||||
{Activity.t(), Actor.t()}
|
||||
]
|
||||
def home_timeline(user, params, actors \\ false) do
|
||||
def home_timeline(user, params) do
|
||||
user =
|
||||
case user.actor do
|
||||
%Ecto.Association.NotLoaded{} ->
|
||||
|
@ -39,15 +37,27 @@ defmodule Clacks.Timeline do
|
|||
end
|
||||
|
||||
Activity
|
||||
|> join_with_actors()
|
||||
|> where(
|
||||
[a],
|
||||
fragment("?->>'actor'", a.data) == ^user.actor.ap_id or
|
||||
fragment("?->>'actor'", a.data) in ^user.actor.followers
|
||||
[activity, actor],
|
||||
fragment("?->>'actor'", activity.data) == ^user.actor.ap_id or
|
||||
^user.actor.ap_id in actor.followers
|
||||
)
|
||||
|> restrict_to_types(@timeline_types)
|
||||
|> paginate(params)
|
||||
|> limit(^Map.get(params, "limit", 20))
|
||||
|> join_with_actors(actors)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec local_timeline(params :: map()) :: [{Activity.t(), Actor.t()}]
|
||||
def local_timeline(params) do
|
||||
Activity
|
||||
|> where([a], a.local)
|
||||
|> restrict_to_public(true)
|
||||
|> restrict_to_types(@timeline_types)
|
||||
|> paginate(params)
|
||||
|> limit(^Map.get(params, "limit", 20))
|
||||
|> join_with_actors()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
|
@ -69,11 +79,9 @@ defmodule Clacks.Timeline do
|
|||
|
||||
defp restrict_to_public(query, false), do: query
|
||||
|
||||
defp join_with_actors(query, true) do
|
||||
defp join_with_actors(query) do
|
||||
query
|
||||
|> join(:left, [o], a in Actor, on: a.ap_id == fragment("?->>'actor'", o.data))
|
||||
|> select([o, a], {o, a})
|
||||
end
|
||||
|
||||
defp join_with_actors(query, false), do: query
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@ defmodule ClacksWeb.FrontendController do
|
|||
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"
|
||||
|
||||
|
@ -12,18 +13,20 @@ defmodule ClacksWeb.FrontendController do
|
|||
render(conn, "home.html", %{
|
||||
user: user,
|
||||
actor: user.actor,
|
||||
statuses_with_authors: Timeline.home_timeline(user, params, true)
|
||||
statuses_with_authors: Timeline.home_timeline(user, params)
|
||||
})
|
||||
end
|
||||
|
||||
def index(conn, params) do
|
||||
Application.get_env(:clacks, :frontend, %{})
|
||||
|> Keyword.get(:unauthenticated_homepage, :public_timeline)
|
||||
|> Keyword.get(:unauthenticated_homepage, :local_timeline)
|
||||
|> index(conn, params)
|
||||
end
|
||||
|
||||
defp index(:public_timeline, conn, params) do
|
||||
# tood: show public timeline
|
||||
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
|
||||
|
@ -36,8 +39,8 @@ defmodule ClacksWeb.FrontendController do
|
|||
})
|
||||
|
||||
_ ->
|
||||
# otherwise show public timeline
|
||||
index(:public_timeline, conn)
|
||||
# otherwise show local timeline
|
||||
index(:local_timeline, conn)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -53,7 +56,7 @@ defmodule ClacksWeb.FrontendController do
|
|||
data:
|
||||
%{
|
||||
"type" => "Create",
|
||||
"object" => %{"type" => "Note", "attributedTo" => author_id} = note
|
||||
"object" => %{"type" => "Note", "attributedTo" => author_id}
|
||||
} = data
|
||||
} = activity <- Activity.get(id),
|
||||
%Actor{} = author <- Actor.get_by_ap_id(author_id) do
|
||||
|
@ -119,7 +122,7 @@ defmodule ClacksWeb.FrontendController do
|
|||
|
||||
case User.get_by_username(username) do
|
||||
nil ->
|
||||
put_status(conn, 404)
|
||||
resp(conn, 404, "Not Found")
|
||||
|
||||
user ->
|
||||
user = Repo.preload(user, :actor)
|
||||
|
@ -127,11 +130,127 @@ defmodule ClacksWeb.FrontendController do
|
|||
render(conn, "profile.html", %{
|
||||
current_user: current_user,
|
||||
actor: user.actor,
|
||||
statuses: actor_statuses(user.actor, params, only_public: true)
|
||||
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)
|
||||
|
||||
|
@ -140,12 +259,7 @@ defmodule ClacksWeb.FrontendController do
|
|||
{:ok, object} = Repo.insert(note_changeset)
|
||||
|
||||
create = ActivityPub.create(note)
|
||||
create_changeset = Activity.changeset_for_creating(create, true)
|
||||
{:ok, activity} = Repo.insert(create_changeset)
|
||||
|
||||
%{id: activity.id, actor_id: current_user.actor.id}
|
||||
|> Clacks.Worker.Federate.new()
|
||||
|> Oban.insert()
|
||||
{: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)
|
||||
|
@ -177,4 +291,32 @@ defmodule ClacksWeb.FrontendController do
|
|||
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()) :: boolean()
|
||||
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
|
||||
|
|
|
@ -58,8 +58,11 @@ defmodule ClacksWeb.Router do
|
|||
pipe_through :browser_authenticated
|
||||
|
||||
post "/post", FrontendController, :post_status
|
||||
|
||||
get "/status/:id/reply", FrontendController, :reply
|
||||
get "/search", FrontendController, :search
|
||||
get "/actors/:id", FrontendController, :actor
|
||||
post "/actors/:id/follow", FrontendController, :follow
|
||||
post "/actors/:id/unfollow", FrontendController, :unfollow
|
||||
end
|
||||
|
||||
scope "/", ClacksWeb do
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="status">
|
||||
<div class="status-meta">
|
||||
<h2 class="status-author-nickname">
|
||||
<a href="<%= @author.ap_id %>">
|
||||
<a href="<%= local_actor_link(@author) %>">
|
||||
<%= @author.data["preferredUsername"] %>
|
||||
</a>
|
||||
</h2>
|
||||
|
@ -12,7 +12,7 @@
|
|||
</h3>
|
||||
<p class="status-meta-right">
|
||||
<span><%= display_timestamp(@note["published"]) %></span>
|
||||
<a href="<%= @note["url"] %>" class="status-permalink">Permalink</a>
|
||||
<a href="<%= @note["url"] || @note["id"] %>" class="status-permalink">Permalink</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="status-content">
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<h1>Local Timeline</h1>
|
||||
|
||||
<%= render "_timeline.html", conn: @conn, statuses_with_authors: @statuses_with_authors %>
|
|
@ -1,5 +1,35 @@
|
|||
<h1><%= @actor.data["preferredUsername"] %></h1>
|
||||
<h2>@<%= @actor.data["name"] %></h2>
|
||||
<h2>
|
||||
<%= if @actor.local do %>
|
||||
<%= display_username(@actor) %>
|
||||
<% else %>
|
||||
<a href="<%= @actor.ap_id %>">
|
||||
<%= display_username(@actor) %>
|
||||
</a>
|
||||
<% end %>
|
||||
</h2>
|
||||
<p><%= @actor.data["summary"] %></p>
|
||||
|
||||
<div class="actor-actions">
|
||||
<%= unless @current_user.actor.ap_id == @actor.ap_id do %>
|
||||
|
||||
<%= case @following_state do %>
|
||||
<% :not_following -> %>
|
||||
<%= form_tag Routes.frontend_path(@conn, :follow, @actor.id), method: :post, class: "follow-form" do %>
|
||||
<%= submit "Follow" %>
|
||||
<% end %>
|
||||
|
||||
<% :following -> %>
|
||||
<%= form_tag Routes.frontend_path(@conn, :unfollow, @actor.id), method: :post, class: "follow-form" do %>
|
||||
<%= submit "Unfollow" %>
|
||||
<% end %>
|
||||
|
||||
<% :pending -> %>
|
||||
<!-- todo: cancel follow request -->
|
||||
<button type="button">Pending</button>
|
||||
<% end %>
|
||||
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= render "_timeline.html", conn: @conn, statuses_with_authors: Enum.map(@statuses, &({&1, @actor})) %>
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<%= form_tag Routes.frontend_path(@conn, :search), method: :get, class: "search-form" do %>
|
||||
<input type="text" name="q" id="q" placeholder="Search Query" value="<%= @q %>">
|
||||
<%= submit "Search" %>
|
||||
<% end %>
|
||||
|
||||
<%= if length(@actor_results) > 0 do %>
|
||||
<hr>
|
||||
|
||||
<%= for actor <- @actor_results do %>
|
||||
<div class="actor">
|
||||
<h2 class="actor-nickname">
|
||||
<a href="<%= local_actor_link(actor) %>">
|
||||
<%= actor.data["preferredUsername"] %>
|
||||
</a>
|
||||
</h2>
|
||||
<h3 class="actor-username">
|
||||
<a href="<%= actor.ap_id %>">
|
||||
<%= display_username(actor) %>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= if length(@status_results) do %>
|
||||
<hr>
|
||||
|
||||
<%= for {status, author} <- @status_results do %>
|
||||
<%= render "_status.html", conn: @conn, author: author, status: status, note: status.data["object"] %>
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -13,11 +13,14 @@
|
|||
<nav role="navigation">
|
||||
<ul>
|
||||
<li><a href="/"><%= instance_name() %></a></li>
|
||||
<%= if @conn.assigns[:user] do %>
|
||||
<li><a href="<%= Routes.frontend_path(@conn, :search) %>">Search</a></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<ul>
|
||||
<%= if @conn.assigns[:user] do %>
|
||||
<li>
|
||||
Logged in as <a href="<%= Routes.actor_path(@conn, :get, @conn.assigns[:user].username) %>"><%= @conn.assigns[:user].username %></a>.
|
||||
Logged in as <a href="<%= Routes.actor_path(@conn, :get, @user.username) %>"><%= @user.username %></a>.
|
||||
</li>
|
||||
<li>
|
||||
<%= form_for @conn, Routes.login_path(@conn, :logout), [method: :post], fn f -> %>
|
||||
|
@ -34,8 +37,12 @@
|
|||
</section>
|
||||
</header>
|
||||
<main role="main" class="container">
|
||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||
<%= if get_flash(@conn, :info) do %>
|
||||
<p class="flash-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||
<% end %>
|
||||
<%= if get_flash(@conn, :error) do %>
|
||||
<p class="flash-error" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||
<% end %>
|
||||
<%= render @view_module, @view_template, assigns %>
|
||||
</main>
|
||||
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
<%= form_tag Routes.login_path(@conn, :login_post), method: :post do %>
|
||||
<%= form_tag Routes.login_path(@conn, :login_post), method: :post, class: "login-form" do %>
|
||||
<%= if @continue do %>
|
||||
<input type="hidden" name="continue" value="<%= @continue %>">
|
||||
<% end %>
|
||||
|
||||
<label for="username">Username</label>
|
||||
<input id="username" type="text" name="username">
|
||||
<br>
|
||||
<label for="password">Password</label>
|
||||
<input id="password" type="password" name="password">
|
||||
<br>
|
||||
<div class="input-group">
|
||||
<label for="username">Username:</label>
|
||||
<input id="username" type="text" name="username">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="password">Password:</label>
|
||||
<input id="password" type="password" name="password">
|
||||
</div>
|
||||
<%= submit "Log In" %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
defmodule ClacksWeb.FrontendView do
|
||||
use ClacksWeb, :view
|
||||
alias Clacks.{Actor, Activity}
|
||||
alias ClacksWeb.Router.Helpers, as: Routes
|
||||
alias ClacksWeb.Endpoint
|
||||
|
||||
@spec display_username(actor :: Actor.t()) :: String.t()
|
||||
|
||||
|
@ -13,6 +15,13 @@ defmodule ClacksWeb.FrontendView do
|
|||
"@" <> name <> "@" <> host
|
||||
end
|
||||
|
||||
def local_actor_link(%Actor{local: true, ap_id: ap_id}), do: ap_id
|
||||
|
||||
def local_actor_link(%Actor{local: false, id: id}),
|
||||
do: Routes.frontend_path(Endpoint, :actor, id)
|
||||
|
||||
@spec display_timestamp(str :: String.t()) :: String.t()
|
||||
|
||||
def display_timestamp(str) when is_binary(str) do
|
||||
display_timestamp(Timex.parse!(str, "{ISO:Extended}"))
|
||||
end
|
||||
|
@ -42,6 +51,9 @@ defmodule ClacksWeb.FrontendView do
|
|||
end
|
||||
end
|
||||
|
||||
@spec prev_page_path(conn :: Plug.Conn.t(), [Activity.t() | {Activity.t(), Actor.t()}]) ::
|
||||
String.t()
|
||||
|
||||
def prev_page_path(conn, activities) do
|
||||
if Map.has_key?(conn.query_params, "max_id") do
|
||||
Phoenix.Controller.current_path(conn, %{
|
||||
|
@ -52,6 +64,9 @@ defmodule ClacksWeb.FrontendView do
|
|||
end
|
||||
end
|
||||
|
||||
@spec next_page_path(conn :: Plug.Conn.t(), [Activity.t() | {Activity.t(), Actor.t()}]) ::
|
||||
String.t()
|
||||
|
||||
def next_page_path(conn, activities) do
|
||||
if length(activities) < 20 do
|
||||
nil
|
||||
|
|
Loading…
Reference in New Issue