diff options
Diffstat (limited to 'lib/pleroma/web/rich_media/card.ex')
| -rw-r--r-- | lib/pleroma/web/rich_media/card.ex | 157 |
1 files changed, 157 insertions, 0 deletions
diff --git a/lib/pleroma/web/rich_media/card.ex b/lib/pleroma/web/rich_media/card.ex new file mode 100644 index 0000000..36a1ae4 --- /dev/null +++ b/lib/pleroma/web/rich_media/card.ex @@ -0,0 +1,157 @@ +defmodule Pleroma.Web.RichMedia.Card do + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query + + alias Pleroma.Activity + alias Pleroma.HTML + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.Web.RichMedia.Backfill + alias Pleroma.Web.RichMedia.Parser + + @cachex Pleroma.Config.get([:cachex, :provider], Cachex) + @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) + + @type t :: %__MODULE__{} + + schema "rich_media_card" do + field(:url_hash, :binary) + field(:fields, :map) + + timestamps() + end + + @doc false + def changeset(card, attrs) do + card + |> cast(attrs, [:url_hash, :fields]) + |> validate_required([:url_hash, :fields]) + |> unique_constraint(:url_hash) + end + + @spec create(String.t(), map()) :: {:ok, t()} + def create(url, fields) do + url_hash = url_to_hash(url) + + fields = Map.put_new(fields, "url", url) + + %__MODULE__{} + |> changeset(%{url_hash: url_hash, fields: fields}) + |> Repo.insert(on_conflict: {:replace, [:fields]}, conflict_target: :url_hash) + end + + @spec delete(String.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | :ok + def delete(url) do + url_hash = url_to_hash(url) + @cachex.del(:rich_media_cache, url_hash) + + case get_by_url(url) do + %__MODULE__{} = card -> Repo.delete(card) + nil -> :ok + end + end + + @spec get_by_url(String.t() | nil) :: t() | nil | :error + def get_by_url(url) when is_binary(url) do + if @config_impl.get([:rich_media, :enabled]) do + url_hash = url_to_hash(url) + + @cachex.fetch!(:rich_media_cache, url_hash, fn _ -> + result = + __MODULE__ + |> where(url_hash: ^url_hash) + |> Repo.one() + + case result do + %__MODULE__{} = card -> {:commit, card} + _ -> {:ignore, nil} + end + end) + else + :error + end + end + + def get_by_url(nil), do: nil + + @spec get_or_backfill_by_url(String.t(), map()) :: t() | nil + def get_or_backfill_by_url(url, backfill_opts \\ %{}) do + case get_by_url(url) do + %__MODULE__{} = card -> + card + + nil -> + backfill_opts = Map.put(backfill_opts, :url, url) + + Backfill.start(backfill_opts) + + nil + + :error -> + nil + end + end + + @spec get_by_object(Object.t()) :: t() | nil | :error + def get_by_object(object) do + case HTML.extract_first_external_url_from_object(object) do + nil -> nil + url -> get_or_backfill_by_url(url) + end + end + + @spec get_by_activity(Activity.t()) :: t() | nil | :error + # Fake/Draft activity + def get_by_activity(%Activity{id: "pleroma:fakeid"} = activity) do + with %Object{} = object <- Object.normalize(activity, fetch: false), + url when not is_nil(url) <- HTML.extract_first_external_url_from_object(object) do + case get_by_url(url) do + # Cache hit + %__MODULE__{} = card -> + card + + # Cache miss, but fetch for rendering the Draft + _ -> + with {:ok, fields} <- Parser.parse(url), + {:ok, card} <- create(url, fields) do + card + else + _ -> nil + end + end + else + _ -> + nil + end + end + + def get_by_activity(activity) do + with %Object{} = object <- Object.normalize(activity, fetch: false), + {_, nil} <- {:cached, get_cached_url(object, activity.id)} do + nil + else + {:cached, url} -> + get_or_backfill_by_url(url, %{activity_id: activity.id}) + + _ -> + :error + end + end + + @spec url_to_hash(String.t()) :: String.t() + def url_to_hash(url) do + :crypto.hash(:sha256, url) |> Base.encode16(case: :lower) + end + + defp get_cached_url(object, activity_id) do + key = "URL|#{activity_id}" + + @cachex.fetch!(:scrubber_cache, key, fn _ -> + url = HTML.extract_first_external_url_from_object(object) + Activity.HTML.add_cache_key_for(activity_id, key) + + {:commit, url} + end) + end +end |
