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