Merge branch 'fixes' into 'main'

Various fixes

Closes #997

See merge request framasoft/mobilizon!1199
This commit is contained in:
Thomas Citharel 2022-04-04 14:27:16 +00:00
commit 0fac23cc4b
13 changed files with 190 additions and 72 deletions

View File

@ -212,7 +212,8 @@ config :mobilizon, :activitypub,
# One day # One day
actor_stale_period: 3_600 * 48, actor_stale_period: 3_600 * 48,
actor_key_rotation_delay: 3_600 * 48, actor_key_rotation_delay: 3_600 * 48,
sign_object_fetches: true sign_object_fetches: true,
stale_actor_search_exclusion_after: 3_600 * 24 * 7
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim

View File

@ -5,14 +5,33 @@
:placeholder="$t('Filter by profile or group name')" :placeholder="$t('Filter by profile or group name')"
v-model="actorFilter" v-model="actorFilter"
/> />
<b-radio-button <transition-group
v-model="selectedActor" tag="ul"
:native-value="availableActor" class="grid grid-cols-1 gap-y-3 m-5 max-w-md mx-auto"
class="list-item" enter-active-class="duration-300 ease-out"
enter-from-class="transform opacity-0"
enter-to-class="opacity-100"
leave-active-class="duration-200 ease-in"
leave-from-class="opacity-100"
leave-to-class="transform opacity-0"
>
<li
class="relative focus-within:shadow-lg"
v-for="availableActor in actualFilteredAvailableActors" v-for="availableActor in actualFilteredAvailableActors"
:key="availableActor.id" :key="availableActor.id"
> >
<div class="media" dir="auto"> <input
class="sr-only peer"
type="radio"
:value="availableActor"
name="availableActors"
v-model="selectedActor"
:id="`availableActor-${availableActor.id}`"
/>
<label
class="flex flex-wrap p-3 bg-white border border-gray-300 rounded-lg cursor-pointer hover:bg-gray-50 peer-checked:ring-primary peer-checked:ring-2 peer-checked:border-transparent"
:for="`availableActor-${availableActor.id}`"
>
<figure class="image is-48x48" v-if="availableActor.avatar"> <figure class="image is-48x48" v-if="availableActor.avatar">
<img <img
class="image is-rounded" class="image is-rounded"
@ -20,18 +39,14 @@
alt="" alt=""
/> />
</figure> </figure>
<b-icon <b-icon v-else size="is-large" icon="account-circle" />
class="media-left" <div>
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content">
<h3>{{ availableActor.name }}</h3> <h3>{{ availableActor.name }}</h3>
<small>{{ `@${availableActor.preferredUsername}` }}</small> <small>{{ `@${availableActor.preferredUsername}` }}</small>
</div> </div>
</div> </label>
</b-radio-button> </li>
</transition-group>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">

View File

