18
lib/service/global_search/event_result.ex
Normal file
18
lib/service/global_search/event_result.ex
Normal file
@@ -0,0 +1,18 @@
|
||||
defmodule Mobilizon.Service.GlobalSearch.EventResult do
|
||||
@moduledoc """
|
||||
The structure holding search result information about an event
|
||||
"""
|
||||
defstruct [
|
||||
:id,
|
||||
:uuid,
|
||||
:url,
|
||||
:title,
|
||||
:begins_on,
|
||||
:ends_on,
|
||||
:picture,
|
||||
:category,
|
||||
:tags,
|
||||
:organizer_actor,
|
||||
:participants
|
||||
]
|
||||
end
|
||||
17
lib/service/global_search/global_search.ex
Normal file
17
lib/service/global_search/global_search.ex
Normal file
@@ -0,0 +1,17 @@
|
||||
defmodule Mobilizon.Service.GlobalSearch do
|
||||
@moduledoc """
|
||||
Module to load the service adapter defined inside the configuration.
|
||||
|
||||
See `Mobilizon.Service.GlobalSearch.Provider`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns the appropriate service adapter.
|
||||
|
||||
According to the config behind
|
||||
`config :mobilizon, Mobilizon.Service.GlobalSearch,
|
||||
service: Mobilizon.Service.GlobalSearch.Module`
|
||||
"""
|
||||
@spec service :: module
|
||||
def service, do: get_in(Application.get_env(:mobilizon, __MODULE__), [:service])
|
||||
end
|
||||
19
lib/service/global_search/group_result.ex
Normal file
19
lib/service/global_search/group_result.ex
Normal file
@@ -0,0 +1,19 @@
|
||||
defmodule Mobilizon.Service.GlobalSearch.GroupResult do
|
||||
@moduledoc """
|
||||
The structure holding search result information about a group
|
||||
"""
|
||||
defstruct [
|
||||
:id,
|
||||
:url,
|
||||
:name,
|
||||
:preferred_username,
|
||||
:domain,
|
||||
:avatar,
|
||||
:summary,
|
||||
:url,
|
||||
:members_count,
|
||||
:follower_count,
|
||||
:type,
|
||||
:physical_address
|
||||
]
|
||||
end
|
||||
40
lib/service/global_search/provider.ex
Normal file
40
lib/service/global_search/provider.ex
Normal file
@@ -0,0 +1,40 @@
|
||||
defmodule Mobilizon.Service.GlobalSearch.Provider do
|
||||
@moduledoc """
|
||||
Provider Behaviour for Global Search.
|
||||
|
||||
## Supported backends
|
||||
|
||||
* `Mobilizon.Service.GlobalSearch.Mobilizon` [🔗](https://framagit.org/framasoft/joinmobilizon/search-index/)
|
||||
|
||||
## Shared options
|
||||
|
||||
* `:lang` Lang in which to prefer results. Used as a request parameter or
|
||||
through an `Accept-Language` HTTP header. Defaults to `"en"`.
|
||||
* `:country_code` An ISO 3166 country code. String or `nil`
|
||||
* `:limit` Maximum limit for the number of results returned by the backend.
|
||||
Defaults to `10`
|
||||
* `:api_key` Allows to override the API key (if the backend requires one) set
|
||||
inside the configuration.
|
||||
* `:endpoint` Allows to override the endpoint set inside the configuration.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Service.GlobalSearch.{EventResult, GroupResult}
|
||||
|
||||
@doc """
|
||||
Get global search results for a search string
|
||||
|
||||
## Examples
|
||||
|
||||
iex> search_events(search: "London")
|
||||
[%EventResult{id: "provider-534", origin_url: "https://somewhere.else", title: "MyEvent", begins_on: "2022-08-25T08:13:47+0200", ends_on: "2022-08-25T10:13:47+0200", category: "FILM_MEDIA", tags: ["something", "what"], participants: 5}]
|
||||
"""
|
||||
@callback search_events(search_options :: keyword) ::
|
||||
Page.t(EventResult.t())
|
||||
@callback search_groups(search_options :: keyword) ::
|
||||
Page.t(GroupResult.t())
|
||||
|
||||
@spec endpoint(atom()) :: String.t()
|
||||
def endpoint(provider) do
|
||||
Application.get_env(:mobilizon, provider) |> get_in([:endpoint])
|
||||
end
|
||||
end
|
||||
225
lib/service/global_search/search_mobilizon.ex
Normal file
225
lib/service/global_search/search_mobilizon.ex
Normal file
@@ -0,0 +1,225 @@
|
||||
defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
|
||||
@moduledoc """
|
||||
[Search Mobilizon](https://search.joinmobilizon.org) backend.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Events.Tag
|
||||
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"
|
||||
|
||||
@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
|
||||
Logger.debug("Search events options, #{inspect(Keyword.keys(options))}")
|
||||
|
||||
options =
|
||||
options
|
||||
|> Keyword.merge(
|
||||
term: options[:search],
|
||||
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],
|
||||
start: (options[:page] - 1) * options[:limit],
|
||||
latlon: to_lat_lon(options[:location])
|
||||
)
|
||||
|> Keyword.take([
|
||||
:search,
|
||||
:startDateMin,
|
||||
:startDateMax,
|
||||
:boostLanguages,
|
||||
:categoryOneOf,
|
||||
:languageOneOf,
|
||||
:latlon,
|
||||
:distance,
|
||||
:sort,
|
||||
:statusOneOf,
|
||||
:start,
|
||||
:count
|
||||
])
|
||||
|> Keyword.reject(fn {_key, val} -> is_nil(val) end)
|
||||
|
||||
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(
|
||||
term: options[:search],
|
||||
languageOneOf: options[:language_one_of],
|
||||
distance: if(options[:radius], do: "#{options[:radius]}_km", else: nil),
|
||||
count: options[:limit],
|
||||
start: (options[:page] - 1) * options[:limit],
|
||||
latlon: to_lat_lon(options[:location])
|
||||
)
|
||||
|> Keyword.take([
|
||||
:search,
|
||||
:boostLanguages,
|
||||
:latlon,
|
||||
:distance,
|
||||
:sort,
|
||||
:start,
|
||||
:count
|
||||
])
|
||||
|> Keyword.reject(fn {_key, val} -> is_nil(val) end)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
%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,
|
||||
category: String.to_existing_atom(String.downcase(data["category"])),
|
||||
organizer_actor: %Actor{
|
||||
id: data["creator"]["id"],
|
||||
name: data["creator"]["displayName"],
|
||||
preferred_username: data["creator"]["name"],
|
||||
avatar: organizer_actor_avatar
|
||||
},
|
||||
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
|
||||
case Geohax.decode(location) do
|
||||
{lon, lat} ->
|
||||
"#{lat}:#{lon}"
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,6 @@
|
||||
defmodule Mobilizon.Service.Pictures.Information do
|
||||
@moduledoc """
|
||||
The structure holding information about a picture
|
||||
"""
|
||||
defstruct [:url, :author, :source]
|
||||
end
|
||||
|
||||
@@ -3,8 +3,8 @@ defmodule Mobilizon.Service.Pictures.Unsplash do
|
||||
[Unsplash](https://unsplash.com) backend.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Service.Pictures.{Information, Provider}
|
||||
alias Mobilizon.Service.HTTP.GenericJSONClient
|
||||
alias Mobilizon.Service.Pictures.{Information, Provider}
|
||||
require Logger
|
||||
|
||||
@unsplash_api "/search/photos"
|
||||
@@ -24,12 +24,12 @@ defmodule Mobilizon.Service.Pictures.Unsplash do
|
||||
GenericJSONClient.client(headers: [{:Authorization, "Client-ID #{unsplash_access_key()}"}])
|
||||
|
||||
with {:ok, %{status: 200, body: body}} <- GenericJSONClient.get(client, url),
|
||||
selectedPicture <- Enum.random(body["results"]) do
|
||||
selected_picture <- Enum.random(body["results"]) do
|
||||
%Information{
|
||||
url: selectedPicture["urls"]["small"],
|
||||
url: selected_picture["urls"]["small"],
|
||||
author: %{
|
||||
name: selectedPicture["user"]["name"],
|
||||
url: "#{selectedPicture["user"]["links"]["html"]}#{unsplash_utm_source()}"
|
||||
name: selected_picture["user"]["name"],
|
||||
url: "#{selected_picture["user"]["links"]["html"]}#{unsplash_utm_source()}"
|
||||
},
|
||||
source: %{
|
||||
name: @unsplash_name,
|
||||
|
||||
Reference in New Issue
Block a user