utils.ex 8.82 KB
Newer Older
1
# Pleroma: A lightweight social networking server
2
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 4
# SPDX-License-Identifier: AGPL-3.0-only

5
defmodule Pleroma.Web.CommonAPI.Utils do
Maksim's avatar
Maksim committed
6 7
  alias Calendar.Strftime
  alias Comeonin.Pbkdf2
Haelwenn's avatar
Haelwenn committed
8 9 10 11
  alias Pleroma.Activity
  alias Pleroma.Formatter
  alias Pleroma.Object
  alias Pleroma.Repo
Maksim's avatar
Maksim committed
12 13
  alias Pleroma.User
  alias Pleroma.Web
14
  alias Pleroma.Web.Endpoint
15
  alias Pleroma.Web.MediaProxy
lain's avatar
lain committed
16 17
  alias Pleroma.Web.ActivityPub.Utils

18 19
  # This is a hack for twidere.
  def get_by_id_or_ap_id(id) do
20
    activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)
lain's avatar
lain committed
21

22 23 24 25
    activity &&
      if activity.data["type"] == "Create" do
        activity
      else
26
        Activity.get_create_by_object_ap_id(activity.data["object"])
27
      end
28 29
  end

30 31
  def get_replied_to_activity(""), do: nil

32 33 34
  def get_replied_to_activity(id) when not is_nil(id) do
    Repo.get(Activity, id)
  end
lain's avatar
lain committed
35

36 37
  def get_replied_to_activity(_), do: nil

38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
  def attachments_from_ids(data) do
    if Map.has_key?(data, "descriptions") do
      attachments_from_ids_descs(data["media_ids"], data["descriptions"])
    else
      attachments_from_ids_no_descs(data["media_ids"])
    end
  end

  def attachments_from_ids_no_descs(ids) do
    Enum.map(ids || [], fn media_id ->
      Repo.get(Object, media_id).data
    end)
  end

  def attachments_from_ids_descs(ids, descs_str) do
53 54 55 56
    {_, descs} = Jason.decode(descs_str)

    Enum.map(ids || [], fn media_id ->
      Map.put(Repo.get(Object, media_id).data, "name", descs[media_id])
lain's avatar
lain committed
57 58 59
    end)
  end

60
  def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
lain's avatar
lain committed
61
    mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
62 63 64

    to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_users]
    cc = [user.follower_address]
lain's avatar
lain committed
65

66
    if inReplyTo do
67
      {Enum.uniq([inReplyTo.data["actor"] | to]), cc}
lain's avatar
lain committed
68
    else
69 70 71 72 73
      {to, cc}
    end
  end

  def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
74 75 76 77 78 79 80 81 82 83
    mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)

    to = [user.follower_address | mentioned_users]
    cc = ["https://www.w3.org/ns/activitystreams#Public"]

    if inReplyTo do
      {Enum.uniq([inReplyTo.data["actor"] | to]), cc}
    else
      {to, cc}
    end
84 85 86 87 88 89 90
  end

  def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do
    {to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "direct")
    {[user.follower_address | to], cc}
  end

91
  def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do
lain's avatar
lain committed
92 93
    mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)