@ -1,5 +1,8 @@
<template> <template>
<div class="organizer-picker" v-if="selectedActor"> <div
class="bg-white border border-gray-300 rounded-lg cursor-pointer"
v-if="selectedActor"
>
<!-- If we have a current actor (inline) --> <!-- If we have a current actor (inline) -->
<div <div
v-if="inline && selectedActor.id" v-if="inline && selectedActor.id"
@ -69,7 +72,8 @@
<p>{{ $t("Add a contact") }}</p> <p>{{ $t("Add a contact") }}</p>
<b-input <b-input
:placeholder="$t('Filter by name')" :placeholder="$t('Filter by name')"
v-model="contactFilter" :value="contactFilter"
@input="debounceSetFilterByName"
dir="auto" dir="auto"
/> />
<div v-if="actorMembers.length > 0"> <div v-if="actorMembers.length > 0">
@ -144,11 +148,12 @@ import EmptyContent from "../Utils/EmptyContent.vue";
import { import {
CURRENT_ACTOR_CLIENT, CURRENT_ACTOR_CLIENT,
IDENTITIES, IDENTITIES,
LOGGED_USER_MEMBERSHIPS, PERSON_GROUP_MEMBERSHIPS,
} from "../../graphql/actor"; } from "../../graphql/actor";
import { Paginate } from "../../types/paginate"; import { Paginate } from "../../types/paginate";
import { GROUP_MEMBERS } from "@/graphql/member"; import { GROUP_MEMBERS } from "@/graphql/member";
import { ActorType, MemberRole } from "@/types/enums"; import { ActorType, MemberRole } from "@/types/enums";
import debounce from "lodash/debounce";
const MEMBER_ROLES = [ const MEMBER_ROLES = [
MemberRole.CREATOR, MemberRole.CREATOR,
@ -179,15 +184,17 @@ const MEMBER_ROLES = [
}, },
}, },
currentActor: CURRENT_ACTOR_CLIENT, currentActor: CURRENT_ACTOR_CLIENT,
userMemberships: { personMemberships: {
query: LOGGED_USER_MEMBERSHIPS, query: PERSON_GROUP_MEMBERSHIPS,
variables() { variables() {
return { return {
id: this.currentActor?.id,
page: 1, page: 1,
limit: 10, limit: 10,
groupId: this.$route.query?.actorId,
}; };
}, },
update: (data) => data.loggedUser.memberships, update: (data) => data.person.memberships,
}, },
identities: IDENTITIES, identities: IDENTITIES,
}, },
@ -197,6 +204,9 @@ export default class OrganizerPickerWrapper extends Vue {
@Prop({ default: true, type: Boolean }) inline!: boolean; @Prop({ default: true, type: Boolean }) inline!: boolean;
@Prop({ type: Array, required: false, default: () => [] })
contacts!: IActor[];
currentActor!: IPerson; currentActor!: IPerson;
identities!: IPerson[]; identities!: IPerson[];
@ -207,13 +217,17 @@ export default class OrganizerPickerWrapper extends Vue {
usernameWithDomain = usernameWithDomain; usernameWithDomain = usernameWithDomain;
@Prop({ type: Array, required: false, default: () => [] })
contacts!: IActor[];
members: Paginate<IMember> = { elements: [], total: 0 }; members: Paginate<IMember> = { elements: [], total: 0 };
membersPage = 1; membersPage = 1;
userMemberships: Paginate<IMember> = { elements: [], total: 0 }; personMemberships: Paginate<IMember> = { elements: [], total: 0 };
data(): Record<string, unknown> {
return {
debounceSetFilterByName: debounce(this.setContactFilter, 1000),
};
}
get actualContacts(): (string | undefined)[] { get actualContacts(): (string | undefined)[] {
return this.contacts.map(({ id }) => id); return this.contacts.map(({ id }) => id);
@ -226,15 +240,17 @@ export default class OrganizerPickerWrapper extends Vue {
); );
} }
@Watch("userMemberships") setContactFilter(contactFilter: string) {
this.contactFilter = contactFilter;
}
@Watch("personMemberships")
setInitialActor(): void { setInitialActor(): void {
if (this.$route.query?.actorId) { if (
const actorId = this.$route.query?.actorId as string; this.personMemberships?.elements[0]?.parent?.id ===
const actor = this.userMemberships.elements.find( this.$route.query?.actorId
({ parent: { id }, role }) => ) {
actorId === id && MEMBER_ROLES.includes(role) this.selectedActor = this.personMemberships?.elements[0]?.parent;
)?.parent as IActor;
this.selectedActor = actor;
} }
} }
@ -276,7 +292,7 @@ export default class OrganizerPickerWrapper extends Vue {
actor.preferredUsername.toLowerCase(), actor.preferredUsername.toLowerCase(),
actor.name?.toLowerCase(), actor.name?.toLowerCase(),
actor.domain?.toLowerCase(), actor.domain?.toLowerCase(),
].some((match) => match?.includes(this.contactFilter.toLowerCase())); ];
}); });
} }

View File

