Merge branch 'fix/credo-issues' 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, _} <- try_send_confirmation_email(user) do
265 {:ok, user}
266 end
267 end
268
269 def try_send_confirmation_email(%User{} = user) do
270 if user.info.confirmation_pending &&
271 Pleroma.Config.get([:instance, :account_activation_required]) do
272 user
273 |> Pleroma.UserEmail.account_confirmation_email()
274 |> Pleroma.Mailer.deliver()
275 else
276 {:ok, :noop}
277 end
278 end
279
280 def needs_update?(%User{local: true}), do: false
281
282 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
283
284 def needs_update?(%User{local: false} = user) do
285 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86400
286 end
287
288 def needs_update?(_), do: true
289
290 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
291 {:ok, follower}
292 end
293
294 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
295 follow(follower, followed)
296 end
297
298 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
299 if not User.ap_enabled?(followed) do
300 follow(follower, followed)
301 else
302 {:ok, follower}
303 end
304 end
305
306 def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
307 if not following?(follower, followed) do
308 follow(follower, followed)
309 else
310 {:ok, follower}
311 end
312 end
313
314 @doc "A mass follow for local users. Respects blocks but does not create activities."
315 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
316 def follow_all(follower, followeds) do
317 followed_addresses =
318 followeds
319 |> Enum.reject(fn %{ap_id: ap_id} -> ap_id in follower.info.blocks end)
320 |> Enum.map(fn %{follower_address: fa} -> fa end)
321
322 q =
323 from(u in User,
324 where: u.id == ^follower.id,
325 update: [
326 set: [
327 following:
328 fragment(
329 "array(select distinct unnest (array_cat(?, ?)))",
330 u.following,
331 ^followed_addresses
332 )
333 ]
334 ]
335 )
336
337 {1, [follower]} = Repo.update_all(q, [], returning: true)
338
339 Enum.each(followeds, fn followed ->
340 update_follower_count(followed)
341 end)
342
343 set_cache(follower)
344 end
345
346 def follow(%User{} = follower, %User{info: info} = followed) do
347 user_config = Application.get_env(:pleroma, :user)
348 deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
349
350 ap_followers = followed.follower_address
351
352 cond do
353 following?(follower, followed) or info.deactivated ->
354 {:error, "Could not follow user: #{followed.nickname} is already on your list."}
355
356 deny_follow_blocked and blocks?(followed, follower) ->
357 {:error, "Could not follow user: #{followed.nickname} blocked you."}
358
359 true ->
360 if !followed.local && follower.local && !ap_enabled?(followed) do
361 Websub.subscribe(follower, followed)
362 end
363
364 q =
365 from(u in User,
366 where: u.id == ^follower.id,
367 update: [push: [following: ^ap_followers]]
368 )
369
370 {1, [follower]} = Repo.update_all(q, [], returning: true)
371
372 {:ok, _} = update_follower_count(followed)
373
374 set_cache(follower)
375 end
376 end
377
378 def unfollow(%User{} = follower, %User{} = followed) do
379 ap_followers = followed.follower_address
380
381 if following?(follower, followed) and follower.ap_id != followed.ap_id do
382 q =
383 from(u in User,
384 where: u.id == ^follower.id,
385 update: [pull: [following: ^ap_followers]]
386 )
387
388 {1, [follower]} = Repo.update_all(q, [], returning: true)
389
390 {:ok, followed} = update_follower_count(followed)
391
392 set_cache(follower)
393
394 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
395 else
396 {:error, "Not subscribed!"}
397 end
398 end
399
400 @spec following?(User.t(), User.t()) :: boolean
401 def following?(%User{} = follower, %User{} = followed) do
402 Enum.member?(follower.following, followed.follower_address)
403 end
404
405 def follow_import(%User{} = follower, followed_identifiers)
406 when is_list(followed_identifiers) do
407 Enum.map(
408 followed_identifiers,
409 fn followed_identifier ->
410 with %User{} = followed <- get_or_fetch(followed_identifier),
411 {:ok, follower} <- maybe_direct_follow(follower, followed),
412 {:ok, _} <- ActivityPub.follow(follower, followed) do
413 followed
414 else
415 err ->
416 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
417 err
418 end
419 end
420 )
421 end
422
423 def locked?(%User{} = user) do
424 user.info.locked || false
425 end
426
427 def get_by_id(id) do
428 Repo.get_by(User, id: id)
429 end
430
431 def get_by_ap_id(ap_id) do
432 Repo.get_by(User, ap_id: ap_id)
433 end
434
435 # 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
436 def get_by_guessed_nickname(ap_id) do
437 domain = URI.parse(ap_id).host
438 name = List.last(String.split(ap_id, "/"))
439 nickname = "#{name}@#{domain}"
440
441 get_by_nickname(nickname)
442 end
443
444 def set_cache(user) do
445 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
446 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
447 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
448 {:ok, user}
449 end
450
451 def update_and_set_cache(changeset) do
452 with {:ok, user} <- Repo.update(changeset) do
453 set_cache(user)
454 else
455 e -> e
456 end
457 end
458
459 def invalidate_cache(user) do
460 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
461 Cachex.del(:user_cache, "nickname:#{user.nickname}")
462 Cachex.del(:user_cache, "user_info:#{user.id}")
463 end
464
465 def get_cached_by_ap_id(ap_id) do
466 key = "ap_id:#{ap_id}"
467 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
468 end
469
470 def get_cached_by_id(id) do
471 key = "id:#{id}"
472
473 ap_id =
474 Cachex.fetch!(:user_cache, key, fn _ ->
475 user = get_by_id(id)
476
477 if user do
478 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
479 {:commit, user.ap_id}
480 else
481 {:ignore, ""}
482 end
483 end)
484
485 get_cached_by_ap_id(ap_id)
486 end
487
488 def get_cached_by_nickname(nickname) do
489 key = "nickname:#{nickname}"
490 Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
491 end
492
493 def get_cached_by_nickname_or_id(nickname_or_id) do
494 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
495 end
496
497 def get_by_nickname(nickname) do
498 Repo.get_by(User, nickname: nickname) ||
499 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
500 Repo.get_by(User, nickname: local_nickname(nickname))
501 end
502 end
503
504 def get_by_nickname_or_email(nickname_or_email) do
505 case user = Repo.get_by(User, nickname: nickname_or_email) do
506 %User{} -> user
507 nil -> Repo.get_by(User, email: nickname_or_email)
508 end
509 end
510
511 def get_cached_user_info(user) do
512 key = "user_info:#{user.id}"
513 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
514 end
515
516 def fetch_by_nickname(nickname) do
517 ap_try = ActivityPub.make_user_from_nickname(nickname)
518
519 case ap_try do
520 {:ok, user} -> {:ok, user}
521 _ -> OStatus.make_user(nickname)
522 end
523 end
524
525 def get_or_fetch_by_nickname(nickname) do
526 with %User{} = user <- get_by_nickname(nickname) do
527 user
528 else
529 _e ->
530 with [_nick, _domain] <- String.split(nickname, "@"),
531 {:ok, user} <- fetch_by_nickname(nickname) do
532 user
533 else
534 _e -> nil
535 end
536 end
537 end
538
539 def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
540 from(
541 u in User,
542 where: fragment("? <@ ?", ^[follower_address], u.following),
543 where: u.id != ^id
544 )
545 end
546
547 def get_followers_query(user, page) do
548 from(
549 u in get_followers_query(user, nil),
550 limit: 20,
551 offset: ^((page - 1) * 20)
552 )
553 end
554
555 def get_followers_query(user), do: get_followers_query(user, nil)
556
557 def get_followers(user, page \\ nil) do
558 q = get_followers_query(user, page)
559
560 {:ok, Repo.all(q)}
561 end
562
563 def get_followers_ids(user, page \\ nil) do
564 q = get_followers_query(user, page)
565
566 Repo.all(from(u in q, select: u.id))
567 end
568
569 def get_friends_query(%User{id: id, following: following}, nil) do
570 from(
571 u in User,
572 where: u.follower_address in ^following,
573 where: u.id != ^id
574 )
575 end
576
577 def get_friends_query(user, page) do
578 from(
579 u in get_friends_query(user, nil),
580 limit: 20,
581 offset: ^((page - 1) * 20)
582 )
583 end
584
585 def get_friends_query(user), do: get_friends_query(user, nil)
586
587 def get_friends(user, page \\ nil) do
588 q = get_friends_query(user, page)
589
590 {:ok, Repo.all(q)}
591 end
592
593 def get_friends_ids(user, page \\ nil) do
594 q = get_friends_query(user, page)
595
596 Repo.all(from(u in q, select: u.id))
597 end
598
599 def get_follow_requests_query(%User{} = user) do
600 from(
601 a in Activity,
602 where:
603 fragment(
604 "? ->> 'type' = 'Follow'",
605 a.data
606 ),
607 where:
608 fragment(
609 "? ->> 'state' = 'pending'",
610 a.data
611 ),
612 where:
613 fragment(
614 "? @> ?",
615 a.data,
616 ^%{"object" => user.ap_id}
617 )
618 )
619 end
620
621 def get_follow_requests(%User{} = user) do
622 q = get_follow_requests_query(user)
623 reqs = Repo.all(q)
624
625 users =
626 Enum.map(reqs, fn req -> req.actor end)
627 |> Enum.uniq()
628 |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
629 |> Enum.filter(fn u -> !is_nil(u) end)
630 |> Enum.filter(fn u -> !following?(u, user) end)
631
632 {:ok, users}
633 end
634
635 def increase_note_count(%User{} = user) do
636 info_cng = User.Info.add_to_note_count(user.info, 1)
637
638 cng =
639 change(user)
640 |> put_embed(:info, info_cng)
641
642 update_and_set_cache(cng)
643 end
644
645 def decrease_note_count(%User{} = user) do
646 info_cng = User.Info.add_to_note_count(user.info, -1)
647
648 cng =
649 change(user)
650 |> put_embed(:info, info_cng)
651
652 update_and_set_cache(cng)
653 end
654
655 def update_note_count(%User{} = user) do
656 note_count_query =
657 from(
658 a in Object,
659 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
660 select: count(a.id)
661 )
662
663 note_count = Repo.one(note_count_query)
664
665 info_cng = User.Info.set_note_count(user.info, note_count)
666
667 cng =
668 change(user)
669 |> put_embed(:info, info_cng)
670
671 update_and_set_cache(cng)
672 end
673
674 def update_follower_count(%User{} = user) do
675 follower_count_query =
676 from(
677 u in User,
678 where: ^user.follower_address in u.following,
679 where: u.id != ^user.id,
680 select: count(u.id)
681 )
682
683 follower_count = Repo.one(follower_count_query)
684
685 info_cng =
686 user.info
687 |> User.Info.set_follower_count(follower_count)
688
689 cng =
690 change(user)
691 |> put_embed(:info, info_cng)
692
693 update_and_set_cache(cng)
694 end
695
696 def get_users_from_set_query(ap_ids, false) do
697 from(
698 u in User,
699 where: u.ap_id in ^ap_ids
700 )
701 end
702
703 def get_users_from_set_query(ap_ids, true) do
704 query = get_users_from_set_query(ap_ids, false)
705
706 from(
707 u in query,
708 where: u.local == true
709 )
710 end
711
712 def get_users_from_set(ap_ids, local_only \\ true) do
713 get_users_from_set_query(ap_ids, local_only)
714 |> Repo.all()
715 end
716
717 def get_recipients_from_activity(%Activity{recipients: to}) do
718 query =
719 from(
720 u in User,
721 where: u.ap_id in ^to,
722 or_where: fragment("? && ?", u.following, ^to)
723 )
724
725 query = from(u in query, where: u.local == true)
726
727 Repo.all(query)
728 end
729
730 def search(query, resolve \\ false, for_user \\ nil) do
731 # Strip the beginning @ off if there is a query
732 query = String.trim_leading(query, "@")
733
734 if resolve, do: User.get_or_fetch_by_nickname(query)
735
736 fts_results = do_search(fts_search_subquery(query), for_user)
737
738 {:ok, trigram_results} =
739 Repo.transaction(fn ->
740 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
741 do_search(trigram_search_subquery(query), for_user)
742 end)
743
744 Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
745 end
746
747 defp do_search(subquery, for_user, options \\ []) do
748 q =
749 from(
750 s in subquery(subquery),
751 order_by: [desc: s.search_rank],
752 limit: ^(options[:limit] || 20)
753 )
754
755 results =
756 q
757 |> Repo.all()
758 |> Enum.filter(&(&1.search_rank > 0))
759
760 boost_search_results(results, for_user)
761 end
762
763 defp fts_search_subquery(query) do
764 processed_query =
765 query
766 |> String.replace(~r/\W+/, " ")
767 |> String.trim()
768 |> String.split()
769 |> Enum.map(&(&1 <> ":*"))
770 |> Enum.join(" | ")
771
772 from(
773 u in User,
774 select_merge: %{
775 search_rank:
776 fragment(
777 """
778 ts_rank_cd(
779 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
780 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
781 to_tsquery('simple', ?),
782 32
783 )
784 """,
785 u.nickname,
786 u.name,
787 ^processed_query
788 )
789 },
790 where:
791 fragment(
792 """
793 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
794 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
795 """,
796 u.nickname,
797 u.name,
798 ^processed_query
799 )
800 )
801 end
802
803 defp trigram_search_subquery(query) do
804 from(
805 u in User,
806 select_merge: %{
807 search_rank:
808 fragment(
809 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
810 ^query,
811 u.nickname,
812 u.name
813 )
814 },
815 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^query)
816 )
817 end
818
819 defp boost_search_results(results, nil), do: results
820
821 defp boost_search_results(results, for_user) do
822 friends_ids = get_friends_ids(for_user)
823 followers_ids = get_followers_ids(for_user)
824
825 Enum.map(
826 results,
827 fn u ->
828 search_rank_coef =
829 cond do
830 u.id in friends_ids ->
831 1.2
832
833 u.id in followers_ids ->
834 1.1
835
836 true ->
837 1
838 end
839
840 Map.put(u, :search_rank, u.search_rank * search_rank_coef)
841 end
842 )
843 |> Enum.sort_by(&(-&1.search_rank))
844 end
845
846 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
847 Enum.map(
848 blocked_identifiers,
849 fn blocked_identifier ->
850 with %User{} = blocked <- get_or_fetch(blocked_identifier),
851 {:ok, blocker} <- block(blocker, blocked),
852 {:ok, _} <- ActivityPub.block(blocker, blocked) do
853 blocked
854 else
855 err ->
856 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
857 err
858 end
859 end
860 )
861 end
862
863 def block(blocker, %User{ap_id: ap_id} = blocked) do
864 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
865 blocker =
866 if following?(blocker, blocked) do
867 {:ok, blocker, _} = unfollow(blocker, blocked)
868 blocker
869 else
870 blocker
871 end
872
873 if following?(blocked, blocker) do
874 unfollow(blocked, blocker)
875 end
876
877 info_cng =
878 blocker.info
879 |> User.Info.add_to_block(ap_id)
880
881 cng =
882 change(blocker)
883 |> put_embed(:info, info_cng)
884
885 update_and_set_cache(cng)
886 end
887
888 # helper to handle the block given only an actor's AP id
889 def block(blocker, %{ap_id: ap_id}) do
890 block(blocker, User.get_by_ap_id(ap_id))
891 end
892
893 def unblock(blocker, %{ap_id: ap_id}) do
894 info_cng =
895 blocker.info
896 |> User.Info.remove_from_block(ap_id)
897
898 cng =
899 change(blocker)
900 |> put_embed(:info, info_cng)
901
902 update_and_set_cache(cng)
903 end
904
905 def blocks?(user, %{ap_id: ap_id}) do
906 blocks = user.info.blocks
907 domain_blocks = user.info.domain_blocks
908 %{host: host} = URI.parse(ap_id)
909
910 Enum.member?(blocks, ap_id) ||
911 Enum.any?(domain_blocks, fn domain ->
912 host == domain
913 end)
914 end
915
916 def blocked_users(user),
917 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
918
919 def block_domain(user, domain) do
920 info_cng =
921 user.info
922 |> User.Info.add_to_domain_block(domain)
923
924 cng =
925 change(user)
926 |> put_embed(:info, info_cng)
927
928 update_and_set_cache(cng)
929 end
930
931 def unblock_domain(user, domain) do
932 info_cng =
933 user.info
934 |> User.Info.remove_from_domain_block(domain)
935
936 cng =
937 change(user)
938 |> put_embed(:info, info_cng)
939
940 update_and_set_cache(cng)
941 end
942
943 def local_user_query do
944 from(
945 u in User,
946 where: u.local == true,
947 where: not is_nil(u.nickname)
948 )
949 end
950
951 def active_local_user_query do
952 from(
953 u in local_user_query(),
954 where: fragment("not (?->'deactivated' @> 'true')", u.info)
955 )
956 end
957
958 def moderator_user_query do
959 from(
960 u in User,
961 where: u.local == true,
962 where: fragment("?->'is_moderator' @> 'true'", u.info)
963 )
964 end
965
966 def deactivate(%User{} = user, status \\ true) do
967 info_cng = User.Info.set_activation_status(user.info, status)
968
969 cng =
970 change(user)
971 |> put_embed(:info, info_cng)
972
973 update_and_set_cache(cng)
974 end
975
976 def delete(%User{} = user) do
977 {:ok, user} = User.deactivate(user)
978
979 # Remove all relationships
980 {:ok, followers} = User.get_followers(user)
981
982 followers
983 |> Enum.each(fn follower -> User.unfollow(follower, user) end)
984
985 {:ok, friends} = User.get_friends(user)
986
987 friends
988 |> Enum.each(fn followed -> User.unfollow(user, followed) end)
989
990 query = from(a in Activity, where: a.actor == ^user.ap_id)
991
992 Repo.all(query)
993 |> Enum.each(fn activity ->
994 case activity.data["type"] do
995 "Create" ->
996 ActivityPub.delete(Object.normalize(activity.data["object"]))
997
998 # TODO: Do something with likes, follows, repeats.
999 _ ->
1000 "Doing nothing"
1001 end
1002 end)
1003
1004 {:ok, user}
1005 end
1006
1007 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1008 Pleroma.HTML.Scrubber.TwitterText
1009 end
1010
1011 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1012
1013 def html_filter_policy(_), do: @default_scrubbers
1014
1015 def get_or_fetch_by_ap_id(ap_id) do
1016 user = get_by_ap_id(ap_id)
1017
1018 if !is_nil(user) and !User.needs_update?(user) do
1019 user
1020 else
1021 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1022
1023 case ap_try do
1024 {:ok, user} ->
1025 user
1026
1027 _ ->
1028 case OStatus.make_user(ap_id) do
1029 {:ok, user} -> user
1030 _ -> {:error, "Could not fetch by AP id"}
1031 end
1032 end
1033 end
1034 end
1035
1036 def get_or_create_instance_user do
1037 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1038
1039 if user = get_by_ap_id(relay_uri) do
1040 user
1041 else
1042 changes =
1043 %User{info: %User.Info{}}
1044 |> cast(%{}, [:ap_id, :nickname, :local])
1045 |> put_change(:ap_id, relay_uri)
1046 |> put_change(:nickname, nil)
1047 |> put_change(:local, true)
1048 |> put_change(:follower_address, relay_uri <> "/followers")
1049
1050 {:ok, user} = Repo.insert(changes)
1051 user
1052 end
1053 end
1054
1055 # AP style
1056 def public_key_from_info(%{
1057 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1058 }) do
1059 key =
1060 public_key_pem
1061 |> :public_key.pem_decode()
1062 |> hd()
1063 |> :public_key.pem_entry_decode()
1064
1065 {:ok, key}
1066 end
1067
1068 # OStatus Magic Key
1069 def public_key_from_info(%{magic_key: magic_key}) do
1070 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1071 end
1072
1073 def get_public_key_for_ap_id(ap_id) do
1074 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1075 {:ok, public_key} <- public_key_from_info(user.info) do
1076 {:ok, public_key}
1077 else
1078 _ -> :error
1079 end
1080 end
1081
1082 defp blank?(""), do: nil
1083 defp blank?(n), do: n
1084
1085 def insert_or_update_user(data) do
1086 data =
1087 data
1088 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1089
1090 cs = User.remote_user_creation(data)
1091
1092 Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
1093 end
1094
1095 def ap_enabled?(%User{local: true}), do: true
1096 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1097 def ap_enabled?(_), do: false
1098
1099 @doc "Gets or fetch a user by uri or nickname."
1100 @spec get_or_fetch(String.t()) :: User.t()
1101 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1102 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1103
1104 # wait a period of time and return newest version of the User structs
1105 # this is because we have synchronous follow APIs and need to simulate them
1106 # with an async handshake
1107 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1108 with %User{} = a <- Repo.get(User, a.id),
1109 %User{} = b <- Repo.get(User, b.id) do
1110 {:ok, a, b}
1111 else
1112 _e ->
1113 :error
1114 end
1115 end
1116
1117 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1118 with :ok <- :timer.sleep(timeout),
1119 %User{} = a <- Repo.get(User, a.id),
1120 %User{} = b <- Repo.get(User, b.id) do
1121 {:ok, a, b}
1122 else
1123 _e ->
1124 :error
1125 end
1126 end
1127
1128 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1129 def parse_bio(nil, _user), do: ""
1130 def parse_bio(bio, _user) when bio == "", do: bio
1131
1132 def parse_bio(bio, user) do
1133 mentions = Formatter.parse_mentions(bio)
1134 tags = Formatter.parse_tags(bio)
1135
1136 emoji =
1137 (user.info.source_data["tag"] || [])
1138 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1139 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1140 {String.trim(name, ":"), url}
1141 end)
1142
1143 bio
1144 |> CommonUtils.format_input(mentions, tags, "text/plain", user_links: [format: :full])
1145 |> Formatter.emojify(emoji)
1146 end
1147
1148 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1149 Repo.transaction(fn ->
1150 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1151 end)
1152 end
1153
1154 def tag(nickname, tags) when is_binary(nickname),
1155 do: tag(User.get_by_nickname(nickname), tags)
1156
1157 def tag(%User{} = user, tags),
1158 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1159
1160 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1161 Repo.transaction(fn ->
1162 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1163 end)
1164 end
1165
1166 def untag(nickname, tags) when is_binary(nickname),
1167 do: untag(User.get_by_nickname(nickname), tags)
1168
1169 def untag(%User{} = user, tags),
1170 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1171
1172 defp update_tags(%User{} = user, new_tags) do
1173 {:ok, updated_user} =
1174 user
1175 |> change(%{tags: new_tags})
1176 |> Repo.update()
1177
1178 updated_user
1179 end
1180
1181 def bookmark(%User{} = user, status_id) do
1182 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1183 update_bookmarks(user, bookmarks)
1184 end
1185
1186 def unbookmark(%User{} = user, status_id) do
1187 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1188 update_bookmarks(user, bookmarks)
1189 end
1190
1191 def update_bookmarks(%User{} = user, bookmarks) do
1192 user
1193 |> change(%{bookmarks: bookmarks})
1194 |> update_and_set_cache
1195 end
1196
1197 defp normalize_tags(tags) do
1198 [tags]
1199 |> List.flatten()
1200 |> Enum.map(&String.downcase(&1))
1201 end
1202
1203 defp local_nickname_regex() do
1204 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1205 @extended_local_nickname_regex
1206 else
1207 @strict_local_nickname_regex
1208 end
1209 end
1210
1211 def local_nickname(nickname_or_mention) do
1212 nickname_or_mention
1213 |> full_nickname()
1214 |> String.split("@")
1215 |> hd()
1216 end
1217
1218 def full_nickname(nickname_or_mention),
1219 do: String.trim_leading(nickname_or_mention, "@")
1220
1221 def error_user(ap_id) do
1222 %User{
1223 name: ap_id,
1224 ap_id: ap_id,
1225 info: %User.Info{},
1226 nickname: "erroruser@example.com",
1227 inserted_at: NaiveDateTime.utc_now()
1228 }
1229 end
1230 end