94 95 96 97
    if inReplyTo do
      {Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
    else
      {mentioned_users, []}
lain's avatar
lain committed
98 99 100
    end
  end

101 102 103 104 105 106 107 108
  def make_content_html(
        status,
        mentions,
        attachments,
        tags,
        content_type,
        no_attachment_links \\ false
      ) do
109
    status
110
    |> format_input(mentions, tags, content_type)
111
    |> maybe_add_attachments(attachments, no_attachment_links)
112 113 114
  end

  def make_context(%Activity{data: %{"context" => context}}), do: context
lain's avatar
lain committed
115
  def make_context(_), do: Utils.generate_context_id()
116

117
  def maybe_add_attachments(text, _attachments, true = _no_links), do: text
lain's avatar
lain committed
118

119 120 121
  def maybe_add_attachments(text, attachments, _no_links) do
    add_attachments(text, attachments)
  end
lain's avatar
lain committed
122

lain's avatar
lain committed
123
  def add_attachments(text, attachments) do
lain's avatar
lain committed
124 125
    attachment_text =
      Enum.map(attachments, fn
126 127
        %{"url" => [%{"href" => href} | _]} = attachment ->
          name = attachment["name"] || URI.decode(Path.basename(href))
128
          href = MediaProxy.url(href)
lain's avatar
lain committed
129 130 131 132 133 134
          "<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>"

        _ ->
          ""
      end)

135
    Enum.join([text | attachment_text], "<br>")
lain's avatar
lain committed
136 137
  end

138 139
  def format_input(text, mentions, tags, format, options \\ [])

Maksim's avatar
Maksim committed
140 141 142
  @doc """
  Formatting text to plain text.
  """
143
  def format_input(text, mentions, tags, "text/plain", options) do
144
    text
145
    |> Formatter.html_escape("text/plain")
146
    |> String.replace(~r/\r?\n/, "<br>")
lain's avatar
lain committed
147 148
    |> (&{[], &1}).()
    |> Formatter.add_links()
149
    |> Formatter.add_user_links(mentions, options[:user_links] || [])
lain's avatar
lain committed
150
    |> Formatter.add_hashtag_links(tags)
lain's avatar
lain committed
151
    |> Formatter.finalize()
lain's avatar
lain committed
152 153
  end

Maksim's avatar
Maksim committed
154 155 156
  @doc """
  Formatting text to html.
  """
157
  def format_input(text, mentions, _tags, "text/html", options) do
158 159 160
    text
    |> Formatter.html_escape("text/html")
    |> (&{[], &1}).()
161
    |> Formatter.add_user_links(mentions, options[:user_links] || [])
162 163 164
    |> Formatter.finalize()
  end

Maksim's avatar
Maksim committed
165 166 167
  @doc """
  Formatting text to markdown.
  """
168
  def format_input(text, mentions, tags, "text/markdown", options) do
169
    text
Maksim's avatar
Maksim committed
170
    |> Formatter.mentions_escape(mentions)
171 172 173
    |> Earmark.as_html!()
    |> Formatter.html_escape("text/html")
    |> (&{[], &1}).()
174
    |> Formatter.add_user_links(mentions, options[:user_links] || [])
175
    |> Formatter.add_hashtag_links(tags)
176 177 178
    |> Formatter.finalize()
  end

lain's avatar
lain committed
179
  def add_tag_links(text, tags) do
lain's avatar
lain committed
180 181 182
    tags =
      tags
      |> Enum.sort_by(fn {tag, _} -> -String.length(tag) end)
lain's avatar
lain committed
183

lain's avatar
lain committed
184
    Enum.reduce(tags, text, fn {full, tag}, text ->
Maksim's avatar
Maksim committed
185
      url = "<a href='#{Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>"
lain's avatar
lain committed
186 187
      String.replace(text, full, url)
    end)
lain's avatar
lain committed
188 189
  end

lain's avatar
lain committed
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
  def make_note_data(
        actor,
        to,
        context,
        content_html,
        attachments,
        inReplyTo,
        tags,
        cw \\ nil,
        cc \\ []
      ) do
    object = %{
      "type" => "Note",
      "to" => to,
      "cc" => cc,
      "content" => content_html,
      "summary" => cw,
      "context" => context,
      "attachment" => attachments,
      "actor" => actor,
210
      "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
lain's avatar
lain committed
211
    }
lain's avatar
lain committed
212 213 214 215 216 217 218 219 220

    if inReplyTo do
      object
      |> Map.put("inReplyTo", inReplyTo.data["object"]["id"])
      |> Map.put("inReplyToStatusId", inReplyTo.id)
    else
      object
    end
  end
221 222 223 224 225 226 227 228 229 230

  def format_naive_asctime(date) do
    date |> DateTime.from_naive!("Etc/UTC") |> format_asctime
  end

  def format_asctime(date) do
    Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")
  end

  def date_to_asctime(date) do
lain's avatar
lain committed
231
    with {:ok, date, _offset} <- date |> DateTime.from_iso8601() do
232
      format_asctime(date)
lain's avatar
lain committed
233 234
    else
      _e ->
235 236 237
        ""
    end
  end
238

239 240
  def to_masto_date(%NaiveDateTime{} = date) do
    date
lain's avatar
lain committed
241
    |> NaiveDateTime.to_iso8601()
242 243 244 245 246 247
    |> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
  end

  def to_masto_date(date) do
    try do
      date
lain's avatar
lain committed
248 249
      |> NaiveDateTime.from_iso8601!()
      |> NaiveDateTime.to_iso8601()
250 251 252 253 254 255
      |> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
    rescue
      _e -> ""
    end
  end

256 257 258 259 260 261 262
  defp shortname(name) do
    if String.length(name) < 30 do
      name
    else
      String.slice(name, 0..30) <> "…"
    end
  end
263

264
  def confirm_current_password(user, password) do
265
    with %User{local: true} = db_user <- Repo.get(User, user.id),
266
         true <- Pbkdf2.checkpw(password, db_user.password_hash) do
267 268 269
      {:ok, db_user}
    else
      _ -> {:error, "Invalid password."}
270 271
    end
  end
272

Maksim's avatar
Maksim committed
273
  def emoji_from_profile(%{info: _info} = user) do
274 275 276 277
    (Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
    |> Enum.map(fn {shortcode, url} ->
      %{
        "type" => "Emoji",
eal's avatar
eal committed
278
        "icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},
279 280 281 282
        "name" => ":#{shortcode}:"
      }
    end)
  end
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324

  def maybe_notify_to_recipients(
        recipients,
        %Activity{data: %{"to" => to, "type" => _type}} = _activity
      ) do
    recipients ++ to
  end

  def maybe_notify_mentioned_recipients(
        recipients,
        %Activity{data: %{"to" => _to, "type" => type} = data} = _activity
      )
      when type == "Create" do
    object = Object.normalize(data["object"])

    object_data =
      cond do
        !is_nil(object) ->
          object.data

        is_map(data["object"]) ->
          data["object"]

        true ->
          %{}
      end

    tagged_mentions = maybe_extract_mentions(object_data)

    recipients ++ tagged_mentions
  end

  def maybe_notify_mentioned_recipients(recipients, _), do: recipients

  def maybe_extract_mentions(%{"tag" => tag}) do
    tag
    |> Enum.filter(fn x -> is_map(x) end)
    |> Enum.filter(fn x -> x["type"] == "Mention" end)
    |> Enum.map(fn x -> x["href"] end)
  end

  def maybe_extract_mentions(_), do: []
minibikini's avatar
minibikini committed
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342

  def make_report_content_html(nil), do: {:ok, nil}

  def make_report_content_html(comment) do
    max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000)

    if String.length(comment) <= max_size do
      {:ok, format_input(comment, [], [], "text/plain")}
    else
      {:error, "Comment must be up to #{max_size} characters"}
    end
  end

  def get_report_statuses(%User{ap_id: actor}, %{"status_ids" => status_ids}) do
    {:ok, Activity.all_by_actor_and_id(actor, status_ids)}
  end

  def get_report_statuses(_, _), do: {:ok, nil}
lain's avatar
lain committed
343
end