c6a562a6143f22318df27cdf6fea6fbcbbc51f4e
[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])
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 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
713 def get_users_from_set(ap_ids, local_only \\ true) do
714 criteria = %{ap_id: ap_ids, deactivated: false}
715 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
716
717 User.Query.build(criteria)
718 |> Repo.all()
719 end
720
721 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
722 def get_recipients_from_activity(%Activity{recipients: to}) do
723 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
724 |> Repo.all()
725 end
726
727 def search(query, resolve \\ false, for_user \\ nil) do
728 # Strip the beginning @ off if there is a query
729 query = String.trim_leading(query, "@")
730
731 if resolve, do: get_or_fetch(query)
732
733 {:ok, results} =
734 Repo.transaction(fn ->
735 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
736 Repo.all(search_query(query, for_user))
737 end)
738
739 results
740 end
741
742 def search_query(query, for_user) do
743 fts_subquery = fts_search_subquery(query)
744 trigram_subquery = trigram_search_subquery(query)
745 union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
746 distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
747
748 from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
749 order_by: [desc: s.search_rank],
750 limit: 20
751 )
752 end
753
754 defp boost_search_rank_query(query, nil), do: query
755
756 defp boost_search_rank_query(query, for_user) do
757 friends_ids = get_friends_ids(for_user)
758 followers_ids = get_followers_ids(for_user)
759
760 from(u in subquery(query),
761 select_merge: %{
762 search_rank:
763 fragment(
764 """
765 CASE WHEN (?) THEN (?) * 1.3
766 WHEN (?) THEN (?) * 1.2
767 WHEN (?) THEN (?) * 1.1
768 ELSE (?) END
769 """,
770 u.id in ^friends_ids and u.id in ^followers_ids,
771 u.search_rank,
772 u.id in ^friends_ids,
773 u.search_rank,
774 u.id in ^followers_ids,
775 u.search_rank,
776 u.search_rank
777 )
778 }
779 )
780 end
781
782 defp fts_search_subquery(term, query \\ User) do
783 processed_query =
784 term
785 |> String.replace(~r/\W+/, " ")
786 |> String.trim()
787 |> String.split()
788 |> Enum.map(&(&1 <> ":*"))
789 |> Enum.join(" | ")
790
791 from(
792 u in query,
793 select_merge: %{
794 search_type: ^0,
795 search_rank:
796 fragment(
797 """
798 ts_rank_cd(
799 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
800 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
801 to_tsquery('simple', ?),
802 32
803 )
804 """,
805 u.nickname,
806 u.name,
807 ^processed_query
808 )
809 },
810 where:
811 fragment(
812 """
813 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
814 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
815 """,
816 u.nickname,
817 u.name,
818 ^processed_query
819 )
820 )
821 |> restrict_deactivated()
822 end
823
824 defp trigram_search_subquery(term) do
825 from(
826 u in User,
827 select_merge: %{
828 # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
829 search_type: fragment("?", 1),
830 search_rank:
831 fragment(
832 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
833 ^term,
834 u.nickname,
835 u.name
836 )
837 },
838 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
839 )
840 |> restrict_deactivated()
841 end
842
843 def mute(muter, %User{ap_id: ap_id}) do
844 info_cng =
845 muter.info
846 |> User.Info.add_to_mutes(ap_id)
847
848 cng =
849 change(muter)
850 |> put_embed(:info, info_cng)
851
852 update_and_set_cache(cng)
853 end
854
855 def unmute(muter, %{ap_id: ap_id}) do
856 info_cng =
857 muter.info
858 |> User.Info.remove_from_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 subscribe(subscriber, %{ap_id: ap_id}) do
868 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
869
870 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
871 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
872
873 if blocked do
874 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
875 else
876 info_cng =
877 subscribed.info
878 |> User.Info.add_to_subscribers(subscriber.ap_id)
879
880 change(subscribed)
881 |> put_embed(:info, info_cng)
882 |> update_and_set_cache()
883 end
884 end
885 end
886
887 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
888 with %User{} = user <- get_cached_by_ap_id(ap_id) do
889 info_cng =
890 user.info
891 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
892
893 change(user)
894 |> put_embed(:info, info_cng)
895 |> update_and_set_cache()
896 end
897 end
898
899 def block(blocker, %User{ap_id: ap_id} = blocked) do
900 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
901 blocker =
902 if following?(blocker, blocked) do
903 {:ok, blocker, _} = unfollow(blocker, blocked)
904 blocker
905 else
906 blocker
907 end
908
909 blocker =
910 if subscribed_to?(blocked, blocker) do
911 {:ok, blocker} = unsubscribe(blocked, blocker)
912 blocker
913 else
914 blocker
915 end
916
917 if following?(blocked, blocker) do
918 unfollow(blocked, blocker)
919 end
920
921 {:ok, blocker} = update_follower_count(blocker)
922
923 info_cng =
924 blocker.info
925 |> User.Info.add_to_block(ap_id)
926
927 cng =
928 change(blocker)
929 |> put_embed(:info, info_cng)
930
931 update_and_set_cache(cng)
932 end
933
934 # helper to handle the block given only an actor's AP id
935 def block(blocker, %{ap_id: ap_id}) do
936 block(blocker, get_cached_by_ap_id(ap_id))
937 end
938
939 def unblock(blocker, %{ap_id: ap_id}) do
940 info_cng =
941 blocker.info
942 |> User.Info.remove_from_block(ap_id)
943
944 cng =
945 change(blocker)
946 |> put_embed(:info, info_cng)
947
948 update_and_set_cache(cng)
949 end
950
951 def mutes?(nil, _), do: false
952 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
953
954 def blocks?(user, %{ap_id: ap_id}) do
955 blocks = user.info.blocks
956 domain_blocks = user.info.domain_blocks
957 %{host: host} = URI.parse(ap_id)
958
959 Enum.member?(blocks, ap_id) ||
960 Enum.any?(domain_blocks, fn domain ->
961 host == domain
962 end)
963 end
964
965 def subscribed_to?(user, %{ap_id: ap_id}) do
966 with %User{} = target <- get_cached_by_ap_id(ap_id) do
967 Enum.member?(target.info.subscribers, user.ap_id)
968 end
969 end
970
971 @spec muted_users(User.t()) :: [User.t()]
972 def muted_users(user) do
973 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
974 |> Repo.all()
975 end
976
977 @spec blocked_users(User.t()) :: [User.t()]
978 def blocked_users(user) do
979 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
980 |> Repo.all()
981 end
982
983 @spec subscribers(User.t()) :: [User.t()]
984 def subscribers(user) do
985 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
986 |> Repo.all()
987 end
988
989 def block_domain(user, domain) do
990 info_cng =
991 user.info
992 |> User.Info.add_to_domain_block(domain)
993
994 cng =
995 change(user)
996 |> put_embed(:info, info_cng)
997
998 update_and_set_cache(cng)
999 end
1000
1001 def unblock_domain(user, domain) do
1002 info_cng =
1003 user.info
1004 |> User.Info.remove_from_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 deactivate_async(user, status \\ true) do
1014 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1015 end
1016
1017 def deactivate(%User{} = user, status \\ true) do
1018 info_cng = User.Info.set_activation_status(user.info, status)
1019
1020 with {:ok, friends} <- User.get_friends(user),
1021 {:ok, followers} <- User.get_followers(user),
1022 {:ok, user} <-
1023 user
1024 |> change()
1025 |> put_embed(:info, info_cng)
1026 |> update_and_set_cache() do
1027 Enum.each(followers, &invalidate_cache(&1))
1028 Enum.each(friends, &update_follower_count(&1))
1029
1030 {:ok, user}
1031 end
1032 end
1033
1034 def update_notification_settings(%User{} = user, settings \\ %{}) do
1035 info_changeset = User.Info.update_notification_settings(user.info, settings)
1036
1037 change(user)
1038 |> put_embed(:info, info_changeset)
1039 |> update_and_set_cache()
1040 end
1041
1042 @spec delete(User.t()) :: :ok
1043 def delete(%User{} = user),
1044 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1045
1046 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1047 def perform(:delete, %User{} = user) do
1048 {:ok, user} = User.deactivate(user)
1049
1050 # Remove all relationships
1051 {:ok, followers} = User.get_followers(user)
1052
1053 Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
1054
1055 {:ok, friends} = User.get_friends(user)
1056
1057 Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
1058
1059 delete_user_activities(user)
1060 end
1061
1062 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1063 def perform(:fetch_initial_posts, %User{} = user) do
1064 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1065
1066 Enum.each(
1067 # Insert all the posts in reverse order, so they're in the right order on the timeline
1068 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1069 &Pleroma.Web.Federator.incoming_ap_doc/1
1070 )
1071
1072 {:ok, user}
1073 end
1074
1075 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1076
1077 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1078 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1079 when is_list(blocked_identifiers) do
1080 Enum.map(
1081 blocked_identifiers,
1082 fn blocked_identifier ->
1083 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1084 {:ok, blocker} <- block(blocker, blocked),
1085 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1086 blocked
1087 else
1088 err ->
1089 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1090 err
1091 end
1092 end
1093 )
1094 end
1095
1096 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1097 def perform(:follow_import, %User{} = follower, followed_identifiers)
1098 when is_list(followed_identifiers) do
1099 Enum.map(
1100 followed_identifiers,
1101 fn followed_identifier ->
1102 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1103 {:ok, follower} <- maybe_direct_follow(follower, followed),
1104 {:ok, _} <- ActivityPub.follow(follower, followed) do
1105 followed
1106 else
1107 err ->
1108 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1109 err
1110 end
1111 end
1112 )
1113 end
1114
1115 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1116 do:
1117 PleromaJobQueue.enqueue(:background, __MODULE__, [
1118 :blocks_import,
1119 blocker,
1120 blocked_identifiers
1121 ])
1122
1123 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1124 do:
1125 PleromaJobQueue.enqueue(:background, __MODULE__, [
1126 :follow_import,
1127 follower,
1128 followed_identifiers
1129 ])
1130
1131 def delete_user_activities(%User{ap_id: ap_id} = user) do
1132 stream =
1133 ap_id
1134 |> Activity.query_by_actor()
1135 |> Activity.with_preloaded_object()
1136 |> Repo.stream()
1137
1138 Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
1139
1140 {:ok, user}
1141 end
1142
1143 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1144 Object.normalize(activity) |> ActivityPub.delete()
1145 end
1146
1147 defp delete_activity(_activity), do: "Doing nothing"
1148
1149 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1150 Pleroma.HTML.Scrubber.TwitterText
1151 end
1152
1153 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1154
1155 def html_filter_policy(_), do: @default_scrubbers
1156
1157 def fetch_by_ap_id(ap_id) do
1158 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1159
1160 case ap_try do
1161 {:ok, user} ->
1162 {:ok, user}
1163
1164 _ ->
1165 case OStatus.make_user(ap_id) do
1166 {:ok, user} -> {:ok, user}
1167 _ -> {:error, "Could not fetch by AP id"}
1168 end
1169 end
1170 end
1171
1172 def get_or_fetch_by_ap_id(ap_id) do
1173 user = get_cached_by_ap_id(ap_id)
1174
1175 if !is_nil(user) and !User.needs_update?(user) do
1176 {:ok, user}
1177 else
1178 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1179 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1180
1181 resp = fetch_by_ap_id(ap_id)
1182
1183 if should_fetch_initial do
1184 with {:ok, %User{} = user} <- resp do
1185 fetch_initial_posts(user)
1186 end
1187 end
1188
1189 resp
1190 end
1191 end
1192
1193 def get_or_create_instance_user do
1194 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1195
1196 if user = get_cached_by_ap_id(relay_uri) do
1197 user
1198 else
1199 changes =
1200 %User{info: %User.Info{}}
1201 |> cast(%{}, [:ap_id, :nickname, :local])
1202 |> put_change(:ap_id, relay_uri)
1203 |> put_change(:nickname, nil)
1204 |> put_change(:local, true)
1205 |> put_change(:follower_address, relay_uri <> "/followers")
1206
1207 {:ok, user} = Repo.insert(changes)
1208 user
1209 end
1210 end
1211
1212 # AP style
1213 def public_key_from_info(%{
1214 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1215 }) do
1216 key =
1217 public_key_pem
1218 |> :public_key.pem_decode()
1219 |> hd()
1220 |> :public_key.pem_entry_decode()
1221
1222 {:ok, key}
1223 end
1224
1225 # OStatus Magic Key
1226 def public_key_from_info(%{magic_key: magic_key}) do
1227 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1228 end
1229
1230 def get_public_key_for_ap_id(ap_id) do
1231 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1232 {:ok, public_key} <- public_key_from_info(user.info) do
1233 {:ok, public_key}
1234 else
1235 _ -> :error
1236 end
1237 end
1238
1239 defp blank?(""), do: nil
1240 defp blank?(n), do: n
1241
1242 def insert_or_update_user(data) do
1243 data
1244 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1245 |> remote_user_creation()
1246 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1247 |> set_cache()
1248 end
1249
1250 def ap_enabled?(%User{local: true}), do: true
1251 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1252 def ap_enabled?(_), do: false
1253
1254 @doc "Gets or fetch a user by uri or nickname."
1255 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1256 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1257 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1258
1259 # wait a period of time and return newest version of the User structs
1260 # this is because we have synchronous follow APIs and need to simulate them
1261 # with an async handshake
1262 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1263 with %User{} = a <- User.get_cached_by_id(a.id),
1264 %User{} = b <- User.get_cached_by_id(b.id) do
1265 {:ok, a, b}
1266 else
1267 _e ->
1268 :error
1269 end
1270 end
1271
1272 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1273 with :ok <- :timer.sleep(timeout),
1274 %User{} = a <- User.get_cached_by_id(a.id),
1275 %User{} = b <- User.get_cached_by_id(b.id) do
1276 {:ok, a, b}
1277 else
1278 _e ->
1279 :error
1280 end
1281 end
1282
1283 def parse_bio(bio) when is_binary(bio) and bio != "" do
1284 bio
1285 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1286 |> elem(0)
1287 end
1288
1289 def parse_bio(_), do: ""
1290
1291 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1292 # TODO: get profile URLs other than user.ap_id
1293 profile_urls = [user.ap_id]
1294
1295 bio
1296 |> CommonUtils.format_input("text/plain",
1297 mentions_format: :full,
1298 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1299 )
1300 |> elem(0)
1301 end
1302
1303 def parse_bio(_, _), do: ""
1304
1305 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1306 Repo.transaction(fn ->
1307 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1308 end)
1309 end
1310
1311 def tag(nickname, tags) when is_binary(nickname),
1312 do: tag(get_by_nickname(nickname), tags)
1313
1314 def tag(%User{} = user, tags),
1315 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1316
1317 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1318 Repo.transaction(fn ->
1319 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1320 end)
1321 end
1322
1323 def untag(nickname, tags) when is_binary(nickname),
1324 do: untag(get_by_nickname(nickname), tags)
1325
1326 def untag(%User{} = user, tags),
1327 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1328
1329 defp update_tags(%User{} = user, new_tags) do
1330 {:ok, updated_user} =
1331 user
1332 |> change(%{tags: new_tags})
1333 |> update_and_set_cache()
1334
1335 updated_user
1336 end
1337
1338 defp normalize_tags(tags) do
1339 [tags]
1340 |> List.flatten()
1341 |> Enum.map(&String.downcase(&1))
1342 end
1343
1344 defp local_nickname_regex do
1345 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1346 @extended_local_nickname_regex
1347 else
1348 @strict_local_nickname_regex
1349 end
1350 end
1351
1352 def local_nickname(nickname_or_mention) do
1353 nickname_or_mention
1354 |> full_nickname()
1355 |> String.split("@")
1356 |> hd()
1357 end
1358
1359 def full_nickname(nickname_or_mention),
1360 do: String.trim_leading(nickname_or_mention, "@")
1361
1362 def error_user(ap_id) do
1363 %User{
1364 name: ap_id,
1365 ap_id: ap_id,
1366 info: %User.Info{},
1367 nickname: "erroruser@example.com",
1368 inserted_at: NaiveDateTime.utc_now()
1369 }
1370 end
1371
1372 @spec all_superusers() :: [User.t()]
1373 def all_superusers do
1374 User.Query.build(%{super_users: true, local: true, deactivated: false})
1375 |> Repo.all()
1376 end
1377
1378 def showing_reblogs?(%User{} = user, %User{} = target) do
1379 target.ap_id not in user.info.muted_reblogs
1380 end
1381 end