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