Pleroma/lib/pleroma/web/twitter_api/views/activity_view.ex

360 lines
11 KiB
Elixir
Raw Normal View History

# Pleroma: A lightweight social networking server
2018-12-31 16:41:47 +01:00
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
2018-03-27 22:24:19 +02:00
defmodule Pleroma.Web.TwitterAPI.ActivityView do
use Pleroma.Web, :view
alias Pleroma.Activity
2018-03-27 22:24:19 +02:00
alias Pleroma.Formatter
alias Pleroma.HTML
2019-02-09 16:16:26 +01:00
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
2018-03-27 22:24:19 +02:00
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView
2019-02-09 16:16:26 +01:00
alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
2019-02-09 16:16:26 +01:00
alias Pleroma.Web.TwitterAPI.UserView
2018-03-27 22:24:19 +02:00
2018-04-02 14:46:56 +02:00
import Ecto.Query
require Logger
2018-04-02 14:46:56 +02:00
defp query_context_ids([]), do: []
2018-04-02 16:27:36 +02:00
2018-04-02 14:46:56 +02:00
defp query_context_ids(contexts) do
2018-04-02 16:27:36 +02:00
query = from(o in Object, where: fragment("(?)->>'id' = ANY(?)", o.data, ^contexts))
Repo.all(query)
end
defp query_users([]), do: []
defp query_users(user_ids) do
query = from(user in User, where: user.ap_id in ^user_ids)
2018-04-02 14:46:56 +02:00
Repo.all(query)
end
defp collect_context_ids(activities) do
2018-05-04 23:16:02 +02:00
_contexts =
2018-04-02 16:27:36 +02:00
activities
|> Enum.reject(& &1.data["context_id"])
|> Enum.map(fn %{data: data} ->
data["context"]
end)
|> Enum.filter(& &1)
|> query_context_ids()
|> Enum.reduce(%{}, fn %{data: %{"id" => ap_id}, id: id}, acc ->
Map.put(acc, ap_id, id)
end)
end
defp collect_users(activities) do
activities
|> Enum.map(fn activity ->
case activity.data do
data = %{"type" => "Follow"} ->
[data["actor"], data["object"]]
data ->
[data["actor"]]
end ++ activity.recipients
2018-04-02 14:46:56 +02:00
end)
2018-04-02 16:27:36 +02:00
|> List.flatten()
|> Enum.uniq()
|> query_users()
|> Enum.reduce(%{}, fn user, acc ->
Map.put(acc, user.ap_id, user)
2018-04-02 14:46:56 +02:00
end)
end
2018-04-02 16:27:36 +02:00
defp get_context_id(%{data: %{"context_id" => context_id}}, _) when not is_nil(context_id),
do: context_id
defp get_context_id(%{data: %{"context" => nil}}, _), do: nil
2018-04-02 16:27:36 +02:00
2018-04-02 14:46:56 +02:00
defp get_context_id(%{data: %{"context" => context}}, options) do
cond do
id = options[:context_ids][context] -> id
true -> Utils.context_to_conversation_id(context)
2018-04-02 14:46:56 +02:00
end
end
2018-04-02 16:27:36 +02:00
defp get_context_id(_, _), do: nil
2018-04-02 14:46:56 +02:00
2018-04-02 16:27:36 +02:00
defp get_user(ap_id, opts) do
cond do
user = opts[:users][ap_id] ->
user
String.ends_with?(ap_id, "/followers") ->
nil
ap_id == "https://www.w3.org/ns/activitystreams#Public" ->
nil
user = User.get_cached_by_ap_id(ap_id) ->
user
user = User.get_by_guessed_nickname(ap_id) ->
user
2018-04-02 16:27:36 +02:00
true ->
User.error_user(ap_id)
2018-04-02 16:27:36 +02:00
end
end
def render("index.json", opts) do
2018-04-02 14:46:56 +02:00
context_ids = collect_context_ids(opts.activities)
2018-04-02 16:27:36 +02:00
users = collect_users(opts.activities)
opts =
opts
|> Map.put(:context_ids, context_ids)
|> Map.put(:users, users)
2018-04-02 14:46:56 +02:00
safe_render_many(
opts.activities,
ActivityView,
"activity.json",
opts
)
end
def render("activity.json", %{activity: %{data: %{"type" => "Delete"}} = activity} = opts) do
2018-04-02 16:27:36 +02:00
user = get_user(activity.data["actor"], opts)
created_at = activity.data["published"] |> Utils.date_to_asctime()
%{
"id" => activity.id,
"uri" => activity.data["object"],
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
"attentions" => [],
"statusnet_html" => "deleted notice {{tag",
"text" => "deleted notice {{tag",
"is_local" => activity.local,
"is_post_verb" => false,
"created_at" => created_at,
"in_reply_to_status_id" => nil,
"external_url" => activity.data["id"],
"activity_type" => "delete"
}
end
2018-03-30 15:17:13 +02:00
def render("activity.json", %{activity: %{data: %{"type" => "Follow"}} = activity} = opts) do
2018-04-02 16:27:36 +02:00
user = get_user(activity.data["actor"], opts)
2018-03-30 15:17:13 +02:00
created_at = activity.data["published"] || DateTime.to_iso8601(activity.inserted_at)
created_at = created_at |> Utils.date_to_asctime()
2018-04-02 16:27:36 +02:00
followed = get_user(activity.data["object"], opts)
2018-03-30 15:17:13 +02:00
text = "#{user.nickname} started following #{followed.nickname}"
%{
"id" => activity.id,
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
"attentions" => [],
"statusnet_html" => text,
"text" => text,
"is_local" => activity.local,
"is_post_verb" => false,
"created_at" => created_at,
"in_reply_to_status_id" => nil,
"external_url" => activity.data["id"],
"activity_type" => "follow"
}
end
2018-03-30 14:46:36 +02:00
def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activity} = opts) do
2018-04-02 16:27:36 +02:00
user = get_user(activity.data["actor"], opts)
2018-03-30 15:01:53 +02:00
created_at = activity.data["published"] |> Utils.date_to_asctime()
announced_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
2018-03-30 14:46:36 +02:00
text = "#{user.nickname} repeated a status."
2018-03-30 14:46:36 +02:00
retweeted_status = render("activity.json", Map.merge(opts, %{activity: announced_activity}))
%{
"id" => activity.id,
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
"statusnet_html" => text,
"text" => text,
"is_local" => activity.local,
"is_post_verb" => false,
"uri" => "tag:#{activity.data["id"]}:objectType=note",
"created_at" => created_at,
"retweeted_status" => retweeted_status,
2018-04-02 14:46:56 +02:00
"statusnet_conversation_id" => get_context_id(announced_activity, opts),
2018-03-30 14:46:36 +02:00
"external_url" => activity.data["id"],
"activity_type" => "repeat"
}
end
def render("activity.json", %{activity: %{data: %{"type" => "Like"}} = activity} = opts) do
2018-04-02 16:27:36 +02:00
user = get_user(activity.data["actor"], opts)
liked_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
2018-08-27 16:07:26 +02:00
liked_activity_id = if liked_activity, do: liked_activity.id, else: nil
2018-03-30 15:01:53 +02:00
created_at =
activity.data["published"]
|> Utils.date_to_asctime()
text = "#{user.nickname} favorited a status."
favorited_status =
if liked_activity,
do: render("activity.json", Map.merge(opts, %{activity: liked_activity})),
else: nil
2018-08-27 16:07:26 +02:00
%{
"id" => activity.id,
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
"statusnet_html" => text,
"text" => text,
"is_local" => activity.local,
"is_post_verb" => false,
"uri" => "tag:#{activity.data["id"]}:objectType=Favourite",
"created_at" => created_at,
"favorited_status" => favorited_status,
2018-08-27 16:07:26 +02:00
"in_reply_to_status_id" => liked_activity_id,
"external_url" => activity.data["id"],
"activity_type" => "like"
2018-08-27 16:07:26 +02:00
}
end
2018-03-30 15:01:53 +02:00
def render(
"activity.json",
2018-12-02 00:08:23 +01:00
%{activity: %{data: %{"type" => "Create", "object" => object_id}} = activity} = opts
2018-03-30 15:01:53 +02:00
) do
2018-04-02 16:27:36 +02:00
user = get_user(activity.data["actor"], opts)
2018-03-27 22:24:19 +02:00
2018-12-02 00:08:23 +01:00
object = Object.normalize(object_id)
created_at = object.data["published"] |> Utils.date_to_asctime()
like_count = object.data["like_count"] || 0
announcement_count = object.data["announcement_count"] || 0
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
repeated = opts[:for] && opts[:for].ap_id in (object.data["announcements"] || [])
pinned = activity.id in user.info.pinned_activities
2018-03-27 22:24:19 +02:00
2018-03-30 15:01:53 +02:00
attentions =
[]
|> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_mentioned_recipients(activity)
2018-04-02 16:27:36 +02:00
|> Enum.map(fn ap_id -> get_user(ap_id, opts) end)
2018-03-30 15:01:53 +02:00
|> Enum.filter(& &1)
|> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
2018-03-27 22:24:19 +02:00
2018-04-02 14:46:56 +02:00
conversation_id = get_context_id(activity, opts)
2018-03-27 22:24:19 +02:00
tags = object.data["tag"] || []
possibly_sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
2018-03-27 22:24:19 +02:00
tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags
2018-12-02 00:08:23 +01:00
{summary, content} = render_content(object.data)
2018-03-30 15:01:53 +02:00
html =
2018-12-14 10:41:55 +01:00
content
|> HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
"twitterapi:content"
)
2018-12-02 00:08:23 +01:00
|> Formatter.emojify(object.data["emoji"])
2018-03-27 22:24:19 +02:00
2018-12-17 20:41:36 +01:00
text =
if content do
content
|> String.replace(~r/<br\s?\/?>/, "\n")
|> HTML.get_cached_stripped_html_for_activity(activity, "twitterapi:content")
2019-03-22 22:10:50 +01:00
else
""
end
2018-12-17 20:41:36 +01:00
reply_parent = Activity.get_in_reply_to_activity(activity)
reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor)
summary = HTML.strip_tags(summary)
card =
StatusView.render(
"card.json",
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
)
2018-03-27 22:24:19 +02:00
%{
"id" => activity.id,
"uri" => object.data["id"],
2018-03-27 22:24:19 +02:00
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
"statusnet_html" => html,
2018-12-17 20:41:36 +01:00
"text" => text,
2018-03-27 22:24:19 +02:00
"is_local" => activity.local,
"is_post_verb" => true,
"created_at" => created_at,
2019-04-15 10:50:36 +02:00
"in_reply_to_status_id" => reply_parent && reply_parent.id,
"in_reply_to_screen_name" => reply_user && reply_user.nickname,
"in_reply_to_profileurl" => User.profile_url(reply_user),
"in_reply_to_ostatus_uri" => reply_user && reply_user.ap_id,
"in_reply_to_user_id" => reply_user && reply_user.id,
2018-03-27 22:24:19 +02:00
"statusnet_conversation_id" => conversation_id,
2018-12-02 00:08:23 +01:00
"attachments" => (object.data["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
2018-03-27 22:24:19 +02:00
"attentions" => attentions,
"fave_num" => like_count,
"repeat_num" => announcement_count,
"favorited" => !!favorited,
"repeated" => !!repeated,
"pinned" => pinned,
2018-12-02 00:08:23 +01:00
"external_url" => object.data["external_url"] || object.data["id"],
2018-03-27 22:24:19 +02:00
"tags" => tags,
"activity_type" => "post",
2018-05-20 18:01:24 +02:00
"possibly_sensitive" => possibly_sensitive,
"visibility" => StatusView.get_visibility(object),
"summary" => summary,
"summary_html" => summary |> Formatter.emojify(object.data["emoji"]),
"card" => card,
"muted" => CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user)
2018-03-27 22:24:19 +02:00
}
end
def render("activity.json", %{activity: unhandled_activity}) do
Logger.warn("#{__MODULE__} unhandled activity: #{inspect(unhandled_activity)}")
nil
end
def render_content(%{"type" => "Note"} = object) do
summary = object["summary"]
2018-07-12 18:12:54 +02:00
content =
if !!summary and summary != "" do
"<p>#{summary}</p>#{object["content"]}"
else
object["content"]
end
{summary, content}
end
def render_content(%{"type" => object_type} = object)
when object_type in ["Article", "Page", "Video"] do
summary = object["name"] || object["summary"]
2018-07-12 18:12:54 +02:00
content =
if !!summary and summary != "" and is_bitstring(object["url"]) do
"<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
else
object["content"]
end
{summary, content}
end
def render_content(object) do
summary = object["summary"] || "Unhandled activity type: #{object["type"]}"
content = "<p>#{summary}</p>#{object["content"]}"
{summary, content}
end
2018-03-27 22:24:19 +02:00
end