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