@ -351,6 +351,30 @@ export const PERSON_STATUS_GROUP = gql`
${ACTOR_FRAGMENT} ${ACTOR_FRAGMENT}
`; `;
export const PERSON_GROUP_MEMBERSHIPS = gql`
query PersonGroupMemberships($id: ID!, $groupId: ID!) {
person(id: $id) {
id
memberships(groupId: $groupId) {
total
elements {
id
role
parent {
...ActorFragment
}
invitedBy {
...ActorFragment
}
insertedAt
updatedAt
}
}
}
}
${ACTOR_FRAGMENT}
`;
export const GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED = gql` export const GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED = gql`
subscription GroupMembershipSubscriptionChanged( subscription GroupMembershipSubscriptionChanged(
$actorId: ID! $actorId: ID!

View File

@ -226,6 +226,15 @@ export const FETCH_GROUP = gql`
${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT} ${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT}
`; `;
export const FETCH_GROUP_BY_ID = gql`
query FetchGroupById($id: ID!) {
groupById(id: $name) {
...GroupFullFields
}
}
${GROUP_FIELDS_FRAGMENTS}
`;
export const GET_GROUP = gql` export const GET_GROUP = gql`
query GetGroup( query GetGroup(
$id: ID! $id: ID!

View File

@ -95,7 +95,7 @@
> >
<b-button <b-button
size="is-small" size="is-small"
v-if="!user.confirmedAt || !user.disabled" v-if="!user.confirmedAt || user.disabled"
@click="isConfirmationModalActive = true" @click="isConfirmationModalActive = true"
type="is-text" type="is-text"
icon-left="check" icon-left="check"

View File

@ -49,7 +49,8 @@ defmodule Mobilizon.GraphQL.API.Search do
location: Map.get(args, :location), location: Map.get(args, :location),
minimum_visibility: Map.get(args, :minimum_visibility, :public), minimum_visibility: Map.get(args, :minimum_visibility, :public),
current_actor_id: Map.get(args, :current_actor_id), current_actor_id: Map.get(args, :current_actor_id),
exclude_my_groups: Map.get(args, :exclude_my_groups, false) exclude_my_groups: Map.get(args, :exclude_my_groups, false),
exclude_stale_actors: true
], ],
page, page,
limit limit

View File

