Remove duplicated entries in users' following lists
[akkoma] / lib / pleroma / user.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.User do
6 use Ecto.Schema
7
8 import Ecto.Changeset
9 import Ecto.Query
10
11 alias Comeonin.Pbkdf2
12 alias Pleroma.Activity
13 alias Pleroma.Notification
14 alias Pleroma.Object
15 alias Pleroma.Registration
16 alias Pleroma.Repo
17 alias Pleroma.User
18 alias Pleroma.Web
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Utils
21 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
22 alias Pleroma.Web.OAuth
23 alias Pleroma.Web.OStatus
24 alias Pleroma.Web.RelMe
25 alias Pleroma.Web.Websub
26
27 require Logger
28
29 @type t :: %__MODULE__{}
30
31 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
32
33 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
34 @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
35
36 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
37 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
38
39 schema "users" do
40 field(:bio, :string)
41 field(:email, :string)
42 field(:name, :string)
43 field(:nickname, :string)
44 field(:password_hash, :string)
45 field(:password, :string, virtual: true)
46 field(:password_confirmation, :string, virtual: true)
47 field(:following, {:array, :string}, default: [])
48 field(:ap_id, :string)
49 field(:avatar, :map)
50 field(:local, :boolean, default: true)
51 field(:follower_address, :string)
52 field(:search_rank, :float, virtual: true)
53 field(:search_type, :integer, virtual: true)
54 field(:tags, {:array, :string}, default: [])
55 field(:last_refreshed_at, :naive_datetime_usec)
56 has_many(:notifications, Notification)
57 has_many(:registrations, Registration)
58 embeds_one(:info, Pleroma.User.Info)
59
60 timestamps()
61 end
62
63 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
64 do: !Pleroma.Config.get([:instance, :account_activation_required])
65
66 def auth_active?(%User{}), do: true
67
68 def visible_for?(user, for_user \\ nil)
69
70 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
71
72 def visible_for?(%User{} = user, for_user) do
73 auth_active?(user) || superuser?(for_user)
74 end
75
76 def visible_for?(_, _), do: false
77
78 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
79 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
80 def superuser?(_), do: false
81
82 def avatar_url(user, options \\ []) do
83 case user.avatar do
84 %{"url" => [%{"href" => href} | _]} -> href
85 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
86 end
87 end
88
89 def banner_url(user, options \\ []) do
90 case user.info.banner do
91 %{"url" => [%{"href" => href} | _]} -> href
92 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
93 end
94 end
95
96 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
97 def profile_url(%User{ap_id: ap_id}), do: ap_id
98 def profile_url(_), do: nil
99
100 def ap_id(%User{nickname: nickname}) do
101 "#{Web.base_url()}/users/#{nickname}"
102 end
103
104 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
105 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
106
107 def user_info(%User{} = user) do
108 %{
109 following_count: following_count(user),
110 note_count: user.info.note_count,
111 follower_count: user.info.follower_count,
112 locked: user.info.locked,
113 confirmation_pending: user.info.confirmation_pending,
114 default_scope: user.info.default_scope
115 }
116 end
117
118 def restrict_deactivated(query) do
119 from(u in query,
120 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
121 )
122 end
123
124 def following_count(%User{following: []}), do: 0
125
126 def following_count(%User{} = user) do
127 user
128 |> get_friends_query()
129 |> Repo.aggregate(:count, :id)
130 end
131
132 def remote_user_creation(params) do
133 params =
134 params
135 |> Map.put(:info, params[:info] || %{})
136
137 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
138
139 changes =
140 %User{}
141 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
142 |> validate_required([:name, :ap_id])
143 |> unique_constraint(:nickname)
144 |> validate_format(:nickname, @email_regex)
145 |> validate_length(:bio, max: 5000)
146 |> validate_length(:name, max: 100)
147 |> put_change(:local, false)
148 |> put_embed(:info, info_cng)
149
150 if changes.valid? do
151 case info_cng.changes[:source_data] do
152 %{"followers" => followers} ->
153 changes
154 |> put_change(:follower_address, followers)
155
156 _ ->
157 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
158
159 changes
160 |> put_change(:follower_address, followers)
161 end
162 else
163 changes
164 end
165 end
166
167 def update_changeset(struct, params \\ %{}) do
168 struct
169 |> cast(params, [:bio, :name, :avatar, :following])
170 |> unique_constraint(:nickname)
171 |> validate_format(:nickname, local_nickname_regex())
172 |> validate_length(:bio, max: 5000)
173 |> validate_length(:name, min: 1, max: 100)
174 end
175
176 def upgrade_changeset(struct, params \\ %{}) do
177 params =
178 params
179 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
180
181 info_cng =
182 struct.info
183 |> User.Info.user_upgrade(params[:info])
184
185 struct
186 |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
187 |> unique_constraint(:nickname)
188 |> validate_format(:nickname, local_nickname_regex())
189 |> validate_length(:bio, max: 5000)
190 |> validate_length(:name, max: 100)
191 |> put_embed(:info, info_cng)
192 end
193
194 def password_update_changeset(struct, params) do
195 changeset =
196 struct
197 |> cast(params, [:password, :password_confirmation])
198 |> validate_required([:password, :password_confirmation])
199 |> validate_confirmation(:password)
200
201 OAuth.Token.delete_user_tokens(struct)
202 OAuth.Authorization.delete_user_authorizations(struct)
203
204 if changeset.valid? do
205 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
206
207 changeset
208 |> put_change(:password_hash, hashed)
209 else
210 changeset
211 end
212 end
213
214 def reset_password(user, data) do
215 update_and_set_cache(password_update_changeset(user, data))
216 end
217
218 def register_changeset(struct, params \\ %{}, opts \\ []) do
219 need_confirmation? =
220 if is_nil(opts[:need_confirmation]) do
221 Pleroma.Config.get([:instance, :account_activation_required])
222 else
223 opts[:need_confirmation]
224 end
225
226 info_change =
227 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
228
229 changeset =
230 struct
231 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
232 |> validate_required([:name, :nickname, :password, :password_confirmation])
233 |> validate_confirmation(:password)
234 |> unique_constraint(:email)
235 |> unique_constraint(:nickname)
236 |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
237 |> validate_format(:nickname, local_nickname_regex())
238 |> validate_format(:email, @email_regex)
239 |> validate_length(:bio, max: 1000)
240 |> validate_length(:name, min: 1, max: 100)
241 |> put_change(:info, info_change)
242
243 changeset =
244 if opts[:external] do
245 changeset
246 else
247 validate_required(changeset, [:email])
248 end
249
250 if changeset.valid? do
251 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
252 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
253 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
254
255 changeset
256 |> put_change(:password_hash, hashed)
257 |> put_change(:ap_id, ap_id)
258 |> unique_constraint(:ap_id)
259 |> put_change(:following, [followers])
260 |> put_change(:follower_address, followers)
261 else
262 changeset
263 end
264 end
265
266 defp autofollow_users(user) do
267 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
268
269 autofollowed_users =
270 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
271 |> Repo.all()
272
273 follow_all(user, autofollowed_users)
274 end
275
276 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
277 def register(%Ecto.Changeset{} = changeset) do
278 with {:ok, user} <- Repo.insert(changeset),
279 {:ok, user} <- autofollow_users(user),
280 {:ok, user} <- set_cache(user),
281 {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
282 {:ok, _} <- try_send_confirmation_email(user) do
283 {:ok, user}
284 end
285 end
286
287 def try_send_confirmation_email(%User{} = user) do
288 if user.info.confirmation_pending &&
289 Pleroma.Config.get([:instance, :account_activation_required]) do
290 user
291 |> Pleroma.Emails.UserEmail.account_confirmation_email()
292 |> Pleroma.Emails.Mailer.deliver_async()
293
294 {:ok, :enqueued}
295 else
296 {:ok, :noop}
297 end
298 end
299
300 def needs_update?(%User{local: true}), do: false
301
302 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
303
304 def needs_update?(%User{local: false} = user) do
305 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
306 end
307
308 def needs_update?(_), do: true
309
310 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
311 {:ok, follower}
312 end
313
314 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
315 follow(follower, followed)
316 end
317
318 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
319 if not User.ap_enabled?(followed) do
320 follow(follower, followed)
321 else
322 {:ok, follower}
323 end
324 end
325
326 def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
327 if not following?(follower, followed) do
328 follow(follower, followed)
329 else
330 {:ok, follower}
331 end
332 end
333
334 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
335 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
336 def follow_all(follower, followeds) do
337 followed_addresses =
338 followeds
339 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
340 |> Enum.map(fn %{follower_address: fa} -> fa end)
341
342 q =
343 from(u in User,
344 where: u.id == ^follower.id,
345 update: [
346 set: [
347 following:
348 fragment(
349 "array(select distinct unnest (array_cat(?, ?)))",
350 u.following,
351 ^followed_addresses
352 )
353 ]
354 ],
355 select: u
356 )
357
358 {1, [follower]} = Repo.update_all(q, [])
359
360 Enum.each(followeds, fn followed ->
361 update_follower_count(followed)
362 end)
363
364 set_cache(follower)
365 end
366
367 def follow(%User{} = follower, %User{info: info} = followed) do
368 user_config = Application.get_env(:pleroma, :user)
369 deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
370
371 ap_followers = followed.follower_address
372
373 cond do
374 following?(follower, followed) or info.deactivated ->
375 {:error, "Could not follow user: #{followed.nickname} is already on your list."}
376
377 deny_follow_blocked and blocks?(followed, follower) ->
378 {:error, "Could not follow user: #{followed.nickname} blocked you."}
379
380 true ->
381 if !followed.local && follower.local && !ap_enabled?(followed) do
382 Websub.subscribe(follower, followed)
383 end
384
385 q =
386 from(u in User,
387 where: u.id == ^follower.id,
388 update: [push: [following: ^ap_followers]],
389 select: u
390 )
391
392 {1, [follower]} = Repo.update_all(q, [])
393
394 {:ok, _} = update_follower_count(followed)
395
396 set_cache(follower)
397 end
398 end
399
400 def unfollow(%User{} = follower, %User{} = followed) do
401 ap_followers = followed.follower_address
402
403 if following?(follower, followed) and follower.ap_id != followed.ap_id do
404 q =
405 from(u in User,
406 where: u.id == ^follower.id,
407 update: [pull: [following: ^ap_followers]],
408 select: u
409 )
410
411 {1, [follower]} = Repo.update_all(q, [])
412
413 {:ok, followed} = update_follower_count(followed)
414
415 set_cache(follower)
416
417 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
418 else
419 {:error, "Not subscribed!"}
420 end
421 end
422
423 @spec following?(User.t(), User.t()) :: boolean
424 def following?(%User{} = follower, %User{} = followed) do
425 Enum.member?(follower.following, followed.follower_address)
426 end
427
428 def locked?(%User{} = user) do
429 user.info.locked || false
430 end
431
432 def get_by_id(id) do
433 Repo.get_by(User, id: id)
434 end
435
436 def get_by_ap_id(ap_id) do
437 Repo.get_by(User, ap_id: ap_id)
438 end
439
440 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
441 # of the ap_id and the domain and tries to get that user
442 def get_by_guessed_nickname(ap_id) do
443 domain = URI.parse(ap_id).host
444 name = List.last(String.split(ap_id, "/"))
445 nickname = "#{name}@#{domain}"
446
447 get_cached_by_nickname(nickname)
448 end
449
450 def set_cache({:ok, user}), do: set_cache(user)
451 def set_cache({:error, err}), do: {:error, err}
452
453 def set_cache(%User{} = user) do
454 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
455 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
456 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
457 {:ok, user}
458 end
459
460 def update_and_set_cache(changeset) do
461 with {:ok, user} <- Repo.update(changeset) do
462 set_cache(user)
463 else
464 e -> e
465 end
466 end
467
468 def invalidate_cache(user) do
469 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
470 Cachex.del(:user_cache, "nickname:#{user.nickname}")
471 Cachex.del(:user_cache, "user_info:#{user.id}")
472 end
473
474 def get_cached_by_ap_id(ap_id) do
475 key = "ap_id:#{ap_id}"
476 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
477 end
478
479 def get_cached_by_id(id) do
480 key = "id:#{id}"
481
482 ap_id =
483 Cachex.fetch!(:user_cache, key, fn _ ->
484 user = get_by_id(id)
485
486 if user do
487 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
488 {:commit, user.ap_id}
489 else
490 {:ignore, ""}
491 end
492 end)
493
494 get_cached_by_ap_id(ap_id)
495 end
496
497 def get_cached_by_nickname(nickname) do
498 key = "nickname:#{nickname}"
499
500 Cachex.fetch!(:user_cache, key, fn ->
501 user_result = get_or_fetch_by_nickname(nickname)
502
503 case user_result do
504 {:ok, user} -> {:commit, user}
505 {:error, _error} -> {:ignore, nil}
506 end
507 end)
508 end
509
510 def get_cached_by_nickname_or_id(nickname_or_id) do
511 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
512 end
513
514 def get_by_nickname(nickname) do
515 Repo.get_by(User, nickname: nickname) ||
516 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
517 Repo.get_by(User, nickname: local_nickname(nickname))
518 end
519 end
520
521 def get_by_email(email), do: Repo.get_by(User, email: email)
522
523 def get_by_nickname_or_email(nickname_or_email) do
524 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
525 end
526
527 def get_cached_user_info(user) do
528 key = "user_info:#{user.id}"
529 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
530 end
531
532 def fetch_by_nickname(nickname) do
533 ap_try = ActivityPub.make_user_from_nickname(nickname)
534
535 case ap_try do
536 {:ok, user} -> {:ok, user}
537 _ -> OStatus.make_user(nickname)
538 end
539 end
540
541 def get_or_fetch_by_nickname(nickname) do
542 with %User{} = user <- get_by_nickname(nickname) do
543 {:ok, user}
544 else
545 _e ->
546 with [_nick, _domain] <- String.split(nickname, "@"),
547 {:ok, user} <- fetch_by_nickname(nickname) do
548 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
549 fetch_initial_posts(user)
550 end
551
552 {:ok, user}
553 else
554 _e -> {:error, "not found " <> nickname}
555 end
556 end
557 end
558
559 @doc "Fetch some posts when the user has just been federated with"
560 def fetch_initial_posts(user),
561 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
562
563 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
564 def get_followers_query(%User{} = user, nil) do
565 User.Query.build(%{followers: user, deactivated: false})
566 end
567
568 def get_followers_query(user, page) do
569 from(u in get_followers_query(user, nil))
570 |> User.Query.paginate(page, 20)
571 end
572
573 @spec get_followers_query(User.t()) :: Ecto.Query.t()
574 def get_followers_query(user), do: get_followers_query(user, nil)
575
576 def get_followers(user, page \\ nil) do
577 q = get_followers_query(user, page)
578
579 {:ok, Repo.all(q)}
580 end
581
582 def get_followers_ids(user, page \\ nil) do
583 q = get_followers_query(user, page)
584
585 Repo.all(from(u in q, select: u.id))
586 end
587
588 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
589 def get_friends_query(%User{} = user, nil) do
590 User.Query.build(%{friends: user, deactivated: false})
591 end
592
593 def get_friends_query(user, page) do
594 from(u in get_friends_query(user, nil))
595 |> User.Query.paginate(page, 20)
596 end
597
598 @spec get_friends_query(User.t()) :: Ecto.Query.t()
599 def get_friends_query(user), do: get_friends_query(user, nil)
600
601 def get_friends(user, page \\ nil) do
602 q = get_friends_query(user, page)
603
604 {:ok, Repo.all(q)}
605 end
606
607 def get_friends_ids(user, page \\ nil) do
608 q = get_friends_query(user, page)
609
610 Repo.all(from(u in q, select: u.id))
611 end
612
613 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
614 def get_follow_requests(%User{} = user) do
615 users =
616 Activity.follow_requests_for_actor(user)
617 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
618 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
619 |> group_by([a, u], u.id)
620 |> select([a, u], u)
621 |> Repo.all()
622
623 {:ok, users}
624 end
625
626 def increase_note_count(%User{} = user) do
627 User
628 |> where(id: ^user.id)
629 |> update([u],
630 set: [
631 info:
632 fragment(
633 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
634 u.info,
635 u.info
636 )
637 ]
638 )
639 |> select([u], u)
640 |> Repo.update_all([])
641 |> case do
642 {1, [user]} -> set_cache(user)
643 _ -> {:error, user}
644 end
645 end
646
647 def decrease_note_count(%User{} = user) do
648 User
649 |> where(id: ^user.id)
650 |> update([u],
651 set: [
652 info:
653 fragment(
654 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
655 u.info,
656 u.info
657 )
658 ]
659 )
660 |> select([u], u)
661 |> Repo.update_all([])
662 |> case do
663 {1, [user]} -> set_cache(user)
664 _ -> {:error, user}
665 end
666 end
667
668 def update_note_count(%User{} = user) do
669 note_count_query =
670 from(
671 a in Object,
672 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
673 select: count(a.id)
674 )
675
676 note_count = Repo.one(note_count_query)
677
678 info_cng = User.Info.set_note_count(user.info, note_count)
679
680 user
681 |> change()
682 |> put_embed(:info, info_cng)
683 |> update_and_set_cache()
684 end
685
686 def update_follower_count(%User{} = user) do
687 follower_count_query =
688 User.Query.build(%{followers: user, deactivated: false})
689 |> select([u], %{count: count(u.id)})
690
691 User
692 |> where(id: ^user.id)
693 |> join(:inner, [u], s in subquery(follower_count_query))
694 |> update([u, s],
695 set: [
696 info:
697 fragment(
698 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
699 u.info,
700 s.count
701 )
702 ]
703 )
704 |> select([u], u)
705 |> Repo.update_all([])
706 |> case do
707 {1, [user]} -> set_cache(user)
708 _ -> {:error, user}
709 end
710 end
711
712 def remove_duplicated_following(%User{following: following} = user) do
713 uniq_following = Enum.uniq(following)
714
715 if length(following) == length(uniq_following) do
716 {:ok, user}
717 else
718 user
719 |> update_changeset(%{following: uniq_following})
720 |> update_and_set_cache()
721 end
722 end
723
724 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
725 def get_users_from_set(ap_ids, local_only \\ true) do
726 criteria = %{ap_id: ap_ids, deactivated: false}
727 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
728
729 User.Query.build(criteria)
730 |> Repo.all()
731 end
732
733 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
734 def get_recipients_from_activity(%Activity{recipients: to}) do
735 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
736 |> Repo.all()
737 end
738
739 def search(query, resolve \\ false, for_user \\ nil) do
740 # Strip the beginning @ off if there is a query
741 query = String.trim_leading(query, "@")
742
743 if resolve, do: get_or_fetch(query)
744
745 {:ok, results} =
746 Repo.transaction(fn ->
747 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
748 Repo.all(search_query(query, for_user))
749 end)
750
751 results
752 end
753
754 def search_query(query, for_user) do
755 fts_subquery = fts_search_subquery(query)
756 trigram_subquery = trigram_search_subquery(query)
757 union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
758 distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
759
760 from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
761 order_by: [desc: s.search_rank],
762 limit: 20
763 )
764 end
765
766 defp boost_search_rank_query(query, nil), do: query
767
768 defp boost_search_rank_query(query, for_user) do
769 friends_ids = get_friends_ids(for_user)
770 followers_ids = get_followers_ids(for_user)
771
772 from(u in subquery(query),
773 select_merge: %{
774 search_rank:
775 fragment(
776 """
777 CASE WHEN (?) THEN (?) * 1.3
778 WHEN (?) THEN (?) * 1.2
779 WHEN (?) THEN (?) * 1.1
780 ELSE (?) END
781 """,
782 u.id in ^friends_ids and u.id in ^followers_ids,
783 u.search_rank,
784 u.id in ^friends_ids,
785 u.search_rank,
786 u.id in ^followers_ids,
787 u.search_rank,
788 u.search_rank
789 )
790 }
791 )
792 end
793
794 defp fts_search_subquery(term, query \\ User) do
795 processed_query =
796 term
797 |> String.replace(~r/\W+/, " ")
798 |> String.trim()
799 |> String.split()
800 |> Enum.map(&(&1 <> ":*"))
801 |> Enum.join(" | ")
802
803 from(
804 u in query,
805 select_merge: %{
806 search_type: ^0,
807 search_rank:
808 fragment(
809 """
810 ts_rank_cd(
811 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
812 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
813 to_tsquery('simple', ?),
814 32
815 )
816 """,
817 u.nickname,
818 u.name,
819 ^processed_query
820 )
821 },
822 where:
823 fragment(
824 """
825 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
826 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
827 """,
828 u.nickname,
829 u.name,
830 ^processed_query
831 )
832 )
833 |> restrict_deactivated()
834 end
835
836 defp trigram_search_subquery(term) do
837 from(
838 u in User,
839 select_merge: %{
840 # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
841 search_type: fragment("?", 1),
842 search_rank:
843 fragment(
844 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
845 ^term,
846 u.nickname,
847 u.name
848 )
849 },
850 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
851 )
852 |> restrict_deactivated()
853 end
854
855 def mute(muter, %User{ap_id: ap_id}) do
856 info_cng =
857 muter.info
858 |> User.Info.add_to_mutes(ap_id)
859
860 cng =
861 change(muter)
862 |> put_embed(:info, info_cng)
863
864 update_and_set_cache(cng)
865 end
866
867 def unmute(muter, %{ap_id: ap_id}) do
868 info_cng =
869 muter.info
870 |> User.Info.remove_from_mutes(ap_id)
871
872 cng =
873 change(muter)
874 |> put_embed(:info, info_cng)
875
876 update_and_set_cache(cng)
877 end
878
879 def subscribe(subscriber, %{ap_id: ap_id}) do
880 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
881
882 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
883 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
884
885 if blocked do
886 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
887 else
888 info_cng =
889 subscribed.info
890 |> User.Info.add_to_subscribers(subscriber.ap_id)
891
892 change(subscribed)
893 |> put_embed(:info, info_cng)
894 |> update_and_set_cache()
895 end
896 end
897 end
898
899 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
900 with %User{} = user <- get_cached_by_ap_id(ap_id) do
901 info_cng =
902 user.info
903 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
904
905 change(user)
906 |> put_embed(:info, info_cng)
907 |> update_and_set_cache()
908 end
909 end
910
911 def block(blocker, %User{ap_id: ap_id} = blocked) do
912 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
913 blocker =
914 if following?(blocker, blocked) do
915 {:ok, blocker, _} = unfollow(blocker, blocked)
916 blocker
917 else
918 blocker
919 end
920
921 blocker =
922 if subscribed_to?(blocked, blocker) do
923 {:ok, blocker} = unsubscribe(blocked, blocker)
924 blocker
925 else
926 blocker
927 end
928
929 if following?(blocked, blocker) do
930 unfollow(blocked, blocker)
931 end
932
933 {:ok, blocker} = update_follower_count(blocker)
934
935 info_cng =
936 blocker.info
937 |> User.Info.add_to_block(ap_id)
938
939 cng =
940 change(blocker)
941 |> put_embed(:info, info_cng)
942
943 update_and_set_cache(cng)
944 end
945
946 # helper to handle the block given only an actor's AP id
947 def block(blocker, %{ap_id: ap_id}) do
948 block(blocker, get_cached_by_ap_id(ap_id))
949 end
950
951 def unblock(blocker, %{ap_id: ap_id}) do
952 info_cng =
953 blocker.info
954 |> User.Info.remove_from_block(ap_id)
955
956 cng =
957 change(blocker)
958 |> put_embed(:info, info_cng)
959
960 update_and_set_cache(cng)
961 end
962
963 def mutes?(nil, _), do: false
964 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
965
966 def blocks?(user, %{ap_id: ap_id}) do
967 blocks = user.info.blocks
968 domain_blocks = user.info.domain_blocks
969 %{host: host} = URI.parse(ap_id)
970
971 Enum.member?(blocks, ap_id) ||
972 Enum.any?(domain_blocks, fn domain ->
973 host == domain
974 end)
975 end
976
977 def subscribed_to?(user, %{ap_id: ap_id}) do
978 with %User{} = target <- get_cached_by_ap_id(ap_id) do
979 Enum.member?(target.info.subscribers, user.ap_id)
980 end
981 end
982
983 @spec muted_users(User.t()) :: [User.t()]
984 def muted_users(user) do
985 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
986 |> Repo.all()
987 end
988
989 @spec blocked_users(User.t()) :: [User.t()]
990 def blocked_users(user) do
991 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
992 |> Repo.all()
993 end
994
995 @spec subscribers(User.t()) :: [User.t()]
996 def subscribers(user) do
997 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
998 |> Repo.all()
999 end
1000
1001 def block_domain(user, domain) do
1002 info_cng =
1003 user.info
1004 |> User.Info.add_to_domain_block(domain)
1005
1006 cng =
1007 change(user)
1008 |> put_embed(:info, info_cng)
1009
1010 update_and_set_cache(cng)
1011 end
1012
1013 def unblock_domain(user, domain) do
1014 info_cng =
1015 user.info
1016 |> User.Info.remove_from_domain_block(domain)
1017
1018 cng =
1019 change(user)
1020 |> put_embed(:info, info_cng)
1021
1022 update_and_set_cache(cng)
1023 end
1024
1025 def deactivate_async(user, status \\ true) do
1026 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1027 end
1028
1029 def deactivate(%User{} = user, status \\ true) do
1030 info_cng = User.Info.set_activation_status(user.info, status)
1031
1032 with {:ok, friends} <- User.get_friends(user),
1033 {:ok, followers} <- User.get_followers(user),
1034 {:ok, user} <-
1035 user
1036 |> change()
1037 |> put_embed(:info, info_cng)
1038 |> update_and_set_cache() do
1039 Enum.each(followers, &invalidate_cache(&1))
1040 Enum.each(friends, &update_follower_count(&1))
1041
1042 {:ok, user}
1043 end
1044 end
1045
1046 def update_notification_settings(%User{} = user, settings \\ %{}) do
1047 info_changeset = User.Info.update_notification_settings(user.info, settings)
1048
1049 change(user)
1050 |> put_embed(:info, info_changeset)
1051 |> update_and_set_cache()
1052 end
1053
1054 @spec delete(User.t()) :: :ok
1055 def delete(%User{} = user),
1056 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1057
1058 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1059 def perform(:delete, %User{} = user) do
1060 {:ok, user} = User.deactivate(user)
1061
1062 # Remove all relationships
1063 {:ok, followers} = User.get_followers(user)
1064
1065 Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
1066
1067 {:ok, friends} = User.get_friends(user)
1068
1069 Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
1070
1071 delete_user_activities(user)
1072 end
1073
1074 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1075 def perform(:fetch_initial_posts, %User{} = user) do
1076 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1077
1078 Enum.each(
1079 # Insert all the posts in reverse order, so they're in the right order on the timeline
1080 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1081 &Pleroma.Web.Federator.incoming_ap_doc/1
1082 )
1083
1084 {:ok, user}
1085 end
1086
1087 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1088
1089 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1090 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1091 when is_list(blocked_identifiers) do
1092 Enum.map(
1093 blocked_identifiers,
1094 fn blocked_identifier ->
1095 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1096 {:ok, blocker} <- block(blocker, blocked),
1097 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1098 blocked
1099 else
1100 err ->
1101 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1102 err
1103 end
1104 end
1105 )
1106 end
1107
1108 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1109 def perform(:follow_import, %User{} = follower, followed_identifiers)
1110 when is_list(followed_identifiers) do
1111 Enum.map(
1112 followed_identifiers,
1113 fn followed_identifier ->
1114 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1115 {:ok, follower} <- maybe_direct_follow(follower, followed),
1116 {:ok, _} <- ActivityPub.follow(follower, followed) do
1117 followed
1118 else
1119 err ->
1120 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1121 err
1122 end
1123 end
1124 )
1125 end
1126
1127 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1128 do:
1129 PleromaJobQueue.enqueue(:background, __MODULE__, [
1130 :blocks_import,
1131 blocker,
1132 blocked_identifiers
1133 ])
1134
1135 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1136 do:
1137 PleromaJobQueue.enqueue(:background, __MODULE__, [
1138 :follow_import,
1139 follower,
1140 followed_identifiers
1141 ])
1142
1143 def delete_user_activities(%User{ap_id: ap_id} = user) do
1144 stream =
1145 ap_id
1146 |> Activity.query_by_actor()
1147 |> Activity.with_preloaded_object()
1148 |> Repo.stream()
1149
1150 Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
1151
1152 {:ok, user}
1153 end
1154
1155 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1156 Object.normalize(activity) |> ActivityPub.delete()
1157 end
1158
1159 defp delete_activity(_activity), do: "Doing nothing"
1160
1161 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1162 Pleroma.HTML.Scrubber.TwitterText
1163 end
1164
1165 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1166
1167 def html_filter_policy(_), do: @default_scrubbers
1168
1169 def fetch_by_ap_id(ap_id) do
1170 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1171
1172 case ap_try do
1173 {:ok, user} ->
1174 {:ok, user}
1175
1176 _ ->
1177 case OStatus.make_user(ap_id) do
1178 {:ok, user} -> {:ok, user}
1179 _ -> {:error, "Could not fetch by AP id"}
1180 end
1181 end
1182 end
1183
1184 def get_or_fetch_by_ap_id(ap_id) do
1185 user = get_cached_by_ap_id(ap_id)
1186
1187 if !is_nil(user) and !User.needs_update?(user) do
1188 {:ok, user}
1189 else
1190 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1191 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1192
1193 resp = fetch_by_ap_id(ap_id)
1194
1195 if should_fetch_initial do
1196 with {:ok, %User{} = user} <- resp do
1197 fetch_initial_posts(user)
1198 end
1199 end
1200
1201 resp
1202 end
1203 end
1204
1205 def get_or_create_instance_user do
1206 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1207
1208 if user = get_cached_by_ap_id(relay_uri) do
1209 user
1210 else
1211 changes =
1212 %User{info: %User.Info{}}
1213 |> cast(%{}, [:ap_id, :nickname, :local])
1214 |> put_change(:ap_id, relay_uri)
1215 |> put_change(:nickname, nil)
1216 |> put_change(:local, true)
1217 |> put_change(:follower_address, relay_uri <> "/followers")
1218
1219 {:ok, user} = Repo.insert(changes)
1220 user
1221 end
1222 end
1223
1224 # AP style
1225 def public_key_from_info(%{
1226 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1227 }) do
1228 key =
1229 public_key_pem
1230 |> :public_key.pem_decode()
1231 |> hd()
1232 |> :public_key.pem_entry_decode()
1233
1234 {:ok, key}
1235 end
1236
1237 # OStatus Magic Key
1238 def public_key_from_info(%{magic_key: magic_key}) do
1239 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1240 end
1241
1242 def get_public_key_for_ap_id(ap_id) do
1243 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1244 {:ok, public_key} <- public_key_from_info(user.info) do
1245 {:ok, public_key}
1246 else
1247 _ -> :error
1248 end
1249 end
1250
1251 defp blank?(""), do: nil
1252 defp blank?(n), do: n
1253
1254 def insert_or_update_user(data) do
1255 data
1256 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1257 |> remote_user_creation()
1258 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1259 |> set_cache()
1260 end
1261
1262 def ap_enabled?(%User{local: true}), do: true
1263 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1264 def ap_enabled?(_), do: false
1265
1266 @doc "Gets or fetch a user by uri or nickname."
1267 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1268 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1269 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1270
1271 # wait a period of time and return newest version of the User structs
1272 # this is because we have synchronous follow APIs and need to simulate them
1273 # with an async handshake
1274 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1275 with %User{} = a <- User.get_cached_by_id(a.id),
1276 %User{} = b <- User.get_cached_by_id(b.id) do
1277 {:ok, a, b}
1278 else
1279 _e ->
1280 :error
1281 end
1282 end
1283
1284 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1285 with :ok <- :timer.sleep(timeout),
1286 %User{} = a <- User.get_cached_by_id(a.id),
1287 %User{} = b <- User.get_cached_by_id(b.id) do
1288 {:ok, a, b}
1289 else
1290 _e ->
1291 :error
1292 end
1293 end
1294
1295 def parse_bio(bio) when is_binary(bio) and bio != "" do
1296 bio
1297 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1298 |> elem(0)
1299 end
1300
1301 def parse_bio(_), do: ""
1302
1303 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1304 # TODO: get profile URLs other than user.ap_id
1305 profile_urls = [user.ap_id]
1306
1307 bio
1308 |> CommonUtils.format_input("text/plain",
1309 mentions_format: :full,
1310 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1311 )
1312 |> elem(0)
1313 end
1314
1315 def parse_bio(_, _), do: ""
1316
1317 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1318 Repo.transaction(fn ->
1319 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1320 end)
1321 end
1322
1323 def tag(nickname, tags) when is_binary(nickname),
1324 do: tag(get_by_nickname(nickname), tags)
1325
1326 def tag(%User{} = user, tags),
1327 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1328
1329 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1330 Repo.transaction(fn ->
1331 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1332 end)
1333 end
1334
1335 def untag(nickname, tags) when is_binary(nickname),
1336 do: untag(get_by_nickname(nickname), tags)
1337
1338 def untag(%User{} = user, tags),
1339 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1340
1341 defp update_tags(%User{} = user, new_tags) do
1342 {:ok, updated_user} =
1343 user
1344 |> change(%{tags: new_tags})
1345 |> update_and_set_cache()
1346
1347 updated_user
1348 end
1349
1350 defp normalize_tags(tags) do
1351 [tags]
1352 |> List.flatten()
1353 |> Enum.map(&String.downcase(&1))
1354 end
1355
1356 defp local_nickname_regex do
1357 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1358 @extended_local_nickname_regex
1359 else
1360 @strict_local_nickname_regex
1361 end
1362 end
1363
1364 def local_nickname(nickname_or_mention) do
1365 nickname_or_mention
1366 |> full_nickname()
1367 |> String.split("@")
1368 |> hd()
1369 end
1370
1371 def full_nickname(nickname_or_mention),
1372 do: String.trim_leading(nickname_or_mention, "@")
1373
1374 def error_user(ap_id) do
1375 %User{
1376 name: ap_id,
1377 ap_id: ap_id,
1378 info: %User.Info{},
1379 nickname: "erroruser@example.com",
1380 inserted_at: NaiveDateTime.utc_now()
1381 }
1382 end
1383
1384 @spec all_superusers() :: [User.t()]
1385 def all_superusers do
1386 User.Query.build(%{super_users: true, local: true, deactivated: false})
1387 |> Repo.all()
1388 end
1389
1390 def showing_reblogs?(%User{} = user, %User{} = target) do
1391 target.ap_id not in user.info.muted_reblogs
1392 end
1393
1394 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1395 def toggle_confirmation(%User{} = user) do
1396 need_confirmation? = !user.info.confirmation_pending
1397
1398 info_changeset =
1399 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1400
1401 user
1402 |> change()
1403 |> put_embed(:info, info_changeset)
1404 |> update_and_set_cache()
1405 end
1406 end