From c61a54d80294de5088b1291f7bb8ce16144526f2 Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Wed, 10 Jun 2020 14:28:27 +0200
Subject: [PATCH] Add a command to refresh a single actor or all actors

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 lib/mix/tasks/mobilizon/actors/refresh.ex | 98 +++++++++++++++++++++++
 lib/mobilizon/actors/actors.ex            | 27 ++++++-
 2 files changed, 123 insertions(+), 2 deletions(-)
 create mode 100644 lib/mix/tasks/mobilizon/actors/refresh.ex

diff --git a/lib/mix/tasks/mobilizon/actors/refresh.ex b/lib/mix/tasks/mobilizon/actors/refresh.ex
new file mode 100644
index 000000000..55efdbc17
--- /dev/null
+++ b/lib/mix/tasks/mobilizon/actors/refresh.ex
@@ -0,0 +1,98 @@
+defmodule Mix.Tasks.Mobilizon.Actors.Refresh do
+  @moduledoc """
+  Task to display an actor details
+  """
+  use Mix.Task
+  alias Mobilizon.Actors.Actor
+  alias Mobilizon.Federation.ActivityPub
+  alias Mobilizon.Storage.Repo
+  import Ecto.Query
+  require Logger
+
+  @shortdoc "Refresh an actor or all actors"
+
+  @impl Mix.Task
+  def run(["--all" | options]) do
+    {options, [], []} =
+      OptionParser.parse(
+        options,
+        strict: [
+          verbose: :boolean
+        ],
+        aliases: [
+          v: :verbose
+        ]
+      )
+
+    verbose = Keyword.get(options, :verbose, false)
+
+    Mix.Task.run("app.start")
+
+    total = count_actors()
+
+    Mix.shell().info("""
+    #{total} actors to process
+    """)
+
+    query = from(a in Actor, where: not is_nil(a.domain))
+
+    {:ok, _res} =
+      Repo.transaction(
+        fn ->
+          query
+          |> Repo.stream(timeout: :infinity)
+          |> Stream.map(&"#{&1.preferred_username}@#{&1.domain}")
+          |> Stream.each(
+            if verbose,
+              do: &Logger.info("Processing #{inspect(&1)}"),
+              else: &Logger.debug("Processing #{inspect(&1)}")
+          )
+          |> Stream.map(fn username -> make_actor(username, verbose) end)
+          |> Stream.scan(0, fn _, acc -> acc + 1 end)
+          |> Stream.each(fn index ->
+            if verbose,
+              do: Logger.info("#{index}/#{total}"),
+              else: ProgressBar.render(index, total)
+          end)
+          |> Stream.run()
+        end,
+        timeout: :infinity
+      )
+  end
+
+  @impl Mix.Task
+  def run([preferred_username]) do
+    Mix.Task.run("app.start")
+
+    case ActivityPub.make_actor_from_nickname(preferred_username) do
+      {:ok, %Actor{}} ->
+        Mix.shell().info("""
+        Actor #{preferred_username} refreshed
+        """)
+
+      {:actor, nil} ->
+        Mix.raise("Error: No such actor")
+    end
+  end
+
+  @impl Mix.Task
+  def run(_) do
+    Mix.raise("mobilizon.actors.refresh requires an username as argument or --all as an option")
+  end
+
+  @spec make_actor(String.t(), boolean()) :: any()
+  defp make_actor(username, verbose) do
+    ActivityPub.make_actor_from_nickname(username)
+  rescue
+    _ ->
+      if verbose do
+        Logger.warn("Failed to refresh #{username}")
+      end
+
+      nil
+  end
+
+  defp count_actors do
+    Repo.aggregate(from(a in Actor, where: not is_nil(a.domain)), :count)
+  end
+end
diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex
index ae339d4f5..93b82483d 100644
--- a/lib/mobilizon/actors/actors.ex
+++ b/lib/mobilizon/actors/actors.ex
@@ -210,12 +210,23 @@ defmodule Mobilizon.Actors do
   Conflicts on actor's URL/AP ID, replaces keys, avatar and banner, name and summary.
   """
   @spec upsert_actor(map, boolean) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
-  def upsert_actor(%{keys: keys, name: name, summary: summary} = data, preload \\ false) do
+  def upsert_actor(
+        %{keys: keys, name: name, summary: summary, avatar: avatar, banner: banner} = data,
+        preload \\ false
+      ) do
     insert =
       data
       |> Actor.remote_actor_creation_changeset()
       |> Repo.insert(
-        on_conflict: [set: [keys: keys, name: name, summary: summary]],
+        on_conflict: [
+          set: [
+            keys: keys,
+            name: name,
+            summary: summary,
+            avatar: transform_media_file(avatar),
+            banner: transform_media_file(banner)
+          ]
+        ],
         conflict_target: [:url]
       )
 
@@ -232,6 +243,18 @@ defmodule Mobilizon.Actors do
     end
   end
 
+  defp transform_media_file(nil), do: nil
+
+  defp transform_media_file(file) do
+    file = for({key, val} <- file, into: %{}, do: {String.to_atom(key), val})
+
+    if is_nil(file) do
+      nil
+    else
+      struct(Mobilizon.Media.File, file)
+    end
+  end
+
   def delete_actor(%Actor{} = actor) do
     Workers.Background.enqueue("delete_actor", %{"actor_id" => actor.id})
   end