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