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