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