aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/user
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/user')
-rw-r--r--lib/pleroma/user/info.ex58
-rw-r--r--lib/pleroma/user/query.ex19
-rw-r--r--lib/pleroma/user/search.ex223
-rw-r--r--lib/pleroma/user/welcome_message.ex4
4 files changed, 290 insertions, 14 deletions
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index 6397e2737..9beb3ddbd 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -24,6 +24,7 @@ defmodule Pleroma.User.Info do
field(:domain_blocks, {:array, :string}, default: [])
field(:mutes, {:array, :string}, default: [])
field(:muted_reblogs, {:array, :string}, default: [])
+ field(:muted_notifications, {:array, :string}, default: [])
field(:subscribers, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false)
@@ -42,14 +43,21 @@ defmodule Pleroma.User.Info do
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
field(:pinned_activities, {:array, :string}, default: [])
- field(:flavour, :string, default: nil)
field(:mascot, :map, default: nil)
field(:emoji, {:array, :map}, default: [])
+ field(:pleroma_settings_store, :map, default: %{})
field(:notification_settings, :map,
- default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true}
+ default: %{
+ "followers" => true,
+ "follows" => true,
+ "non_follows" => true,
+ "non_followers" => true
+ }
)
+ field(:skip_thread_containment, :boolean, default: false)
+
# Found in the wild
# ap_id -> Where is this used?
# bio -> Where is this used?
@@ -68,10 +76,15 @@ defmodule Pleroma.User.Info do
end
def update_notification_settings(info, settings) do
+ settings =
+ settings
+ |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
+ |> Map.new()
+
notification_settings =
info.notification_settings
|> Map.merge(settings)
- |> Map.take(["remote", "local", "followers", "follows"])
+ |> Map.take(["followers", "follows", "non_follows", "non_followers"])
params = %{notification_settings: notification_settings}
@@ -108,6 +121,16 @@ defmodule Pleroma.User.Info do
|> validate_required([:mutes])
end
+ @spec set_notification_mutes(Changeset.t(), [String.t()], boolean()) :: Changeset.t()
+ def set_notification_mutes(changeset, muted_notifications, notifications?) do
+ if notifications? do
+ put_change(changeset, :muted_notifications, muted_notifications)
+ |> validate_required([:muted_notifications])
+ else
+ changeset
+ end
+ end
+
def set_blocks(info, blocks) do
params = %{blocks: blocks}
@@ -124,14 +147,31 @@ defmodule Pleroma.User.Info do
|> validate_required([:subscribers])
end
+ @spec add_to_mutes(Info.t(), String.t()) :: Changeset.t()
def add_to_mutes(info, muted) do
set_mutes(info, Enum.uniq([muted | info.mutes]))
end
+ @spec add_to_muted_notifications(Changeset.t(), Info.t(), String.t(), boolean()) ::
+ Changeset.t()
+ def add_to_muted_notifications(changeset, info, muted, notifications?) do
+ set_notification_mutes(
+ changeset,
+ Enum.uniq([muted | info.muted_notifications]),
+ notifications?
+ )
+ end
+
+ @spec remove_from_mutes(Info.t(), String.t()) :: Changeset.t()
def remove_from_mutes(info, muted) do
set_mutes(info, List.delete(info.mutes, muted))
end
+ @spec remove_from_muted_notifications(Changeset.t(), Info.t(), String.t()) :: Changeset.t()
+ def remove_from_muted_notifications(changeset, info, muted) do
+ set_notification_mutes(changeset, List.delete(info.muted_notifications, muted), true)
+ end
+
def add_to_block(info, blocked) do
set_blocks(info, Enum.uniq([blocked | info.blocks]))
end
@@ -209,7 +249,9 @@ defmodule Pleroma.User.Info do
:hide_followers,
:hide_favorites,
:background,
- :show_role
+ :show_role,
+ :skip_thread_containment,
+ :pleroma_settings_store
])
end
@@ -241,14 +283,6 @@ defmodule Pleroma.User.Info do
|> validate_required([:settings])
end
- def mastodon_flavour_update(info, flavour) do
- params = %{flavour: flavour}
-
- info
- |> cast(params, [:flavour])
- |> validate_required([:flavour])
- end
-
def mascot_update(info, url) do
params = %{mascot: url}
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index ace9c05f2..f9bcc9e19 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.User.Query do
User query builder module. Builds query from new query or another user query.
## Example:
- query = Pleroma.User.Query(%{nickname: "nickname"})
+ query = Pleroma.User.Query.build(%{nickname: "nickname"})
another_query = Pleroma.User.Query.build(query, %{email: "email@example.com"})
Pleroma.Repo.all(query)
Pleroma.Repo.all(another_query)
@@ -47,7 +47,10 @@ defmodule Pleroma.User.Query do
friends: User.t(),
recipients_from_activity: [String.t()],
nickname: [String.t()],
- ap_id: [String.t()]
+ ap_id: [String.t()],
+ order_by: term(),
+ select: term(),
+ limit: pos_integer()
}
| %{}
@@ -141,6 +144,18 @@ defmodule Pleroma.User.Query do
where(query, [u], u.ap_id in ^to or fragment("? && ?", u.following, ^to))
end
+ defp compose_query({:order_by, key}, query) do
+ order_by(query, [u], field(u, ^key))
+ end
+
+ defp compose_query({:select, keys}, query) do
+ select(query, [u], ^keys)
+ end
+
+ defp compose_query({:limit, limit}, query) do
+ limit(query, ^limit)
+ end
+
defp compose_query(_unsupported_param, query), do: query
defp prepare_tag_criteria(tag, query) do
diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex
new file mode 100644
index 000000000..46620b89a
--- /dev/null
+++ b/lib/pleroma/user/search.ex
@@ -0,0 +1,223 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.User.Search do
+ alias Pleroma.Pagination
+ alias Pleroma.Repo
+ alias Pleroma.User
+ import Ecto.Query
+
+ @similarity_threshold 0.25
+ @limit 20
+
+ def search(query_string, opts \\ []) do
+ resolve = Keyword.get(opts, :resolve, false)
+ following = Keyword.get(opts, :following, false)
+ result_limit = Keyword.get(opts, :limit, @limit)
+ offset = Keyword.get(opts, :offset, 0)
+
+ for_user = Keyword.get(opts, :for_user)
+
+ query_string = format_query(query_string)
+
+ maybe_resolve(resolve, for_user, query_string)
+
+ {:ok, results} =
+ Repo.transaction(fn ->
+ Ecto.Adapters.SQL.query(
+ Repo,
+ "select set_limit(#{@similarity_threshold})",
+ []
+ )
+
+ query_string
+ |> search_query(for_user, following)
+ |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
+ end)
+
+ results
+ end
+
+ defp format_query(query_string) do
+ # Strip the beginning @ off if there is a query
+ query_string = String.trim_leading(query_string, "@")
+
+ with [name, domain] <- String.split(query_string, "@"),
+ formatted_domain <- String.replace(domain, ~r/[!-\-|@|[-`|{-~|\/|:]+/, "") do
+ name <> "@" <> to_string(:idna.encode(formatted_domain))
+ else
+ _ -> query_string
+ end
+ end
+
+ defp search_query(query_string, for_user, following) do
+ for_user
+ |> base_query(following)
+ |> filter_blocked_user(for_user)
+ |> filter_blocked_domains(for_user)
+ |> search_subqueries(query_string)
+ |> union_subqueries
+ |> distinct_query()
+ |> boost_search_rank_query(for_user)
+ |> subquery()
+ |> order_by(desc: :search_rank)
+ |> maybe_restrict_local(for_user)
+ end
+
+ defp base_query(_user, false), do: User
+ defp base_query(user, true), do: User.get_followers_query(user)
+
+ defp filter_blocked_user(query, %User{info: %{blocks: blocks}})
+ when length(blocks) > 0 do
+ from(q in query, where: not (q.ap_id in ^blocks))
+ end
+
+ defp filter_blocked_user(query, _), do: query
+
+ defp filter_blocked_domains(query, %User{info: %{domain_blocks: domain_blocks}})
+ when length(domain_blocks) > 0 do
+ domains = Enum.join(domain_blocks, ",")
+
+ from(
+ q in query,
+ where: fragment("substring(ap_id from '.*://([^/]*)') NOT IN (?)", ^domains)
+ )
+ end
+
+ defp filter_blocked_domains(query, _), do: query
+
+ defp union_subqueries({fts_subquery, trigram_subquery}) do
+ from(s in trigram_subquery, union_all: ^fts_subquery)
+ end
+
+ defp search_subqueries(base_query, query_string) do
+ {
+ fts_search_subquery(base_query, query_string),
+ trigram_search_subquery(base_query, query_string)
+ }
+ end
+
+ defp distinct_query(q) do
+ from(s in subquery(q), order_by: s.search_type, distinct: s.id)
+ end
+
+ defp maybe_resolve(true, user, query) do
+ case {limit(), user} do
+ {:all, _} -> :noop
+ {:unauthenticated, %User{}} -> User.get_or_fetch(query)
+ {:unauthenticated, _} -> :noop
+ {false, _} -> User.get_or_fetch(query)
+ end
+ end
+
+ defp maybe_resolve(_, _, _), do: :noop
+
+ defp maybe_restrict_local(q, user) do
+ case {limit(), user} do
+ {:all, _} -> restrict_local(q)
+ {:unauthenticated, %User{}} -> q
+ {:unauthenticated, _} -> restrict_local(q)
+ {false, _} -> q
+ end
+ end
+
+ defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
+
+ defp restrict_local(q), do: where(q, [u], u.local == true)
+
+ defp boost_search_rank_query(query, nil), do: query
+
+ defp boost_search_rank_query(query, for_user) do
+ friends_ids = User.get_friends_ids(for_user)
+ followers_ids = User.get_followers_ids(for_user)
+
+ from(u in subquery(query),
+ select_merge: %{
+ search_rank:
+ fragment(
+ """
+ CASE WHEN (?) THEN 0.5 + (?) * 1.3
+ WHEN (?) THEN 0.5 + (?) * 1.2
+ WHEN (?) THEN (?) * 1.1
+ ELSE (?) END
+ """,
+ u.id in ^friends_ids and u.id in ^followers_ids,
+ u.search_rank,
+ u.id in ^friends_ids,
+ u.search_rank,
+ u.id in ^followers_ids,
+ u.search_rank,
+ u.search_rank
+ )
+ }
+ )
+ end
+
+ @spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
+ defp fts_search_subquery(query, term) do
+ processed_query =
+ String.trim_trailing(term, "@" <> local_domain())
+ |> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ")
+ |> String.trim()
+ |> String.split()
+ |> Enum.map(&(&1 <> ":*"))
+ |> Enum.join(" | ")
+
+ from(
+ u in query,
+ select_merge: %{
+ search_type: ^0,
+ search_rank:
+ fragment(
+ """
+ ts_rank_cd(
+ setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
+ setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
+ to_tsquery('simple', ?),
+ 32
+ )
+ """,
+ u.nickname,
+ u.name,
+ ^processed_query
+ )
+ },
+ where:
+ fragment(
+ """
+ (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
+ setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
+ """,
+ u.nickname,
+ u.name,
+ ^processed_query
+ )
+ )
+ |> User.restrict_deactivated()
+ end
+
+ @spec trigram_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
+ defp trigram_search_subquery(query, term) do
+ term = String.trim_trailing(term, "@" <> local_domain())
+
+ from(
+ u in query,
+ select_merge: %{
+ # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
+ search_type: fragment("?", 1),
+ search_rank:
+ fragment(
+ "similarity(?, trim(? || ' ' || coalesce(?, '')))",
+ ^term,
+ u.nickname,
+ u.name
+ )
+ },
+ where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
+ )
+ |> User.restrict_deactivated()
+ end
+
+ defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
+end
diff --git a/lib/pleroma/user/welcome_message.ex b/lib/pleroma/user/welcome_message.ex
index 2ba65b75a..99fba729e 100644
--- a/lib/pleroma/user/welcome_message.ex
+++ b/lib/pleroma/user/welcome_message.ex
@@ -1,3 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
defmodule Pleroma.User.WelcomeMessage do
alias Pleroma.User
alias Pleroma.Web.CommonAPI