diff options
Diffstat (limited to 'lib')
23 files changed, 139 insertions, 141 deletions
diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex index 2976085ba..c01cf054d 100644 --- a/lib/mix/pleroma.ex +++ b/lib/mix/pleroma.ex @@ -14,7 +14,8 @@ defmodule Mix.Pleroma do :swoosh, :timex, :fast_html, - :oban + :oban, + :logger_backends ] @cachex_children ["object", "user", "scrubber", "web_resp"] @doc "Common functions to be reused in mix tasks" diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 13ac6536c..b82d1f079 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -351,7 +351,7 @@ defmodule Mix.Tasks.Pleroma.Database do ) end - shell_info('Done.') + shell_info(~c"Done.") end end diff --git a/lib/mix/tasks/pleroma/test_runner.ex b/lib/mix/tasks/pleroma/test_runner.ex new file mode 100644 index 000000000..69fefb001 --- /dev/null +++ b/lib/mix/tasks/pleroma/test_runner.ex @@ -0,0 +1,25 @@ +defmodule Mix.Tasks.Pleroma.TestRunner do + @shortdoc "Retries tests once if they fail" + + use Mix.Task + + def run(args \\ []) do + case System.cmd("mix", ["test"] ++ args, into: IO.stream(:stdio, :line)) do + {_, 0} -> + :ok + + _ -> + retry(args) + end + end + + def retry(args) do + case System.cmd("mix", ["test", "--failed"] ++ args, into: IO.stream(:stdio, :line)) do + {_, 0} -> + :ok + + _ -> + exit(1) + end + end +end diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index 91885347f..ffc95f144 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -1,5 +1,5 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Config.TransferTask do @@ -44,14 +44,9 @@ defmodule Pleroma.Config.TransferTask do with {_, true} <- {:configurable, Config.get(:configurable_from_database)} do # We need to restart applications for loaded settings take effect - {logger, other} = + settings = (Repo.all(ConfigDB) ++ deleted_settings) |> Enum.map(&merge_with_default/1) - |> Enum.split_with(fn {group, _, _, _} -> group in [:logger] end) - - logger - |> Enum.sort() - |> Enum.each(&configure/1) started_applications = Application.started_applications() @@ -64,7 +59,7 @@ defmodule Pleroma.Config.TransferTask do [:pleroma | reject] end - other + settings |> Enum.map(&update/1) |> Enum.uniq() |> Enum.reject(&(&1 in reject)) @@ -102,38 +97,6 @@ defmodule Pleroma.Config.TransferTask do {group, key, value, merged} end - # change logger configuration in runtime, without restart - defp configure({_, :backends, _, merged}) do - # removing current backends - Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1) - - Enum.each(merged, &Logger.add_backend/1) - - :ok = update_env(:logger, :backends, merged) - end - - defp configure({_, key, _, merged}) when key in [:console, :ex_syslogger] do - merged = - if key == :console do - put_in(merged[:format], merged[:format] <> "\n") - else - merged - end - - backend = - if key == :ex_syslogger, - do: {ExSyslogger, :ex_syslogger}, - else: key - - Logger.configure_backend(backend, merged) - :ok = update_env(:logger, key, merged) - end - - defp configure({_, key, _, merged}) do - Logger.configure([{key, merged}]) - :ok = update_env(:logger, key, merged) - end - defp update({group, key, value, merged}) do try do :ok = update_env(group, key, merged) diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex index e28fcb124..89d3050d6 100644 --- a/lib/pleroma/config_db.ex +++ b/lib/pleroma/config_db.ex @@ -165,8 +165,7 @@ defmodule Pleroma.ConfigDB do {:pleroma, :ecto_repos}, {:mime, :types}, {:cors_plug, [:max_age, :methods, :expose, :headers]}, - {:swarm, :node_blacklist}, - {:logger, :backends} + {:swarm, :node_blacklist} ] Enum.any?(full_key_update, fn @@ -385,7 +384,12 @@ defmodule Pleroma.ConfigDB do @spec module_name?(String.t()) :: boolean() def module_name?(string) do - Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Ueberauth|Swoosh)\./, string) or - string in ["Oban", "Ueberauth", "ExSyslogger", "ConcurrentLimiter"] + if String.contains?(string, ".") do + [name | _] = String.split(string, ".", parts: 2) + + name in ~w[Pleroma Phoenix Tesla Ueberauth Swoosh Logger LoggerBackends] + else + string in ~w[Oban Ueberauth ExSyslogger ConcurrentLimiter] + end end end diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index afc341853..785fdb8b2 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -416,10 +416,10 @@ defmodule Pleroma.Emoji.Pack do end defp create_archive_and_cache(pack, hash) do - files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)] + files = [~c"pack.json" | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)] {:ok, {_, result}} = - :zip.zip('#{pack.name}.zip', files, [:memory, cwd: to_charlist(pack.path)]) + :zip.zip(~c"#{pack.name}.zip", files, [:memory, cwd: to_charlist(pack.path)]) ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file]) overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files)) @@ -586,7 +586,7 @@ defmodule Pleroma.Emoji.Pack do with :ok <- File.mkdir_p!(local_pack.path) do files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end) # Fallback cannot contain a pack.json file - files = if pack_info[:fallback], do: files, else: ['pack.json' | files] + files = if pack_info[:fallback], do: files, else: [~c"pack.json" | files] :zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files) end diff --git a/lib/pleroma/gun/connection_pool/reclaimer.ex b/lib/pleroma/gun/connection_pool/reclaimer.ex index 35e7f4b2e..3580d38f5 100644 --- a/lib/pleroma/gun/connection_pool/reclaimer.ex +++ b/lib/pleroma/gun/connection_pool/reclaimer.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Gun.ConnectionPool.Reclaimer do def start_monitor do pid = - case GenServer.start_link(__MODULE__, [], name: {:via, Registry, {registry(), "reclaimer"}}) do + case GenServer.start(__MODULE__, [], name: {:via, Registry, {registry(), "reclaimer"}}) do {:ok, pid} -> pid diff --git a/lib/pleroma/gun/connection_pool/worker_supervisor.ex b/lib/pleroma/gun/connection_pool/worker_supervisor.ex index eb83962d8..b9dedf61e 100644 --- a/lib/pleroma/gun/connection_pool/worker_supervisor.ex +++ b/lib/pleroma/gun/connection_pool/worker_supervisor.ex @@ -5,6 +5,9 @@ defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do @moduledoc "Supervisor for pool workers. Does not do anything except enforce max connection limit" + alias Pleroma.Config + alias Pleroma.Gun.ConnectionPool.Worker + use DynamicSupervisor def start_link(opts) do @@ -14,21 +17,28 @@ defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do def init(_opts) do DynamicSupervisor.init( strategy: :one_for_one, - max_children: Pleroma.Config.get([:connections_pool, :max_connections]) + max_children: Config.get([:connections_pool, :max_connections]) ) end - def start_worker(opts, last_attempt \\ false) do - case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do + def start_worker(opts, last_attempt \\ false) + + def start_worker(opts, true) do + case DynamicSupervisor.start_child(__MODULE__, {Worker, opts}) do + {:error, :max_children} -> + :telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts}) + {:error, :pool_full} + + res -> + res + end + end + + def start_worker(opts, false) do + case DynamicSupervisor.start_child(__MODULE__, {Worker, opts}) do {:error, :max_children} -> - funs = [fn -> last_attempt end, fn -> match?(:error, free_pool()) end] - - if Enum.any?(funs, fn fun -> fun.() end) do - :telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts}) - {:error, :pool_full} - else - start_worker(opts, true) - end + free_pool() + start_worker(opts, true) res -> res diff --git a/lib/pleroma/object/updater.ex b/lib/pleroma/object/updater.ex index b1e4870ba..b80bc7faf 100644 --- a/lib/pleroma/object/updater.ex +++ b/lib/pleroma/object/updater.ex @@ -134,7 +134,10 @@ defmodule Pleroma.Object.Updater do else %{updated_object: updated_data} = updated_data - |> maybe_update_history(original_data, updated: updated, use_history_in_new_object?: false) + |> maybe_update_history(original_data, + updated: updated, + use_history_in_new_object?: false + ) updated_data |> Map.put("updated", date) diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 35c7c02a5..b0aef2592 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -249,14 +249,16 @@ defmodule Pleroma.Upload do defp url_from_spec(_upload, _base_url, {:url, url}), do: url + @spec base_url() :: binary def base_url do uploader = @config_impl.get([Pleroma.Upload, :uploader]) - upload_base_url = @config_impl.get([Pleroma.Upload, :base_url]) + upload_fallback_url = Pleroma.Web.Endpoint.url() <> "/media/" + upload_base_url = @config_impl.get([Pleroma.Upload, :base_url]) || upload_fallback_url public_endpoint = @config_impl.get([uploader, :public_endpoint]) case uploader do Pleroma.Uploaders.Local -> - upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/" + upload_base_url Pleroma.Uploaders.S3 -> bucket = @config_impl.get([Pleroma.Uploaders.S3, :bucket]) @@ -268,11 +270,14 @@ defmodule Pleroma.Upload do !is_nil(truncated_namespace) -> truncated_namespace - !is_nil(namespace) -> + !is_nil(namespace) and !is_nil(bucket) -> namespace <> ":" <> bucket - true -> + !is_nil(bucket) -> bucket + + true -> + "" end if public_endpoint do @@ -285,7 +290,7 @@ defmodule Pleroma.Upload do @config_impl.get([Pleroma.Uploaders.IPFS, :get_gateway_url]) _ -> - public_endpoint || upload_base_url || Pleroma.Web.Endpoint.url() <> "/media/" + public_endpoint || upload_base_url end end end diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex index 65e0baccd..1821de667 100644 --- a/lib/pleroma/user/backup.ex +++ b/lib/pleroma/user/backup.ex @@ -197,12 +197,12 @@ defmodule Pleroma.User.Backup do end @files [ - 'actor.json', - 'outbox.json', - 'likes.json', - 'bookmarks.json', - 'followers.json', - 'following.json' + ~c"actor.json", + ~c"outbox.json", + ~c"likes.json", + ~c"bookmarks.json", + ~c"followers.json", + ~c"following.json" ] @spec export(Pleroma.User.Backup.t(), pid()) :: {:ok, String.t()} | :error def export(%__MODULE__{} = backup, caller_pid) do diff --git a/lib/pleroma/web/api_spec/operations/search_operation.ex b/lib/pleroma/web/api_spec/operations/search_operation.ex index 539743ba3..c2afe8e18 100644 --- a/lib/pleroma/web/api_spec/operations/search_operation.ex +++ b/lib/pleroma/web/api_spec/operations/search_operation.ex @@ -79,7 +79,9 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do %Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]}, "Search type" ), - Operation.parameter(:q, :query, %Schema{type: :string}, "The search query", required: true), + Operation.parameter(:q, :query, %Schema{type: :string}, "The search query", + required: true + ), Operation.parameter( :resolve, :query, diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex index e8cd4491c..17ffd820d 100644 --- a/lib/pleroma/web/auth/ldap_authenticator.ex +++ b/lib/pleroma/web/auth/ldap_authenticator.ex @@ -110,7 +110,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do } params = - case List.keyfind(attributes, 'mail', 0) do + case List.keyfind(attributes, ~c"mail", 0) do {_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail)) _ -> params end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 0c16749a4..d9d7e516a 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -30,7 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do # pagination is restricted to 40 activities at a time defp fetch_rich_media_for_activities(activities) do Enum.each(activities, fn activity -> - spawn(fn -> Card.get_by_activity(activity) end) + Card.get_by_activity(activity) end) end diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index bb27d806d..730295a4c 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -104,7 +104,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do end def handle_info(:close, state) do - {:stop, {:closed, 'connection closed by server'}, state} + {:stop, {:closed, ~c"connection closed by server"}, state} end def handle_info(msg, state) do diff --git a/lib/pleroma/web/media_proxy.ex b/lib/pleroma/web/media_proxy.ex index d64760fc2..29882542c 100644 --- a/lib/pleroma/web/media_proxy.ex +++ b/lib/pleroma/web/media_proxy.ex @@ -75,8 +75,7 @@ defmodule Pleroma.Web.MediaProxy do %{host: domain} = URI.parse(url) mediaproxy_whitelist_domains = - [:media_proxy, :whitelist] - |> Config.get() + Config.get([:media_proxy, :whitelist], []) |> Kernel.++(["#{Upload.base_url()}"]) |> Enum.map(&maybe_get_domain_from_url/1) diff --git a/lib/pleroma/web/plugs/o_auth_scopes_plug.ex b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex index faf0fd8c6..08c2f61ea 100644 --- a/lib/pleroma/web/plugs/o_auth_scopes_plug.ex +++ b/lib/pleroma/web/plugs/o_auth_scopes_plug.ex @@ -34,7 +34,9 @@ defmodule Pleroma.Web.Plugs.OAuthScopesPlug do permissions = Enum.join(missing_scopes, " #{op} ") error_message = - dgettext("errors", "Insufficient permissions: %{permissions}.", permissions: permissions) + dgettext("errors", "Insufficient permissions: %{permissions}.", + permissions: permissions + ) conn |> put_resp_content_type("application/json") diff --git a/lib/pleroma/web/rich_media/backfill.ex b/lib/pleroma/web/rich_media/backfill.ex index 4ec50e132..1d8cc87d4 100644 --- a/lib/pleroma/web/rich_media/backfill.ex +++ b/lib/pleroma/web/rich_media/backfill.ex @@ -6,35 +6,25 @@ defmodule Pleroma.Web.RichMedia.Backfill do alias Pleroma.Web.RichMedia.Card alias Pleroma.Web.RichMedia.Parser alias Pleroma.Web.RichMedia.Parser.TTL - alias Pleroma.Workers.RichMediaExpirationWorker + alias Pleroma.Workers.RichMediaWorker require Logger - @backfiller Pleroma.Config.get([__MODULE__, :provider], Pleroma.Web.RichMedia.Backfill.Task) @cachex Pleroma.Config.get([:cachex, :provider], Cachex) - @max_attempts 3 - @retry 5_000 - def start(%{url: url} = args) when is_binary(url) do + @spec run(map()) :: + :ok | {:error, {:invalid_metadata, any()} | :body_too_large | {:content, any()} | any()} + def run(%{"url" => url} = args) do url_hash = Card.url_to_hash(url) - args = - args - |> Map.put(:attempt, 1) - |> Map.put(:url_hash, url_hash) - - @backfiller.run(args) - end - - def run(%{url: url, url_hash: url_hash, attempt: attempt} = args) - when attempt <= @max_attempts do case Parser.parse(url) do {:ok, fields} -> {:ok, card} = Card.create(url, fields) maybe_schedule_expiration(url, fields) - if Map.has_key?(args, :activity_id) do + with %{"activity_id" => activity_id} <- args, + false <- is_nil(activity_id) do stream_update(args) end @@ -54,25 +44,16 @@ defmodule Pleroma.Web.RichMedia.Backfill do e -> Logger.debug("Rich media error for #{url}: #{inspect(e)}") - - :timer.sleep(@retry * attempt) - - run(%{args | attempt: attempt + 1}) + {:error, e} end end - def run(%{url: url, url_hash: url_hash}) do - Logger.debug("Rich media failure for #{url}") - - negative_cache(url_hash, :timer.minutes(15)) - end - defp maybe_schedule_expiration(url, fields) do case TTL.process(fields, url) do {:ok, ttl} when is_number(ttl) -> timestamp = DateTime.from_unix!(ttl) - RichMediaExpirationWorker.new(%{"url" => url}, scheduled_at: timestamp) + RichMediaWorker.new(%{"op" => "expire", "url" => url}, scheduled_at: timestamp) |> Oban.insert() _ -> @@ -80,22 +61,14 @@ defmodule Pleroma.Web.RichMedia.Backfill do end end - defp stream_update(%{activity_id: activity_id}) do + defp stream_update(%{"activity_id" => activity_id}) do Pleroma.Activity.get_by_id(activity_id) |> Pleroma.Activity.normalize() |> Pleroma.Web.ActivityPub.ActivityPub.stream_out() end defp warm_cache(key, val), do: @cachex.put(:rich_media_cache, key, val) - defp negative_cache(key, ttl \\ nil), do: @cachex.put(:rich_media_cache, key, nil, ttl: ttl) -end -defmodule Pleroma.Web.RichMedia.Backfill.Task do - alias Pleroma.Web.RichMedia.Backfill - - def run(args) do - Task.Supervisor.start_child(Pleroma.TaskSupervisor, Backfill, :run, [args], - name: {:global, {:rich_media, args.url_hash}} - ) - end + defp negative_cache(key, ttl \\ :timer.minutes(15)), + do: @cachex.put(:rich_media_cache, key, nil, ttl: ttl) end diff --git a/lib/pleroma/web/rich_media/card.ex b/lib/pleroma/web/rich_media/card.ex index 040066f36..72ff5e791 100644 --- a/lib/pleroma/web/rich_media/card.ex +++ b/lib/pleroma/web/rich_media/card.ex @@ -7,8 +7,8 @@ defmodule Pleroma.Web.RichMedia.Card do alias Pleroma.HTML alias Pleroma.Object alias Pleroma.Repo - alias Pleroma.Web.RichMedia.Backfill alias Pleroma.Web.RichMedia.Parser + alias Pleroma.Workers.RichMediaWorker @cachex Pleroma.Config.get([:cachex, :provider], Cachex) @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config) @@ -75,17 +75,18 @@ defmodule Pleroma.Web.RichMedia.Card do 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 + @spec get_or_backfill_by_url(String.t(), keyword()) :: t() | nil + def get_or_backfill_by_url(url, opts \\ []) do if @config_impl.get([:rich_media, :enabled]) do case get_by_url(url) do %__MODULE__{} = card -> card nil -> - backfill_opts = Map.put(backfill_opts, :url, url) + activity_id = Keyword.get(opts, :activity, nil) - Backfill.start(backfill_opts) + RichMediaWorker.new(%{"op" => "backfill", "url" => url, "activity_id" => activity_id}) + |> Oban.insert() nil @@ -137,7 +138,7 @@ defmodule Pleroma.Web.RichMedia.Card do nil else {:cached, url} -> - get_or_backfill_by_url(url, %{activity_id: activity.id}) + get_or_backfill_by_url(url, activity_id: activity.id) _ -> :error diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 7065fdab2..9abdfae30 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -172,7 +172,13 @@ defmodule Pleroma.Web.Streamer do def stream(topics, items) do if should_env_send?() do for topic <- List.wrap(topics), item <- List.wrap(items) do - spawn(fn -> do_stream(topic, item) end) + fun = fn -> do_stream(topic, item) end + + if Config.get([__MODULE__, :sync_streaming], false) do + fun.() + else + spawn(fun) + end end end end diff --git a/lib/pleroma/web/xml.ex b/lib/pleroma/web/xml.ex index 64329e4ba..3997e1661 100644 --- a/lib/pleroma/web/xml.ex +++ b/lib/pleroma/web/xml.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.XML do def string_from_xpath(xpath, doc) do try do - {:xmlObj, :string, res} = :xmerl_xpath.string('string(#{xpath})', doc) + {:xmlObj, :string, res} = :xmerl_xpath.string(~c"string(#{xpath})", doc) res = res diff --git a/lib/pleroma/workers/rich_media_expiration_worker.ex b/lib/pleroma/workers/rich_media_expiration_worker.ex deleted file mode 100644 index 0b74687cf..000000000 --- a/lib/pleroma/workers/rich_media_expiration_worker.ex +++ /dev/null @@ -1,15 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.RichMediaExpirationWorker do - alias Pleroma.Web.RichMedia.Card - - use Oban.Worker, - queue: :background - - @impl Oban.Worker - def perform(%Job{args: %{"url" => url} = _args}) do - Card.delete(url) - end -end diff --git a/lib/pleroma/workers/rich_media_worker.ex b/lib/pleroma/workers/rich_media_worker.ex new file mode 100644 index 000000000..f18ac658a --- /dev/null +++ b/lib/pleroma/workers/rich_media_worker.ex @@ -0,0 +1,19 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.RichMediaWorker do + alias Pleroma.Web.RichMedia.Backfill + alias Pleroma.Web.RichMedia.Card + + use Oban.Worker, queue: :background, max_attempts: 3, unique: [period: 300] + + @impl Oban.Worker + def perform(%Job{args: %{"op" => "expire", "url" => url} = _args}) do + Card.delete(url) + end + + def perform(%Job{args: %{"op" => "backfill", "url" => _url} = args}) do + Backfill.run(args) + end +end |