From a544e6aee796100822f408c26df53de3a213ea51 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Sun, 24 Apr 2022 17:57:56 +0200 Subject: [PATCH] WIP Signed-off-by: Thomas Citharel --- config/config.exs | 5 + .../activity_stream/converter/event.ex | 13 +- lib/graphql/schema/search.ex | 3 + lib/mobilizon/events/categories.ex | 14 ++ lib/service/search/external.ex | 19 +++ lib/service/search/provider.ex | 10 ++ lib/service/search/search_index.ex | 141 ++++++++++++++++++ 7 files changed, 193 insertions(+), 12 deletions(-) create mode 100644 lib/service/search/external.ex create mode 100644 lib/service/search/provider.ex create mode 100644 lib/service/search/search_index.ex diff --git a/config/config.exs b/config/config.exs index f5dc26cb5..f415e7041 100644 --- a/config/config.exs +++ b/config/config.exs @@ -248,6 +248,11 @@ config :mobilizon, :maps, type: :openstreetmap ] +config :mobilizon, Mobilizon.Service.Search, extra_provider: Mobilizon.Service.Search.SearchIndex + +config :mobilizon, Mobilizon.Service.Search.SearchIndex, + endpoint: "https://search.joinmobilizon.org" + config :mobilizon, :http_security, enabled: true, sts: false, diff --git a/lib/federation/activity_stream/converter/event.ex b/lib/federation/activity_stream/converter/event.ex index 6328b56f6..e40aab157 100644 --- a/lib/federation/activity_stream/converter/event.ex +++ b/lib/federation/activity_stream/converter/event.ex @@ -74,7 +74,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do medias: medias, begins_on: object["startTime"], ends_on: object["endTime"], - category: get_category(object["category"]), + category: Categories.get_category(object["category"]), visibility: visibility, join_options: Map.get(object, "joinMode", "free"), local: is_local?(object["id"]), @@ -331,15 +331,4 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do _participant_count ), do: nil - - @spec get_category(String.t() | nil) :: String.t() - defp get_category(nil), do: "MEETING" - - defp get_category(category) when is_binary(category) do - if category in Enum.map(Categories.list(), &String.upcase(to_string(&1.id))) do - category - else - get_category(nil) - end - end end diff --git a/lib/graphql/schema/search.ex b/lib/graphql/schema/search.ex index 2da1d8fa4..3a228f486 100644 --- a/lib/graphql/schema/search.ex +++ b/lib/graphql/schema/search.ex @@ -81,6 +81,8 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do description: "Radius around the location to search in" ) + arg(:external, :boolean, default_value: false, description: "Also return external results") + arg(:page, :integer, default_value: 1, description: "Result page") arg(:limit, :integer, default_value: 10, description: "Results limit per page") @@ -100,6 +102,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do description: "Radius around the location to search in" ) + arg(:external, :boolean, default_value: false, description: "Also return external results") arg(:page, :integer, default_value: 1, description: "Result page") arg(:limit, :integer, default_value: 10, description: "Results limit per page") arg(:begins_on, :datetime, description: "Filter events by their start date") diff --git a/lib/mobilizon/events/categories.ex b/lib/mobilizon/events/categories.ex index 0a210f56a..3bcfe43e1 100644 --- a/lib/mobilizon/events/categories.ex +++ b/lib/mobilizon/events/categories.ex @@ -9,6 +9,20 @@ defmodule Mobilizon.Events.Categories do build_in_categories() ++ extra_categories() end + @doc """ + Get a category for an input string + """ + @spec get_category(String.t() | nil) :: String.t() + def get_category(nil), do: "MEETING" + + def get_category(category) when is_binary(category) do + if category in Enum.map(list(), &String.upcase(to_string(&1.id))) do + category + else + get_category(nil) + end + end + defp build_in_categories do [ %{ diff --git a/lib/service/search/external.ex b/lib/service/search/external.ex new file mode 100644 index 000000000..6b1738ef8 --- /dev/null +++ b/lib/service/search/external.ex @@ -0,0 +1,19 @@ +defmodule Mobilizon.Service.Search.External do + @moduledoc """ + Search providers manager + """ + + @doc """ + Queries the external search provider + """ + def search(options) do + provider().search(options) + end + + @spec provider :: module() + defp provider do + :mobilizon + |> Application.get_env(Mobilizon.Service.Search, []) + |> Keyword.get(:extra_provider, Mobilizon.Service.Search.SearchIndex) + end +end diff --git a/lib/service/search/provider.ex b/lib/service/search/provider.ex new file mode 100644 index 000000000..4999fbf5f --- /dev/null +++ b/lib/service/search/provider.ex @@ -0,0 +1,10 @@ +defmodule Mobilizon.Service.Search.Provider do + @moduledoc """ + Behaviour for a search provider + """ + + @doc """ + Returns a paginated list of events + """ + @callback search(options :: Keyword.t()) :: Mobilizon.Storage.Page.t(Mobilizon.Events.Event.t()) +end diff --git a/lib/service/search/search_index.ex b/lib/service/search/search_index.ex new file mode 100644 index 000000000..554fb81bb --- /dev/null +++ b/lib/service/search/search_index.ex @@ -0,0 +1,141 @@ +defmodule Mobilizon.Service.Search.SearchIndex do + @moduledoc """ + Search provider for https://search.joinmobilizon.org + """ + + alias Mobilizon.Addresses.Address + alias Mobilizon.Actors.Actor + alias Mobilizon.Events.{Categories, Event, EventOptions} + alias Mobilizon.Service.HTTP.GeospatialClient + alias Mobilizon.Service.Search.Provider + alias Mobilizon.Storage.Page + import Plug.Conn.Query, only: [encode: 1] + + @behaviour Provider + + @default_endpoint "https://search.joinmobilizon.org" + + @events_api_endpoint "/api/v1/search/events" + @groups_api_endpoint "/api/v1/search/groups" + + @default_options [ + start: 0, + count: 10, + distance: "10_km" + ] + + # "?search=test&startDateMin=2022-04-21T16:08:32.675Z&boostLanguages[]=fr&boostLanguages[]=en&distance=10_km&sort=-match&start=0&count=5" + + @doc """ + Returns a paginated list of events + """ + @impl Provider + def search(options) do + case fetch(options) do + {:ok, %Tesla.Env{body: body, status: 200}} -> + {:ok, transform_results(body, Keyword.get(options, :type, :events))} + + err -> + require Logger + Logger.error(inspect(err)) + {:error, :http_error} + end + end + + defp fetch(options) do + @default_options + |> Keyword.merge(options) + |> build_url() + |> GeospatialClient.get() + end + + @spec transform_results(%{String.t() => list(map()) | non_neg_integer()}, atom()) :: + Page.t(Event.t() | Actor.t()) + defp transform_results(%{"data" => data, "total" => total}, type) do + %Page{ + total: total, + elements: + data + |> Enum.sort(&(&1["score"] >= &2["score"])) + |> Enum.map(fn element -> transform_result(element, type) end) + } + end + + @spec transform_result(map(), :events | :groups) :: Event.t() | Actor.t() + defp transform_result(event, :events) do + %Event{ + category: Categories.get_category(event["category"]), + organizer_actor: transform_actor(event["creator"]), + ends_on: event["endTime"], + attributed_to: transform_actor(event["group"], :Group), + id: event["id"], + join_options: event["joinMode"], + options: %EventOptions{ + is_online: event["isOnline"], + maximum_attendee_capacity: event["maximumAttendeeCapacity"] + }, + physical_address: transform_address(event["location"]), + title: event["name"], + publish_at: event["published"], + begins_on: event["startTime"], + status: String.downcase(event["status"]), + tags: event["tags"], + uuid: event["uuid"], + url: event["url"] + } + end + + defp transform_result(group, :groups) do + group = transform_actor(group) + + %Actor{ + group + | type: :Group, + physical_address: transform_address(group["location"]) + } + end + + defp transform_actor(actor, type \\ :Person) do + %Actor{ + type: type, + id: actor["id"], + url: actor["url"], + summary: actor["description"], + preferred_username: actor["name"], + name: actor["displayName"], + domain: actor["host"] + } + end + + defp transform_address(address) when is_map(address) do + %Address{ + description: address["name"], + geom: %Geo.Point{ + coordinates: {address["location"]["lon"], address["location"]["lat"]}, + srid: 4326 + }, + country: address["address"]["addressCountry"], + locality: address["address"]["addressLocality"], + region: address["address"]["addressRegion"], + postal_code: address["address"]["postalCode"], + street: address["address"]["streetAddress"] + } + end + + defp transform_address(_), do: nil + + @spec endpoint :: String.t() + defp endpoint do + :mobilizon + |> Application.get_env(__MODULE__, []) + |> Keyword.get(:endpoint, @default_endpoint) + end + + defp build_url(options) do + {type, options} = Keyword.pop(options, :type, :events) + "#{endpoint()}#{api_endpoint(type)}?#{encode(options)}" + end + + defp api_endpoint(:events), do: @events_api_endpoint + defp api_endpoint(:groups), do: @groups_api_endpoint +end