Merge branch 'update-instance-actor-when-updating-instance-description' into 'master'

Update Instance Actor when updating instance settings

Closes #365

See merge request framasoft/mobilizon!514
This commit is contained in:
Thomas Citharel 2020-07-07 17:34:52 +02:00
commit bec1c69d4b
11 changed files with 298 additions and 55 deletions

View file

@ -43,18 +43,20 @@
</template> </template>
</b-table-column> </b-table-column>
<b-table-column field="actor.updatedAt" :label="$t('Date')" sortable>{{ <b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable>
props.row.updatedAt | formatDateTimeString <span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{
}}</b-table-column> timeago(props.row.updatedAt)
}}</span></b-table-column
>
</template> </template>
<template slot="detail" slot-scope="props"> <template slot="detail" slot-scope="props">
<article> <article>
<div class="content"> <div class="content">
<strong>{{ props.row.actor.domain }}</strong> <strong>{{ props.row.actor.name }}</strong>
<small>@{{ props.row.actor.preferredUsername }}</small> <small v-if="props.row.actor.preferredUsername !== 'relay'"
<small>31m</small> >@{{ props.row.actor.preferredUsername }}</small
<br /> >
<p v-html="props.row.actor.summary" /> <p v-html="props.row.actor.summary" />
</div> </div>
</article> </article>

View file

@ -56,18 +56,20 @@
</template> </template>
</b-table-column> </b-table-column>
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable>{{ <b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable>
props.row.updatedAt | formatDateTimeString <span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{
}}</b-table-column> timeago(props.row.updatedAt)
}}</span></b-table-column
>
</template> </template>
<template slot="detail" slot-scope="props"> <template slot="detail" slot-scope="props">
<article> <article>
<div class="content"> <div class="content">
<strong>{{ props.row.targetActor.domain }}</strong> <strong>{{ props.row.targetActor.name }}</strong>
<small>@{{ props.row.targetActor.preferredUsername }}</small> <small v-if="props.row.actor.preferredUsername !== 'relay'"
<small>31m</small> >@{{ props.row.targetActor.preferredUsername }}</small
<br /> >
<p v-html="props.row.targetActor.summary" /> <p v-html="props.row.targetActor.summary" />
</div> </div>
</article> </article>

View file

@ -1,6 +1,7 @@
import { Component, Vue, Ref } from "vue-property-decorator"; import { Component, Vue, Ref } from "vue-property-decorator";
import { ActorType, IActor } from "@/types/actor"; import { ActorType, IActor } from "@/types/actor";
import { IFollower } from "@/types/actor/follower.model"; import { IFollower } from "@/types/actor/follower.model";
import TimeAgo from "javascript-time-ago";
@Component @Component
export default class RelayMixin extends Vue { export default class RelayMixin extends Vue {
@ -12,6 +13,15 @@ export default class RelayMixin extends Vue {
perPage = 10; 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) { toggle(row: object) {
this.table.toggleDetails(row); this.table.toggleDetails(row);
} }
@ -43,4 +53,11 @@ export default class RelayMixin extends Vue {
(actor.preferredUsername === "relay" || actor.preferredUsername === actor.domain) (actor.preferredUsername === "relay" || actor.preferredUsername === actor.domain)
); );
} }
timeago(dateTime: string): string {
if (this.timeAgoInstance != null) {
return this.timeAgoInstance.format(new Date(dateTime));
}
return "";
}
} }

View file

@ -720,7 +720,7 @@ defmodule Mobilizon.Federation.ActivityPub do
""" """
# credo:disable-for-lines:47 # credo:disable-for-lines:47
@spec publish(Actor.t(), Activity.t()) :: :ok @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("Publishing an activity")
Logger.debug(inspect(activity)) Logger.debug(inspect(activity))
@ -733,24 +733,26 @@ defmodule Mobilizon.Federation.ActivityPub do
Relay.publish(activity) Relay.publish(activity)
end end
followers = {recipients, followers} =
if actor.followers_url in activity.recipients do 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 else
[] {recipients, []}
end end
# If we want to send to all members of the group, because this server is the one the group is on # 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 if is_announce_activity?(activity) and actor.type == :Group and
actor.members_url in activity.recipients and is_nil(actor.domain) do 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 else
[] {recipients, []}
end end
remote_inboxes = 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.map(fn follower -> follower.shared_inbox_url || follower.inbox_url end)
|> Enum.uniq() |> Enum.uniq()
@ -1045,7 +1047,7 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
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 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), 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), %TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),

