diff --git a/js/src/components/Admin/Followers.vue b/js/src/components/Admin/Followers.vue
index 7dfcefee0..6b491fc24 100644
--- a/js/src/components/Admin/Followers.vue
+++ b/js/src/components/Admin/Followers.vue
@@ -43,18 +43,20 @@
- {{
- props.row.updatedAt | formatDateTimeString
- }}
+
+ {{
+ timeago(props.row.updatedAt)
+ }}
-
{{ props.row.actor.domain }}
-
@{{ props.row.actor.preferredUsername }}
-
31m
-
+
{{ props.row.actor.name }}
+
@{{ props.row.actor.preferredUsername }}
diff --git a/js/src/components/Admin/Followings.vue b/js/src/components/Admin/Followings.vue
index 88e7f831d..985898227 100644
--- a/js/src/components/Admin/Followings.vue
+++ b/js/src/components/Admin/Followings.vue
@@ -56,18 +56,20 @@
- {{
- props.row.updatedAt | formatDateTimeString
- }}
+
+ {{
+ timeago(props.row.updatedAt)
+ }}
-
{{ props.row.targetActor.domain }}
-
@{{ props.row.targetActor.preferredUsername }}
-
31m
-
+
{{ props.row.targetActor.name }}
+
@{{ props.row.targetActor.preferredUsername }}
diff --git a/js/src/mixins/relay.ts b/js/src/mixins/relay.ts
index 477db1d2b..a01fbdb46 100644
--- a/js/src/mixins/relay.ts
+++ b/js/src/mixins/relay.ts
@@ -1,6 +1,7 @@
import { Component, Vue, Ref } from "vue-property-decorator";
import { ActorType, IActor } from "@/types/actor";
import { IFollower } from "@/types/actor/follower.model";
+import TimeAgo from "javascript-time-ago";
@Component
export default class RelayMixin extends Vue {
@@ -12,6 +13,15 @@ export default class RelayMixin extends Vue {
perPage = 10;
+ timeAgoInstance: TimeAgo | null = null;
+
+ async mounted() {
+ const localeName = this.$i18n.locale;
+ const locale = await import(`javascript-time-ago/locale/${localeName}`);
+ TimeAgo.addLocale(locale);
+ this.timeAgoInstance = new TimeAgo(localeName);
+ }
+
toggle(row: object) {
this.table.toggleDetails(row);
}
@@ -43,4 +53,11 @@ export default class RelayMixin extends Vue {
(actor.preferredUsername === "relay" || actor.preferredUsername === actor.domain)
);
}
+
+ timeago(dateTime: string): string {
+ if (this.timeAgoInstance != null) {
+ return this.timeAgoInstance.format(new Date(dateTime));
+ }
+ return "";
+ }
}
diff --git a/lib/federation/activity_pub/activity_pub.ex b/lib/federation/activity_pub/activity_pub.ex
index d6b489d92..a71c9e262 100644
--- a/lib/federation/activity_pub/activity_pub.ex
+++ b/lib/federation/activity_pub/activity_pub.ex
@@ -720,7 +720,7 @@ defmodule Mobilizon.Federation.ActivityPub do
"""
# credo:disable-for-lines:47
@spec publish(Actor.t(), Activity.t()) :: :ok
- def publish(actor, activity) do
+ def publish(actor, %Activity{recipients: recipients} = activity) do
Logger.debug("Publishing an activity")
Logger.debug(inspect(activity))
@@ -733,24 +733,26 @@ defmodule Mobilizon.Federation.ActivityPub do
Relay.publish(activity)
end
- followers =
+ {recipients, followers} =
if actor.followers_url in activity.recipients do
- Actors.list_external_followers_for_actor(actor)
+ {Enum.filter(recipients, fn recipient -> recipient != actor.followers_url end),
+ Actors.list_external_followers_for_actor(actor)}
else
- []
+ {recipients, []}
end
# If we want to send to all members of the group, because this server is the one the group is on
- members =
+ {recipients, members} =
if is_announce_activity?(activity) and actor.type == :Group and
actor.members_url in activity.recipients and is_nil(actor.domain) do
- Actors.list_external_members_for_group(actor)
+ {Enum.filter(recipients, fn recipient -> recipient != actor.members_url end),
+ Actors.list_external_members_for_group(actor)}
else
- []
+ {recipients, []}
end
remote_inboxes =
- (remote_actors(activity) ++ followers ++ members)
+ (remote_actors(recipients) ++ followers ++ members)
|> Enum.map(fn follower -> follower.shared_inbox_url || follower.inbox_url end)
|> Enum.uniq()
@@ -1045,7 +1047,7 @@ defmodule Mobilizon.Federation.ActivityPub do
end
end
- @spec update_actor(Todo.t(), map, map) :: {:ok, Todo.t(), Activity.t()} | any
+ @spec update_todo(Todo.t(), map, map) :: {:ok, Todo.t(), Activity.t()} | any
defp update_todo(%Todo{} = old_todo, args, additional) do
with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args),
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
diff --git a/lib/federation/activity_pub/utils.ex b/lib/federation/activity_pub/utils.ex
index 5e969eb9d..7a8d1d860 100644
--- a/lib/federation/activity_pub/utils.ex
+++ b/lib/federation/activity_pub/utils.ex
@@ -114,10 +114,9 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
def maybe_federate(_), do: :ok
- def remote_actors(%{data: %{"to" => to} = data}) do
- to = to ++ (data["cc"] || [])
-
- to
+ @spec remote_actors(list(String.t())) :: list(Actor.t())
+ def remote_actors(recipients) do
+ recipients
|> Enum.map(fn url -> ActivityPub.get_or_fetch_actor_by_url(url) end)
|> Enum.map(fn {status, actor} ->
case status do
diff --git a/lib/federation/activity_stream/converter/actor.ex b/lib/federation/activity_stream/converter/actor.ex
index 950d431b4..39fd5dd3c 100644
--- a/lib/federation/activity_stream/converter/actor.ex
+++ b/lib/federation/activity_stream/converter/actor.ex
@@ -21,12 +21,14 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
defdelegate model_to_as(actor), to: ActorConverter
end
+ @allowed_types ["Application", "Group", "Organization", "Person", "Service"]
+
@doc """
Converts an AP object data to our internal data structure.
"""
@impl Converter
@spec as_to_model_data(map()) :: {:ok, map()}
- def as_to_model_data(data) do
+ def as_to_model_data(%{"type" => type} = data) when type in @allowed_types do
avatar =
data["icon"]["url"] &&
%{
@@ -62,6 +64,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
}
end
+ def as_to_model_data(_), do: :error
+
@doc """
Convert an actor struct to an ActivityStream representation.
"""
diff --git a/lib/graphql/resolvers/admin.ex b/lib/graphql/resolvers/admin.ex
index 29cd1470f..5ccb9eea5 100644
--- a/lib/graphql/resolvers/admin.ex
+++ b/lib/graphql/resolvers/admin.ex
@@ -11,6 +11,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
alias Mobilizon.Config
alias Mobilizon.Conversations.Comment
alias Mobilizon.Events.Event
+ alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Relay
alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Service.Statistics
@@ -158,21 +159,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
context: %{current_user: %User{role: role}}
})
when is_admin(role) do
- {:ok,
- %{
- instance_description: Config.instance_description(),
- instance_long_description: Config.instance_long_description(),
- instance_name: Config.instance_name(),
- registrations_open: Config.instance_registrations_open?(),
- contact: Config.contact(),
- instance_terms: Config.instance_terms(),
- instance_terms_type: Config.instance_terms_type(),
- instance_terms_url: Config.instance_terms_url(),
- instance_privacy_policy: Config.instance_privacy(),
- instance_privacy_policy_type: Config.instance_privacy_type(),
- instance_privacy_policy_url: Config.instance_privacy_url(),
- instance_rules: Config.instance_rules()
- }}
+ {:ok, Config.admin_settings()}
end
def get_settings(_parent, _args, _resolution) do
@@ -183,19 +170,20 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
context: %{current_user: %User{role: role}}
})
when is_admin(role) do
- with {:ok, res} <- Admin.save_settings("instance", args) do
- res =
- res
- |> Enum.map(fn {key, %Setting{value: value}} ->
- case value do
- "true" -> {key, true}
- "false" -> {key, false}
- value -> {key, value}
- end
- end)
- |> Enum.into(%{})
-
+ with {:ok, res} <- Admin.save_settings("instance", args),
+ res <-
+ res
+ |> Enum.map(fn {key, %Setting{value: value}} ->
+ case value do
+ "true" -> {key, true}
+ "false" -> {key, false}
+ value -> {key, value}
+ end
+ end)
+ |> Enum.into(%{}),
+ :ok <- eventually_update_instance_actor(res) do
Config.clear_config_cache()
+ Cachex.put(:config, :admin_config, res)
{:ok, res}
end
@@ -284,4 +272,39 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
{:error, err}
end
end
+
+ @spec eventually_update_instance_actor(map()) :: :ok
+ defp eventually_update_instance_actor(admin_setting_args) do
+ args = %{}
+ new_instance_description = Map.get(admin_setting_args, :instance_description)
+ new_instance_name = Map.get(admin_setting_args, :instance_name)
+
+ %{
+ instance_description: old_instance_description,
+ instance_name: old_instance_name
+ } = Config.admin_settings()
+
+ args =
+ if not is_nil(new_instance_description) &&
+ new_instance_description != old_instance_description,
+ do: Map.put(args, :summary, new_instance_description),
+ else: args
+
+ args =
+ if not is_nil(new_instance_name) && new_instance_name != old_instance_name,
+ do: Map.put(args, :name, new_instance_name),
+ else: args
+
+ with {:changes, true} <- {:changes, args != %{}},
+ %Actor{} = instance_actor <- Relay.get_actor(),
+ {:ok, _activity, _actor} <- ActivityPub.update(:actor, instance_actor, args, true) do
+ :ok
+ else
+ {:changes, false} ->
+ :ok
+
+ err ->
+ err
+ end
+ end
end
diff --git a/lib/mobilizon/admin/admin.ex b/lib/mobilizon/admin/admin.ex
index f16d2c773..07f1854e4 100644
--- a/lib/mobilizon/admin/admin.ex
+++ b/lib/mobilizon/admin/admin.ex
@@ -99,6 +99,10 @@ defmodule Mobilizon.Admin do
|> Repo.transaction()
end
+ def clear_settings(group) do
+ Setting |> where([s], s.group == ^group) |> Repo.delete_all()
+ end
+
defp do_save_setting(transaction, _group, args) when args == %{}, do: transaction
defp do_save_setting(transaction, group, args) do
diff --git a/lib/mobilizon/config.ex b/lib/mobilizon/config.ex
index 77e1ffadb..fbe38aeb1 100644
--- a/lib/mobilizon/config.ex
+++ b/lib/mobilizon/config.ex
@@ -231,6 +231,7 @@ defmodule Mobilizon.Config do
def anonymous_actor_id, do: get_cached_value(:anonymous_actor_id)
def relay_actor_id, do: get_cached_value(:relay_actor_id)
+ def admin_settings, do: get_cached_value(:admin_config)
@spec get(module | atom) :: any
def get(key), do: get(key, nil)
@@ -300,6 +301,24 @@ defmodule Mobilizon.Config do
end
end
+ @spec create_cache(atom()) :: map()
+ defp create_cache(:admin_config) do
+ %{
+ instance_description: instance_description(),
+ instance_long_description: instance_long_description(),
+ instance_name: instance_name(),
+ registrations_open: instance_registrations_open?(),
+ contact: contact(),
+ instance_terms: instance_terms(),
+ instance_terms_type: instance_terms_type(),
+ instance_terms_url: instance_terms_url(),
+ instance_privacy_policy: instance_privacy(),
+ instance_privacy_policy_type: instance_privacy_type(),
+ instance_privacy_policy_url: instance_privacy_url(),
+ instance_rules: instance_rules()
+ }
+ end
+
def clear_config_cache do
Cachex.clear(:config)
end
diff --git a/test/graphql/resolvers/admin_test.exs b/test/graphql/resolvers/admin_test.exs
index affb40fb1..8b94e16d0 100644
--- a/test/graphql/resolvers/admin_test.exs
+++ b/test/graphql/resolvers/admin_test.exs
@@ -217,4 +217,167 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
}
end
end
+
+ @admin_settings_fragment """
+ fragment adminSettingsFragment on AdminSettings {
+ instanceName
+ instanceDescription
+ instanceLongDescription
+ contact
+ instanceTerms
+ instanceTermsType
+ instanceTermsUrl
+ instancePrivacyPolicy
+ instancePrivacyPolicyType
+ instancePrivacyPolicyUrl
+ instanceRules
+ registrationsOpen
+ }
+ """
+
+ describe "Resolver: Get the instance admin settings" do
+ @admin_settings_query """
+ query {
+ adminSettings {
+ ...adminSettingsFragment
+ }
+ }
+ #{@admin_settings_fragment}
+ """
+
+ setup %{conn: conn} do
+ Cachex.clear(:config)
+ [conn: conn]
+ end
+
+ test "from config files", %{conn: conn} do
+ admin = insert(:user, role: :administrator)
+
+ res =
+ conn
+ |> auth_conn(admin)
+ |> AbsintheHelpers.graphql_query(query: @admin_settings_query)
+
+ assert res["data"]["adminSettings"]["instanceName"] ==
+ Application.get_env(:mobilizon, :instance)[:name]
+
+ assert res["data"]["adminSettings"]["registrationsOpen"] ==
+ Application.get_env(:mobilizon, :instance)[:registrations_open]
+ end
+
+ @instance_name "My Awesome Instance"
+ test "from DB", %{conn: conn} do
+ admin = insert(:user, role: :administrator)
+ insert(:admin_setting, group: "instance", name: "instance_name", value: @instance_name)
+ insert(:admin_setting, group: "instance", name: "registrations_open", value: "false")
+
+ res =
+ conn
+ |> auth_conn(admin)
+ |> AbsintheHelpers.graphql_query(query: @admin_settings_query)
+
+ assert res["data"]["adminSettings"]["instanceName"] == @instance_name
+
+ assert res["data"]["adminSettings"]["registrationsOpen"] == false
+ end
+
+ test "unless user isn't admin", %{conn: conn} do
+ admin = insert(:user)
+
+ res =
+ conn
+ |> auth_conn(admin)
+ |> AbsintheHelpers.graphql_query(query: @admin_settings_query)
+
+ assert hd(res["errors"])["message"] ==
+ "You need to be logged-in and an administrator to access admin settings"
+ end
+ end
+
+ describe "Resolver: Update the instance admin settings" do
+ setup %{conn: conn} do
+ Cachex.clear(:config)
+ [conn: conn]
+ end
+
+ @update_instance_admin_settings_mutation """
+ mutation SaveAdminSettings(
+ $instanceName: String
+ $instanceDescription: String
+ $instanceLongDescription: String
+ $contact: String
+ $instanceTerms: String
+ $instanceTermsType: InstanceTermsType
+ $instanceTermsUrl: String
+ $instancePrivacyPolicy: String
+ $instancePrivacyPolicyType: InstancePrivacyType
+ $instancePrivacyPolicyUrl: String
+ $instanceRules: String
+ $registrationsOpen: Boolean
+ ) {
+ saveAdminSettings(
+ instanceName: $instanceName
+ instanceDescription: $instanceDescription
+ instanceLongDescription: $instanceLongDescription
+ contact: $contact
+ instanceTerms: $instanceTerms
+ instanceTermsType: $instanceTermsType
+ instanceTermsUrl: $instanceTermsUrl
+ instancePrivacyPolicy: $instancePrivacyPolicy
+ instancePrivacyPolicyType: $instancePrivacyPolicyType
+ instancePrivacyPolicyUrl: $instancePrivacyPolicyUrl
+ instanceRules: $instanceRules
+ registrationsOpen: $registrationsOpen
+ ) {
+ ...adminSettingsFragment
+ }
+ }
+ #{@admin_settings_fragment}
+ """
+
+ @new_instance_name "new Instance Name"
+
+ test "does the setting update and updates instance actor as well", %{conn: conn} do
+ admin = insert(:user, role: :administrator)
+
+ res =
+ conn
+ |> auth_conn(admin)
+ |> AbsintheHelpers.graphql_query(query: @admin_settings_query)
+
+ assert res["data"]["adminSettings"]["instanceName"] ==
+ Application.get_env(:mobilizon, :instance)[:name]
+
+ assert res["data"]["adminSettings"]["registrationsOpen"] ==
+ Application.get_env(:mobilizon, :instance)[:registrations_open]
+
+ res =
+ conn
+ |> auth_conn(admin)
+ |> AbsintheHelpers.graphql_query(
+ query: @update_instance_admin_settings_mutation,
+ variables: %{"instanceName" => @new_instance_name, "registrationsOpen" => false}
+ )
+
+ assert res["data"]["saveAdminSettings"]["instanceName"] == @new_instance_name
+ assert res["data"]["saveAdminSettings"]["registrationsOpen"] == false
+
+ assert %Actor{name: @new_instance_name} = Relay.get_actor()
+ end
+
+ test "unless user isn't admin", %{conn: conn} do
+ admin = insert(:user)
+
+ res =
+ conn
+ |> auth_conn(admin)
+ |> AbsintheHelpers.graphql_query(
+ query: @update_instance_admin_settings_mutation,
+ variables: %{"instanceName" => @new_instance_name, "registrationsOpen" => false}
+ )
+
+ assert hd(res["errors"])["message"] ==
+ "You need to be logged-in and an administrator to save admin settings"
+ end
+ end
end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 220e7755d..7a9784fe7 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -316,4 +316,12 @@ defmodule Mobilizon.Factory do
path: "/#{title}"
}
end
+
+ def admin_setting_factory do
+ %Mobilizon.Admin.Setting{
+ group: sequence("group"),
+ name: sequence("name"),
+ value: sequence("value")
+ }
+ end
end