From 9aaea50f594bd8826d4dd6bda4d00e76599f89f1 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Wed, 24 Nov 2021 09:28:59 +0100 Subject: [PATCH 001/506] Order my group upcoming events by begins_on Signed-off-by: Thomas Citharel --- lib/mobilizon/followed_group_activity.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mobilizon/followed_group_activity.ex b/lib/mobilizon/followed_group_activity.ex index e13863c59..4045aa05d 100644 --- a/lib/mobilizon/followed_group_activity.ex +++ b/lib/mobilizon/followed_group_activity.ex @@ -16,7 +16,7 @@ defmodule Mobilizon.FollowedGroupActivity do ) :: Page.t(Event.t()) def user_followed_group_events(user_id, after_datetime \\ nil, page \\ nil, limit \\ nil) do Event - |> distinct([e], e.id) + |> distinct([e], [e.begins_on, e.id]) |> join(:left, [e], p in Participant, on: e.id == p.event_id) |> join(:inner, [_e, p], pa in Actor, on: p.actor_id == pa.id) |> join(:inner, [e], g in Actor, on: e.attributed_to_id == g.id) From baa8582df72f8e3b9f0a506ffbeb66105cde8fc7 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Wed, 24 Nov 2021 16:05:52 +0100 Subject: [PATCH 002/506] Fix event participants pagination Signed-off-by: Thomas Citharel --- js/src/views/Event/Participants.vue | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/js/src/views/Event/Participants.vue b/js/src/views/Event/Participants.vue index b90a09872..247f7fa71 100644 --- a/js/src/views/Event/Participants.vue +++ b/js/src/views/Event/Participants.vue @@ -90,6 +90,7 @@ :show-detail-icon="false" :loading="this.$apollo.loading" paginated + :current-page="page" backend-pagination :pagination-simple="true" :aria-next-label="$t('Next page')" @@ -259,7 +260,7 @@ diff --git a/js/src/views/Admin/Instance.vue b/js/src/views/Admin/Instance.vue new file mode 100644 index 000000000..85233234f --- /dev/null +++ b/js/src/views/Admin/Instance.vue @@ -0,0 +1,268 @@ + + diff --git a/js/src/views/Admin/Instances.vue b/js/src/views/Admin/Instances.vue new file mode 100644 index 000000000..e2c64940f --- /dev/null +++ b/js/src/views/Admin/Instances.vue @@ -0,0 +1,305 @@ + + + + diff --git a/lib/graphql/resolvers/admin.ex b/lib/graphql/resolvers/admin.ex index 36857e7dd..54c407ec4 100644 --- a/lib/graphql/resolvers/admin.ex +++ b/lib/graphql/resolvers/admin.ex @@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do import Mobilizon.Users.Guards - alias Mobilizon.{Actors, Admin, Config, Events} + alias Mobilizon.{Actors, Admin, Config, Events, Instances} alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Admin.{ActionLog, Setting} alias Mobilizon.Cldr.Language @@ -329,6 +329,79 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do {:error, :unauthenticated} end + def get_instances( + _parent, + args, + %{ + context: %{current_user: %User{role: role}} + } + ) + when is_admin(role) do + {:ok, + Instances.instances( + args + |> Keyword.new() + |> Keyword.take([ + :page, + :limit, + :order_by, + :direction, + :filter_domain, + :filter_follow_status, + :filter_suspend_status + ]) + )} + end + + def get_instances(_parent, _args, %{context: %{current_user: %User{}}}) do + {:error, :unauthorized} + end + + def get_instances(_parent, _args, _resolution) do + {:error, :unauthenticated} + end + + def get_instance(_parent, %{domain: domain}, %{ + context: %{current_user: %User{role: role}} + }) + when is_admin(role) do + has_relay = Actors.has_relay?(domain) + remote_relay = Actors.get_actor_by_name("relay@#{domain}") + local_relay = Relay.get_actor() + + result = %{ + has_relay: has_relay, + follower_status: follow_status(remote_relay, local_relay), + followed_status: follow_status(local_relay, remote_relay) + } + + {:ok, Map.merge(Instances.instance(domain), result)} + end + + def get_instance(_parent, _args, %{context: %{current_user: %User{}}}) do + {:error, :unauthorized} + end + + def get_instance(_parent, _args, _resolution) do + {:error, :unauthenticated} + end + + def create_instance( + parent, + %{domain: domain} = args, + %{context: %{current_user: %User{role: role}}} = resolution + ) + when is_admin(role) do + case Relay.follow(domain) do + {:ok, _activity, _follow} -> + Instances.refresh() + get_instance(parent, args, resolution) + + {:error, err} -> + {:error, err} + end + end + @spec create_relay(any(), map(), Absinthe.Resolution.t()) :: {:ok, Follower.t()} | {:error, any()} def create_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}}) @@ -425,4 +498,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do :ok end end + + @spec follow_status(Actor.t() | nil, Actor.t() | nil) :: :approved | :pending | :none + defp follow_status(follower, followed) when follower != nil and followed != nil do + case Actors.check_follow(follower, followed) do + %Follower{approved: true} -> :approved + %Follower{approved: false} -> :pending + _ -> :none + end + end + + defp follow_status(_, _), do: :none end diff --git a/lib/graphql/schema/admin.ex b/lib/graphql/schema/admin.ex index dfe02c093..44b70274d 100644 --- a/lib/graphql/schema/admin.ex +++ b/lib/graphql/schema/admin.ex @@ -153,6 +153,80 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do value(:custom, as: "CUSTOM", description: "Custom privacy policy text") end + enum :instance_follow_status do + value(:approved, description: "The instance follow was approved") + value(:pending, description: "The instance follow is still pending") + value(:none, description: "There's no instance follow etablished") + end + + enum :instances_sort_fields do + value(:event_count) + value(:person_count) + value(:group_count) + value(:followers_count) + value(:followings_count) + value(:reports_count) + value(:media_size) + end + + enum :instance_filter_follow_status do + value(:all) + value(:following) + value(:followed) + end + + enum :instance_filter_suspend_status do + value(:all) + value(:suspended) + end + + @desc """ + An instance representation + """ + object :instance do + field(:domain, :id, description: "The domain name of the instance") + field(:has_relay, :boolean, description: "Whether this instance has a Mobilizon relay actor") + field(:follower_status, :instance_follow_status, description: "Do we follow this instance") + field(:followed_status, :instance_follow_status, description: "Does this instance follow us?") + + field(:event_count, :integer, description: "The number of events on this instance we know of") + + field(:person_count, :integer, + description: "The number of profiles on this instance we know of" + ) + + field(:group_count, :integer, description: "The number of grouo on this instance we know of") + + field(:followers_count, :integer, + description: "The number of their profiles who follow our groups" + ) + + field(:followings_count, :integer, + description: "The number of our profiles who follow their groups" + ) + + field(:reports_count, :integer, + description: "The number of reports made against profiles from this instance" + ) + + field(:media_size, :integer, + description: "The size of all the media files sent by actors from this instance" + ) + + field(:has_relay, :boolean, + description: + "Whether this instance has a relay, meaning that it's a Mobilizon instance that we can follow" + ) + end + + @desc """ + A paginated list of instances + """ + object :paginated_instance_list do + field(:elements, list_of(:instance), description: "A list of instances") + field(:total, :integer, description: "The total number of instances in the list") + end + object :admin_queries do @desc "Get the list of action logs" field :action_logs, type: :paginated_action_log_list do @@ -226,9 +300,59 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do arg(:direction, :string, default_value: :desc, description: "The sorting direction") resolve(&Admin.list_relay_followings/3) end + + @desc """ + List instances + """ + field :instances, :paginated_instance_list do + arg(:page, :integer, + default_value: 1, + description: "The page in the paginated relay followings list" + ) + + arg(:limit, :integer, + default_value: 10, + description: "The limit of relay followings per page" + ) + + arg(:order_by, :instances_sort_fields, + default_value: :event_count, + description: "The field to order by the list" + ) + + arg(:filter_domain, :string, default_value: nil, description: "Filter by domain") + + arg(:filter_follow_status, :instance_filter_follow_status, + default_value: :all, + description: "Whether or not to filter instances by the follow status" + ) + + arg(:filter_suspend_status, :instance_filter_suspend_status, + default_value: :all, + description: "Whether or not to filter instances by the suspended status" + ) + + arg(:direction, :string, default_value: :desc, description: "The sorting direction") + resolve(&Admin.get_instances/3) + end + + @desc """ + Get an instance's details + """ + field :instance, :instance do + arg(:domain, non_null(:id), description: "The instance domain") + resolve(&Admin.get_instance/3) + end end object :admin_mutations do + @desc "Add an instance subscription" + field :add_instance, type: :instance do + arg(:domain, non_null(:string), description: "The instance domain to add") + + resolve(&Admin.create_instance/3) + end + @desc "Add a relay subscription" field :add_relay, type: :follower do arg(:address, non_null(:string), description: "The relay hostname to add") diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index 527aa5eef..b67b85e11 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -1256,6 +1256,16 @@ defmodule Mobilizon.Actors do :ok end + @spec has_relay?(String.t()) :: boolean() + def has_relay?(domain) do + Actor + |> where( + [a], + a.preferred_username == "relay" and a.domain == ^domain and a.type == :Application + ) + |> Repo.exists?() + end + @spec delete_files_if_media_changed(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp delete_files_if_media_changed(%Ecto.Changeset{changes: changes, data: data} = changeset) do Enum.each([:avatar, :banner], fn key -> diff --git a/lib/mobilizon/instances/instance.ex b/lib/mobilizon/instances/instance.ex new file mode 100644 index 000000000..a3aceddef --- /dev/null +++ b/lib/mobilizon/instances/instance.ex @@ -0,0 +1,19 @@ +defmodule Mobilizon.Instances.Instance do + @moduledoc """ + An instance representation + + Using a MATERIALIZED VIEW underneath + """ + use Ecto.Schema + + @primary_key {:domain, :string, []} + schema "instances" do + field(:event_count, :integer) + field(:person_count, :integer) + field(:group_count, :integer) + field(:followers_count, :integer) + field(:followings_count, :integer) + field(:reports_count, :integer) + field(:media_size, :integer) + end +end diff --git a/lib/mobilizon/instances/instances.ex b/lib/mobilizon/instances/instances.ex new file mode 100644 index 000000000..470e06c1b --- /dev/null +++ b/lib/mobilizon/instances/instances.ex @@ -0,0 +1,115 @@ +defmodule Mobilizon.Instances do + @moduledoc """ + The instances context + """ + alias Ecto.Adapters.SQL + alias Mobilizon.Actors.{Actor, Follower} + alias Mobilizon.Instances.Instance + alias Mobilizon.Storage.{Page, Repo} + import Ecto.Query + + @is_null_fragment "CASE WHEN ? IS NULL THEN FALSE ELSE TRUE END" + + @spec instances(Keyword.t()) :: Page.t(Instance.t()) + def instances(options) do + page = Keyword.get(options, :page) + limit = Keyword.get(options, :limit) + order_by = Keyword.get(options, :order_by) + direction = Keyword.get(options, :direction) + filter_domain = Keyword.get(options, :filter_domain) + # suspend_status = Keyword.get(options, :filter_suspend_status) + follow_status = Keyword.get(options, :filter_follow_status) + + order_by_options = Keyword.new([{direction, order_by}]) + + subquery = + Actor + |> where( + [a], + a.preferred_username == "relay" and a.type == :Application and not is_nil(a.domain) + ) + |> join(:left, [a], f1 in Follower, on: f1.target_actor_id == a.id) + |> join(:left, [a], f2 in Follower, on: f2.actor_id == a.id) + |> select([a, f1, f2], %{ + domain: a.domain, + has_relay: fragment(@is_null_fragment, a.id), + following: fragment(@is_null_fragment, f2.id), + following_approved: f2.approved, + follower: fragment(@is_null_fragment, f1.id), + follower_approved: f1.approved + }) + + query = + Instance + |> join(:left, [i], s in subquery(subquery), on: i.domain == s.domain) + |> select([i, s], {i, s}) + |> order_by(^order_by_options) + + query = + if is_nil(filter_domain) or filter_domain == "" do + query + else + where(query, [i], like(i.domain, ^"%#{filter_domain}%")) + end + + query = + case follow_status do + :following -> where(query, [i, s], s.following == true) + :followed -> where(query, [i, s], s.follower == true) + :all -> query + end + + %Page{elements: elements} = paged_instances = Page.build_page(query, page, limit, :domain) + + %Page{ + paged_instances + | elements: Enum.map(elements, &convert_instance_meta/1) + } + end + + @spec instance(String.t()) :: Instance.t() + def instance(domain) do + Instance + |> where(domain: ^domain) + |> Repo.one() + end + + @spec all_domains :: list(Instance.t()) + def all_domains do + Instance + |> distinct(true) + |> select([:domain]) + |> Repo.all() + end + + @spec refresh :: %{ + :rows => nil | [[term()] | binary()], + :num_rows => non_neg_integer(), + optional(atom()) => any() + } + def refresh do + SQL.query!(Repo, "REFRESH MATERIALIZED VIEW instances") + end + + defp convert_instance_meta( + {instance, + %{ + domain: _domain, + follower: follower, + follower_approved: follower_approved, + following: following, + following_approved: following_approved, + has_relay: has_relay + }} + ) do + instance + |> Map.put(:follower_status, follow_status(following, following_approved)) + |> Map.put(:followed_status, follow_status(follower, follower_approved)) + |> Map.put(:has_relay, has_relay) + end + + defp follow_status(true, true), do: :approved + defp follow_status(true, false), do: :pending + defp follow_status(false, _), do: :none + defp follow_status(nil, _), do: :none +end diff --git a/lib/service/workers/refresh_instances.ex b/lib/service/workers/refresh_instances.ex new file mode 100644 index 000000000..23515b3fa --- /dev/null +++ b/lib/service/workers/refresh_instances.ex @@ -0,0 +1,31 @@ +defmodule Mobilizon.Service.Workers.RefreshInstances do + @moduledoc """ + Worker to refresh the instances materialized view and the relay actors + """ + + use Oban.Worker, unique: [period: :infinity, keys: [:event_uuid, :action]] + + alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor + alias Mobilizon.Instances + alias Mobilizon.Instances.Instance + alias Oban.Job + + @impl Oban.Worker + @spec perform(Oban.Job.t()) :: :ok + def perform(%Job{}) do + Instances.refresh() + + Instances.all_domains() + |> Enum.each(&refresh_instance_actor/1) + end + + @spec refresh_instance_actor(Instance.t()) :: + {:ok, Mobilizon.Actors.Actor.t()} + | {:error, + Mobilizon.Federation.ActivityPub.Actor.make_actor_errors() + | Mobilizon.Federation.WebFinger.finger_errors()} + + defp refresh_instance_actor(%Instance{domain: domain}) do + ActivityPubActor.find_or_make_actor_from_nickname("relay@#{domain}") + end +end diff --git a/priv/repo/migrations/20211223141104_add_instance_materialized_view.exs b/priv/repo/migrations/20211223141104_add_instance_materialized_view.exs new file mode 100644 index 000000000..a997321c9 --- /dev/null +++ b/priv/repo/migrations/20211223141104_add_instance_materialized_view.exs @@ -0,0 +1,64 @@ +defmodule Mobilizon.Storage.Repo.Migrations.AddInstanceMaterializedView do + use Ecto.Migration + + def up do + execute(""" + CREATE MATERIALIZED VIEW instances AS + SELECT + a.domain, + COUNT(DISTINCT(p.id)) AS person_count, + COUNT(DISTINCT(g.id)) AS group_count, + COUNT(DISTINCT(e.id)) AS event_count, + COUNT(f1.id) AS followers_count, + COUNT(f2.id) AS followings_count, + COUNT(r.id) AS reports_count, + SUM(COALESCE((m.file->>'size')::int, 0)) AS media_size + FROM actors a + LEFT JOIN actors p ON a.id = p.id AND p.type = 'Person' + LEFT JOIN actors g ON a.id = g.id AND g.type = 'Group' + LEFT JOIN events e ON a.id = e.organizer_actor_id + LEFT JOIN followers f1 ON a.id = f1.actor_id + LEFT JOIN followers f2 ON a.id = f2.target_actor_id + LEFT JOIN reports r ON r.reported_id = a.id + LEFT JOIN medias m ON m.actor_id = a.id + WHERE a.domain IS NOT NULL + GROUP BY a.domain; + """) + + execute(""" + CREATE OR REPLACE FUNCTION refresh_instances() + RETURNS trigger AS $$ + BEGIN + REFRESH MATERIALIZED VIEW instances; + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + """) + + execute(""" + DROP TRIGGER IF EXISTS refresh_instances_trigger ON actors; + """) + + execute(""" + CREATE TRIGGER refresh_instances_trigger + AFTER INSERT OR UPDATE OR DELETE + ON actors + FOR EACH STATEMENT + EXECUTE PROCEDURE refresh_instances(); + """) + + create_if_not_exists(unique_index("instances", [:domain])) + end + + def down do + drop_if_exists(unique_index("instances", [:domain])) + + execute(""" + DROP FUNCTION IF EXISTS refresh_instances() CASCADE; + """) + + execute(""" + DROP MATERIALIZED VIEW IF EXISTS instances; + """) + end +end diff --git a/schema.graphql b/schema.graphql index fdb2a86aa..70c117702 100644 --- a/schema.graphql +++ b/schema.graphql @@ -211,6 +211,9 @@ type Config { "The instance's features" features: Features + "The instance's restrictions" + restrictions: Restrictions + "The instance's version" version: String @@ -240,6 +243,9 @@ type Config { "Web Push settings for the instance" webPush: WebPush + + "The instance list of export formats" + exportFormats: ExportFormats } "A tag" @@ -306,7 +312,13 @@ type TodoList { actor: Actor "The todo-list's todos" - todos: PaginatedTodoList + todos( + "The page in the paginated todos list" + page: Int + + "The limit of todos per page" + limit: Int + ): PaginatedTodoList } "Represents a participant to an event" @@ -455,7 +467,7 @@ type Comment implements ActivityObject & ActionLogObject { isAnnouncement: Boolean! "The comment language" - language: String! + language: String } "An attached media or a link to a media" @@ -690,6 +702,9 @@ enum ExportFormatEnum { "PDF format" PDF + + "ODS format" + ODS } "The list of visibility options for a comment" @@ -770,6 +785,14 @@ interface Interactable { url: String } +enum EventType { + "The event will happen in person. It can also be livestreamed, but has a physical address" + IN_PERSON + + "The event will only happen online. It has no physical address" + ONLINE +} + enum EventMetadataType { "A string" STRING @@ -794,6 +817,14 @@ type DeletedObject { id: ID } +"A follow group event" +type FollowedGroupEvent { + user: User + profile: Person + group: Group + event: Event +} + "A paginated list of comments" type PaginatedCommentList { "A list of comments" @@ -851,7 +882,7 @@ type Post implements ActivityObject { updatedAt: DateTime "The post language" - language: String! + language: String "The post's tags" tags: [Tag] @@ -941,6 +972,12 @@ type Statistics { numberOfInstanceFollowings: Int } +"Export formats configuration" +type ExportFormats { + "The list of formats the event participants can be exported to" + eventParticipants: [String] +} + "Search persons result" type Persons { "Total elements" @@ -1128,6 +1165,18 @@ type Person implements ActionLogObject & Actor { "The limit of memberships per page" limit: Int ): PaginatedMemberList + + "The list of groups this person follows" + follows( + "Filter by group federated username" + group: String + + "The page in the follows list" + page: Int + + "The limit of follows per page" + limit: Int + ): PaginatedFollowerList } "Root Mutation" @@ -1396,6 +1445,30 @@ type RootMutationType { groupId: ID! ): DeletedObject + "Follow a group" + followGroup( + "The group ID" + groupId: ID! + + "Whether to notify profile from group activity" + notify: Boolean + ): Follower + + "Update a group follow" + updateGroupFollow( + "The follow ID" + followId: ID! + + "Whether to notify profile from group activity" + notify: Boolean + ): Follower + + "Unfollow a group" + unfollowGroup( + "The group ID" + groupId: ID! + ): Follower + "Create an event" createEvent( "The event's title" @@ -1589,6 +1662,9 @@ type RootMutationType { "The anonymous participant's locale" locale: String + + "The anonymous participant's timezone" + timezone: String ): Participant "Leave an event" @@ -1663,6 +1739,18 @@ type RootMutationType { id: ID! ): Member + "Approve a membership request" + approveMember( + "The member ID" + memberId: ID! + ): Member + + "Reject a membership request" + rejectMember( + "The member ID" + memberId: ID! + ): Member + "Update a member's role" updateMember( "The member ID" @@ -1674,11 +1762,11 @@ type RootMutationType { "Remove a member from a group" removeMember( - "The group ID" - groupId: ID! - "The member ID" memberId: ID! + + "Whether the member should be excluded from the group" + exclude: Boolean ): Member "Create a Feed Token" @@ -2108,6 +2196,12 @@ type RootQueryType { "A geohash for coordinates" location: String + "Whether to include the groups the current actor is member or follower" + excludeMyGroups: Boolean + + "The minimum visibility the group must have" + minimumVisibility: GroupVisibility + "Radius around the location to search in" radius: Float @@ -2128,6 +2222,9 @@ type RootQueryType { "A geohash for coordinates" location: String + "Whether the event is online or in person" + type: EventType + "Radius around the location to search in" radius: Float @@ -2389,6 +2486,12 @@ type RootQueryType { direction: String ): PaginatedFollowerList + "Get an instance's details" + instance( + "The instance domain" + domain: ID! + ): Instance + "Get a todo list" todoList( "The todo-list ID" @@ -2461,6 +2564,39 @@ string. """ scalar NaiveDateTime +"An instance representation" +type Instance { + "The domain name of the instance" + domain: ID + + "Whether this instance has a Mobilizon relay actor" + hasRelay: Boolean + + "Do we follow this instance" + followerStatus: InstanceFollowStatus + + "Does this instance follow us?" + followedStatus: InstanceFollowStatus + + "The number of profiles on this instance we know of" + personCount: Int + + "The number of grouo on this instance we know of" + groupCount: Int + + "The number of their profiles who follow our groups" + followersCount: Int + + "The number of our profiles who follow their groups" + followingsCount: Int + + "The number of reports made against profiles from this instance" + reportsCount: Int + + "The size of all the media files sent by actors from this instance" + mediaSize: Int +} + """ The `DateTime` scalar type represents a date and time in the UTC timezone. The DateTime appears in a JSON response as an ISO8601 formatted @@ -2573,8 +2709,14 @@ input EventOptionsInput { "Show event end time" showEndTime: Boolean + "The event's timezone" + timezone: String + "Whether to show or hide the person organizer when event is organized by a group" hideOrganizerWhenGroupEvent: Boolean + + "Whether the event is fully online" + isOnline: Boolean } "A report object" @@ -2765,7 +2907,7 @@ type Event implements ActivityObject & Interactable & ActionLogObject { metadata: [EventMetadata] "The event language" - language: String! + language: String } "An event offer" @@ -2835,6 +2977,18 @@ input AddressInput { "The address's original ID from the provider" originId: String + + "The (estimated) timezone of the location" + timezone: String +} + +"The instance's restrictions" +type Restrictions { + "Whether groups creation is allowed only for admin, not for all users" + onlyAdminCanCreateGroups: Boolean + + "Whether events creation is allowed only for groups, not for persons" + onlyGroupsCanCreateEvents: Boolean } "Instance anonymous configuration" @@ -2916,8 +3070,14 @@ type EventOptions { "Show event end time" showEndTime: Boolean + "The event's timezone" + timezone: String + "Whether to show or hide the person organizer when event is organized by a group" hideOrganizerWhenGroupEvent: Boolean + + "Whether the event is fully online" + isOnline: Boolean } "A resource provider details" @@ -2958,6 +3118,9 @@ type Follower { "Whether the follow has been approved by the target actor" approved: Boolean + "Whether the follower will be notified by the target actor's activity or not (applicable for profile\/group follows)" + notify: Boolean + "When the follow was created" insertedAt: DateTime @@ -3322,6 +3485,15 @@ type PaginatedGroupList { total: Int } +"A paginated list of follow group events" +type PaginatedFollowedGroupEvents { + "A list of follow group events" + elements: [FollowedGroupEvent] + + "The total number of follow group events in the list" + total: Int +} + "Instance map tiles configuration" type Tiles { "The instance's tiles endpoint" @@ -3377,6 +3549,20 @@ type Address { "The address's original ID from the provider" originId: String + + "The (estimated) timezone of the location" + timezone: String +} + +enum InstanceFollowStatus { + "The instance follow was approved" + APPROVED + + "The instance follow is still pending" + PENDING + + "There's no instance follow etablished" + NONE } "The instance's terms configuration" @@ -3599,6 +3785,9 @@ type User implements ActionLogObject { "The list of memberships for this user" memberships( + "A name to filter members by" + name: String + "The page in the paginated memberships list" page: Int @@ -3615,6 +3804,18 @@ type User implements ActionLogObject { limit: Int ): [Event] + "The suggested events from the groups this user follows" + followedGroupEvents( + "The page in the follow group events list" + page: Int + + "The limit of follow group events per page" + limit: Int + + "Filter follow group events by event start datetime" + afterDatetime: DateTime + ): PaginatedFollowedGroupEvents + "The list of settings for this user" settings: UserSettings @@ -3731,6 +3932,9 @@ type Group implements ActionLogObject & ActivityObject & Interactable & Actor { "A paginated list of group members" members( + "A name to filter members by" + name: String + "The page in the paginated member list" page: Int @@ -3760,7 +3964,13 @@ type Group implements ActionLogObject & ActivityObject & Interactable & Actor { ): PaginatedPostList "A paginated list of the todo lists this group has" - todoLists: PaginatedTodoListList + todoLists( + "The page in the paginated todo-lists list" + page: Int + + "The limit of todo-lists per page" + limit: Int + ): PaginatedTodoListList "A paginated list of the followers this group has" followers( From bc6cec45fa9f8cbdb74daa9278dd00248f3526ce Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Wed, 29 Dec 2021 14:58:38 +0100 Subject: [PATCH 141/506] Improve logging in module and handle more Webfinger errors Signed-off-by: Thomas Citharel --- lib/federation/web_finger/web_finger.ex | 30 +++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/federation/web_finger/web_finger.ex b/lib/federation/web_finger/web_finger.ex index 9f1ae34ec..7568a9b7a 100644 --- a/lib/federation/web_finger/web_finger.ex +++ b/lib/federation/web_finger/web_finger.ex @@ -125,7 +125,11 @@ defmodule Mobilizon.Federation.WebFinger do defp maybe_add_profile_page(data, _actor), do: data @type finger_errors :: - :host_not_found | :address_invalid | :http_error | :webfinger_information_not_json + :host_not_found + | :address_invalid + | :http_error + | :webfinger_information_not_json + | :no_url_in_webfinger_data @doc """ Finger an actor to retreive it's ActivityPub ID/URL @@ -144,6 +148,10 @@ defmodule Mobilizon.Federation.WebFinger do {:ok, %{"url" => url}} -> {:ok, url} + {:ok, _} -> + Logger.debug("No URL found for actor from webfinger data") + {:error, :no_url_in_webfinger_data} + {:error, err} -> Logger.debug("Couldn't process webfinger data for #{actor}") {:error, err} @@ -158,11 +166,14 @@ defmodule Mobilizon.Federation.WebFinger do @spec fetch_webfinger_data(String.t()) :: {:ok, map()} | {:error, :webfinger_information_not_json | :http_error} defp fetch_webfinger_data(address) do + Logger.debug("Calling WebfingerClient with #{inspect(address)}") + case WebfingerClient.get(address) do {:ok, %{body: body, status: code}} when code in 200..299 -> webfinger_from_json(body) - _ -> + err -> + Logger.debug("Failed to fetch webfinger data #{inspect(err)}") {:error, :http_error} end end @@ -173,12 +184,14 @@ defmodule Mobilizon.Federation.WebFinger do case apply_webfinger_endpoint(actor) do address when is_binary(address) -> if address_invalid(address) do + Logger.info("Webfinger endpoint seems to be an invalid URL #{inspect(address)}") {:error, :address_invalid} else {:ok, address} end _ -> + Logger.info("Host not found in actor address #{inspect(actor)}") {:error, :host_not_found} end end @@ -188,12 +201,15 @@ defmodule Mobilizon.Federation.WebFinger do @spec find_webfinger_endpoint(String.t()) :: {:ok, String.t()} | {:error, :link_not_found} | {:error, any()} defp find_webfinger_endpoint(domain) when is_binary(domain) do + Logger.debug("Calling HostMetaClient for #{domain}") + with {:ok, %Tesla.Env{status: 200, body: body}} <- - HostMetaClient.get("http://#{domain}/.well-known/host-meta"), + HostMetaClient.get("https://#{domain}/.well-known/host-meta"), link_template when is_binary(link_template) <- find_link_from_template(body) do {:ok, link_template} else {:ok, %Tesla.Env{status: 404}} -> {:error, :entity_not_found} + {:ok, %Tesla.Env{}} -> {:error, :http_error} {:error, :link_not_found} -> {:error, :link_not_found} {:error, error} -> {:error, error} end @@ -204,10 +220,12 @@ defmodule Mobilizon.Federation.WebFinger do with {:ok, domain} <- domain_from_federated_actor(actor) do case find_webfinger_endpoint(domain) do {:ok, link_template} -> + Logger.debug("Using webfinger location provided by host-meta endpoint") String.replace(link_template, "{uri}", "acct:#{actor}") _ -> - "http://#{domain}/.well-known/webfinger?resource=acct:#{actor}" + Logger.debug("Using default webfinger location") + "https://#{domain}/.well-known/webfinger?resource=acct:#{actor}" end end end @@ -233,6 +251,10 @@ defmodule Mobilizon.Federation.WebFinger do {"application/activity+json", "self"} -> Map.put(data, "url", link["href"]) + {nil, _rel} -> + Logger.debug("No type declared for the following link #{inspect(link)}") + data + _ -> Logger.debug(fn -> "Unhandled type to finger: #{inspect(link["type"])}" From 1319985047e7e07ba180448e547bde64faa9fba8 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Wed, 29 Dec 2021 14:59:33 +0100 Subject: [PATCH 142/506] Rename Mobilizon.Actors.is_following/2 to check_follow/2 Signed-off-by: Thomas Citharel --- lib/graphql/api/follows.ex | 4 +-- lib/mobilizon/actors/actors.ex | 8 +++--- .../transmogrifier/follow_test.exs | 28 +++++++++---------- .../activity_pub/transmogrifier/undo_test.exs | 2 +- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/graphql/api/follows.ex b/lib/graphql/api/follows.ex index f30219fe6..05c87b0b1 100644 --- a/lib/graphql/api/follows.ex +++ b/lib/graphql/api/follows.ex @@ -41,7 +41,7 @@ defmodule Mobilizon.GraphQL.API.Follows do "We're trying to accept a follow: #{followed_url} is accepting #{follower_url} follow request." ) - case Actors.is_following(follower, followed) do + case Actors.check_follow(follower, followed) do %Follower{approved: false} = follow -> Actions.Accept.accept( :follow, @@ -68,7 +68,7 @@ defmodule Mobilizon.GraphQL.API.Follows do "We're trying to reject a follow: #{followed_url} is rejecting #{follower_url} follow request." ) - case Actors.is_following(follower, followed) do + case Actors.check_follow(follower, followed) do %Follower{approved: false} -> {:error, "Follow already rejected"} diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index b67b85e11..b264c4dc4 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -1176,7 +1176,7 @@ defmodule Mobilizon.Actors do if followed.suspended do {:error, :followed_suspended} else - case is_following(follower, followed) do + case check_follow(follower, followed) do %Follower{} -> {:error, :already_following} @@ -1202,7 +1202,7 @@ defmodule Mobilizon.Actors do @spec unfollow(Actor.t(), Actor.t()) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t() | String.t()} def unfollow(%Actor{} = followed, %Actor{} = follower) do - case {:already_following, is_following(follower, followed)} do + case {:already_following, check_follow(follower, followed)} do {:already_following, %Follower{} = follow} -> delete_follower(follow) @@ -1214,8 +1214,8 @@ defmodule Mobilizon.Actors do @doc """ Checks whether an actor is following another actor. """ - @spec is_following(Actor.t(), Actor.t()) :: Follower.t() | nil - def is_following(%Actor{} = follower_actor, %Actor{} = followed_actor) do + @spec check_follow(Actor.t(), Actor.t()) :: Follower.t() | nil + def check_follow(%Actor{} = follower_actor, %Actor{} = followed_actor) do get_follower_by_followed_and_following(followed_actor, follower_actor) end diff --git a/test/federation/activity_pub/transmogrifier/follow_test.exs b/test/federation/activity_pub/transmogrifier/follow_test.exs index 285fea0cb..925bdd82c 100644 --- a/test/federation/activity_pub/transmogrifier/follow_test.exs +++ b/test/federation/activity_pub/transmogrifier/follow_test.exs @@ -36,7 +36,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do end) =~ "Only group and instances can be followed" actor = Actors.get_actor_with_preload(actor.id) - refute Actors.is_following(Actors.get_actor_by_url!(data["actor"], true), actor) + refute Actors.check_follow(Actors.get_actor_by_url!(data["actor"], true), actor) end test "it works for incoming follow requests" do @@ -68,7 +68,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do assert data["id"] == "https://social.tcit.fr/users/tcit#follows/2" actor = Actors.get_actor_with_preload(actor.id) - assert Actors.is_following(Actors.get_actor_by_url!(data["actor"], true), actor) + assert Actors.check_follow(Actors.get_actor_by_url!(data["actor"], true), actor) end test "it rejects activities without a valid ID" do @@ -97,7 +97,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do # assert data["actor"] == "https://hubzilla.example.org/channel/kaniini" # assert data["type"] == "Follow" # assert data["id"] == "https://hubzilla.example.org/channel/kaniini#follows/2" - # assert User.is_following(User.get_by_ap_id(data["actor"]), user) + # assert User.check_follow(User.get_by_ap_id(data["actor"]), user) # end end @@ -106,10 +106,10 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do follower = insert(:actor) followed = insert(:group, manually_approves_followers: false) - refute Actors.is_following(follower, followed) + refute Actors.check_follow(follower, followed) {:ok, follow_activity, _} = Actions.Follow.follow(follower, followed) - assert Actors.is_following(follower, followed) + assert Actors.check_follow(follower, followed) follow_object_id = follow_activity.data["id"] @@ -131,17 +131,17 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - assert Actors.is_following(follower, followed) + assert Actors.check_follow(follower, followed) end test "it works for incoming accepts which were pre-accepted" do follower = insert(:actor) followed = insert(:group, manually_approves_followers: true) - refute Actors.is_following(follower, followed) + refute Actors.check_follow(follower, followed) {:ok, follow_activity, _} = Actions.Follow.follow(follower, followed) - assert Actors.is_following(follower, followed) + assert Actors.check_follow(follower, followed) follow_object_id = follow_activity.data["id"] @@ -166,7 +166,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - assert Actors.is_following(follower, followed) + assert Actors.check_follow(follower, followed) end test "it works for incoming accepts which are referenced by IRI only" do @@ -188,7 +188,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - assert Actors.is_following(follower, followed) + assert Actors.check_follow(follower, followed) end test "it fails for incoming accepts which cannot be correlated" do @@ -207,7 +207,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - refute Actors.is_following(follower, followed) + refute Actors.check_follow(follower, followed) end end @@ -228,7 +228,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, follower} = Actors.get_actor_by_url(follower.url) - refute Actors.is_following(follower, followed) + refute Actors.check_follow(follower, followed) end test "it works for incoming rejects which are referenced by IRI only" do @@ -237,7 +237,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, follow_activity, _} = Actions.Follow.follow(follower, followed) - assert Actors.is_following(follower, followed) + assert Actors.check_follow(follower, followed) reject_data = File.read!("test/fixtures/mastodon-reject-activity.json") @@ -247,7 +247,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.FollowTest do {:ok, %Activity{data: _}, _} = Transmogrifier.handle_incoming(reject_data) - refute Actors.is_following(follower, followed) + refute Actors.check_follow(follower, followed) end end end diff --git a/test/federation/activity_pub/transmogrifier/undo_test.exs b/test/federation/activity_pub/transmogrifier/undo_test.exs index 74d9b30f6..35632690c 100644 --- a/test/federation/activity_pub/transmogrifier/undo_test.exs +++ b/test/federation/activity_pub/transmogrifier/undo_test.exs @@ -82,7 +82,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.UndoTest do assert data["actor"] == "https://social.tcit.fr/users/tcit" {:ok, followed} = Actors.get_actor_by_url(data["actor"]) - refute Actors.is_following(followed, actor) + refute Actors.check_follow(followed, actor) end end end From 26b1ea401ac782e9e239e4a56743e070f9b6192a Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Thu, 6 Jan 2022 18:48:48 +0100 Subject: [PATCH 143/506] Fix various issues reported by Dializer Signed-off-by: Thomas Citharel --- lib/federation/activity_pub/actor.ex | 6 +++--- lib/federation/activity_pub/audience.ex | 2 +- lib/federation/activity_pub/refresher.ex | 2 +- lib/graphql/api/follows.ex | 3 ++- lib/mix/tasks/mobilizon/instance.ex | 2 +- lib/mix/tasks/mobilizon/users/new.ex | 4 ---- lib/mobilizon/actors/actors.ex | 2 +- lib/service/workers/clean_unconfirmed_users_worker.ex | 7 ++++++- lib/service/workers/helper.ex | 2 +- test/federation/activity_pub/actor_test.exs | 8 ++++---- test/federation/web_finger/web_finger_test.exs | 10 +++++----- test/graphql/resolvers/member_test.exs | 4 ++-- 12 files changed, 27 insertions(+), 25 deletions(-) diff --git a/lib/federation/activity_pub/actor.ex b/lib/federation/activity_pub/actor.ex index 42bd31d8b..8f4f07b49 100644 --- a/lib/federation/activity_pub/actor.ex +++ b/lib/federation/activity_pub/actor.ex @@ -108,15 +108,15 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do @doc """ Create an actor inside our database from username, using WebFinger to find out its AP ID and then fetch it """ - @spec make_actor_from_nickname(nickname :: String.t(), preload :: boolean) :: + @spec make_actor_from_nickname(nickname :: String.t(), options :: Keyword.t()) :: {:ok, Actor.t()} | {:error, make_actor_errors | WebFinger.finger_errors()} - def make_actor_from_nickname(nickname, preload \\ false) do + def make_actor_from_nickname(nickname, options \\ []) do Logger.debug("Fingering actor from nickname #{nickname}") case WebFinger.finger(nickname) do {:ok, url} when is_binary(url) -> Logger.debug("Matched #{nickname} to URL #{url}, now making actor") - make_actor_from_url(url, preload: preload) + make_actor_from_url(url, options) {:error, e} -> {:error, e} diff --git a/lib/federation/activity_pub/audience.ex b/lib/federation/activity_pub/audience.ex index 540ba76e9..19502dfaa 100644 --- a/lib/federation/activity_pub/audience.ex +++ b/lib/federation/activity_pub/audience.ex @@ -20,7 +20,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do @doc """ Get audience for an entity """ - @spec get_audience(Entity.t()) :: audience() + @spec get_audience(Entity.t() | Participant.t()) :: audience() def get_audience(%Event{} = event) do extract_actors_from_event(event) end diff --git a/lib/federation/activity_pub/refresher.ex b/lib/federation/activity_pub/refresher.ex index bfa4f6c16..4a8fc8c66 100644 --- a/lib/federation/activity_pub/refresher.ex +++ b/lib/federation/activity_pub/refresher.ex @@ -190,7 +190,7 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do # If we're handling an activity @spec handling_element(map()) :: {:ok, any, struct} | :error - @spec handling_element(String.t()) :: {:ok, struct} | {:error, any()} + @spec handling_element(String.t()) :: {:ok, struct} | {:ok, atom, struct} | {:error, any()} defp handling_element(%{"type" => activity_type} = data) when activity_type in ["Create", "Update", "Delete"] do object = get_in(data, ["object"]) diff --git a/lib/graphql/api/follows.ex b/lib/graphql/api/follows.ex index 05c87b0b1..d981c89b8 100644 --- a/lib/graphql/api/follows.ex +++ b/lib/graphql/api/follows.ex @@ -69,7 +69,8 @@ defmodule Mobilizon.GraphQL.API.Follows do ) case Actors.check_follow(follower, followed) do - %Follower{approved: false} -> + %Follower{approved: false} = follow -> + Actors.delete_follower(follow) {:error, "Follow already rejected"} %Follower{} = follow -> diff --git a/lib/mix/tasks/mobilizon/instance.ex b/lib/mix/tasks/mobilizon/instance.ex index 3d3dccc66..180bb17bf 100644 --- a/lib/mix/tasks/mobilizon/instance.ex +++ b/lib/mix/tasks/mobilizon/instance.ex @@ -130,7 +130,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do options, :listen_port, "What port will the app listen to (leave it if you are using the default setup with nginx)?", - 4000 + "4000" ) instance_secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) diff --git a/lib/mix/tasks/mobilizon/users/new.ex b/lib/mix/tasks/mobilizon/users/new.ex index db013f7c1..df39c596b 100644 --- a/lib/mix/tasks/mobilizon/users/new.ex +++ b/lib/mix/tasks/mobilizon/users/new.ex @@ -67,10 +67,6 @@ defmodule Mix.Tasks.Mobilizon.Users.New do {:error, %Ecto.Changeset{errors: errors}} -> shell_error(inspect(errors)) shell_error("User has not been created because of the above reason.") - - err -> - shell_error(inspect(err)) - shell_error("User has not been created because of an unknown reason.") end end diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index b264c4dc4..1df299fb6 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -1475,7 +1475,7 @@ defmodule Mobilizon.Actors do |> where([_q, ..., a], like(a.name, ^"%#{name}%") or like(a.preferred_username, ^"%#{name}%")) end - @spec join_members_actor(Ecto.Query.t()) :: Ecto.Query.t() + @spec join_members_actor(Ecto.Queryable.t()) :: Ecto.Query.t() defp join_members_actor(query) do join(query, :inner, [q], a in Actor, on: q.actor_id == a.id) end diff --git a/lib/service/workers/clean_unconfirmed_users_worker.ex b/lib/service/workers/clean_unconfirmed_users_worker.ex index 73646beb3..2df01b271 100644 --- a/lib/service/workers/clean_unconfirmed_users_worker.ex +++ b/lib/service/workers/clean_unconfirmed_users_worker.ex @@ -8,7 +8,12 @@ defmodule Mobilizon.Service.Workers.CleanUnconfirmedUsersWorker do @impl Oban.Worker def perform(%Job{}) do - if Mobilizon.Config.get!([:instance, :remove_unconfirmed_users]) and should_perform?() do + remove_unconfirmed_users = + :mobilizon + |> Application.get_env(:instance) + |> Keyword.get(:remove_unconfirmed_users, false) + + if remove_unconfirmed_users and should_perform?() do CleanUnconfirmedUsers.clean() end end diff --git a/lib/service/workers/helper.ex b/lib/service/workers/helper.ex index 28bb0d7b8..8cb14f399 100644 --- a/lib/service/workers/helper.ex +++ b/lib/service/workers/helper.ex @@ -41,7 +41,7 @@ defmodule Mobilizon.Service.Workers.Helper do alias Oban.Job - @spec enqueue(String.t(), map(), Keyword.t()) :: + @spec enqueue(String.t() | :atom, map(), Keyword.t()) :: {:ok, Job.t()} | {:error, Ecto.Changeset.t()} def enqueue(operation, params, worker_args \\ []) do params = Map.merge(%{"op" => operation}, params) diff --git a/test/federation/activity_pub/actor_test.exs b/test/federation/activity_pub/actor_test.exs index 5298f0e84..d402822bd 100644 --- a/test/federation/activity_pub/actor_test.exs +++ b/test/federation/activity_pub/actor_test.exs @@ -30,7 +30,7 @@ defmodule Mobilizon.Federation.ActivityPub.ActorTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://framapiaf.org/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://framapiaf.org/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 404, body: ""}} end) @@ -43,7 +43,7 @@ defmodule Mobilizon.Federation.ActivityPub.ActorTest do |> expect(:call, fn %{ method: :get, - url: "http://framapiaf.org/.well-known/webfinger?resource=acct:tcit@framapiaf.org" + url: "https://framapiaf.org/.well-known/webfinger?resource=acct:tcit@framapiaf.org" }, _opts -> {:ok, %Tesla.Env{status: 200, body: webfinger_data}} @@ -69,7 +69,7 @@ defmodule Mobilizon.Federation.ActivityPub.ActorTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://framapiaf.org/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://framapiaf.org/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 404, body: ""}} end) @@ -82,7 +82,7 @@ defmodule Mobilizon.Federation.ActivityPub.ActorTest do |> expect(:call, fn %{ method: :get, - url: "http://framapiaf.org/.well-known/webfinger?resource=acct:tcit@framapiaf.org" + url: "https://framapiaf.org/.well-known/webfinger?resource=acct:tcit@framapiaf.org" }, _opts -> {:ok, %Tesla.Env{status: 200, body: webfinger_data}} diff --git a/test/federation/web_finger/web_finger_test.exs b/test/federation/web_finger/web_finger_test.exs index c3cb48afb..5cd5f2f29 100644 --- a/test/federation/web_finger/web_finger_test.exs +++ b/test/federation/web_finger/web_finger_test.exs @@ -72,7 +72,7 @@ defmodule Mobilizon.Federation.WebFingerTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://social.tcit.fr/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://social.tcit.fr/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 200, body: host_meta_xml}} end) @@ -100,7 +100,7 @@ defmodule Mobilizon.Federation.WebFingerTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://pleroma.soykaf.com/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://pleroma.soykaf.com/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 200, body: host_meta_xml}} end) @@ -127,7 +127,7 @@ defmodule Mobilizon.Federation.WebFingerTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://framatube.org/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://framatube.org/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 200, body: host_meta_xml}} end) @@ -154,7 +154,7 @@ defmodule Mobilizon.Federation.WebFingerTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://squeet.me/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://squeet.me/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 200, body: host_meta_xml}} end) @@ -182,7 +182,7 @@ defmodule Mobilizon.Federation.WebFingerTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://demo.gancio.org/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://demo.gancio.org/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 200, body: host_meta_xml}} end) diff --git a/test/graphql/resolvers/member_test.exs b/test/graphql/resolvers/member_test.exs index 30e4a0cc7..e2ed49903 100644 --- a/test/graphql/resolvers/member_test.exs +++ b/test/graphql/resolvers/member_test.exs @@ -301,7 +301,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do HostMetaClientMock |> expect(:call, fn - %{method: :get, url: "http://nowhere.absolute/.well-known/host-meta"}, _opts -> + %{method: :get, url: "https://nowhere.absolute/.well-known/host-meta"}, _opts -> {:ok, %Tesla.Env{status: 404, body: ""}} end) @@ -310,7 +310,7 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do %{ method: :get, url: - "http://nowhere.absolute/.well-known/webfinger?resource=acct:not_existing@nowhere.absolute" + "https://nowhere.absolute/.well-known/webfinger?resource=acct:not_existing@nowhere.absolute" }, _opts -> {:ok, %Tesla.Env{status: 404, body: ""}} From 193fcde123d461f0d0fdb4e7adb2481c2efdcc67 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 10 Jan 2022 10:17:50 +0100 Subject: [PATCH 144/506] Allow to filter reports by domain Signed-off-by: Thomas Citharel --- js/src/graphql/report.ts | 9 ++- js/src/router/settings.ts | 4 +- js/src/views/Admin/Instance.vue | 5 +- js/src/views/Moderation/ReportList.vue | 84 +++++++++++++++++--------- lib/graphql/resolvers/report.ex | 12 +++- lib/graphql/schema/report.ex | 1 + lib/mobilizon/reports/reports.ex | 38 +++++++----- 7 files changed, 104 insertions(+), 49 deletions(-) diff --git a/js/src/graphql/report.ts b/js/src/graphql/report.ts index 3fcffe8e8..d25436182 100644 --- a/js/src/graphql/report.ts +++ b/js/src/graphql/report.ts @@ -2,8 +2,13 @@ import gql from "graphql-tag"; import { ACTOR_FRAGMENT } from "./actor"; export const REPORTS = gql` - query Reports($status: ReportStatus, $page: Int, $limit: Int) { - reports(status: $status, page: $page, limit: $limit) { + query Reports( + $status: ReportStatus + $domain: String + $page: Int + $limit: Int + ) { + reports(status: $status, domain: $domain, page: $page, limit: $limit) { total elements { id diff --git a/js/src/router/settings.ts b/js/src/router/settings.ts index fcd8eeca2..c71730ba0 100644 --- a/js/src/router/settings.ts +++ b/js/src/router/settings.ts @@ -20,7 +20,7 @@ export enum SettingsRouteName { ADMIN_GROUPS = "ADMIN_GROUPS", ADMIN_GROUP_PROFILE = "ADMIN_GROUP_PROFILE", MODERATION = "MODERATION", - REPORTS = "Reports", + REPORTS = "REPORTS", REPORT = "Report", REPORT_LOGS = "Logs", CREATE_IDENTITY = "CreateIdentity", @@ -234,7 +234,7 @@ export const settingsRoutes: RouteConfig[] = [ meta: { requiredAuth: true, announcer: { skip: true } }, }, { - path: "/moderation/reports/:filter?", + path: "/moderation/reports", name: SettingsRouteName.REPORTS, component: (): Promise => import( diff --git a/js/src/views/Admin/Instance.vue b/js/src/views/Admin/Instance.vue index 85233234f..bfb4bd18c 100644 --- a/js/src/views/Admin/Instance.vue +++ b/js/src/views/Admin/Instance.vue @@ -70,7 +70,10 @@ {{ $t("Followers") }}
- + {{ instance.reportsCount }} diff --git a/js/src/views/Moderation/ReportList.vue b/js/src/views/Moderation/ReportList.vue index bce76169d..a1e278b9b 100644 --- a/js/src/views/Moderation/ReportList.vue +++ b/js/src/views/Moderation/ReportList.vue @@ -15,23 +15,37 @@
- - {{ $t("Open") }} + + {{ $t("Open") }} + {{ $t("Resolved") }} + {{ $t("Closed") }} + + - {{ $t("Resolved") }} - {{ $t("Closed") }} - + + +
  • diff --git a/js/src/main.ts b/js/src/main.ts index 317f2b37b..0e6695195 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -12,6 +12,7 @@ import { NotifierPlugin } from "./plugins/notifier"; import filters from "./filters"; import { i18n } from "./utils/i18n"; import apolloProvider from "./vue-apollo"; +import Breadcrumbs from "@/components/Utils/Breadcrumbs.vue"; import "./registerServiceWorker"; import "./assets/tailwind.css"; @@ -25,6 +26,7 @@ Vue.use(VueScrollTo); Vue.use(VTooltip); Vue.use(VueAnnouncer); Vue.use(VueSkipTo); +Vue.component("breadcrumbs-nav", Breadcrumbs); // Register the router hooks with their names Component.registerHooks([ diff --git a/js/src/views/Account/children/EditIdentity.vue b/js/src/views/Account/children/EditIdentity.vue index 0bba7acde..d0e56dd43 100644 --- a/js/src/views/Account/children/EditIdentity.vue +++ b/js/src/views/Account/children/EditIdentity.vue @@ -1,28 +1,6 @@