1 defmodule Pleroma.Web.RichMedia.Card do
10 alias Pleroma.Web.RichMedia.Backfill
11 alias Pleroma.Web.RichMedia.Parser
13 @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
14 @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
16 @type t :: %__MODULE__{}
18 schema "rich_media_card" do
19 field(:url_hash, :binary)
26 def changeset(card, attrs) do
28 |> cast(attrs, [:url_hash, :fields])
29 |> validate_required([:url_hash, :fields])
30 |> unique_constraint(:url_hash)
33 @spec create(String.t(), map()) :: {:ok, t()}
34 def create(url, fields) do
35 url_hash = url_to_hash(url)
37 fields = Map.put_new(fields, "url", url)
40 |> changeset(%{url_hash: url_hash, fields: fields})
41 |> Repo.insert(on_conflict: {:replace, [:fields]}, conflict_target: :url_hash)
44 @spec delete(String.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | :ok
46 url_hash = url_to_hash(url)
47 @cachex.del(:rich_media_cache, url_hash)
49 case get_by_url(url) do
50 %__MODULE__{} = card -> Repo.delete(card)
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)
60 @cachex.fetch!(:rich_media_cache, url_hash, fn _ ->
63 |> where(url_hash: ^url_hash)
67 %__MODULE__{} = card -> {:commit, card}
76 def get_by_url(nil), do: nil
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 ->
85 backfill_opts = Map.put(backfill_opts, :url, url)
87 Backfill.start(backfill_opts)
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
100 url -> get_or_backfill_by_url(url)
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
111 %__MODULE__{} = card ->
114 # Cache miss, but fetch for rendering the Draft
116 with {:ok, fields} <- Parser.parse(url),
117 {:ok, card} <- create(url, fields) do
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
135 get_or_backfill_by_url(url, %{activity_id: activity.id})
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)
147 defp get_cached_url(object, activity_id) do
148 key = "URL|#{activity_id}"
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)