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