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