utils.ex 9.02 KB
Newer Older
1
2
3
4
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# 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
8
  alias Pleroma.{Activity, Formatter, Object, Repo, HTML}
Maksim's avatar
Maksim committed
9
10
  alias Pleroma.User
  alias Pleroma.Web
lain's avatar
lain committed
11
  alias Pleroma.Web.ActivityPub.Utils
12
  alias Pleroma.Web.Endpoint
13
  alias Pleroma.Web.MediaProxy
lain's avatar
lain committed
14

15
16
17
  # This is a hack for twidere.
  def get_by_id_or_ap_id(id) do
    activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id)
lain's avatar
lain committed
18

19
20
21
22
23
24
    activity &&
      if activity.data["type"] == "Create" do
        activity
      else
        Activity.get_create_activity_by_object_ap_id(activity.data["object"])
      end
25
26
  end

lain's avatar
lain committed
27
28
  def get_replied_to_activity(""), do: nil

29
30
31
  def get_replied_to_activity(id) when not is_nil(id) do
    Repo.get(Activity, id)
  end
lain's avatar
lain committed
32

33
34
  def get_replied_to_activity(_), do: nil

lain's avatar
lain committed
35
  def attachments_from_ids(ids) do
lain's avatar
lain committed
36
    Enum.map(ids || [], fn media_id ->
lain's avatar
lain committed
37
38
39
40
      Repo.get(Object, media_id).data
    end)
  end

41
  def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
lain's avatar
lain committed
42
    mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
43
44
45

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

47
    if inReplyTo do
48
      {Enum.uniq([inReplyTo.data["actor"] | to]), cc}
lain's avatar
lain committed
49
    else
50
51
52
53
54
      {to, cc}
    end
  end

  def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
55
56
57
58
59
60
61
62
63
64
    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
65
66
67
68
69
70
71
  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

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

75
76
77
78
    if inReplyTo do
      {Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
    else
      {mentioned_users, []}
lain's avatar
lain committed
79
80
81
    end
  end

82
83
84
85
86
87
88
89
  def make_content_html(
        status,
        mentions,
        attachments,
        tags,
        content_type,
        no_attachment_links \\ false
      ) do
90
    status
91
    |> format_input(mentions, tags, content_type)
eal's avatar
eal committed
92
    |> maybe_add_attachments(attachments, no_attachment_links)
93
94
95
  end

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

feld's avatar
feld committed
98
  def maybe_add_attachments(text, _attachments, _no_links = true), do: text
lain's avatar
lain committed
99

eal's avatar
eal committed
100
101
102
  def maybe_add_attachments(text, attachments, _no_links) do
    add_attachments(text, attachments)
  end
lain's avatar
lain committed
103

lain's avatar
lain committed
104
  def add_attachments(text, attachments) do
lain's avatar
lain committed
105
106
    attachment_text =
      Enum.map(attachments, fn
107
108
        %{"url" => [%{"href" => href} | _]} = attachment ->
          name = attachment["name"] || URI.decode(Path.basename(href))
109
          href = MediaProxy.url(href)
lain's avatar
lain committed
110
111
112
113
114
115
          "<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>"

        _ ->
          ""
      end)

eal's avatar
eal committed
116
    Enum.join([text | attachment_text], "<br>")
lain's avatar
lain committed
117
118
  end

Maksim's avatar
Maksim committed
119
120
121
  @doc """
  Formatting text to plain text.
  """
122
  def format_input(text, mentions, tags, "text/plain") do
eal's avatar
eal committed
123
    text
124
    |> Formatter.html_escape("text/plain")
125
    |> String.replace(~r/\r?\n/, "<br>")
lain's avatar
lain committed
126
127
    |> (&{[], &1}).()
    |> Formatter.add_links()
lain's avatar
lain committed
128
129
    |> Formatter.add_user_links(mentions)
    |> Formatter.add_hashtag_links(tags)
lain's avatar
lain committed
130
    |> Formatter.finalize()
lain's avatar
lain committed
131
132
  end

Maksim's avatar
Maksim committed
133
134
135
  @doc """
  Formatting text to html.
  """
Maksim's avatar
Maksim committed
136
  def format_input(text, mentions, _tags, "text/html") do
137
138
139
140
141
142
143
144
    text
    |> Formatter.html_escape("text/html")
    |> String.replace(~r/\r?\n/, "<br>")
    |> (&{[], &1}).()
    |> Formatter.add_user_links(mentions)
    |> Formatter.finalize()
  end

Maksim's avatar
Maksim committed
145
146
147
  @doc """
  Formatting text to markdown.
  """
148
149
  def format_input(text, mentions, tags, "text/markdown") do
    text
Maksim's avatar
Maksim committed
150
    |> Formatter.mentions_escape(mentions)
151
152
153
154
155
    |> Earmark.as_html!()
    |> Formatter.html_escape("text/html")
    |> String.replace(~r/\r?\n/, "")
    |> (&{[], &1}).()
    |> Formatter.add_user_links(mentions)
156
    |> Formatter.add_hashtag_links(tags)
157
158
159
    |> Formatter.finalize()
  end

lain's avatar
lain committed
160
  def add_tag_links(text, tags) do
lain's avatar
lain committed
161
162
163
    tags =
      tags
      |> Enum.sort_by(fn {tag, _} -> -String.length(tag) end)
lain's avatar
lain committed
164

lain's avatar
lain committed
165
    Enum.reduce(tags, text, fn {full, tag}, text ->
Maksim's avatar
Maksim committed
166
      url = "<a href='#{Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>"
lain's avatar
lain committed
167
168
      String.replace(text, full, url)
    end)
lain's avatar
lain committed
169
170
  end

lain's avatar
lain committed
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
  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,
191
      "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
lain's avatar
lain committed
192
    }
