|
|
|
@ -1,26 +1,35 @@
|
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
@spec post_status(
|
|
|
|
|
author :: User.t(),
|
|
|
|
|
content :: String.t(),
|
|
|
|
|
content_type :: String.t(),
|
|
|
|
|
in_reply_to :: String.t() | Activity.t() | nil
|
|
|
|
|
) :: {: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
|
|
|
|
|
nil ->
|
|
|
|
|
{:error, "Could find post to reply to with AP ID '#{in_reply_to_ap_id}'"}
|
|
|
|
|
|
|
|
|
|
in_reply_to ->
|
|
|
|
|
post_status(author, content, in_reply_to)
|
|
|
|
|
post_status(author, content, content_type, in_reply_to)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def post_status(author, content, in_reply_to) do
|
|
|
|
|
note = note_for_posting(author, content, in_reply_to)
|
|
|
|
|
def post_status(author, content, content_type, in_reply_to) do
|
|
|
|
|
{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)
|
|
|
|
|
{:ok, _object} = Repo.insert(note_changeset)
|
|
|
|
|
|
|
|
|
@ -35,18 +44,48 @@ defmodule Clacks.UserActionsHelper do
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp note_for_posting(author, content, %Activity{
|
|
|
|
|
data: %{"id" => in_reply_to_ap_id, "context" => context, "actor" => in_reply_to_actor}
|
|
|
|
|
}) do
|
|
|
|
|
to = [in_reply_to_actor, @public]
|
|
|
|
|
@spec get_addressed(String.t(), [{String.t(), Actor.t()}], String.t() | nil) ::
|
|
|
|
|
{[String.t()], [String.t()]}
|
|
|
|
|
defp get_addressed(_author, mentions, in_reply_to_actor) do
|
|
|
|
|
to = [@public | Enum.map(mentions, fn {_, actor} -> actor.ap_id end)]
|
|
|
|
|
# todo: followers
|
|
|
|
|
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(
|
|
|
|
|
author.actor.ap_id,
|
|
|
|
|
content,
|
|
|
|
|
context,
|
|
|
|
|
in_reply_to_ap_id,
|
|
|
|
|
mentions,
|
|
|
|
|
nil,
|
|
|
|
|
DateTime.utc_now(),
|
|
|
|
|
to,
|
|
|
|
@ -54,7 +93,76 @@ defmodule Clacks.UserActionsHelper do
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp note_for_posting(author, content, _in_reply_to) do
|
|
|
|
|
ActivityPub.note(author.actor.ap_id, content)
|
|
|
|
|
@spec convert_to_html(String.t(), String.t()) :: String.t()
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|