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