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