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