diff --git a/lib/teiserver/account/libs/account_test_lib.ex b/lib/teiserver/account/libs/account_test_lib.ex index 63d4234cf..d36edea57 100644 --- a/lib/teiserver/account/libs/account_test_lib.ex +++ b/lib/teiserver/account/libs/account_test_lib.ex @@ -14,7 +14,8 @@ defmodule Teiserver.Account.AccountTestLib do name: data["name"] || "name_#{r}", email: data["email"] || "email_#{r}", colour: data["colour"] || "colour", - icon: data["icon"] || "icon" + icon: data["icon"] || "icon", + last_login_timex: data["last_login_timex"] || Timex.now() }, :script ) diff --git a/lib/teiserver/account/libs/relationship_lib.ex b/lib/teiserver/account/libs/relationship_lib.ex index e186021cb..5945a032b 100644 --- a/lib/teiserver/account/libs/relationship_lib.ex +++ b/lib/teiserver/account/libs/relationship_lib.ex @@ -388,4 +388,48 @@ defmodule Teiserver.Account.RelationshipLib do results.rows end + + # Deletes inactive users in the ignore/avoid/block list of a user + # Returns the number of deletions + def delete_inactive_ignores_avoids_blocks(user_id, days_not_logged_in) do + query = """ + delete from account_relationships ar + using account_users au + where au.id = ar.to_user_id + and ar.from_user_id = $1 + and (au.last_login_timex is null OR + abs(DATE_PART('day', (now()- au.last_login_timex ))) > $2); + """ + + results = + Ecto.Adapters.SQL.query!(Repo, query, [ + user_id, + days_not_logged_in + ]) + + decache_relationships(user_id) + + results.num_rows + end + + def get_inactive_ignores_avoids_blocks_count(user_id, days_not_logged_in) do + query = + """ + select count(*) from account_relationships ar + join account_users au + on au.id = ar.to_user_id + and ar.from_user_id = $1 + and (au.last_login_timex is null OR + abs(DATE_PART('day', (now()- au.last_login_timex ))) > $2); + """ + + result = + Ecto.Adapters.SQL.query!(Repo, query, [ + user_id, + days_not_logged_in + ]) + + [[inactive_count]] = result.rows + inactive_count + end end diff --git a/lib/teiserver_web/live/account/relationship/index.ex b/lib/teiserver_web/live/account/relationship/index.ex index 596b47829..a9c6c0657 100644 --- a/lib/teiserver_web/live/account/relationship/index.ex +++ b/lib/teiserver_web/live/account/relationship/index.ex @@ -1,5 +1,6 @@ defmodule TeiserverWeb.Account.RelationshipLive.Index do @moduledoc false + alias Teiserver.Account.RelationshipLib use TeiserverWeb, :live_view alias Teiserver.Account @@ -12,6 +13,8 @@ defmodule TeiserverWeb.Account.RelationshipLive.Index do |> assign(:view_colour, Account.RelationshipLib.colour()) |> assign(:show_help, false) |> put_empty_relationships + |> assign(:purge_cutoff, get_default_purge_cutoff_option()) + |> assign(:purge_cutoff_options, get_purge_cutoff_options()) {:ok, socket} end @@ -54,6 +57,14 @@ defmodule TeiserverWeb.Account.RelationshipLive.Index do |> update_user_search end + defp apply_action(socket, :clean, _params) do + socket + |> assign(:page_title, "Relationships - Cleanup") + |> assign(:tab, :clean) + |> get_friends() + |> get_inactive_relationship_count() + end + @impl true def handle_event("show-help", _, socket) do {:noreply, socket |> assign(:show_help, true)} @@ -255,6 +266,56 @@ defmodule TeiserverWeb.Account.RelationshipLive.Index do {:noreply, socket} end + def handle_event("purge-avoids", _params, socket) do + userid = socket.assigns.current_user.id + duration = socket.assigns[:purge_cutoff] + days = get_purge_days_cutoff(duration) + num_rows = RelationshipLib.delete_inactive_ignores_avoids_blocks(userid, days) + + socket = + socket |> assign(:purge_avoids_message, "#{num_rows} inactive users purged.") + + {:noreply, socket} + end + + def handle_event("purge-friends", _params, socket) do + duration = socket.assigns[:purge_cutoff] + days_cutoff = get_purge_days_cutoff(duration) + + # Get all friends of this user + friends = socket.assigns[:friends] + + num_friends_deleted = + get_inactive_friends(friends, days_cutoff) + |> Enum.map(fn friend -> + Account.delete_friend(friend) + nil + end) + |> length() + + socket = + socket |> assign(:purge_friends_message, "#{num_friends_deleted} inactive friends purged.") + + {:noreply, socket} + end + + @doc """ + Handles the dropdown for purge cutoff time + """ + @impl true + def handle_event("update-purge-cutoff", event, socket) do + [key] = event["_target"] + value = event[key] + + socket = + socket + |> assign(:purge_cutoff, value) + |> get_inactive_relationship_count() + |> get_inactive_friend_count() + + {:noreply, socket} + end + defp update_user_search( %{assigns: %{live_action: :search, search_terms: terms} = assigns} = socket ) do @@ -320,6 +381,20 @@ defmodule TeiserverWeb.Account.RelationshipLive.Index do |> assign(:found_relationship, nil) |> assign(:found_friendship, nil) |> assign(:found_friendship_request, nil) + |> assign(:inactive_friend_count, 0) + end + + defp get_inactive_relationship_count( + %{assigns: %{current_user: current_user, purge_cutoff: purge_cutoff}} = socket + ) do + user_id = current_user.id + days = get_purge_days_cutoff(purge_cutoff) + + inactive_relationship_count = + RelationshipLib.get_inactive_ignores_avoids_blocks_count(user_id, days) + + socket = socket |> assign(:inactive_relationship_count, inactive_relationship_count) + socket end defp get_friends(%{assigns: %{current_user: current_user}} = socket) do @@ -362,6 +437,7 @@ defmodule TeiserverWeb.Account.RelationshipLive.Index do |> assign(:incoming_friend_requests, incoming_friend_requests) |> assign(:outgoing_friend_requests, outgoing_friend_requests) |> assign(:friends, friends) + |> get_inactive_friend_count() end defp get_follows(%{assigns: %{current_user: current_user}} = socket) do @@ -413,4 +489,62 @@ defmodule TeiserverWeb.Account.RelationshipLive.Index do |> assign(:ignores, ignores) |> assign(:blocks, blocks) end + + def get_purge_cutoff_options() do + ["1 month", "3 months", "6 months", "1 year"] + end + + def get_default_purge_cutoff_option() do + "6 months" + end + + @spec get_purge_days_cutoff(String.t()) :: float() + def get_purge_days_cutoff(duration) do + with [_, raw_number, type] <- Regex.run(~r/(\d)+ (month|year)/, duration), + {number, ""} <- Integer.parse(raw_number) do + cond do + type == "year" -> + number * 365 + + type == "month" -> + number * 365.0 / 12 + end + else + nil -> {:error, "invalid duration passed: #{duration}"} + {_, _rest} -> {:error, "invalid number in duration #{duration}"} + end + end + + defp get_inactive_friend_count(socket) do + friends = socket.assigns.friends + purge_cutoff = socket.assigns.purge_cutoff + + socket = + socket |> assign(:inactive_friend_count, get_inactive_friend_count(friends, purge_cutoff)) + + socket + end + + defp get_inactive_friend_count(friends, purge_cutoff_text) do + days_cutoff = get_purge_days_cutoff(purge_cutoff_text) + + get_inactive_friends(friends, days_cutoff) + |> length() + end + + def get_inactive_friends(friends, days_cutoff) do + Enum.filter(friends, fn friend -> + last_login = friend.other_user.last_login_timex + days = get_days_diff(last_login, Timex.now()) + days > days_cutoff + end) + end + + def get_days_diff(datetime1, datetime2) do + cond do + datetime1 == nil -> 0 + datetime2 == nil -> 0 + true -> abs(DateTime.diff(datetime1, datetime2, :day)) + end + end end diff --git a/lib/teiserver_web/live/account/relationship/index.html.heex b/lib/teiserver_web/live/account/relationship/index.html.heex index 3c6f2fcc2..7d6cf01ed 100644 --- a/lib/teiserver_web/live/account/relationship/index.html.heex +++ b/lib/teiserver_web/live/account/relationship/index.html.heex @@ -23,6 +23,9 @@ <.tab_nav url={~p"/account/relationship/search"} selected={@tab == :search}> Search users + <.tab_nav url={~p"/account/relationship/clean"} selected={@tab == :clean}> + Cleanup + @@ -412,3 +415,62 @@ + +
+
+

