First
[anni] / lib / pleroma / web / metadata / providers / open_graph.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
6   alias Pleroma.User
7   alias Pleroma.Web.MediaProxy
8   alias Pleroma.Web.Metadata
9   alias Pleroma.Web.Metadata.Providers.Provider
10   alias Pleroma.Web.Metadata.Utils
11
12   @behaviour Provider
13   @media_types ["image", "audio", "video"]
14
15   @impl Provider
16   def build_tags(%{
17         object: object,
18         url: url,
19         user: user
20       }) do
21     attachments = build_attachments(object)
22     scrubbed_content = Utils.scrub_html_and_truncate(object)
23
24     [
25       {:meta,
26        [
27          property: "og:title",
28          content: Utils.user_name_string(user)
29        ], []},
30       {:meta, [property: "og:url", content: url], []},
31       {:meta,
32        [
33          property: "og:description",
34          content: scrubbed_content
35        ], []},
36       {:meta, [property: "og:type", content: "article"], []}
37     ] ++
38       if attachments == [] or Metadata.activity_nsfw?(object) do
39         [
40           {:meta, [property: "og:image", content: MediaProxy.preview_url(User.avatar_url(user))],
41            []},
42           {:meta, [property: "og:image:width", content: 150], []},
43           {:meta, [property: "og:image:height", content: 150], []}
44         ]
45       else
46         attachments
47       end
48   end
49
50   @impl Provider
51   def build_tags(%{user: user}) do
52     with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do
53       [
54         {:meta,
55          [
56            property: "og:title",
57            content: Utils.user_name_string(user)
58          ], []},
59         {:meta, [property: "og:url", content: user.uri || user.ap_id], []},
60         {:meta, [property: "og:description", content: truncated_bio], []},
61         {:meta, [property: "og:type", content: "article"], []},
62         {:meta, [property: "og:image", content: MediaProxy.preview_url(User.avatar_url(user))],
63          []},
64         {:meta, [property: "og:image:width", content: 150], []},
65         {:meta, [property: "og:image:height", content: 150], []}
66       ]
67     end
68   end
69
70   defp build_attachments(%{data: %{"attachment" => attachments}}) do
71     Enum.reduce(attachments, [], fn attachment, acc ->
72       rendered_tags =
73         Enum.reduce(attachment["url"], [], fn url, acc ->
74           # TODO: Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image
75           # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview.
76           case Utils.fetch_media_type(@media_types, url["mediaType"]) do
77             "audio" ->
78               [
79                 {:meta, [property: "og:audio", content: MediaProxy.url(url["href"])], []}
80                 | acc
81               ]
82
83             # Not using preview_url for this. It saves bandwidth, but the image dimensions will
84             # be wrong. We generate it on the fly and have no way to capture or analyze the
85             # image to get the dimensions. This can be an issue for apps/FEs rendering images
86             # in timelines too, but you can get clever with the aspect ratio metadata as a
87             # workaround.
88             "image" ->
89               [
90                 {:meta, [property: "og:image", content: MediaProxy.url(url["href"])], []},
91                 {:meta, [property: "og:image:alt", content: attachment["name"]], []}
92                 | acc
93               ]
94               |> maybe_add_dimensions(url)
95
96             "video" ->
97               [
98                 {:meta, [property: "og:video", content: MediaProxy.url(url["href"])], []}
99                 | acc
100               ]
101               |> maybe_add_dimensions(url)
102               |> maybe_add_video_thumbnail(url)
103
104             _ ->
105               acc
106           end
107         end)
108
109       acc ++ rendered_tags
110     end)
111   end
112
113   defp build_attachments(_), do: []
114
115   # We can use url["mediaType"] to dynamically fill the metadata
116   defp maybe_add_dimensions(metadata, url) do
117     type = url["mediaType"] |> String.split("/") |> List.first()
118
119     cond do
120       !is_nil(url["height"]) && !is_nil(url["width"]) ->
121         metadata ++
122           [
123             {:meta, [property: "og:#{type}:width", content: "#{url["width"]}"], []},
124             {:meta, [property: "og:#{type}:height", content: "#{url["height"]}"], []}
125           ]
126
127       true ->
128         metadata
129     end
130   end
131
132   # Media Preview Proxy makes thumbnails of videos without resizing, so we can trust the
133   # width and height of the source video.
134   defp maybe_add_video_thumbnail(metadata, url) do
135     cond do
136       Pleroma.Config.get([:media_preview_proxy, :enabled], false) ->
137         metadata ++
138           [
139             {:meta, [property: "og:image:width", content: "#{url["width"]}"], []},
140             {:meta, [property: "og:image:height", content: "#{url["height"]}"], []},
141             {:meta, [property: "og:image", content: MediaProxy.preview_url(url["href"])], []}
142           ]
143
144       true ->
145         metadata
146     end
147   end
148 end