From 3ea1ed42750c417b63f5937947e43b5d646f609f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20W=C3=B6ginger?= Date: Tue, 15 Apr 2025 21:49:05 +0200 Subject: [PATCH 1/3] refactor: rename Resources => ResourcesRepository --- lib/radiator/resources/node_changed_worker.ex | 4 +-- .../{resources.ex => resources_repository.ex} | 2 +- ...test.exs => resources_repository_test.exs} | 32 +++++++++---------- test/support/fixtures/resources_fixtures.ex | 4 +-- 4 files changed, 21 insertions(+), 21 deletions(-) rename lib/radiator/{resources.ex => resources_repository.ex} (98%) rename test/radiator/{resources_test.exs => resources_repository_test.exs} (71%) diff --git a/lib/radiator/resources/node_changed_worker.ex b/lib/radiator/resources/node_changed_worker.ex index b8992d6f0..6935d0ede 100644 --- a/lib/radiator/resources/node_changed_worker.ex +++ b/lib/radiator/resources/node_changed_worker.ex @@ -6,7 +6,7 @@ defmodule Radiator.Resources.NodeChangedWorker do alias Radiator.EpisodeOutliner alias Radiator.NodeAnalyzer alias Radiator.Outline.NodeRepository - alias Radiator.Resources + alias Radiator.ResourcesRepository def trigger_analyze(node_id) do Radiator.Job.start_job( @@ -30,7 +30,7 @@ defmodule Radiator.Resources.NodeChangedWorker do |> Map.put(:episode_id, episode_id) end) - _created_urls = Resources.rebuild_node_urls(node_id, url_attributes) + _created_urls = ResourcesRepository.rebuild_node_urls(node_id, url_attributes) :ok end end diff --git a/lib/radiator/resources.ex b/lib/radiator/resources_repository.ex similarity index 98% rename from lib/radiator/resources.ex rename to lib/radiator/resources_repository.ex index 57f91f361..051369f3b 100644 --- a/lib/radiator/resources.ex +++ b/lib/radiator/resources_repository.ex @@ -1,4 +1,4 @@ -defmodule Radiator.Resources do +defmodule Radiator.ResourcesRepository do @moduledoc """ The Web context. All web related stuff, handing URLs, scraped Websites etc.. diff --git a/test/radiator/resources_test.exs b/test/radiator/resources_repository_test.exs similarity index 71% rename from test/radiator/resources_test.exs rename to test/radiator/resources_repository_test.exs index d92e54fc7..7b7ed3071 100644 --- a/test/radiator/resources_test.exs +++ b/test/radiator/resources_repository_test.exs @@ -1,4 +1,4 @@ -defmodule Radiator.ResourcesbTest do +defmodule Radiator.ResourcesRepositoryTest do use Radiator.DataCase import Ecto.Query, warn: false @@ -6,7 +6,7 @@ defmodule Radiator.ResourcesbTest do alias Radiator.OutlineFixtures alias Radiator.PodcastFixtures - alias Radiator.Resources + alias Radiator.ResourcesRepository alias Radiator.Resources.Url @invalid_attrs %{url: nil, start_bytes: nil, size_bytes: nil} @@ -16,7 +16,7 @@ defmodule Radiator.ResourcesbTest do test "returns all urls of an episode", %{episode: episode, node: node} do url = url_fixture(node_id: node.uuid, episode_id: episode.id) - assert Resources.list_urls_by_episode(episode.id) == [url] + assert ResourcesRepository.list_urls_by_episode(episode.id) == [url] end end @@ -25,7 +25,7 @@ defmodule Radiator.ResourcesbTest do test "get_url!/1 returns the url with given id" do url = url_fixture() - assert Resources.get_url!(url.id) == url + assert ResourcesRepository.get_url!(url.id) == url end end @@ -35,14 +35,14 @@ defmodule Radiator.ResourcesbTest do test "creates a url with valid data", %{node: node} do valid_attrs = %{url: "some url", start_bytes: 42, size_bytes: 42, node_id: node.uuid} - assert {:ok, %Url{} = url} = Resources.create_url(valid_attrs) + assert {:ok, %Url{} = url} = ResourcesRepository.create_url(valid_attrs) assert url.url == "some url" assert url.start_bytes == 42 assert url.size_bytes == 42 end test "with invalid data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = Resources.create_url(@invalid_attrs) + assert {:error, %Ecto.Changeset{}} = ResourcesRepository.create_url(@invalid_attrs) end end @@ -60,7 +60,7 @@ defmodule Radiator.ResourcesbTest do episode_id = episode.id assert [%Url{url: ^url_text, start_bytes: 42, size_bytes: 42, episode_id: ^episode_id}] = - Resources.rebuild_node_urls(node.uuid, [ + ResourcesRepository.rebuild_node_urls(node.uuid, [ %{ url: url_text, start_bytes: 42, @@ -70,7 +70,7 @@ defmodule Radiator.ResourcesbTest do } ]) - assert_raise Ecto.NoResultsError, fn -> Resources.get_url!(old_url.id) end + assert_raise Ecto.NoResultsError, fn -> ResourcesRepository.get_url!(old_url.id) end end end @@ -81,7 +81,7 @@ defmodule Radiator.ResourcesbTest do url = url_fixture() update_attrs = %{url: "some updated url", start_bytes: 43, size_bytes: 43} - assert {:ok, %Url{} = url} = Resources.update_url(url, update_attrs) + assert {:ok, %Url{} = url} = ResourcesRepository.update_url(url, update_attrs) assert url.url == "some updated url" assert url.start_bytes == 43 assert url.size_bytes == 43 @@ -89,16 +89,16 @@ defmodule Radiator.ResourcesbTest do test "with invalid data returns error changeset" do url = url_fixture() - assert {:error, %Ecto.Changeset{}} = Resources.update_url(url, @invalid_attrs) - assert url == Resources.get_url!(url.id) + assert {:error, %Ecto.Changeset{}} = ResourcesRepository.update_url(url, @invalid_attrs) + assert url == ResourcesRepository.get_url!(url.id) end end describe "delete_url/1" do test " deletes the url" do url = url_fixture() - assert {:ok, %Url{}} = Resources.delete_url(url) - assert_raise Ecto.NoResultsError, fn -> Resources.get_url!(url.id) end + assert {:ok, %Url{}} = ResourcesRepository.delete_url(url) + assert_raise Ecto.NoResultsError, fn -> ResourcesRepository.get_url!(url.id) end end end @@ -107,15 +107,15 @@ defmodule Radiator.ResourcesbTest do node = OutlineFixtures.node_fixture() url = url_fixture(node_id: node.uuid) - assert 1 = Resources.delete_urls_for_node(node) - assert_raise Ecto.NoResultsError, fn -> Resources.get_url!(url.id) end + assert 1 = ResourcesRepository.delete_urls_for_node(node) + assert_raise Ecto.NoResultsError, fn -> ResourcesRepository.get_url!(url.id) end end end describe "change_url/1" do test "returns a url changeset" do url = url_fixture() - assert %Ecto.Changeset{} = Resources.change_url(url) + assert %Ecto.Changeset{} = ResourcesRepository.change_url(url) end end diff --git a/test/support/fixtures/resources_fixtures.ex b/test/support/fixtures/resources_fixtures.ex index 53636a5fb..2269b2f60 100644 --- a/test/support/fixtures/resources_fixtures.ex +++ b/test/support/fixtures/resources_fixtures.ex @@ -5,7 +5,7 @@ defmodule Radiator.ResourcesFixtures do """ alias Radiator.OutlineFixtures alias Radiator.PodcastFixtures - alias Radiator.Resources + alias Radiator.ResourcesRepository @doc """ Generate a url. @@ -24,7 +24,7 @@ defmodule Radiator.ResourcesFixtures do node_id: node_id, episode_id: episode_id }) - |> Resources.create_url() + |> ResourcesRepository.create_url() url end From 078ec2599d09dedfdec7a1a37db2afa8c6fa6ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20W=C3=B6ginger?= Date: Thu, 17 Apr 2025 22:11:27 +0200 Subject: [PATCH 2/3] enhance seeds, add some urls to the inbox --- priv/repo/seeds.exs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index b031009a9..2aee5a3ff 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -122,16 +122,24 @@ inbox_id = show.inbox_node_container_id {:ok, inbox11} = NodeRepository.create_node(%{ - "content" => "Inbox 1", + "content" => "https://metaebene.me", "container_id" => inbox_id, "parent_id" => inbox1.uuid, "prev_id" => nil }) -{:ok, _inbox12} = +{:ok, inbox12} = NodeRepository.create_node(%{ - "content" => "Inbox 2", + "content" => "https://freakshow.fm", "container_id" => inbox_id, "parent_id" => inbox1.uuid, "prev_id" => inbox11.uuid }) + +{:ok, _inbox13} = + NodeRepository.create_node(%{ + "content" => "https://logbuch-netzpolitik.de", + "container_id" => inbox_id, + "parent_id" => inbox1.uuid, + "prev_id" => inbox12.uuid + }) From b3dcc5bd9f92f63d319a3fba9a81944cbb83667f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Wo=CC=88ginger?= Date: Sat, 19 Apr 2025 17:02:36 +0200 Subject: [PATCH 3/3] store node id for raindrop inbox folder in mapping --- lib/radiator/accounts/raindrop.ex | 84 ++++++++++++++++++- .../accounts/web_service/raindrop_service.ex | 14 +++- lib/radiator/outline/node_change_listener.ex | 17 +++- lib/radiator/podcast.ex | 19 +++++ test/radiator/accounts/web_service_test.exs | 59 +++++++++++++ test/radiator/resources_repository_test.exs | 2 +- test/radiator_web/live/outline_live_test.exs | 2 +- 7 files changed, 188 insertions(+), 9 deletions(-) diff --git a/lib/radiator/accounts/raindrop.ex b/lib/radiator/accounts/raindrop.ex index c0537a0a4..965de8bbc 100644 --- a/lib/radiator/accounts/raindrop.ex +++ b/lib/radiator/accounts/raindrop.ex @@ -77,13 +77,55 @@ defmodule Radiator.Accounts.Raindrop do iex> connect_show_with_raindrop(999, 23, 42) {:error, "No Raindrop tokens found"} """ - def connect_show_with_raindrop(user_id, show_id, collection_id, node_id \\ nil) do + def connect_show_with_raindrop(user_id, show_id, collection_id) do case get_raindrop_tokens(user_id) do nil -> {:error, "No Raindrop tokens found"} %{data: data} = service -> - updated_mappings = update_mappings(data.mappings, show_id, collection_id, node_id) + # Ensure mappings is a list + mappings = data.mappings || [] + updated_mappings = update_mappings(mappings, show_id, collection_id, nil) + + service + |> WebService.changeset(%{ + data: %{ + access_token: data.access_token, + refresh_token: data.refresh_token, + expires_at: data.expires_at, + mappings: updated_mappings + } + }) + |> Repo.update() + end + end + + @doc """ + Saves an inbox node for a show for the Randrop-service. + Preserves the existing collection_id that was previously set for this show and user. + + ## Examples + + iex> set_inbox_node_for_raindrop(1, 23, 42) + {:ok, %WebService{}} + + iex> set_inbox_node_for_raindrop(999, 23, 42) + {:error, "No Raindrop tokens found"} + """ + def set_inbox_node_for_raindrop(user_id, show_id, node_id) do + case get_raindrop_tokens(user_id) do + nil -> + {:error, "No Raindrop tokens found"} + + %{data: data} = service -> + # Ensure mappings is a list + mappings = data.mappings || [] + + # Find existing collection_id for this specific user and show + existing_mapping = Enum.find(mappings, fn mapping -> mapping.show_id == show_id end) + collection_id = if existing_mapping, do: existing_mapping.collection_id, else: nil + + updated_mappings = update_mappings(mappings, show_id, collection_id, node_id) service |> WebService.changeset(%{ @@ -100,9 +142,45 @@ defmodule Radiator.Accounts.Raindrop do # Filter out any existing mapping with the same show_id and convert to maps defp update_mappings(mappings, show_id, collection_id, node_id) do - mappings + (mappings || []) |> Enum.reject(fn mapping -> mapping.show_id == show_id end) |> Enum.map(&Map.from_struct/1) |> Kernel.++([%{show_id: show_id, node_id: node_id, collection_id: collection_id}]) end + + @doc """ + Finds the user_id associated with a given show_id through Raindrop mappings. + This is only a temporal hack, should be replaced because this might be ambigous in the future!! + + ## Examples + + iex> find_user_id_by_show_id(42) + {:ok, 23} + + iex> find_user_id_by_show_id(999) + {:error, :not_found} + + """ + def find_user_id_by_show_id(show_id) do + service_name = WebService.raindrop_service_name() + + query = + from(ws in "web_services", + where: ws.service_name == ^service_name, + join: + m in fragment( + "jsonb_array_elements(?->'mappings')", + ws.data + ), + on: true, + where: fragment("(?->>'show_id')::int = ?", m, ^show_id), + where: fragment("?->'node_id' IS NULL", m), + select: ws.user_id + ) + + case Repo.one(query) do + nil -> {:error, :not_found} + user_id -> {:ok, user_id} + end + end end diff --git a/lib/radiator/accounts/web_service/raindrop_service.ex b/lib/radiator/accounts/web_service/raindrop_service.ex index ed28277ff..bca4a6b6d 100644 --- a/lib/radiator/accounts/web_service/raindrop_service.ex +++ b/lib/radiator/accounts/web_service/raindrop_service.ex @@ -33,8 +33,16 @@ defmodule Radiator.Accounts.WebService.RaindropService do end defp mapping_changeset(mapping, attrs) do - mapping - |> cast(attrs, [:show_id, :collection_id, :node_id]) - |> validate_required([:show_id, :collection_id]) + changeset = + mapping + |> cast(attrs, [:show_id, :collection_id, :node_id]) + |> validate_required([:show_id]) + + # Make collection_id required only if node_id is not being set + if get_change(changeset, :node_id) do + changeset + else + validate_required(changeset, [:collection_id]) + end end end diff --git a/lib/radiator/outline/node_change_listener.ex b/lib/radiator/outline/node_change_listener.ex index 0538e1ae4..0b873d887 100644 --- a/lib/radiator/outline/node_change_listener.ex +++ b/lib/radiator/outline/node_change_listener.ex @@ -14,6 +14,7 @@ defmodule Radiator.Outline.NodeChangeListener do NodeMovedEvent } + alias Radiator.Accounts.Raindrop alias Radiator.Outline.Dispatch alias Radiator.Resources.NodeChangedWorker @@ -59,8 +60,22 @@ defmodule Radiator.Outline.NodeChangeListener do defp process_system_nodes_if(%NodeInsertedEvent{ user_id: nil, - node: %{content: "raindrop", container_id: _container_id, uuid: _uuid} + node: %{content: "raindrop", container_id: container_id, uuid: raindrop_node_uuid} }) do + show = Radiator.Podcast.get_show_with_inbox_id(container_id) + + case Raindrop.find_user_id_by_show_id(show.id) do + {:error, :not_found} -> + Logger.error("User not found for show #{show.id}") + + {:ok, user_id} -> + Raindrop.set_inbox_node_for_raindrop( + user_id, + show.id, + raindrop_node_uuid + ) + end + :ok end diff --git a/lib/radiator/podcast.ex b/lib/radiator/podcast.ex index 5e364e60d..d4e6ee504 100644 --- a/lib/radiator/podcast.ex +++ b/lib/radiator/podcast.ex @@ -351,6 +351,25 @@ defmodule Radiator.Podcast do Show.changeset(show, attrs) end + @doc """ + Returns a show with the given inbox node container id. + + ## Examples + + iex> get_show_with_inbox_id(container_id) + %Show{} + + iex> get_show_with_inbox_id(container_id) + nil + + """ + def get_show_with_inbox_id(container_id) do + from(e in Show, + where: [inbox_node_container_id: ^container_id] + ) + |> Repo.one() + end + @doc """ A forced reload of preloaded associations. Usefull when only the associations have changed and Show does need to be reloaded. diff --git a/test/radiator/accounts/web_service_test.exs b/test/radiator/accounts/web_service_test.exs index cc6417e79..c0931448f 100644 --- a/test/radiator/accounts/web_service_test.exs +++ b/test/radiator/accounts/web_service_test.exs @@ -132,4 +132,63 @@ defmodule Radiator.Accounts.WebServiceTest do ] end end + + describe "set_inbox_node_for_raindrop/3" do + setup do + %{web_service: raindrop_service_fixture(), show: PodcastFixtures.show_fixture()} + end + + test "sets a node_id for a show while preserving the collection_id", %{ + web_service: web_service, + show: show + } do + # First connect a show with a collection + {:ok, _} = Raindrop.connect_show_with_raindrop(web_service.user_id, show.id, 42) + + # Now set a node_id for this show + node_id = Ecto.UUID.generate() + {:ok, _} = Raindrop.set_inbox_node_for_raindrop(web_service.user_id, show.id, node_id) + + # Get the updated service and check + service = Raindrop.get_raindrop_tokens(web_service.user_id) + + mappings = Enum.map(service.data.mappings, &Map.from_struct/1) + assert length(mappings) == 1 + mapping = List.first(mappings) + assert mapping.collection_id == 42 + assert mapping.node_id == node_id + assert mapping.show_id == show.id + end + + test "sets a node_id for a show without existing collection_id", %{ + web_service: web_service, + show: show + } do + # Set a node_id directly without first creating a mapping + node_id = Ecto.UUID.generate() + {:ok, _} = Raindrop.set_inbox_node_for_raindrop(web_service.user_id, show.id, node_id) + + # Get the updated service and check + service = Raindrop.get_raindrop_tokens(web_service.user_id) + + mappings = Enum.map(service.data.mappings, &Map.from_struct/1) + assert length(mappings) == 1 + mapping = List.first(mappings) + assert mapping.collection_id == nil + assert mapping.node_id == node_id + assert mapping.show_id == show.id + end + + test "returns an error when no raindrop tokens are found", %{ + show: show + } do + # Use a non-existent user ID + non_existent_user_id = 999_999 + node_id = Ecto.UUID.generate() + + result = Raindrop.set_inbox_node_for_raindrop(non_existent_user_id, show.id, node_id) + + assert result == {:error, "No Raindrop tokens found"} + end + end end diff --git a/test/radiator/resources_repository_test.exs b/test/radiator/resources_repository_test.exs index 7b7ed3071..327ab9bd2 100644 --- a/test/radiator/resources_repository_test.exs +++ b/test/radiator/resources_repository_test.exs @@ -6,8 +6,8 @@ defmodule Radiator.ResourcesRepositoryTest do alias Radiator.OutlineFixtures alias Radiator.PodcastFixtures - alias Radiator.ResourcesRepository alias Radiator.Resources.Url + alias Radiator.ResourcesRepository @invalid_attrs %{url: nil, start_bytes: nil, size_bytes: nil} diff --git a/test/radiator_web/live/outline_live_test.exs b/test/radiator_web/live/outline_live_test.exs index 0c2d4915f..855318144 100644 --- a/test/radiator_web/live/outline_live_test.exs +++ b/test/radiator_web/live/outline_live_test.exs @@ -13,7 +13,7 @@ defmodule RadiatorWeb.OutlineLiveTest do user = user_fixture() show = show_fixture() - %{id: episode_id, outline_node_container_id: outline_node_container_id} = + %{outline_node_container_id: outline_node_container_id} = episode_fixture(%{show_id: show.id}) node_1 =