lain's avatar
lain committed
193
194
195
196
197
198
199
200
201

    if inReplyTo do
      object
      |> Map.put("inReplyTo", inReplyTo.data["object"]["id"])
      |> Map.put("inReplyToStatusId", inReplyTo.id)
    else
      object
    end
  end
dtluna's avatar
dtluna committed
202
203
204
205
206
207
208
209
210
211

  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
212
    with {:ok, date, _offset} <- date |> DateTime.from_iso8601() do
dtluna's avatar
dtluna committed
213
      format_asctime(date)
lain's avatar
lain committed
214
215
    else
      _e ->
dtluna's avatar
dtluna committed
216
217
218
        ""
    end
  end
219

220
221
  def to_masto_date(%NaiveDateTime{} = date) do
    date
lain's avatar
lain committed
222
    |> NaiveDateTime.to_iso8601()
223
224
225
226
227
228
    |> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
  end

  def to_masto_date(date) do
    try do
      date
lain's avatar
lain committed
229
230
      |> NaiveDateTime.from_iso8601!()
      |> NaiveDateTime.to_iso8601()
231
232
233
234
235
236
      |> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
    rescue
      _e -> ""
    end
  end

237
238
239
240
241
242
243
  defp shortname(name) do
    if String.length(name) < 30 do
      name
    else
      String.slice(name, 0..30) <> "…"
    end
  end
244

245
  def confirm_current_password(user, password) do
246
    with %User{local: true} = db_user <- Repo.get(User, user.id),
247
         true <- Pbkdf2.checkpw(password, db_user.password_hash) do
248
249
250
      {:ok, db_user}
    else
      _ -> {:error, "Invalid password."}
251
252
    end
  end
253

Maksim's avatar
Maksim committed
254
  def emoji_from_profile(%{info: _info} = user) do
255
256
257
258
    (Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
    |> Enum.map(fn {shortcode, url} ->
      %{
        "type" => "Emoji",
eal's avatar
eal committed
259
        "icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},
260
261
262
263
        "name" => ":#{shortcode}:"
      }
    end)
  end
264

265
266
267
  def get_scrubbed_html_for_object(content, scrubber, activity) when is_atom(scrubber) do
    get_scrubbed_html_for_object(content, [scrubber], activity)
  end
268
269
270
  @doc """
  Get sanitized HTML from cache, or scrub it and save to cache.
  """
271
  def get_scrubbed_html_for_object(
272
273
274
275
276
        content,
        scrubbers,
        %{data: %{"object" => object}} = activity
      ) do
    scrubber_cache =
277
      if is_list(object["scrubber_cache"]) do
278
279
280
281
282
        object["scrubber_cache"]
      else
        []
      end

283
    signature = generate_scrubber_signature(scrubbers)
284
285

    {new_scrubber_cache, scrubbed_html} =
286
      Enum.map_reduce(scrubber_cache, nil, fn
287
        entry, content ->
288
289
290
291
292
293
294
          if Map.keys(entry["scrubbers"]) == Map.keys(signature) do
            if entry["scrubbers"] == signature do
              {entry, entry["content"]}
            else
              # Remove the entry if scrubber version is outdated
              {nil, nil}
            end
295
296
          else
            {entry, content}
297
298
          end
      end)
299
300

    # Remove nil objects
301
    new_scrubber_cache = Enum.reject(new_scrubber_cache, &is_nil/1)
302

Rin Toshaka's avatar
Rin Toshaka committed
303
    if scrubbed_html == nil or new_scrubber_cache != scrubber_cache do
304
      scrubbed_html = HTML.filter_tags(content, scrubbers)
305
306
307
308
309

      new_scrubber_cache = [
        %{:scrubbers => signature, :content => scrubbed_html} | new_scrubber_cache
      ]

Rin Toshaka's avatar
Rin Toshaka committed
310
      update_scrubber_cache(activity, new_scrubber_cache)
311
312
313
      scrubbed_html
    else
      scrubbed_html
314
315
316
    end
  end

317
318
  defp generate_scrubber_signature(scrubbers) do
    Enum.reduce(scrubbers, %{}, fn scrubber, signature ->
319
320
321
322
323
324
325
326
327
328
      Map.put(
        signature,
        to_string(scrubber),
        # If a scrubber does not have a version(e.g HtmlSanitizeEx.Scrubber) it is assumed it is always 0)
        if Kernel.function_exported?(scrubber, :version, 0) do
          scrubber.version
        else
          0
        end
      )
329
330
    end)
  end
Rin Toshaka's avatar
Rin Toshaka committed
331

332
333
334
335
336
337
338
339
  defp update_scrubber_cache(activity, scrubber_cache) do
    cng =
      Object.change(activity, %{
        data: Kernel.put_in(activity.data, ["object", "scrubber_cache"], scrubber_cache)
      })

    {:ok, _struct} = Repo.update(cng)
  end
340
341
342
343

  def get_stripped_html_for_object(content, activity) do
    get_scrubbed_html_for_object(content, [HtmlSanitizeEx.Scrubber.StripTags], activity)
  end
lain's avatar
lain committed
344
end