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;
|
border: none;
|
||||||
color: $link-color;
|
color: $link-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
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;
|
font-family: $sans-serif;
|
||||||
padding: 0.25rem 2rem;
|
|
||||||
background-color: $tint-color;
|
background-color: $tint-color;
|
||||||
border: 1px solid darken($tint-color, 20%);
|
border: 1px solid darken($tint-color, 20%);
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -56,6 +62,19 @@ h1, h2, h3 {
|
||||||
margin: 0.5rem 0;
|
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 {
|
header {
|
||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -69,6 +88,10 @@ header {
|
||||||
li {
|
li {
|
||||||
display: inline;
|
display: inline;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,3 +164,29 @@ ul.status-list {
|
||||||
box-sizing: border-box;
|
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
|
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()
|
@spec object_id(id :: String.t()) :: String.t()
|
||||||
def object_id(id) do
|
def object_id(id) do
|
||||||
url = Application.get_env(:clacks, ClacksWeb.Endpoint)[:url]
|
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"] ->
|
@public in activity["to"] or @public in activity["cc"] ->
|
||||||
shared_inbox_for(actor)
|
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)
|
shared_inbox_for(actor)
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
|
|
|
@ -7,7 +7,7 @@ defmodule Clacks.ActivityPub.Fetcher do
|
||||||
|
|
||||||
with %{"type" => type, "id" => remote_id} = actor <- fetch(id),
|
with %{"type" => type, "id" => remote_id} = actor <- fetch(id),
|
||||||
"person" <- String.downcase(type),
|
"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
|
actor
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
|
@ -20,7 +20,7 @@ defmodule Clacks.ActivityPub.Fetcher do
|
||||||
%{host: id_host} = URI.parse(id)
|
%{host: id_host} = URI.parse(id)
|
||||||
|
|
||||||
with %{"actor" => remote_actor} = object <- fetch(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
|
object
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
|
@ -40,15 +40,10 @@ defmodule Clacks.ActivityPub.Fetcher do
|
||||||
headers = [Accept: "application/activity+json, application/ld+json"]
|
headers = [Accept: "application/activity+json, application/ld+json"]
|
||||||
opts = [hackney: Application.get_env(:clacks, :hackney_opts, [])]
|
opts = [hackney: Application.get_env(:clacks, :hackney_opts, [])]
|
||||||
|
|
||||||
with {:ok, %HTTPoison.Response{status_code: status_code, body: body}}
|
with {:ok, %HTTPoison.Response{body: body}} <- Clacks.HTTP.get(uri, headers, opts),
|
||||||
when status_code in 200..299 <-
|
|
||||||
HTTPoison.get(uri, headers, opts),
|
|
||||||
{:ok, data} <- Jason.decode(body) do
|
{:ok, data} <- Jason.decode(body) do
|
||||||
data
|
data
|
||||||
else
|
else
|
||||||
{:ok, %HTTPoison.Response{}} ->
|
|
||||||
nil
|
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
Logger.warn("Couldn't fetch AP object at #{uri}: #{inspect(reason)}")
|
Logger.warn("Couldn't fetch AP object at #{uri}: #{inspect(reason)}")
|
||||||
nil
|
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
|
require Logger
|
||||||
alias Clacks.{Repo, Activity, Object, Actor, ActivityPub}
|
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 =
|
changeset =
|
||||||
Activity.changeset(%Activity{}, %{
|
Activity.changeset(Activity.get_cached_by_ap_id(ap_id) || %Activity{}, %{
|
||||||
data: activity,
|
data: activity,
|
||||||
local: local,
|
local: local,
|
||||||
actor: actor
|
actor: actor
|
||||||
})
|
})
|
||||||
|
|
||||||
Repo.insert(changeset)
|
Repo.insert_or_update(changeset)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec handle(activity :: map()) :: :ok | {:error, reason :: any()}
|
@spec handle(activity :: map()) :: :ok | {:error, reason :: any()}
|
||||||
|
@ -42,12 +43,13 @@ defmodule Clacks.Inbox do
|
||||||
|
|
||||||
store_activity(activity)
|
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
|
case Repo.update(changeset) do
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
Logger.error("Couldn't store Follow activity: #{inspect(changeset)}")
|
Logger.error("Couldn't store updated followers: #{inspect(changeset)}")
|
||||||
{:error, "Couldn't store Follow activity"}
|
{:error, "Couldn't store updated followers"}
|
||||||
|
|
||||||
{:ok, _followed} ->
|
{:ok, _followed} ->
|
||||||
accept = ActivityPub.accept_follow(activity)
|
accept = ActivityPub.accept_follow(activity)
|
||||||
|
@ -63,8 +65,49 @@ defmodule Clacks.Inbox do
|
||||||
end
|
end
|
||||||
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
|
# as a fallback, just store the activity
|
||||||
def handle(activity) do
|
def handle(activity) do
|
||||||
|
Logger.debug("Unhandled activity: #{inspect(activity)}")
|
||||||
|
|
||||||
case store_activity(activity) do
|
case store_activity(activity) do
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
Logger.error("Could not store activity: #{inspect(changeset)}")
|
Logger.error("Could not store activity: #{inspect(changeset)}")
|
||||||
|
|
|
@ -24,6 +24,17 @@ defmodule Clacks.Object do
|
||||||
changeset(%__MODULE__{}, %{data: data})
|
changeset(%__MODULE__{}, %{data: data})
|
||||||
end
|
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(
|
@spec get_by_ap_id(
|
||||||
ap_id :: String.t(),
|
ap_id :: String.t(),
|
||||||
force_refetch :: boolean(),
|
force_refetch :: boolean(),
|
||||||
|
@ -42,8 +53,9 @@ defmodule Clacks.Object do
|
||||||
Repo.one(from o in __MODULE__, where: fragment("?->>'id'", o.data) == ^ap_id)
|
Repo.one(from o in __MODULE__, where: fragment("?->>'id'", o.data) == ^ap_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec fetch(ap_id :: String.t(), synthesize_create :: boolean()) :: t() | nil
|
@spec fetch(ap_id :: String.t(), synthesize_create :: boolean(), return :: :object | :activity) ::
|
||||||
def fetch(ap_id, synthesize_create \\ true) do
|
t() | Activity.t() | nil
|
||||||
|
def fetch(ap_id, synthesize_create \\ true, return \\ :object) do
|
||||||
case Clacks.ActivityPub.Fetcher.fetch_object(ap_id) do
|
case Clacks.ActivityPub.Fetcher.fetch_object(ap_id) do
|
||||||
nil ->
|
nil ->
|
||||||
nil
|
nil
|
||||||
|
@ -61,21 +73,34 @@ defmodule Clacks.Object do
|
||||||
actor = data["actor"] || data["attributedTo"]
|
actor = data["actor"] || data["attributedTo"]
|
||||||
_ = Clacks.Actor.get_by_ap_id(actor)
|
_ = Clacks.Actor.get_by_ap_id(actor)
|
||||||
|
|
||||||
if synthesize_create do
|
activity =
|
||||||
create = Clacks.ActivityPub.synthesized_create(data)
|
case Clacks.Activity.get_by_object_ap_id(ap_id) do
|
||||||
|
nil ->
|
||||||
|
if synthesize_create do
|
||||||
|
create = Clacks.ActivityPub.synthesized_create(data)
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
Clacks.Activity.changeset(%Clacks.Activity{}, %{
|
Clacks.Activity.changeset(%Clacks.Activity{}, %{
|
||||||
data: create,
|
data: create,
|
||||||
local: false,
|
local: false,
|
||||||
actor: actor
|
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
|
end
|
||||||
|
|
||||||
object
|
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
Logger.error("Couldn't store remote object #{ap_id}: #{inspect(changeset)}")
|
Logger.error("Couldn't store remote object #{ap_id}: #{inspect(changeset)}")
|
||||||
nil
|
nil
|
||||||
|
|
|
@ -9,26 +9,24 @@ defmodule Clacks.Timeline do
|
||||||
@spec actor_timeline(
|
@spec actor_timeline(
|
||||||
actor :: Actor.t(),
|
actor :: Actor.t(),
|
||||||
params :: map(),
|
params :: map(),
|
||||||
only_public :: boolean(),
|
only_public :: boolean()
|
||||||
actors :: boolean()
|
|
||||||
) :: [
|
) :: [
|
||||||
Activity.t()
|
Activity.t()
|
||||||
]
|
]
|
||||||
def actor_timeline(actor, params, only_public \\ true, actors \\ false) do
|
def actor_timeline(actor, params, only_public \\ true) do
|
||||||
Activity
|
Activity
|
||||||
|> restrict_to_actor(actor.ap_id)
|
|> restrict_to_actor(actor.ap_id)
|
||||||
|> restrict_to_types(@timeline_types)
|
|> restrict_to_types(@timeline_types)
|
||||||
|> restrict_to_public(only_public)
|
|> restrict_to_public(only_public)
|
||||||
|> paginate(params)
|
|> paginate(params)
|
||||||
|> limit(^Map.get(params, "limit", 20))
|
|> limit(^Map.get(params, "limit", 20))
|
||||||
|> join_with_actors(actors)
|
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec home_timeline(user :: User.t(), params :: map(), actors :: boolean()) :: [
|
@spec home_timeline(user :: User.t(), params :: map()) :: [
|
||||||
Activity.t()
|
{Activity.t(), Actor.t()}
|
||||||
]
|
]
|
||||||
def home_timeline(user, params, actors \\ false) do
|
def home_timeline(user, params) do
|
||||||
user =
|
user =
|
||||||
case user.actor do
|
case user.actor do
|
||||||
%Ecto.Association.NotLoaded{} ->
|
%Ecto.Association.NotLoaded{} ->
|
||||||
|
@ -39,15 +37,27 @@ defmodule Clacks.Timeline do
|
||||||
end
|
end
|
||||||
|
|
||||||
Activity
|
Activity
|
||||||
|
|> join_with_actors()
|
||||||
|> where(
|
|> where(
|
||||||
[a],
|
[activity, actor],
|
||||||
fragment("?->>'actor'", a.data) == ^user.actor.ap_id or
|
fragment("?->>'actor'", activity.data) == ^user.actor.ap_id or
|
||||||
fragment("?->>'actor'", a.data) in ^user.actor.followers
|
^user.actor.ap_id in actor.followers
|
||||||
)
|
)
|
||||||
|> restrict_to_types(@timeline_types)
|
|> restrict_to_types(@timeline_types)
|
||||||
|> paginate(params)
|
|> paginate(params)
|
||||||
|> limit(^Map.get(params, "limit", 20))
|
|> 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()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -69,11 +79,9 @@ defmodule Clacks.Timeline do
|
||||||
|
|
||||||
defp restrict_to_public(query, false), do: query
|
defp restrict_to_public(query, false), do: query
|
||||||
|
|
||||||
defp join_with_actors(query, true) do
|
defp join_with_actors(query) do
|
||||||
query
|
query
|
||||||
|> join(:left, [o], a in Actor, on: a.ap_id == fragment("?->>'actor'", o.data))
|
|> join(:left, [o], a in Actor, on: a.ap_id == fragment("?->>'actor'", o.data))
|
||||||
|> select([o, a], {o, a})
|
|> select([o, a], {o, a})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp join_with_actors(query, false), do: query
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,7 @@ defmodule ClacksWeb.FrontendController do
|
||||||
alias Clacks.{Actor, User, Timeline, Repo, ActivityPub, Activity, Object}
|
alias Clacks.{Actor, User, Timeline, Repo, ActivityPub, Activity, Object}
|
||||||
alias ClacksWeb.Router.Helpers, as: Routes
|
alias ClacksWeb.Router.Helpers, as: Routes
|
||||||
alias ClacksWeb.Endpoint
|
alias ClacksWeb.Endpoint
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
@public "https://www.w3.org/ns/activitystreams#Public"
|
@public "https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
|
||||||
|
@ -12,18 +13,20 @@ defmodule ClacksWeb.FrontendController do
|
||||||
render(conn, "home.html", %{
|
render(conn, "home.html", %{
|
||||||
user: user,
|
user: user,
|
||||||
actor: user.actor,
|
actor: user.actor,
|
||||||
statuses_with_authors: Timeline.home_timeline(user, params, true)
|
statuses_with_authors: Timeline.home_timeline(user, params)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def index(conn, params) do
|
def index(conn, params) do
|
||||||
Application.get_env(:clacks, :frontend, %{})
|
Application.get_env(:clacks, :frontend, %{})
|
||||||
|> Keyword.get(:unauthenticated_homepage, :public_timeline)
|
|> Keyword.get(:unauthenticated_homepage, :local_timeline)
|
||||||
|> index(conn, params)
|
|> index(conn, params)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp index(:public_timeline, conn, params) do
|
defp index(:local_timeline, conn, params) do
|
||||||
# tood: show public timeline
|
render(conn, "local_timeline.html", %{
|
||||||
|
statuses_with_authors: Timeline.local_timeline(params)
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp index({:profile, nickname}, conn, params) do
|
defp index({:profile, nickname}, conn, params) do
|
||||||
|
@ -36,8 +39,8 @@ defmodule ClacksWeb.FrontendController do
|
||||||
})
|
})
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
# otherwise show public timeline
|
# otherwise show local timeline
|
||||||
index(:public_timeline, conn)
|
index(:local_timeline, conn)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -53,7 +56,7 @@ defmodule ClacksWeb.FrontendController do
|
||||||
data:
|
data:
|
||||||
%{
|
%{
|
||||||
"type" => "Create",
|
"type" => "Create",
|
||||||
"object" => %{"type" => "Note", "attributedTo" => author_id} = note
|
"object" => %{"type" => "Note", "attributedTo" => author_id}
|
||||||
} = data
|
} = data
|
||||||
} = activity <- Activity.get(id),
|
} = activity <- Activity.get(id),
|
||||||
%Actor{} = author <- Actor.get_by_ap_id(author_id) do
|
%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
|
case User.get_by_username(username) do
|
||||||
nil ->
|
nil ->
|
||||||
put_status(conn, 404)
|
resp(conn, 404, "Not Found")
|
||||||
|
|
||||||
user ->
|
user ->
|
||||||
user = Repo.preload(user, :actor)
|
user = Repo.preload(user, :actor)
|
||||||
|
@ -127,11 +130,127 @@ defmodule ClacksWeb.FrontendController do
|
||||||
render(conn, "profile.html", %{
|
render(conn, "profile.html", %{
|
||||||
current_user: current_user,
|
current_user: current_user,
|
||||||
actor: user.actor,
|
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
|
||||||
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
|
def post_status(conn, %{"content" => content} = params) do
|
||||||
current_user = conn.assigns[:user] |> Repo.preload(:actor)
|
current_user = conn.assigns[:user] |> Repo.preload(:actor)
|
||||||
|
|
||||||
|
@ -140,12 +259,7 @@ defmodule ClacksWeb.FrontendController do
|
||||||
{:ok, object} = Repo.insert(note_changeset)
|
{:ok, object} = Repo.insert(note_changeset)
|
||||||
|
|
||||||
create = ActivityPub.create(note)
|
create = ActivityPub.create(note)
|
||||||
create_changeset = Activity.changeset_for_creating(create, true)
|
{:ok, activity} = ActivityPub.Helper.save_and_federate(create, current_user.actor)
|
||||||
{:ok, activity} = Repo.insert(create_changeset)
|
|
||||||
|
|
||||||
%{id: activity.id, actor_id: current_user.actor.id}
|
|
||||||
|> Clacks.Worker.Federate.new()
|
|
||||||
|> Oban.insert()
|
|
||||||
|
|
||||||
path = Map.get(params, "continue", Routes.frontend_path(Endpoint, :status, activity.id))
|
path = Map.get(params, "continue", Routes.frontend_path(Endpoint, :status, activity.id))
|
||||||
redirect(conn, to: path)
|
redirect(conn, to: path)
|
||||||
|
@ -177,4 +291,32 @@ defmodule ClacksWeb.FrontendController do
|
||||||
defp note_for_posting(current_user, %{"content" => content}) do
|
defp note_for_posting(current_user, %{"content" => content}) do
|
||||||
ActivityPub.note(current_user.actor.ap_id, content)
|
ActivityPub.note(current_user.actor.ap_id, content)
|
||||||
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()) :: 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
|
end
|
||||||
|
|
|
@ -58,8 +58,11 @@ defmodule ClacksWeb.Router do
|
||||||
pipe_through :browser_authenticated
|
pipe_through :browser_authenticated
|
||||||
|
|
||||||
post "/post", FrontendController, :post_status
|
post "/post", FrontendController, :post_status
|
||||||
|
|
||||||
get "/status/:id/reply", FrontendController, :reply
|
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
|
end
|
||||||
|
|
||||||
scope "/", ClacksWeb do
|
scope "/", ClacksWeb do
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<div class="status-meta">
|
<div class="status-meta">
|
||||||
<h2 class="status-author-nickname">
|
<h2 class="status-author-nickname">
|
||||||
<a href="<%= @author.ap_id %>">
|
<a href="<%= local_actor_link(@author) %>">
|
||||||
<%= @author.data["preferredUsername"] %>
|
<%= @author.data["preferredUsername"] %>
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
</h3>
|
</h3>
|
||||||
<p class="status-meta-right">
|
<p class="status-meta-right">
|
||||||
<span><%= display_timestamp(@note["published"]) %></span>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-content">
|
<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>
|
<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>
|
<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})) %>
|
<%= 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">
|
<nav role="navigation">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/"><%= instance_name() %></a></li>
|
<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>
|
||||||
<ul>
|
<ul>
|
||||||
<%= if @conn.assigns[:user] do %>
|
<%= if @conn.assigns[:user] do %>
|
||||||
<li>
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<%= form_for @conn, Routes.login_path(@conn, :logout), [method: :post], fn f -> %>
|
<%= form_for @conn, Routes.login_path(@conn, :logout), [method: :post], fn f -> %>
|
||||||
|
@ -34,8 +37,12 @@
|
||||||
</section>
|
</section>
|
||||||
</header>
|
</header>
|
||||||
<main role="main" class="container">
|
<main role="main" class="container">
|
||||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
<%= if get_flash(@conn, :info) do %>
|
||||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
<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 %>
|
<%= render @view_module, @view_template, assigns %>
|
||||||
</main>
|
</main>
|
||||||
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
|
<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 %>
|
<%= if @continue do %>
|
||||||
<input type="hidden" name="continue" value="<%= @continue %>">
|
<input type="hidden" name="continue" value="<%= @continue %>">
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<label for="username">Username</label>
|
<div class="input-group">
|
||||||
<input id="username" type="text" name="username">
|
<label for="username">Username:</label>
|
||||||
<br>
|
<input id="username" type="text" name="username">
|
||||||
<label for="password">Password</label>
|
</div>
|
||||||
<input id="password" type="password" name="password">
|
<div class="input-group">
|
||||||
<br>
|
<label for="password">Password:</label>
|
||||||
|
<input id="password" type="password" name="password">
|
||||||
|
</div>
|
||||||
<%= submit "Log In" %>
|
<%= submit "Log In" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
defmodule ClacksWeb.FrontendView do
|
defmodule ClacksWeb.FrontendView do
|
||||||
use ClacksWeb, :view
|
use ClacksWeb, :view
|
||||||
alias Clacks.{Actor, Activity}
|
alias Clacks.{Actor, Activity}
|
||||||
|
alias ClacksWeb.Router.Helpers, as: Routes
|
||||||
|
alias ClacksWeb.Endpoint
|
||||||
|
|
||||||
@spec display_username(actor :: Actor.t()) :: String.t()
|
@spec display_username(actor :: Actor.t()) :: String.t()
|
||||||
|
|
||||||
|
@ -13,6 +15,13 @@ defmodule ClacksWeb.FrontendView do
|
||||||
"@" <> name <> "@" <> host
|
"@" <> name <> "@" <> host
|
||||||
end
|
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
|
def display_timestamp(str) when is_binary(str) do
|
||||||
display_timestamp(Timex.parse!(str, "{ISO:Extended}"))
|
display_timestamp(Timex.parse!(str, "{ISO:Extended}"))
|
||||||
end
|
end
|
||||||
|
@ -42,6 +51,9 @@ defmodule ClacksWeb.FrontendView do
|
||||||
end
|
end
|
||||||
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
|
def prev_page_path(conn, activities) do
|
||||||
if Map.has_key?(conn.query_params, "max_id") do
|
if Map.has_key?(conn.query_params, "max_id") do
|
||||||
Phoenix.Controller.current_path(conn, %{
|
Phoenix.Controller.current_path(conn, %{
|
||||||
|
@ -52,6 +64,9 @@ defmodule ClacksWeb.FrontendView do
|
||||||
end
|
end
|
||||||
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
|
def next_page_path(conn, activities) do
|
||||||
if length(activities) < 20 do
|
if length(activities) < 20 do
|
||||||
nil
|
nil
|
||||||
|
|
Loading…
Reference in New Issue