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