2022-08-26 14:08:58 +00:00
|
|
|
defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
|
|
|
|
@moduledoc """
|
|
|
|
[Search Mobilizon](https://search.joinmobilizon.org) backend.
|
|
|
|
"""
|
|
|
|
|
|
|
|
alias Mobilizon.Actors.Actor
|
|
|
|
alias Mobilizon.Addresses.Address
|
2022-10-06 07:32:47 +00:00
|
|
|
alias Mobilizon.Events.{Categories, Tag}
|
2022-08-26 14:08:58 +00:00
|
|
|
alias Mobilizon.Service.GlobalSearch.{EventResult, GroupResult, Provider}
|
|
|
|
alias Mobilizon.Service.HTTP.GenericJSONClient
|
|
|
|
alias Mobilizon.Storage.Page
|
|
|
|
require Logger
|
|
|
|
import Plug.Conn.Query, only: [encode: 1]
|
|
|
|
|
|
|
|
@search_events_api "/api/v1/search/events"
|
|
|
|
@search_groups_api "/api/v1/search/groups"
|
|
|
|
|
2022-09-26 08:29:20 +00:00
|
|
|
@sort_by_options %{
|
|
|
|
match_desc: "-match",
|
2023-04-26 14:33:11 +00:00
|
|
|
start_time_asc: "startTime",
|
2022-09-26 08:29:20 +00:00
|
|
|
start_time_desc: "-startTime",
|
|
|
|
created_at_desc: "-createdAt",
|
|
|
|
created_at_asc: "createdAt",
|
|
|
|
participant_count_desc: "-participantCount",
|
|
|
|
member_count_desc: "-memberCount"
|
|
|
|
}
|
|
|
|
|
2022-08-26 14:08:58 +00:00
|
|
|
@behaviour Provider
|
|
|
|
|
|
|
|
@impl Provider
|
|
|
|
@doc """
|
|
|
|
Mobilizon Search implementation for `c:Mobilizon.Service.GlobalSearch.Provider.search_events/3`.
|
|
|
|
"""
|
|
|
|
@spec search_events(keyword()) :: Page.t(EventResult.t())
|
|
|
|
def search_events(options \\ []) do
|
2023-04-19 10:14:03 +00:00
|
|
|
Logger.debug("Search events options, #{inspect(Keyword.delete(options, :current_user))}")
|
2022-08-26 14:08:58 +00:00
|
|
|
|
|
|
|
options =
|
|
|
|
options
|
|
|
|
|> Keyword.merge(
|
2022-10-06 07:53:54 +00:00
|
|
|
search: options[:term],
|
2022-08-26 14:08:58 +00:00
|
|
|
startDateMin: to_date(options[:begins_on]),
|
|
|
|
startDateMax: to_date(options[:ends_on]),
|
|
|
|
categoryOneOf: options[:category_one_of],
|
|
|
|
languageOneOf: options[:language_one_of],
|
|
|
|
statusOneOf:
|
|
|
|
Enum.map(options[:status_one_of] || [], fn status ->
|
|
|
|
status |> Atom.to_string() |> String.upcase()
|
|
|
|
end),
|
|
|
|
distance: if(options[:radius], do: "#{options[:radius]}_km", else: nil),
|
|
|
|
count: options[:limit],
|
2022-09-26 08:29:20 +00:00
|
|
|
start: (Keyword.get(options, :page, 1) - 1) * Keyword.get(options, :limit, 16),
|
2022-09-01 08:00:17 +00:00
|
|
|
latlon: to_lat_lon(options[:location]),
|
2022-09-26 08:29:20 +00:00
|
|
|
bbox: options[:bbox],
|
|
|
|
sortBy: Map.get(@sort_by_options, options[:sort_by]),
|
|
|
|
boostLanguages: options[:boost_languages]
|
2022-08-26 14:08:58 +00:00
|
|
|
)
|
|
|
|
|> Keyword.take([
|
|
|
|
:search,
|
|
|
|
:startDateMin,
|
|
|
|
:startDateMax,
|
|
|
|
:boostLanguages,
|
|
|
|
:categoryOneOf,
|
|
|
|
:languageOneOf,
|
|
|
|
:latlon,
|
|
|
|
:distance,
|
|
|
|
:sort,
|
|
|
|
:statusOneOf,
|
2022-09-01 08:00:17 +00:00
|
|
|
:bbox,
|
2022-08-26 14:08:58 +00:00
|
|
|
:start,
|
2022-09-26 08:29:20 +00:00
|
|
|
:count,
|
|
|
|
:sortBy
|
2022-08-26 14:08:58 +00:00
|
|
|
])
|
2022-10-12 16:27:05 +00:00
|
|
|
|> Keyword.reject(fn {_key, val} -> is_nil(val) or val == "" end)
|
2022-08-26 14:08:58 +00:00
|
|
|
|
|
|
|
events_url = "#{search_endpoint()}#{@search_events_api}?#{encode(options)}"
|
|
|
|
Logger.debug("Calling global search engine url #{events_url}")
|
|
|
|
|
|
|
|
client = GenericJSONClient.client()
|
|
|
|
|
|
|
|
case GenericJSONClient.get(client, events_url) do
|
|
|
|
{:ok, %{status: 200, body: body}} ->
|
|
|
|
%Page{total: body["total"], elements: Enum.map(body["data"], &build_event/1)}
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl Provider
|
|
|
|
@doc """
|
|
|
|
Mobilizon Search implementation for `c:Mobilizon.Service.GlobalSearch.Provider.search_groups/3`.
|
|
|
|
"""
|
|
|
|
@spec search_groups(keyword()) :: Page.t(GroupResult.t())
|
|
|
|
def search_groups(options \\ []) do
|
|
|
|
options =
|
|
|
|
options
|
|
|
|
|> Keyword.merge(
|
2022-10-06 07:53:54 +00:00
|
|
|
search: options[:term],
|
2022-08-26 14:08:58 +00:00
|
|
|
languageOneOf: options[:language_one_of],
|
2022-09-26 08:29:20 +00:00
|
|
|
boostLanguages: options[:boost_languages],
|
2022-08-26 14:08:58 +00:00
|
|
|
distance: if(options[:radius], do: "#{options[:radius]}_km", else: nil),
|
|
|
|
count: options[:limit],
|
|
|
|
start: (options[:page] - 1) * options[:limit],
|
2022-09-01 08:00:17 +00:00
|
|
|
latlon: to_lat_lon(options[:location]),
|
2022-09-26 08:29:20 +00:00
|
|
|
bbox: options[:bbox],
|
|
|
|
sortBy: Map.get(@sort_by_options, options[:sort_by])
|
2022-08-26 14:08:58 +00:00
|
|
|
)
|
|
|
|
|> Keyword.take([
|
|
|
|
:search,
|
2022-09-26 08:29:20 +00:00
|
|
|
:languageOneOf,
|
2022-08-26 14:08:58 +00:00
|
|
|
:boostLanguages,
|
|
|
|
:latlon,
|
|
|
|
:distance,
|
|
|
|
:sort,
|
|
|
|
:start,
|
2022-09-01 08:00:17 +00:00
|
|
|
:count,
|
2022-09-26 08:29:20 +00:00
|
|
|
:bbox,
|
|
|
|
:sortBy
|
2022-08-26 14:08:58 +00:00
|
|
|
])
|
2022-10-12 16:27:05 +00:00
|
|
|
|> Keyword.reject(fn {_key, val} -> is_nil(val) or val == "" end)
|
2022-08-26 14:08:58 +00:00
|
|
|
|
|
|
|
groups_url = "#{search_endpoint()}#{@search_groups_api}?#{encode(options)}"
|
|
|
|
Logger.debug("Calling global search engine url #{groups_url}")
|
|
|
|
|
|
|
|
client = GenericJSONClient.client()
|
|
|
|
|
|
|
|
case GenericJSONClient.get(client, groups_url) do
|
|
|
|
{:ok, %{status: 200, body: body}} ->
|
|
|
|
%Page{total: body["total"], elements: Enum.map(body["data"], &build_group/1)}
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-08-26 15:18:54 +00:00
|
|
|
@impl Provider
|
|
|
|
@doc """
|
|
|
|
Returns the CSP configuration for this search provider to work
|
|
|
|
"""
|
|
|
|
def csp do
|
|
|
|
:mobilizon
|
|
|
|
|> Application.get_env(__MODULE__, [])
|
|
|
|
|> Keyword.get(:csp_policy, [])
|
|
|
|
end
|
|
|
|
|
2022-08-26 14:08:58 +00:00
|
|
|
defp build_event(data) do
|
|
|
|
picture =
|
|
|
|
if data["banner"] do
|
|
|
|
%{url: data["banner"], id: data["banner"]}
|
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
|
|
|
organizer_actor_avatar =
|
|
|
|
if data["creator"]["avatar"] do
|
|
|
|
%{url: data["creator"]["avatar"], id: data["creator"]["avatar"]}
|
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
2022-09-01 08:00:17 +00:00
|
|
|
address =
|
|
|
|
if data["location"] do
|
|
|
|
%Address{
|
|
|
|
id: data["location"]["id"],
|
|
|
|
country: data["location"]["address"]["addressCountry"],
|
|
|
|
locality: data["location"]["address"]["addressLocality"],
|
|
|
|
region: data["location"]["address"]["addressRegion"],
|
|
|
|
postal_code: data["location"]["address"]["postalCode"],
|
|
|
|
street: data["location"]["address"]["streetAddress"],
|
|
|
|
url: data["location"]["id"],
|
|
|
|
description: data["location"]["name"],
|
|
|
|
geom: %Geo.Point{
|
|
|
|
coordinates:
|
|
|
|
{data["location"]["location"]["lon"], data["location"]["location"]["lat"]},
|
|
|
|
srid: 4326
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
2022-08-26 14:08:58 +00:00
|
|
|
%EventResult{
|
|
|
|
id: data["id"],
|
|
|
|
uuid: data["uuid"],
|
|
|
|
title: data["name"],
|
|
|
|
begins_on: parse_date(data["startTime"]),
|
|
|
|
ends_on: parse_date(data["endTime"]),
|
|
|
|
url: data["url"],
|
|
|
|
picture: picture,
|
2022-10-12 17:30:34 +00:00
|
|
|
category:
|
|
|
|
data["category"]
|
|
|
|
|> Categories.get_category()
|
|
|
|
|> String.downcase()
|
|
|
|
|> String.to_existing_atom(),
|
2022-08-26 14:08:58 +00:00
|
|
|
organizer_actor: %Actor{
|
|
|
|
id: data["creator"]["id"],
|
|
|
|
name: data["creator"]["displayName"],
|
|
|
|
preferred_username: data["creator"]["name"],
|
|
|
|
avatar: organizer_actor_avatar
|
|
|
|
},
|
2022-09-01 08:00:17 +00:00
|
|
|
physical_address: address,
|
2022-09-26 08:29:20 +00:00
|
|
|
participant_stats: %{participant: data["participantCount"]},
|
2022-08-26 14:08:58 +00:00
|
|
|
tags:
|
|
|
|
Enum.map(data["tags"], fn tag ->
|
|
|
|
tag = String.trim_leading(tag, "#")
|
|
|
|
%Tag{id: tag, slug: tag, title: tag}
|
|
|
|
end)
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
defp build_group(data) do
|
|
|
|
avatar =
|
|
|
|
if data["avatar"] do
|
|
|
|
%{url: data["avatar"], id: data["avatar"]}
|
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
|
|
|
address =
|
|
|
|
if data["location"] do
|
|
|
|
%Address{
|
|
|
|
id: data["location"]["id"],
|
|
|
|
country: data["location"]["address"]["addressCountry"],
|
|
|
|
locality: data["location"]["address"]["addressLocality"],
|
|
|
|
region: data["location"]["address"]["addressRegion"],
|
|
|
|
postal_code: data["location"]["address"]["postalCode"],
|
|
|
|
street: data["location"]["address"]["streetAddress"],
|
|
|
|
url: data["location"]["id"],
|
|
|
|
description: data["location"]["name"],
|
|
|
|
geom: %Geo.Point{
|
|
|
|
coordinates:
|
|
|
|
{data["location"]["location"]["lon"], data["location"]["location"]["lat"]},
|
|
|
|
srid: 4326
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
|
|
|
%GroupResult{
|
|
|
|
id: data["id"],
|
|
|
|
name: data["displayName"],
|
|
|
|
preferred_username: data["name"],
|
|
|
|
domain: data["host"],
|
|
|
|
avatar: avatar,
|
|
|
|
summary: data["description"],
|
|
|
|
url: data["url"],
|
|
|
|
members_count: data["memberCount"],
|
|
|
|
type: :Group,
|
|
|
|
physical_address: address
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
defp search_endpoint do
|
|
|
|
Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint]) ||
|
|
|
|
"https://search.joinmobilizon.org"
|
|
|
|
end
|
|
|
|
|
|
|
|
defp parse_date(nil), do: nil
|
|
|
|
|
|
|
|
defp parse_date(date_string) do
|
|
|
|
case DateTime.from_iso8601(date_string) do
|
|
|
|
{:ok, date, _} -> date
|
|
|
|
{:error, _} -> nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp to_date(nil), do: nil
|
|
|
|
defp to_date(date), do: DateTime.to_iso8601(date)
|
|
|
|
|
|
|
|
defp to_lat_lon(nil), do: nil
|
|
|
|
|
|
|
|
defp to_lat_lon(location) do
|
2022-09-01 08:00:17 +00:00
|
|
|
{lon, lat} = Geohax.decode(location)
|
|
|
|
"#{lat}:#{lon}"
|
2022-08-26 14:08:58 +00:00
|
|
|
end
|
|
|
|
end
|