Add mentioning actors
This commit is contained in:
parent
1e984ba30e
commit
90a653d059
|
@ -33,6 +33,7 @@ defmodule Clacks.ActivityPub do
|
||||||
html :: String.t(),
|
html :: String.t(),
|
||||||
context :: String.t() | nil,
|
context :: String.t() | nil,
|
||||||
in_reply_to :: String.t() | nil,
|
in_reply_to :: String.t() | nil,
|
||||||
|
mentions :: [{String.t(), Clacks.Actor.t()}],
|
||||||
id :: String.t() | nil,
|
id :: String.t() | nil,
|
||||||
published :: DateTime.t(),
|
published :: DateTime.t(),
|
||||||
to :: [String.t()],
|
to :: [String.t()],
|
||||||
|
@ -43,6 +44,7 @@ defmodule Clacks.ActivityPub do
|
||||||
html,
|
html,
|
||||||
context \\ nil,
|
context \\ nil,
|
||||||
in_reply_to \\ nil,
|
in_reply_to \\ nil,
|
||||||
|
mentions \\ [],
|
||||||
id \\ nil,
|
id \\ nil,
|
||||||
published \\ DateTime.utc_now(),
|
published \\ DateTime.utc_now(),
|
||||||
to \\ [@public],
|
to \\ [@public],
|
||||||
|
@ -64,7 +66,15 @@ defmodule Clacks.ActivityPub do
|
||||||
"conversation" => context,
|
"conversation" => context,
|
||||||
"context" => context,
|
"context" => context,
|
||||||
"inReplyTo" => in_reply_to,
|
"inReplyTo" => in_reply_to,
|
||||||
"published" => published |> DateTime.to_iso8601()
|
"published" => published |> DateTime.to_iso8601(),
|
||||||
|
"tag" =>
|
||||||
|
Enum.map(mentions, fn {name, actor} ->
|
||||||
|
%{
|
||||||
|
"href" => actor.ap_id,
|
||||||
|
"name" => name,
|
||||||
|
"type" => "Mention"
|
||||||
|
}
|
||||||
|
end)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,35 @@
|
||||||
defmodule Clacks.UserActionsHelper do
|
defmodule Clacks.UserActionsHelper do
|
||||||
alias Clacks.{User, Repo, Activity, Object, ActivityPub}
|
alias Clacks.{User, Repo, Activity, Object, ActivityPub, Actor}
|
||||||
|
|
||||||
@public "https://www.w3.org/ns/activitystreams#Public"
|
@public "https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
|
||||||
@spec post_status(
|
@spec post_status(
|
||||||
author :: User.t(),
|
author :: User.t(),
|
||||||
content :: String.t(),
|
content :: String.t(),
|
||||||
|
content_type :: String.t(),
|
||||||
in_reply_to :: String.t() | Activity.t() | nil
|
in_reply_to :: String.t() | Activity.t() | nil
|
||||||
) :: {:ok, Activity.t()} | {:error, any()}
|
) :: {:ok, Activity.t()} | {:error, any()}
|
||||||
|
|
||||||
def post_status(author, content, in_reply_to_ap_id) when is_binary(in_reply_to_ap_id) do
|
def post_status(_, _, content_type, _)
|
||||||
|
when not (content_type in ["text/plain", "text/markdown", "text/html"]) do
|
||||||
|
{:error, "invalid content type, must be text/plain, text/markdown, or text/html"}
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_status(author, content, content_type, in_reply_to_ap_id)
|
||||||
|
when is_binary(in_reply_to_ap_id) do
|
||||||
case Activity.get_by_ap_id(in_reply_to_ap_id) do
|
case Activity.get_by_ap_id(in_reply_to_ap_id) do
|
||||||
nil ->
|
nil ->
|
||||||
{:error, "Could find post to reply to with AP ID '#{in_reply_to_ap_id}'"}
|
{:error, "Could find post to reply to with AP ID '#{in_reply_to_ap_id}'"}
|
||||||
|
|
||||||
in_reply_to ->
|
in_reply_to ->
|
||||||
post_status(author, content, in_reply_to)
|
post_status(author, content, content_type, in_reply_to)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_status(author, content, in_reply_to) do
|
def post_status(author, content, content_type, in_reply_to) do
|
||||||
note = note_for_posting(author, content, in_reply_to)
|
{content, mentions} = convert_to_html(content, content_type)
|
||||||
|
|
||||||
|
note = note_for_posting(author, content, mentions, in_reply_to)
|
||||||
note_changeset = Object.changeset_for_creating(note)
|
note_changeset = Object.changeset_for_creating(note)
|
||||||
{:ok, _object} = Repo.insert(note_changeset)
|
{:ok, _object} = Repo.insert(note_changeset)
|
||||||
|
|
||||||
|
@ -35,18 +44,48 @@ defmodule Clacks.UserActionsHelper do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp note_for_posting(author, content, %Activity{
|
@spec get_addressed(String.t(), [{String.t(), Actor.t()}], String.t() | nil) ::
|
||||||
data: %{"id" => in_reply_to_ap_id, "context" => context, "actor" => in_reply_to_actor}
|
{[String.t()], [String.t()]}
|
||||||
}) do
|
defp get_addressed(_author, mentions, in_reply_to_actor) do
|
||||||
to = [in_reply_to_actor, @public]
|
to = [@public | Enum.map(mentions, fn {_, actor} -> actor.ap_id end)]
|
||||||
# todo: followers
|
# todo: followers
|
||||||
cc = []
|
cc = []
|
||||||
|
|
||||||
|
to =
|
||||||
|
case in_reply_to_actor do
|
||||||
|
nil -> to
|
||||||
|
in_reply_to_actor -> [in_reply_to_actor | to]
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
Enum.uniq(to),
|
||||||
|
Enum.uniq(cc)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec note_for_posting(User.t(), String.t(), [{String.t(), Actor.t()}], Activity.t() | nil) ::
|
||||||
|
map()
|
||||||
|
|
||||||
|
defp note_for_posting(author, content, mentions, in_reply_to) do
|
||||||
|
{context, in_reply_to_ap_id, in_reply_to_actor} =
|
||||||
|
case in_reply_to do
|
||||||
|
nil ->
|
||||||
|
{nil, nil, nil}
|
||||||
|
|
||||||
|
%Activity{
|
||||||
|
data: %{"id" => in_reply_to_ap_id, "context" => context, "actor" => in_reply_to_actor}
|
||||||
|
} ->
|
||||||
|
{context, in_reply_to_ap_id, in_reply_to_actor}
|
||||||
|
end
|
||||||
|
|
||||||
|
{to, cc} = get_addressed(author, mentions, in_reply_to_actor)
|
||||||
|
|
||||||
ActivityPub.note(
|
ActivityPub.note(
|
||||||
author.actor.ap_id,
|
author.actor.ap_id,
|
||||||
content,
|
content,
|
||||||
context,
|
context,
|
||||||
in_reply_to_ap_id,
|
in_reply_to_ap_id,
|
||||||
|
mentions,
|
||||||
nil,
|
nil,
|
||||||
DateTime.utc_now(),
|
DateTime.utc_now(),
|
||||||
to,
|
to,
|
||||||
|
@ -54,7 +93,76 @@ defmodule Clacks.UserActionsHelper do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp note_for_posting(author, content, _in_reply_to) do
|
@spec convert_to_html(String.t(), String.t()) :: String.t()
|
||||||
ActivityPub.note(author.actor.ap_id, content)
|
|
||||||
|
defp convert_to_html(content, "text/html") do
|
||||||
|
content
|
||||||
|
|> replace_mentions()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp convert_to_html(content, "text/markdown") do
|
||||||
|
content
|
||||||
|
|> Earmark.as_html!()
|
||||||
|
|> replace_mentions()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp convert_to_html(content, "text/plain") do
|
||||||
|
{content, mentions} =
|
||||||
|
content
|
||||||
|
|> HtmlEntities.encode()
|
||||||
|
|> replace_links()
|
||||||
|
|> replace_mentions()
|
||||||
|
|
||||||
|
{String.replace(content, ["\r\n", "\n"], "<br>"), mentions}
|
||||||
|
end
|
||||||
|
|
||||||
|
@link_regex ~r/\bhttps?\S+\b/i
|
||||||
|
|
||||||
|
defp replace_links(content) do
|
||||||
|
Regex.replace(@link_regex, content, "<a href=\"\\1\">\\1</a>")
|
||||||
|
end
|
||||||
|
|
||||||
|
@mention_regex ~r/@(([a-z0-9_]+)(?:@[a-z0-9_\-.]+)?)/i
|
||||||
|
|
||||||
|
@spec replace_mentions(String.t()) :: {String.t(), [{String.t(), Actor.t()}]}
|
||||||
|
|
||||||
|
defp replace_mentions(content) do
|
||||||
|
Regex.scan(@mention_regex, content, return: :index)
|
||||||
|
|> Enum.reverse()
|
||||||
|
|> Enum.reduce({content, []}, fn [
|
||||||
|
{match_start, match_length},
|
||||||
|
{nickname_start, nickname_length},
|
||||||
|
{username_start, username_length}
|
||||||
|
],
|
||||||
|
{content, mentions} ->
|
||||||
|
nickname = String.slice(content, nickname_start, nickname_length)
|
||||||
|
|
||||||
|
Actor.get_by_nickname(nickname)
|
||||||
|
|> case do
|
||||||
|
%Actor{} = actor ->
|
||||||
|
actor
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
# todo: fetch by webfinger
|
||||||
|
raise "unimplemented"
|
||||||
|
end
|
||||||
|
|> case do
|
||||||
|
nil ->
|
||||||
|
{content, mentions}
|
||||||
|
|
||||||
|
actor ->
|
||||||
|
username = String.slice(content, username_start, username_length)
|
||||||
|
|
||||||
|
html =
|
||||||
|
"<span class=\"h-card\"><a href=\"#{actor.ap_id}\" class=\"u-url\">@#{username}</a></span>"
|
||||||
|
|
||||||
|
new_content =
|
||||||
|
String.slice(content, 0, match_start) <>
|
||||||
|
html <>
|
||||||
|
String.slice(content, (match_start + match_length)..String.length(content))
|
||||||
|
|
||||||
|
{new_content, [{nickname, actor} | mentions]}
|
||||||
|
end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -254,7 +254,12 @@ defmodule ClacksWeb.FrontendController do
|
||||||
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)
|
||||||
|
|
||||||
UserActionsHelper.post_status(current_user, content, Map.get(params, "in_reply_to"))
|
UserActionsHelper.post_status(
|
||||||
|
current_user,
|
||||||
|
content,
|
||||||
|
"text/plain",
|
||||||
|
Map.get(params, "in_reply_to")
|
||||||
|
)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, activity} ->
|
{:ok, activity} ->
|
||||||
path = Map.get(params, "continue", Routes.frontend_path(Endpoint, :status, activity.id))
|
path = Map.get(params, "continue", Routes.frontend_path(Endpoint, :status, activity.id))
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-content e-content">
|
<div class="status-content e-content">
|
||||||
<%= @note["content"] %>
|
<%= raw(@note["content"]) %>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-actions">
|
<div class="status-actions">
|
||||||
<a href="<%= Routes.frontend_path(@conn, :reply, @status.id) %>">Reply</a>
|
<a href="<%= Routes.frontend_path(@conn, :reply, @status.id) %>">Reply</a>
|
||||||
|
|
4
mix.exs
4
mix.exs
|
@ -54,7 +54,9 @@ defmodule Clacks.MixProject do
|
||||||
{:fast_sanitize, "~> 0.1.7"},
|
{:fast_sanitize, "~> 0.1.7"},
|
||||||
{:fast_html, "~> 1.0.3"},
|
{:fast_html, "~> 1.0.3"},
|
||||||
{:dialyxir, "~> 1.0", only: [:dev], runtime: false},
|
{:dialyxir, "~> 1.0", only: [:dev], runtime: false},
|
||||||
{:floki, "~> 0.26.0"}
|
{:floki, "~> 0.26.0"},
|
||||||
|
{:earmark, "~> 1.4.4"},
|
||||||
|
{:html_entities, "~> 0.5.1"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
1
mix.lock
1
mix.lock
|
@ -11,6 +11,7 @@
|
||||||
"db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "5a0e8c1c722dbcd31c0cbd1906b1d1074c863d335c295e4b994849b65a1fbe47"},
|
"db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "5a0e8c1c722dbcd31c0cbd1906b1d1074c863d335c295e4b994849b65a1fbe47"},
|
||||||
"decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm", "52694ef56e60108e5012f8af9673874c66ed58ac1c4fae9b5b7ded31786663f5"},
|
"decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm", "52694ef56e60108e5012f8af9673874c66ed58ac1c4fae9b5b7ded31786663f5"},
|
||||||
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"},
|
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"},
|
||||||
|
"earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"},
|
||||||
"ecto": {:hex, :ecto, "3.2.1", "a0f9af0fb50b19d3bb6237e512ac0ba56ea222c2bbea92e7c6c94897932c76ba", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "e1a1a1d72514b880d6bdd9fe9423d13a800ec1fb041c7239d885e5407b1fabce"},
|
"ecto": {:hex, :ecto, "3.2.1", "a0f9af0fb50b19d3bb6237e512ac0ba56ea222c2bbea92e7c6c94897932c76ba", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "e1a1a1d72514b880d6bdd9fe9423d13a800ec1fb041c7239d885e5407b1fabce"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a2e23cf761668126252418cae07eff7967ad0152fbc5e2d0dc3de487a5ec774c"},
|
"ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a2e23cf761668126252418cae07eff7967ad0152fbc5e2d0dc3de487a5ec774c"},
|
||||||
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
|
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
|
||||||
|
|
Loading…
Reference in New Issue