+ Avoid Cleanup +

+ +
+ + Purge users from your ignore, avoid, and block list that have not logged in for: + + <.input + type="select" + options={@purge_cutoff_options} + name="algorithm" + value={@purge_cutoff} + phx-change="update-purge-cutoff" + /> +
+ Purge inactive ignores / avoids / blocks +
+ <%= if(assigns[:purge_avoids_message]) do %> +

<%= @purge_avoids_message %>

+ <% end %> +
+
+
+
+

+ Friend Cleanup +

+
+ + Purge users from your friend list that have not logged in for: + + <.input + type="select" + options={@purge_cutoff_options} + name="algorithm" + value={@purge_cutoff} + phx-change="update-purge-cutoff" + /> +
+ Purge inactive Friends +
+ <%= if(assigns[:purge_friends_message]) do %> +

<%= @purge_friends_message %>

+ <% end %> +
+
+
diff --git a/lib/teiserver_web/router.ex b/lib/teiserver_web/router.ex index af71d69a0..d43965432 100644 --- a/lib/teiserver_web/router.ex +++ b/lib/teiserver_web/router.ex @@ -226,6 +226,7 @@ defmodule TeiserverWeb.Router do live "/relationship/follow", RelationshipLive.Index, :follow live "/relationship/avoid", RelationshipLive.Index, :avoid live "/relationship/search", RelationshipLive.Index, :search + live "/relationship/clean", RelationshipLive.Index, :clean end live_session :account_settings, diff --git a/test/teiserver/account/relationship_lib_test.exs b/test/teiserver/account/relationship_lib_test.exs new file mode 100644 index 000000000..139620719 --- /dev/null +++ b/test/teiserver/account/relationship_lib_test.exs @@ -0,0 +1,58 @@ +defmodule Teiserver.Account.RelationshipLibTest do + use Teiserver.DataCase, async: true + + alias Teiserver.Account.AccountTestLib + alias Teiserver.Account.RelationshipLib + + test "purging inactive relationships" do + # Create two accounts + user1 = AccountTestLib.user_fixture() + user2 = AccountTestLib.user_fixture() + + old_login = DateTime.add(Timex.now(), -31, :day) + + user3 = + AccountTestLib.user_fixture(%{ + "last_login_timex" => old_login + }) + + assert user1.id != nil + assert user2.id != nil + assert user3.id != nil + + RelationshipLib.avoid_user(user1.id, user2.id) + RelationshipLib.avoid_user(user1.id, user3.id) + + avoid_list = RelationshipLib.list_userids_avoided_by_userid(user1.id) + assert Enum.member?(avoid_list, user2.id) + assert Enum.member?(avoid_list, user3.id) + + # Check count of inactive relationships + inactive_count = RelationshipLib.get_inactive_ignores_avoids_blocks_count(user1.id, 30) + assert inactive_count == 1 + + # Purge old relationships + RelationshipLib.delete_inactive_ignores_avoids_blocks(user1.id, 30) + + avoid_list = RelationshipLib.list_userids_avoided_by_userid(user1.id) + assert Enum.member?(avoid_list, user2.id) + refute Enum.member?(avoid_list, user3.id) + + # Add users to ignore list + RelationshipLib.ignore_user(user1.id, user2.id) + RelationshipLib.ignore_user(user1.id, user3.id) + + # Check ignores + ignore_list = RelationshipLib.list_userids_ignored_by_userid(user1.id) + assert Enum.member?(ignore_list, user2.id) + assert Enum.member?(ignore_list, user3.id) + + # Purge ignores + RelationshipLib.delete_inactive_ignores_avoids_blocks(user1.id, 30) + + # Check ignores again + ignore_list = RelationshipLib.list_userids_ignored_by_userid(user1.id) + assert Enum.member?(ignore_list, user2.id) + refute Enum.member?(ignore_list, user3.id) + end +end diff --git a/test/teiserver_web/live/account/relationship/index_live_test.exs b/test/teiserver_web/live/account/relationship/index_live_test.exs index 9fcf12006..897e0014e 100644 --- a/test/teiserver_web/live/account/relationship/index_live_test.exs +++ b/test/teiserver_web/live/account/relationship/index_live_test.exs @@ -1,6 +1,6 @@ defmodule TeiserverWeb.Account.RelationshipLive.IndexLiveTest do use TeiserverWeb.ConnCase, async: true - + alias TeiserverWeb.Account.RelationshipLive.Index alias Central.Helpers.GeneralTestLib test "account relationship endpoints requires authentication" do @@ -24,4 +24,34 @@ defmodule TeiserverWeb.Account.RelationshipLive.IndexLiveTest do conn = get(conn, ~p"/account/relationship") html_response(conn, 200) end + + test "purge cutoff options are valid" do + options = Index.get_purge_cutoff_options() + assert length(options) > 0 + + default_option = Index.get_default_purge_cutoff_option() + assert Enum.member?(options, default_option) + + Enum.map(options, fn option -> + assert is_number(Index.get_purge_days_cutoff(option)) + end) + end + + test "get_purge_days_cutoff returns error" do + assert Index.get_purge_days_cutoff("20 days") == {:error, "invalid duration passed: 20 days"} + + assert Index.get_purge_days_cutoff("x months") == + {:error, "invalid duration passed: x months"} + end + + test "get_days_diff works" do + now = Timex.now() + other_date = DateTime.add(now, -400, :day) + result = Index.get_days_diff(now, other_date) + assert result == 400 + + other_date = nil + result = Index.get_days_diff(now, other_date) + assert result == 0 + end end