total rebase
[anni] / lib / pleroma / web / rich_media / card.ex
1 defmodule Pleroma.Web.RichMedia.Card do
2   use Ecto.Schema
3   import Ecto.Changeset
4   import Ecto.Query
5
6   alias Pleroma.Activity
7   alias Pleroma.HTML
8   alias Pleroma.Object
9   alias Pleroma.Repo
10   alias Pleroma.Web.RichMedia.Backfill
11   alias Pleroma.Web.RichMedia.Parser
12
13   @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
14   @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
15
16   @type t :: %__MODULE__{}
17
18   schema "rich_media_card" do
19     field(:url_hash, :binary)
20     field(:fields, :map)
21
22     timestamps()
23   end
24
25   @doc false
26   def changeset(card, attrs) do
27     card
28     |> cast(attrs, [:url_hash, :fields])
29     |> validate_required([:url_hash, :fields])
30     |> unique_constraint(:url_hash)
31   end
32
33   @spec create(String.t(), map()) :: {:ok, t()}
34   def create(url, fields) do
35     url_hash = url_to_hash(url)
36
37     fields = Map.put_new(fields, "url", url)
38
39     %__MODULE__{}
40     |> changeset(%{url_hash: url_hash, fields: fields})
41     |> Repo.insert(on_conflict: {:replace, [:fields]}, conflict_target: :url_hash)
42   end
43
44   @spec delete(String.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | :ok
45   def delete(url) do
46     url_hash = url_to_hash(url)
47     @cachex.del(:rich_media_cache, url_hash)
48
49     case get_by_url(url) do
50       %__MODULE__{} = card -> Repo.delete(card)
51       nil -> :ok
52     end
53   end
54
55   @spec get_by_url(String.t() | nil) :: t() | nil | :error
56   def get_by_url(url) when is_binary(url) do
57     if @config_impl.get([:rich_media, :enabled]) do
58       url_hash = url_to_hash(url)
59
60       @cachex.fetch!(:rich_media_cache, url_hash, fn _ ->
61         result =
62           __MODULE__
63           |> where(url_hash: ^url_hash)
64           |> Repo.one()
65
66         case result do
67           %__MODULE__{} = card -> {:commit, card}
68           _ -> {:ignore, nil}
69         end
70       end)
71     else
72       :error
73     end
74   end
75
76   def get_by_url(nil), do: nil
77
78   @spec get_or_backfill_by_url(String.t(), map()) :: t() | nil
79   def get_or_backfill_by_url(url, backfill_opts \\ %{}) do
80     case get_by_url(url) do
81       %__MODULE__{} = card ->
82         card
83
84       nil ->
85         backfill_opts = Map.put(backfill_opts, :url, url)
86
87         Backfill.start(backfill_opts)
88
89         nil
90
91       :error ->
92         nil
93     end
94   end
95
96   @spec get_by_object(Object.t()) :: t() | nil | :error
97   def get_by_object(object) do
98     case HTML.extract_first_external_url_from_object(object) do
99       nil -> nil
100       url -> get_or_backfill_by_url(url)
101     end
102   end
103
104   @spec get_by_activity(Activity.t()) :: t() | nil | :error
105   # Fake/Draft activity
106   def get_by_activity(%Activity{id: "pleroma:fakeid"} = activity) do
107     with %Object{} = object <- Object.normalize(activity, fetch: false),
108          url when not is_nil(url) <- HTML.extract_first_external_url_from_object(object) do
109       case get_by_url(url) do
110         # Cache hit
111         %__MODULE__{} = card ->
112           card
113
114         # Cache miss, but fetch for rendering the Draft
115         _ ->
116           with {:ok, fields} <- Parser.parse(url),
117                {:ok, card} <- create(url, fields) do
118             card
119           else
120             _ -> nil
121           end
122       end
123     else
124       _ ->
125         nil
126     end
127   end
128
129   def get_by_activity(activity) do
130     with %Object{} = object <- Object.normalize(activity, fetch: false),
131          {_, nil} <- {:cached, get_cached_url(object, activity.id)} do
132       nil
133     else
134       {:cached, url} ->
135         get_or_backfill_by_url(url, %{activity_id: activity.id})
136
137       _ ->
138         :error
139     end
140   end
141
142   @spec url_to_hash(String.t()) :: String.t()
143   def url_to_hash(url) do
144     :crypto.hash(:sha256, url) |> Base.encode16(case: :lower)
145   end
146
147   defp get_cached_url(object, activity_id) do
148     key = "URL|#{activity_id}"
149
150     @cachex.fetch!(:scrubber_cache, key, fn _ ->
151       url = HTML.extract_first_external_url_from_object(object)
152       Activity.HTML.add_cache_key_for(activity_id, key)
153
154       {:commit, url}
155     end)
156   end
157 end