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