import Ecto.{Changeset, Query}
alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
alias Comeonin.Pbkdf2
- alias Pleroma.Web.{OStatus, Websub}
+ alias Pleroma.Formatter
+ alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
+ alias Pleroma.Web.{OStatus, Websub, OAuth}
alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
schema "users" do
field(:ap_id, :string)
field(:avatar, :map)
field(:local, :boolean, default: true)
- field(:info, :map, default: %{})
field(:follower_address, :string)
field(:search_distance, :float, virtual: true)
+ field(:last_refreshed_at, :naive_datetime)
has_many(:notifications, Notification)
+ embeds_one(:info, Pleroma.User.Info)
timestamps()
end
end
def banner_url(user) do
- case user.info["banner"] do
+ case user.info.banner do
%{"url" => [%{"href" => href} | _]} -> href
_ -> "#{Web.base_url()}/images/banner.png"
end
end
+ def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
+ def profile_url(%User{ap_id: ap_id}), do: ap_id
+ def profile_url(_), do: nil
+
def ap_id(%User{nickname: nickname}) do
"#{Web.base_url()}/users/#{nickname}"
end
end
def info_changeset(struct, params \\ %{}) do
- struct
- |> cast(params, [:info])
- |> validate_required([:info])
+ raise "NOT VALID ANYMORE"
end
def user_info(%User{} = user) do
%{
following_count: length(user.following) - oneself,
- note_count: user.info["note_count"] || 0,
- follower_count: user.info["follower_count"] || 0,
- locked: user.info["locked"] || false
+ note_count: user.info.note_count,
+ follower_count: user.info.follower_count,
+ locked: user.info.locked,
+ default_scope: user.info.default_scope
}
end
@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])?)*$/
def remote_user_creation(params) do
+ params =
+ params
+ |> Map.put(:info, params[:info] || %{})
+
+ info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
+
changes =
%User{}
- |> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar])
+ |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
|> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
|> put_change(:local, false)
+ |> put_embed(:info, info_cng)
if changes.valid? do
- case changes.changes[:info]["source_data"] do
+ case info_cng.changes[:source_data] do
%{"followers" => followers} ->
changes
|> put_change(:follower_address, followers)
def update_changeset(struct, params \\ %{}) do
struct
- |> cast(params, [:bio, :name])
+ |> cast(params, [:bio, :name, :avatar])
|> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_length(:bio, max: 5000)
end
def upgrade_changeset(struct, params \\ %{}) do
+ params =
+ params
+ |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
+
+ info_cng =
+ struct.info
+ |> User.Info.user_upgrade(params[:info])
+
struct
- |> cast(params, [:bio, :name, :info, :follower_address, :avatar])
+ |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
|> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
+ |> put_embed(:info, info_cng)
end
def password_update_changeset(struct, params) do
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
+ OAuth.Token.delete_user_tokens(struct)
+ OAuth.Authorization.delete_user_authorizations(struct)
+
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: 1000)
|> validate_length(:name, min: 1, max: 100)
+ |> put_change(:info, %Pleroma.User.Info{})
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
end
end
- def maybe_direct_follow(%User{} = follower, %User{info: info} = followed) do
- user_config = Application.get_env(:pleroma, :user)
- deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
+ def needs_update?(%User{local: true}), do: false
- user_info = user_info(followed)
+ def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
- should_direct_follow =
- cond do
- # if the account is locked, don't pre-create the relationship
- user_info[:locked] == true ->
- false
+ def needs_update?(%User{local: false} = user) do
+ NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86400
+ end
- # if the users are blocking each other, we shouldn't even be here, but check for it anyway
- deny_follow_blocked and
- (User.blocks?(follower, followed) or User.blocks?(followed, follower)) ->
- false
+ def needs_update?(_), do: true
- # if OStatus, then there is no three-way handshake to follow
- User.ap_enabled?(followed) != true ->
- true
+ def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
+ {:ok, follower}
+ end
- # if there are no other reasons not to, just pre-create the relationship
- true ->
- true
- end
+ def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
+ follow(follower, followed)
+ end
- if should_direct_follow do
+ def maybe_direct_follow(%User{} = follower, %User{} = followed) do
+ if !User.ap_enabled?(followed) do
follow(follower, followed)
else
{:ok, follower}
ap_followers = followed.follower_address
cond do
- following?(follower, followed) or info["deactivated"] ->
+ following?(follower, followed) or info.deactivated ->
{:error, "Could not follow user: #{followed.nickname} is already on your list."}
deny_follow_blocked and blocks?(followed, follower) ->
end
def locked?(%User{} = user) do
- user.info["locked"] || false
+ user.info.locked || false
end
def get_by_ap_id(ap_id) do
def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}")
+ Cachex.del(:user_cache, "user_info:#{user.id}")
end
def get_cached_by_ap_id(ap_id) do
Enum.map(reqs, fn req -> req.actor end)
|> Enum.uniq()
|> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
+ |> Enum.filter(fn u -> !following?(u, user) end)
{:ok, users}
end
def increase_note_count(%User{} = user) do
- note_count = (user.info["note_count"] || 0) + 1
- new_info = Map.put(user.info, "note_count", note_count)
+ info_cng = User.Info.add_to_note_count(user.info, 1)
- cs = info_changeset(user, %{info: new_info})
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
- update_and_set_cache(cs)
+ update_and_set_cache(cng)
end
def decrease_note_count(%User{} = user) do
- note_count = user.info["note_count"] || 0
- note_count = if note_count <= 0, do: 0, else: note_count - 1
- new_info = Map.put(user.info, "note_count", note_count)
+ info_cng = User.Info.add_to_note_count(user.info, -1)
- cs = info_changeset(user, %{info: new_info})
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
- update_and_set_cache(cs)
+ update_and_set_cache(cng)
end
def update_note_count(%User{} = user) do
note_count = Repo.one(note_count_query)
- new_info = Map.put(user.info, "note_count", note_count)
+ info_cng = User.Info.set_note_count(user.info, note_count)
- cs = info_changeset(user, %{info: new_info})
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
- update_and_set_cache(cs)
+ update_and_set_cache(cng)
end
def update_follower_count(%User{} = user) do
follower_count = Repo.one(follower_count_query)
- new_info = Map.put(user.info, "follower_count", follower_count)
+ info_cng =
+ user.info
+ |> User.Info.set_follower_count(follower_count)
- cs = info_changeset(user, %{info: new_info})
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
- update_and_set_cache(cs)
+ update_and_set_cache(cng)
end
- def get_notified_from_activity(%Activity{recipients: to}) do
- query =
- from(
- u in User,
- where: u.ap_id in ^to,
- where: u.local == true
- )
+ def get_users_from_set_query(ap_ids, false) do
+ from(
+ u in User,
+ where: u.ap_id in ^ap_ids
+ )
+ end
- Repo.all(query)
+ def get_users_from_set_query(ap_ids, true) do
+ query = get_users_from_set_query(ap_ids, false)
+
+ from(
+ u in query,
+ where: u.local == true
+ )
+ end
+
+ def get_users_from_set(ap_ids, local_only \\ true) do
+ get_users_from_set_query(ap_ids, local_only)
+ |> Repo.all()
end
def get_recipients_from_activity(%Activity{recipients: to}) do
Repo.all(query)
end
- def search(query, resolve) do
+ def search(query, resolve \\ false) do
# strip the beginning @ off if there is a query
query = String.trim_leading(query, "@")
u.nickname,
u.name
)
- }
+ },
+ where: not is_nil(u.nickname)
)
q =
unfollow(blocked, blocker)
end
- blocks = blocker.info["blocks"] || []
- new_blocks = Enum.uniq([ap_id | blocks])
- new_info = Map.put(blocker.info, "blocks", new_blocks)
+ info_cng =
+ blocker.info
+ |> User.Info.add_to_block(ap_id)
- cs = User.info_changeset(blocker, %{info: new_info})
- update_and_set_cache(cs)
+ cng =
+ change(blocker)
+ |> put_embed(:info, info_cng)
+
+ update_and_set_cache(cng)
end
# helper to handle the block given only an actor's AP id
block(blocker, User.get_by_ap_id(ap_id))
end
- def unblock(user, %{ap_id: ap_id}) do
- blocks = user.info["blocks"] || []
- new_blocks = List.delete(blocks, ap_id)
- new_info = Map.put(user.info, "blocks", new_blocks)
+ def unblock(blocker, %{ap_id: ap_id}) do
+ info_cng =
+ blocker.info
+ |> User.Info.remove_from_block(ap_id)
+
+ cng =
+ change(blocker)
+ |> put_embed(:info, info_cng)
- cs = User.info_changeset(user, %{info: new_info})
- update_and_set_cache(cs)
+ update_and_set_cache(cng)
end
def blocks?(user, %{ap_id: ap_id}) do
- blocks = user.info["blocks"] || []
- domain_blocks = user.info["domain_blocks"] || []
+ blocks = user.info.blocks
+ domain_blocks = user.info.domain_blocks
%{host: host} = URI.parse(ap_id)
Enum.member?(blocks, ap_id) ||
end
def block_domain(user, domain) do
- domain_blocks = user.info["domain_blocks"] || []
- new_blocks = Enum.uniq([domain | domain_blocks])
- new_info = Map.put(user.info, "domain_blocks", new_blocks)
+ info_cng =
+ user.info
+ |> User.Info.add_to_domain_block(domain)
- cs = User.info_changeset(user, %{info: new_info})
- update_and_set_cache(cs)
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
+
+ update_and_set_cache(cng)
end
def unblock_domain(user, domain) do
- blocks = user.info["domain_blocks"] || []
- new_blocks = List.delete(blocks, domain)
- new_info = Map.put(user.info, "domain_blocks", new_blocks)
+ info_cng =
+ user.info
+ |> User.Info.remove_from_domain_block(domain)
+
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
- cs = User.info_changeset(user, %{info: new_info})
- update_and_set_cache(cs)
+ update_and_set_cache(cng)
end
def local_user_query() do
- from(u in User, where: u.local == true)
+ from(
+ u in User,
+ where: u.local == true,
+ where: not is_nil(u.nickname)
+ )
+ end
+
+ def moderator_user_query() do
+ from(
+ u in User,
+ where: u.local == true,
+ where: fragment("?->'is_moderator' @> 'true'", u.info)
+ )
end
- def deactivate(%User{} = user) do
- new_info = Map.put(user.info, "deactivated", true)
- cs = User.info_changeset(user, %{info: new_info})
- update_and_set_cache(cs)
+ def deactivate(%User{} = user, status \\ true) do
+ info_cng = User.Info.set_activation_status(user.info, status)
+
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
+
+ update_and_set_cache(cng)
end
def delete(%User{} = user) do
end
end)
- :ok
+ {:ok, user}
+ end
+
+ def html_filter_policy(%User{info: %{no_rich_text: true}}) do
+ Pleroma.HTML.Scrubber.TwitterText
end
+ def html_filter_policy(_), do: nil
+
def get_or_fetch_by_ap_id(ap_id) do
- if user = get_by_ap_id(ap_id) do
+ user = get_by_ap_id(ap_id)
+
+ if !is_nil(user) and !User.needs_update?(user) do
user
else
ap_try = ActivityPub.make_user_from_ap_id(ap_id)
end
def get_or_create_instance_user do
- if user = get_by_ap_id(Pleroma.Web.Endpoint.url()) do
+ relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
+
+ if user = get_by_ap_id(relay_uri) do
user
else
changes =
- %User{}
+ %User{info: %User.Info{}}
|> cast(%{}, [:ap_id, :nickname, :local])
- |> put_change(:ap_id, Pleroma.Web.Endpoint.url())
+ |> put_change(:ap_id, relay_uri)
|> put_change(:nickname, nil)
|> put_change(:local, true)
+ |> put_change(:follower_address, relay_uri <> "/followers")
{:ok, user} = Repo.insert(changes)
user
# AP style
def public_key_from_info(%{
- "source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
+ source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
}) do
key =
:public_key.pem_decode(public_key_pem)
end
# OStatus Magic Key
- def public_key_from_info(%{"magic_key" => magic_key}) do
+ def public_key_from_info(%{magic_key: magic_key}) do
{:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
end
|> Map.put(:name, blank?(data[:name]) || data[:nickname])
cs = User.remote_user_creation(data)
+
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
end
- def ap_enabled?(%User{info: info}), do: info["ap_enabled"]
+ def ap_enabled?(%User{local: true}), do: true
+ def ap_enabled?(%User{info: info}), do: info.ap_enabled
def ap_enabled?(_), do: false
def get_or_fetch(uri_or_nickname) do
get_or_fetch_by_nickname(uri_or_nickname)
end
end
+
+ # wait a period of time and return newest version of the User structs
+ # this is because we have synchronous follow APIs and need to simulate them
+ # with an async handshake
+ def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
+ with %User{} = a <- Repo.get(User, a.id),
+ %User{} = b <- Repo.get(User, b.id) do
+ {:ok, a, b}
+ else
+ _e ->
+ :error
+ end
+ end
+
+ def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
+ with :ok <- :timer.sleep(timeout),
+ %User{} = a <- Repo.get(User, a.id),
+ %User{} = b <- Repo.get(User, b.id) do
+ {:ok, a, b}
+ else
+ _e ->
+ :error
+ end
+ end
+
+ def parse_bio(bio, user \\ %User{info: %{source_data: %{}}}) do
+ mentions = Formatter.parse_mentions(bio)
+ tags = Formatter.parse_tags(bio)
+
+ emoji =
+ (user.info.source_data["tag"] || [])
+ |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
+ |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
+ {String.trim(name, ":"), url}
+ end)
+
+ CommonUtils.format_input(bio, mentions, tags, "text/plain") |> Formatter.emojify(emoji)
+ end
end