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