View file

@ -114,10 +114,9 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
def maybe_federate(_), do: :ok def maybe_federate(_), do: :ok
def remote_actors(%{data: %{"to" => to} = data}) do @spec remote_actors(list(String.t())) :: list(Actor.t())
to = to ++ (data["cc"] || []) def remote_actors(recipients) do
recipients
to
|> Enum.map(fn url -> ActivityPub.get_or_fetch_actor_by_url(url) end) |> Enum.map(fn url -> ActivityPub.get_or_fetch_actor_by_url(url) end)
|> Enum.map(fn {status, actor} -> |> Enum.map(fn {status, actor} ->
case status do case status do

View file

@ -21,12 +21,14 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
defdelegate model_to_as(actor), to: ActorConverter defdelegate model_to_as(actor), to: ActorConverter
end end
@allowed_types ["Application", "Group", "Organization", "Person", "Service"]
@doc """ @doc """
Converts an AP object data to our internal data structure. Converts an AP object data to our internal data structure.
""" """
@impl Converter @impl Converter
@spec as_to_model_data(map()) :: {:ok, map()} @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 = avatar =
data["icon"]["url"] && data["icon"]["url"] &&
%{ %{
@ -62,6 +64,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
} }
end end
def as_to_model_data(_), do: :error
@doc """ @doc """
Convert an actor struct to an ActivityStream representation. Convert an actor struct to an ActivityStream representation.
""" """

View file

@ -11,6 +11,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
alias Mobilizon.Config alias Mobilizon.Config
alias Mobilizon.Conversations.Comment alias Mobilizon.Conversations.Comment
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Relay alias Mobilizon.Federation.ActivityPub.Relay
alias Mobilizon.Reports.{Note, Report} alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Service.Statistics alias Mobilizon.Service.Statistics
@ -158,21 +159,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
context: %{current_user: %User{role: role}} context: %{current_user: %User{role: role}}
}) })
when is_admin(role) do when is_admin(role) do
{:ok, {:ok, Config.admin_settings()}
%{
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()
}}
end end
def get_settings(_parent, _args, _resolution) do def get_settings(_parent, _args, _resolution) do
@ -183,19 +170,20 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
context: %{current_user: %User{role: role}} context: %{current_user: %User{role: role}}
}) })
when is_admin(role) do when is_admin(role) do
with {:ok, res} <- Admin.save_settings("instance", args) do with {:ok, res} <- Admin.save_settings("instance", args),
res = res <-
res res
|> Enum.map(fn {key, %Setting{value: value}} -> |> Enum.map(fn {key, %Setting{value: value}} ->
case value do case value do
"true" -> {key, true} "true" -> {key, true}
"false" -> {key, false} "false" -> {key, false}
value -> {key, value} value -> {key, value}
end end
end) end)
|> Enum.into(%{}) |> Enum.into(%{}),
:ok <- eventually_update_instance_actor(res) do
Config.clear_config_cache() Config.clear_config_cache()
Cachex.put(:config, :admin_config, res)
{:ok, res} {:ok, res}
end end
@ -284,4 +272,39 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
{:error, err} {:error, err}
end end
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 end

View file

@ -99,6 +99,10 @@ defmodule Mobilizon.Admin do
|> Repo.transaction() |> Repo.transaction()
end 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) when args == %{}, do: transaction
defp do_save_setting(transaction, group, args) do defp do_save_setting(transaction, group, args) do

View file

@ -231,6 +231,7 @@ defmodule Mobilizon.Config do
def anonymous_actor_id, do: get_cached_value(:anonymous_actor_id) def anonymous_actor_id, do: get_cached_value(:anonymous_actor_id)
def relay_actor_id, do: get_cached_value(:relay_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 @spec get(module | atom) :: any
def get(key), do: get(key, nil) def get(key), do: get(key, nil)
@ -300,6 +301,24 @@ defmodule Mobilizon.Config do
end end
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 def clear_config_cache do
Cachex.clear(:config) Cachex.clear(:config)
end end

View file

@ -217,4 +217,167 @@ defmodule Mobilizon.GraphQL.Resolvers.AdminTest do
} }
end end
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 end

View file

@ -316,4 +316,12 @@ defmodule Mobilizon.Factory do
path: "/#{title}" path: "/#{title}"
} }
end end
def admin_setting_factory do
%Mobilizon.Admin.Setting{
group: sequence("group"),
name: sequence("name"),
value: sequence("value")
}
end
end end