removing synchronization worker
[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 def get_followers(user, page \\ nil) do
590 q = get_followers_query(user, page)
591
592 {:ok, Repo.all(q)}
593 end
594
595 def get_followers_ids(user, page \\ nil) do
596 q = get_followers_query(user, page)
597
598 Repo.all(from(u in q, select: u.id))
599 end
600
601 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
602 def get_friends_query(%User{} = user, nil) do
603 User.Query.build(%{friends: user, deactivated: false})
604 end
605
606 def get_friends_query(user, page) do
607 from(u in get_friends_query(user, nil))
608 |> User.Query.paginate(page, 20)
609 end
610
611 @spec get_friends_query(User.t()) :: Ecto.Query.t()
612 def get_friends_query(user), do: get_friends_query(user, nil)
613
614 def get_friends(user, page \\ nil) do
615 q = get_friends_query(user, page)
616
617 {:ok, Repo.all(q)}
618 end
619
620 def get_friends_ids(user, page \\ nil) do
621 q = get_friends_query(user, page)
622
623 Repo.all(from(u in q, select: u.id))
624 end
625
626 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
627 def get_follow_requests(%User{} = user) do
628 users =
629 Activity.follow_requests_for_actor(user)
630 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
631 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
632 |> group_by([a, u], u.id)
633 |> select([a, u], u)
634 |> Repo.all()
635
636 {:ok, users}
637 end
638
639 def increase_note_count(%User{} = user) do
640 User
641 |> where(id: ^user.id)
642 |> update([u],
643 set: [
644 info:
645 fragment(
646 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
647 u.info,
648 u.info
649 )
650 ]
651 )
652 |> select([u], u)
653 |> Repo.update_all([])
654 |> case do
655 {1, [user]} -> set_cache(user)
656 _ -> {:error, user}
657 end
658 end
659
660 def decrease_note_count(%User{} = user) do
661 User
662 |> where(id: ^user.id)
663 |> update([u],
664 set: [
665 info:
666 fragment(
667 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
668 u.info,
669 u.info
670 )
671 ]
672 )
673 |> select([u], u)
674 |> Repo.update_all([])
675 |> case do
676 {1, [user]} -> set_cache(user)
677 _ -> {:error, user}
678 end
679 end
680
681 def update_note_count(%User{} = user) do
682 note_count_query =
683 from(
684 a in Object,
685 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
686 select: count(a.id)
687 )
688
689 note_count = Repo.one(note_count_query)
690
691 info_cng = User.Info.set_note_count(user.info, note_count)
692
693 user
694 |> change()
695 |> put_embed(:info, info_cng)
696 |> update_and_set_cache()
697 end
698
699 def update_follower_count(%User{} = user) do
700 follower_count_query =
701 User.Query.build(%{followers: user, deactivated: false})
702 |> select([u], %{count: count(u.id)})
703
704 User
705 |> where(id: ^user.id)
706 |> join(:inner, [u], s in subquery(follower_count_query))
707 |> update([u, s],
708 set: [
709 info:
710 fragment(
711 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
712 u.info,
713 s.count
714 )
715 ]
716 )
717 |> select([u], u)
718 |> Repo.update_all([])
719 |> case do
720 {1, [user]} -> set_cache(user)
721 _ -> {:error, user}
722 end
723 end
724
725 def remove_duplicated_following(%User{following: following} = user) do
726 uniq_following = Enum.uniq(following)
727
728 if length(following) == length(uniq_following) do
729 {:ok, user}
730 else
731 user
732 |> update_changeset(%{following: uniq_following})
733 |> update_and_set_cache()
734 end
735 end
736
737 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
738 def get_users_from_set(ap_ids, local_only \\ true) do
739 criteria = %{ap_id: ap_ids, deactivated: false}
740 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
741
742 User.Query.build(criteria)
743 |> Repo.all()
744 end
745
746 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
747 def get_recipients_from_activity(%Activity{recipients: to}) do
748 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
749 |> Repo.all()
750 end
751
752 def mute(muter, %User{ap_id: ap_id}) do
753 info_cng =
754 muter.info
755 |> User.Info.add_to_mutes(ap_id)
756
757 cng =
758 change(muter)
759 |> put_embed(:info, info_cng)
760
761 update_and_set_cache(cng)
762 end
763
764 def unmute(muter, %{ap_id: ap_id}) do
765 info_cng =
766 muter.info
767 |> User.Info.remove_from_mutes(ap_id)
768
769 cng =
770 change(muter)
771 |> put_embed(:info, info_cng)
772
773 update_and_set_cache(cng)
774 end
775
776 def subscribe(subscriber, %{ap_id: ap_id}) do
777 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
778
779 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
780 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
781
782 if blocked do
783 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
784 else
785 info_cng =
786 subscribed.info
787 |> User.Info.add_to_subscribers(subscriber.ap_id)
788
789 change(subscribed)
790 |> put_embed(:info, info_cng)
791 |> update_and_set_cache()
792 end
793 end
794 end
795
796 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
797 with %User{} = user <- get_cached_by_ap_id(ap_id) do
798 info_cng =
799 user.info
800 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
801
802 change(user)
803 |> put_embed(:info, info_cng)
804 |> update_and_set_cache()
805 end
806 end
807
808 def block(blocker, %User{ap_id: ap_id} = blocked) do
809 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
810 blocker =
811 if following?(blocker, blocked) do
812 {:ok, blocker, _} = unfollow(blocker, blocked)
813 blocker
814 else
815 blocker
816 end
817
818 blocker =
819 if subscribed_to?(blocked, blocker) do
820 {:ok, blocker} = unsubscribe(blocked, blocker)
821 blocker
822 else
823 blocker
824 end
825
826 if following?(blocked, blocker) do
827 unfollow(blocked, blocker)
828 end
829
830 {:ok, blocker} = update_follower_count(blocker)
831
832 info_cng =
833 blocker.info
834 |> User.Info.add_to_block(ap_id)
835
836 cng =
837 change(blocker)
838 |> put_embed(:info, info_cng)
839
840 update_and_set_cache(cng)
841 end
842
843 # helper to handle the block given only an actor's AP id
844 def block(blocker, %{ap_id: ap_id}) do
845 block(blocker, get_cached_by_ap_id(ap_id))
846 end
847
848 def unblock(blocker, %{ap_id: ap_id}) do
849 info_cng =
850 blocker.info
851 |> User.Info.remove_from_block(ap_id)
852
853 cng =
854 change(blocker)
855 |> put_embed(:info, info_cng)
856
857 update_and_set_cache(cng)
858 end
859
860 def mutes?(nil, _), do: false
861 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
862
863 def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do
864 blocks = info.blocks
865 domain_blocks = info.domain_blocks
866 %{host: host} = URI.parse(ap_id)
867
868 Enum.member?(blocks, ap_id) || Enum.any?(domain_blocks, &(&1 == host))
869 end
870
871 def subscribed_to?(user, %{ap_id: ap_id}) do
872 with %User{} = target <- get_cached_by_ap_id(ap_id) do
873 Enum.member?(target.info.subscribers, user.ap_id)
874 end
875 end
876
877 @spec muted_users(User.t()) :: [User.t()]
878 def muted_users(user) do
879 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
880 |> Repo.all()
881 end
882
883 @spec blocked_users(User.t()) :: [User.t()]
884 def blocked_users(user) do
885 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
886 |> Repo.all()
887 end
888
889 @spec subscribers(User.t()) :: [User.t()]
890 def subscribers(user) do
891 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
892 |> Repo.all()
893 end
894
895 def block_domain(user, domain) do
896 info_cng =
897 user.info
898 |> User.Info.add_to_domain_block(domain)
899
900 cng =
901 change(user)
902 |> put_embed(:info, info_cng)
903
904 update_and_set_cache(cng)
905 end
906
907 def unblock_domain(user, domain) do
908 info_cng =
909 user.info
910 |> User.Info.remove_from_domain_block(domain)
911
912 cng =
913 change(user)
914 |> put_embed(:info, info_cng)
915
916 update_and_set_cache(cng)
917 end
918
919 def deactivate_async(user, status \\ true) do
920 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
921 end
922
923 def deactivate(%User{} = user, status \\ true) do
924 info_cng = User.Info.set_activation_status(user.info, status)
925
926 with {:ok, friends} <- User.get_friends(user),
927 {:ok, followers} <- User.get_followers(user),
928 {:ok, user} <-
929 user
930 |> change()
931 |> put_embed(:info, info_cng)
932 |> update_and_set_cache() do
933 Enum.each(followers, &invalidate_cache(&1))
934 Enum.each(friends, &update_follower_count(&1))
935
936 {:ok, user}
937 end
938 end
939
940 def update_notification_settings(%User{} = user, settings \\ %{}) do
941 info_changeset = User.Info.update_notification_settings(user.info, settings)
942
943 change(user)
944 |> put_embed(:info, info_changeset)
945 |> update_and_set_cache()
946 end
947
948 @spec delete(User.t()) :: :ok
949 def delete(%User{} = user),
950 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
951
952 @spec perform(atom(), User.t()) :: {:ok, User.t()}
953 def perform(:delete, %User{} = user) do
954 {:ok, _user} = ActivityPub.delete(user)
955
956 # Remove all relationships
957 {:ok, followers} = User.get_followers(user)
958
959 Enum.each(followers, fn follower ->
960 ActivityPub.unfollow(follower, user)
961 User.unfollow(follower, user)
962 end)
963
964 {:ok, friends} = User.get_friends(user)
965
966 Enum.each(friends, fn followed ->
967 ActivityPub.unfollow(user, followed)
968 User.unfollow(user, followed)
969 end)
970
971 delete_user_activities(user)
972 invalidate_cache(user)
973 Repo.delete(user)
974 end
975
976 @spec perform(atom(), User.t()) :: {:ok, User.t()}
977 def perform(:fetch_initial_posts, %User{} = user) do
978 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
979
980 Enum.each(
981 # Insert all the posts in reverse order, so they're in the right order on the timeline
982 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
983 &Pleroma.Web.Federator.incoming_ap_doc/1
984 )
985
986 {:ok, user}
987 end
988
989 def perform(:deactivate_async, user, status), do: deactivate(user, status)
990
991 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
992 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
993 when is_list(blocked_identifiers) do
994 Enum.map(
995 blocked_identifiers,
996 fn blocked_identifier ->
997 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
998 {:ok, blocker} <- block(blocker, blocked),
999 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1000 blocked
1001 else
1002 err ->
1003 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1004 err
1005 end
1006 end
1007 )
1008 end
1009
1010 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1011 def perform(:follow_import, %User{} = follower, followed_identifiers)
1012 when is_list(followed_identifiers) do
1013 Enum.map(
1014 followed_identifiers,
1015 fn followed_identifier ->
1016 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1017 {:ok, follower} <- maybe_direct_follow(follower, followed),
1018 {:ok, _} <- ActivityPub.follow(follower, followed) do
1019 followed
1020 else
1021 err ->
1022 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1023 err
1024 end
1025 end
1026 )
1027 end
1028
1029 @spec external_users_query() :: Ecto.Query.t()
1030 def external_users_query do
1031 User.Query.build(%{
1032 external: true,
1033 active: true,
1034 order_by: :id
1035 })
1036 end
1037
1038 @spec external_users(keyword()) :: [User.t()]
1039 def external_users(opts \\ []) do
1040 query =
1041 external_users_query()
1042 |> select([u], struct(u, [:id, :ap_id, :info]))
1043
1044 query =
1045 if opts[:max_id],
1046 do: where(query, [u], u.id > ^opts[:max_id]),
1047 else: query
1048
1049 query =
1050 if opts[:limit],
1051 do: limit(query, ^opts[:limit]),
1052 else: query
1053
1054 Repo.all(query)
1055 end
1056
1057 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1058 do:
1059 PleromaJobQueue.enqueue(:background, __MODULE__, [
1060 :blocks_import,
1061 blocker,
1062 blocked_identifiers
1063 ])
1064
1065 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1066 do:
1067 PleromaJobQueue.enqueue(:background, __MODULE__, [
1068 :follow_import,
1069 follower,
1070 followed_identifiers
1071 ])
1072
1073 def delete_user_activities(%User{ap_id: ap_id} = user) do
1074 ap_id
1075 |> Activity.query_by_actor()
1076 |> RepoStreamer.chunk_stream(50)
1077 |> Stream.each(fn activities ->
1078 Enum.each(activities, &delete_activity(&1))
1079 end)
1080 |> Stream.run()
1081
1082 {:ok, user}
1083 end
1084
1085 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1086 activity
1087 |> Object.normalize()
1088 |> ActivityPub.delete()
1089 end
1090
1091 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1092 user = get_cached_by_ap_id(activity.actor)
1093 object = Object.normalize(activity)
1094
1095 ActivityPub.unlike(user, object)
1096 end
1097
1098 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1099 user = get_cached_by_ap_id(activity.actor)
1100 object = Object.normalize(activity)
1101
1102 ActivityPub.unannounce(user, object)
1103 end
1104
1105 defp delete_activity(_activity), do: "Doing nothing"
1106
1107 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1108 Pleroma.HTML.Scrubber.TwitterText
1109 end
1110
1111 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1112
1113 def fetch_by_ap_id(ap_id) do
1114 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1115
1116 case ap_try do
1117 {:ok, user} ->
1118 {:ok, user}
1119
1120 _ ->
1121 case OStatus.make_user(ap_id) do
1122 {:ok, user} -> {:ok, user}
1123 _ -> {:error, "Could not fetch by AP id"}
1124 end
1125 end
1126 end
1127
1128 def get_or_fetch_by_ap_id(ap_id) do
1129 user = get_cached_by_ap_id(ap_id)
1130
1131 if !is_nil(user) and !User.needs_update?(user) do
1132 {:ok, user}
1133 else
1134 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1135 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1136
1137 resp = fetch_by_ap_id(ap_id)
1138
1139 if should_fetch_initial do
1140 with {:ok, %User{} = user} <- resp do
1141 fetch_initial_posts(user)
1142 end
1143 end
1144
1145 resp
1146 end
1147 end
1148
1149 def get_or_create_instance_user do
1150 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1151
1152 if user = get_cached_by_ap_id(relay_uri) do
1153 user
1154 else
1155 changes =
1156 %User{info: %User.Info{}}
1157 |> cast(%{}, [:ap_id, :nickname, :local])
1158 |> put_change(:ap_id, relay_uri)
1159 |> put_change(:nickname, nil)
1160 |> put_change(:local, true)
1161 |> put_change(:follower_address, relay_uri <> "/followers")
1162
1163 {:ok, user} = Repo.insert(changes)
1164 user
1165 end
1166 end
1167
1168 # AP style
1169 def public_key_from_info(%{
1170 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1171 }) do
1172 key =
1173 public_key_pem
1174 |> :public_key.pem_decode()
1175 |> hd()
1176 |> :public_key.pem_entry_decode()
1177
1178 {:ok, key}
1179 end
1180
1181 # OStatus Magic Key
1182 def public_key_from_info(%{magic_key: magic_key}) do
1183 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1184 end
1185
1186 def get_public_key_for_ap_id(ap_id) do
1187 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1188 {:ok, public_key} <- public_key_from_info(user.info) do
1189 {:ok, public_key}
1190 else
1191 _ -> :error
1192 end
1193 end
1194
1195 defp blank?(""), do: nil
1196 defp blank?(n), do: n
1197
1198 def insert_or_update_user(data) do
1199 data
1200 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1201 |> remote_user_creation()
1202 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1203 |> set_cache()
1204 end
1205
1206 def ap_enabled?(%User{local: true}), do: true
1207 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1208 def ap_enabled?(_), do: false
1209
1210 @doc "Gets or fetch a user by uri or nickname."
1211 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1212 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1213 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1214
1215 # wait a period of time and return newest version of the User structs
1216 # this is because we have synchronous follow APIs and need to simulate them
1217 # with an async handshake
1218 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1219 with %User{} = a <- User.get_cached_by_id(a.id),
1220 %User{} = b <- User.get_cached_by_id(b.id) do
1221 {:ok, a, b}
1222 else
1223 _e ->
1224 :error
1225 end
1226 end
1227
1228 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1229 with :ok <- :timer.sleep(timeout),
1230 %User{} = a <- User.get_cached_by_id(a.id),
1231 %User{} = b <- User.get_cached_by_id(b.id) do
1232 {:ok, a, b}
1233 else
1234 _e ->
1235 :error
1236 end
1237 end
1238
1239 def parse_bio(bio) when is_binary(bio) and bio != "" do
1240 bio
1241 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1242 |> elem(0)
1243 end
1244
1245 def parse_bio(_), do: ""
1246
1247 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1248 # TODO: get profile URLs other than user.ap_id
1249 profile_urls = [user.ap_id]
1250
1251 bio
1252 |> CommonUtils.format_input("text/plain",
1253 mentions_format: :full,
1254 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1255 )
1256 |> elem(0)
1257 end
1258
1259 def parse_bio(_, _), do: ""
1260
1261 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1262 Repo.transaction(fn ->
1263 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1264 end)
1265 end
1266
1267 def tag(nickname, tags) when is_binary(nickname),
1268 do: tag(get_by_nickname(nickname), tags)
1269
1270 def tag(%User{} = user, tags),
1271 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1272
1273 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1274 Repo.transaction(fn ->
1275 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1276 end)
1277 end
1278
1279 def untag(nickname, tags) when is_binary(nickname),
1280 do: untag(get_by_nickname(nickname), tags)
1281
1282 def untag(%User{} = user, tags),
1283 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1284
1285 defp update_tags(%User{} = user, new_tags) do
1286 {:ok, updated_user} =
1287 user
1288 |> change(%{tags: new_tags})
1289 |> update_and_set_cache()
1290
1291 updated_user
1292 end
1293
1294 defp normalize_tags(tags) do
1295 [tags]
1296 |> List.flatten()
1297 |> Enum.map(&String.downcase(&1))
1298 end
1299
1300 defp local_nickname_regex do
1301 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1302 @extended_local_nickname_regex
1303 else
1304 @strict_local_nickname_regex
1305 end
1306 end
1307
1308 def local_nickname(nickname_or_mention) do
1309 nickname_or_mention
1310 |> full_nickname()
1311 |> String.split("@")
1312 |> hd()
1313 end
1314
1315 def full_nickname(nickname_or_mention),
1316 do: String.trim_leading(nickname_or_mention, "@")
1317
1318 def error_user(ap_id) do
1319 %User{
1320 name: ap_id,
1321 ap_id: ap_id,
1322 info: %User.Info{},
1323 nickname: "erroruser@example.com",
1324 inserted_at: NaiveDateTime.utc_now()
1325 }
1326 end
1327
1328 @spec all_superusers() :: [User.t()]
1329 def all_superusers do
1330 User.Query.build(%{super_users: true, local: true, deactivated: false})
1331 |> Repo.all()
1332 end
1333
1334 def showing_reblogs?(%User{} = user, %User{} = target) do
1335 target.ap_id not in user.info.muted_reblogs
1336 end
1337
1338 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1339 def toggle_confirmation(%User{} = user) do
1340 need_confirmation? = !user.info.confirmation_pending
1341
1342 info_changeset =
1343 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1344
1345 user
1346 |> change()
1347 |> put_embed(:info, info_changeset)
1348 |> update_and_set_cache()
1349 end
1350
1351 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1352 mascot
1353 end
1354
1355 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1356 # use instance-default
1357 config = Pleroma.Config.get([:assets, :mascots])
1358 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1359 mascot = Keyword.get(config, default_mascot)
1360
1361 %{
1362 "id" => "default-mascot",
1363 "url" => mascot[:url],
1364 "preview_url" => mascot[:url],
1365 "pleroma" => %{
1366 "mime_type" => mascot[:mime_type]
1367 }
1368 }
1369 end
1370
1371 def ensure_keys_present(user) do
1372 info = user.info
1373
1374 if info.keys do
1375 {:ok, user}
1376 else
1377 {:ok, pem} = Keys.generate_rsa_pem()
1378
1379 info_cng =
1380 info
1381 |> User.Info.set_keys(pem)
1382
1383 cng =
1384 Ecto.Changeset.change(user)
1385 |> Ecto.Changeset.put_embed(:info, info_cng)
1386
1387 update_and_set_cache(cng)
1388 end
1389 end
1390
1391 def get_ap_ids_by_nicknames(nicknames) do
1392 from(u in User,
1393 where: u.nickname in ^nicknames,
1394 select: u.ap_id
1395 )
1396 |> Repo.all()
1397 end
1398
1399 defdelegate search(query, opts \\ []), to: User.Search
1400
1401 defp put_password_hash(
1402 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1403 ) do
1404 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1405 end
1406
1407 defp put_password_hash(changeset), do: changeset
1408 end