@ -16,15 +16,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
require Logger require Logger
@doc """
Find a group
"""
@spec find_group( @spec find_group(
any, any,
%{:preferred_username => binary, optional(any) => any}, %{:preferred_username => binary, optional(any) => any},
Absinthe.Resolution.t() Absinthe.Resolution.t()
) :: ) ::
{:error, :group_not_found} | {:ok, Actor.t()} {:error, :group_not_found} | {:ok, Actor.t()}
@doc """
Find a group
"""
def find_group( def find_group(
parent, parent,
%{preferred_username: name} = args, %{preferred_username: name} = args,
@ -45,7 +45,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
{:ok, %Actor{}} -> {:ok, %Actor{}} ->
{:error, :group_not_found} {:error, :group_not_found}
{:error, _err} -> {:error, err} ->
Logger.debug("Unable to find group, #{inspect(err)}")
{:error, :group_not_found} {:error, :group_not_found}
end end
end end
@ -59,11 +60,30 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
{:ok, %Actor{}} -> {:ok, %Actor{}} ->
{:error, :group_not_found} {:error, :group_not_found}
{:error, _err} -> {:error, err} ->
Logger.debug("Unable to find group, #{inspect(err)}")
{:error, :group_not_found} {:error, :group_not_found}
end end
end end
def find_group_by_id(_parent, %{id: id} = args, %{
context: %{
current_actor: %Actor{id: actor_id}
}
}) do
with %Actor{suspended: false, id: group_id} = group <- Actors.get_actor_with_preload(id),
true <- Actors.is_member?(actor_id, group_id) do
{:ok, group}
else
_ ->
{:error, :group_not_found}
end
end
def find_group_by_id(_parent, _args, _resolution) do
{:error, :group_not_found}
end
@doc """ @doc """
Get a group Get a group
""" """

View File

@ -358,11 +358,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
Returns this person's group memberships Returns this person's group memberships
""" """
@spec person_memberships(Actor.t(), map(), map()) :: {:ok, Page.t()} | {:error, String.t()} @spec person_memberships(Actor.t(), map(), map()) :: {:ok, Page.t()} | {:error, String.t()}
def person_memberships(%Actor{id: actor_id} = person, %{group: group}, %{ def person_memberships(%Actor{id: actor_id} = person, args, %{
context: %{current_user: %User{} = user} context: %{current_user: %User{} = user}
}) do }) do
if user_can_access_person_details?(person, user) do if user_can_access_person_details?(person, user) do
with {:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(group, :Group)}, with {:group, %Actor{id: group_id}} <- {:group, group_from_args(args)},
{:ok, %Member{} = membership} <- Actors.get_member(actor_id, group_id) do {:ok, %Member{} = membership} <- Actors.get_member(actor_id, group_id) do
{:ok, {:ok,
%Page{ %Page{
@ -373,6 +373,21 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
{:error, :member_not_found} -> {:error, :member_not_found} ->
{:ok, %Page{total: 0, elements: []}} {:ok, %Page{total: 0, elements: []}}
{:group, :none} ->
with {:can_get_memberships, true} <-
{:can_get_memberships, user_can_access_person_details?(person, user)},
memberships <-
Actors.list_members_for_actor(
person,
Map.get(args, :page, 1),
Map.get(args, :limit, 10)
) do
{:ok, memberships}
else
{:can_get_memberships, _} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
end
{:group, nil} -> {:group, nil} ->
{:error, :group_not_found} {:error, :group_not_found}
end end
@ -381,23 +396,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
end end
end end
def person_memberships(
%Actor{} = person,
%{page: page, limit: limit},
%{
context: %{current_user: %User{} = user}
}
) do
with {:can_get_memberships, true} <-
{:can_get_memberships, user_can_access_person_details?(person, user)},
memberships <- Actors.list_members_for_actor(person, page, limit) do
{:ok, memberships}
else
{:can_get_memberships, _} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
end
end
@doc """ @doc """
Returns this person's group follows Returns this person's group follows
""" """
@ -498,4 +496,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
do: actor_user_id == user_id do: actor_user_id == user_id
defp user_can_access_person_details?(_, _), do: false defp user_can_access_person_details?(_, _), do: false
@spec group_from_args(map()) :: Actor.t() | nil
defp group_from_args(%{group: group}) do
Actors.get_actor_by_name(group, :Group)
end
defp group_from_args(%{group_id: group_id}) do
Actors.get_actor(group_id)
end
defp group_from_args(_) do
:none
end
end end

View File

@ -244,6 +244,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
resolve(&Group.find_group/3) resolve(&Group.find_group/3)
end end
@desc "Get a group by its preferred username"
field :group_by_id, :group do
arg(:id, non_null(:id), description: "The group local ID")
resolve(&Group.find_group_by_id/3)
end
end end
object :group_mutations do object :group_mutations do

View File

@ -88,11 +88,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
resolve(&Person.person_participations/3) resolve(&Person.person_participations/3)
end end
@desc "The list of group this person is member of" @desc "The list of groups this person is member of"
field(:memberships, :paginated_member_list, field(:memberships, :paginated_member_list,
description: "The list of group this person is member of" description: "The list of group this person is member of"
) do ) do
arg(:group, :string, description: "Filter by group federated username") arg(:group, :string, description: "Filter by group federated username")
arg(:group_id, :id, description: "Filter by group ID")
arg(:page, :integer, arg(:page, :integer,
default_value: 1, default_value: 1,

View File

@ -461,6 +461,7 @@ defmodule Mobilizon.Actors do
) do ) do
term term
|> build_actors_by_username_or_name_page_query(options) |> build_actors_by_username_or_name_page_query(options)
|> maybe_exclude_stale_actors(Keyword.get(options, :exclude_stale_actors, false))
|> maybe_exclude_my_groups( |> maybe_exclude_my_groups(
Keyword.get(options, :exclude_my_groups, false), Keyword.get(options, :exclude_my_groups, false),
Keyword.get(options, :current_actor_id) Keyword.get(options, :current_actor_id)
@ -477,6 +478,17 @@ defmodule Mobilizon.Actors do
defp maybe_exclude_my_groups(query, _, _), do: query defp maybe_exclude_my_groups(query, _, _), do: query
@spec maybe_exclude_stale_actors(Ecto.Queryable.t(), boolean()) :: Ecto.Query.t()
defp maybe_exclude_stale_actors(query, true) do
actor_stale_period =
Application.get_env(:mobilizon, :activitypub)[:stale_actor_search_exclusion_after]
stale_date = DateTime.utc_now() |> DateTime.add(-actor_stale_period)
where(query, [a], is_nil(a.domain) or a.last_refreshed_at >= ^stale_date)
end
defp maybe_exclude_stale_actors(query, false), do: query
@spec build_actors_by_username_or_name_page_query( @spec build_actors_by_username_or_name_page_query(
String.t(), String.t(),
Keyword.t() Keyword.t()

View File

@ -53,7 +53,8 @@ defmodule Mobilizon.GraphQL.API.SearchTest do
location: nil, location: nil,
minimum_visibility: :public, minimum_visibility: :public,
current_actor_id: nil, current_actor_id: nil,
exclude_my_groups: false exclude_my_groups: false,
exclude_stale_actors: true
], ],
1, 1,
10 10