cbd0b7d1b4fcb3b88b7ddfaebd80ed4ee3129e4e
[akkoma] / lib / pleroma / web / metadata / opengraph.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4 defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
5 alias Pleroma.Web.Metadata.Providers.Provider
6 alias Pleroma.Web.Metadata
7 alias Pleroma.{HTML, Formatter, User}
8 alias Pleroma.Web.MediaProxy
9
10 @behaviour Provider
11
12 @impl Provider
13 def build_tags(%{
14 activity: %{data: %{"object" => %{"id" => object_id}}} = activity,
15 user: user
16 }) do
17 attachments = build_attachments(activity)
18 scrubbed_content = scrub_html_and_truncate(activity)
19 # Zero width space
20 content =
21 if scrubbed_content != "" and scrubbed_content != "\u200B" do
22 ": “" <> scrubbed_content <> "”"
23 else
24 ""
25 end
26
27 # Most previews only show og:title which is inconvenient. Instagram
28 # hacks this by putting the description in the title and making the
29 # description longer prefixed by how many likes and shares the post
30 # has. Here we use the descriptive nickname in the title, and expand
31 # the full account & nickname in the description. We also use the cute^Wevil
32 # smart quotes around the status text like Instagram, too.
33 [
34 {:meta,
35 [
36 property: "og:title",
37 content: "#{user.name}" <> content
38 ], []},
39 {:meta, [property: "og:url", content: object_id], []},
40 {:meta,
41 [
42 property: "og:description",
43 content: "#{user_name_string(user)}" <> content
44 ], []},
45 {:meta, [property: "og:type", content: "website"], []}
46 ] ++
47 if attachments == [] or Metadata.activity_nsfw?(activity) do
48 [
49 {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []},
50 {:meta, [property: "og:image:width", content: 150], []},
51 {:meta, [property: "og:image:height", content: 150], []}
52 ]
53 else
54 attachments
55 end
56 end
57
58 @impl Provider
59 def build_tags(%{user: user}) do
60 with truncated_bio = scrub_html_and_truncate(user.bio || "") do
61 [
62 {:meta,
63 [
64 property: "og:title",
65 content: user_name_string(user)
66 ], []},
67 {:meta, [property: "og:url", content: User.profile_url(user)], []},
68 {:meta, [property: "og:description", content: truncated_bio], []},
69 {:meta, [property: "og:type", content: "website"], []},
70 {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []},
71 {:meta, [property: "og:image:width", content: 150], []},
72 {:meta, [property: "og:image:height", content: 150], []}
73 ]
74 end
75 end
76
77 defp build_attachments(%{data: %{"object" => %{"attachment" => attachments}}} = _activity) do
78 Enum.reduce(attachments, [], fn attachment, acc ->
79 rendered_tags =
80 Enum.reduce(attachment["url"], [], fn url, acc ->
81 media_type =
82 Enum.find(["image", "audio", "video"], fn media_type ->
83 String.starts_with?(url["mediaType"], media_type)
84 end)
85
86 # TODO: Add additional properties to objects when we have the data available.
87 # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image
88 # object when a Video or GIF is attached it will display that in the Whatsapp Rich Preview.
89 case media_type do
90 "audio" ->
91 [
92 {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []}
93 | acc
94 ]
95
96 "image" ->
97 [
98 {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])],
99 []},
100 {:meta, [property: "og:image:width", content: 150], []},
101 {:meta, [property: "og:image:height", content: 150], []}
102 | acc
103 ]
104
105 "video" ->
106 [
107 {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []}
108 | acc
109 ]
110
111 _ ->
112 acc
113 end
114 end)
115
116 acc ++ rendered_tags
117 end)
118 end
119
120 defp scrub_html_and_truncate(%{data: %{"object" => %{"content" => content}}} = activity) do
121 content
122 # html content comes from DB already encoded, decode first and scrub after
123 |> HtmlEntities.decode()
124 |> String.replace(~r/<br\s?\/?>/, " ")
125 |> HTML.get_cached_stripped_html_for_object(activity, __MODULE__)
126 |> Formatter.truncate()
127 end
128
129 defp scrub_html_and_truncate(content) when is_binary(content) do
130 content
131 # html content comes from DB already encoded, decode first and scrub after
132 |> HtmlEntities.decode()
133 |> String.replace(~r/<br\s?\/?>/, " ")
134 |> HTML.strip_tags()
135 |> Formatter.truncate()
136 end
137
138 defp attachment_url(url) do
139 MediaProxy.url(url)
140 end
141
142 defp user_name_string(user) do
143 "#{user.name} " <>
144 if user.local do
145 "(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})"
146 else
147 "(@#{user.nickname})"
148 end
149 end
150 end