allowed_post_formats: [
"text/plain",
"text/html",
- "text/markdown"
+ "text/markdown",
+ "text/bbcode"
],
mrf_transparency: true,
autofollowed_nicknames: [],
follow_redirect: true,
pool: :media
]
- ]
+ ],
+ whitelist: []
config :pleroma, :chat, enabled: true
mailer: 10,
transmogrifier: 20,
scheduled_activities: 10,
- background: 5
++ background: 5,
+ user: 10
config :pleroma, :fetch_initial_posts,
enabled: false,
base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
uid: System.get_env("LDAP_UID") || "cn"
+ config :esshd,
+ enabled: false
+
oauth_consumer_strategies = String.split(System.get_env("OAUTH_CONSUMER_STRATEGIES") || "")
ueberauth_providers =
total_user_limit: 300,
enabled: true
+ config :pleroma, :oauth2,
+ token_expires_in: 600,
+ issue_new_refresh_token: true
+
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
import Ecto.Query
@type t :: %__MODULE__{}
+ @type actor :: String.t()
+
@primary_key {:id, Pleroma.FlakeId, autogenerate: true}
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
end
def get_by_id(id) do
- Repo.get(Activity, id)
+ Activity
+ |> where([a], a.id == ^id)
+ |> restrict_deactivated_users()
+ |> Repo.one()
end
def get_by_id_with_object(id) do
def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
create_by_object_ap_id(ap_id)
+ |> restrict_deactivated_users()
|> Repo.one()
end
|> Repo.all()
end
+ @spec query_by_actor(actor()) :: Ecto.Query.t()
+ def query_by_actor(actor) do
+ from(a in Activity, where: a.actor == ^actor)
+ end
++
+ def restrict_deactivated_users(query) do
+ from(activity in query,
+ where:
+ fragment(
+ "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
+ activity.actor
+ )
+ )
+ end
end
alias Comeonin.Pbkdf2
alias Pleroma.Activity
- alias Pleroma.Formatter
+ alias Pleroma.Bookmark
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: [])
- field(:bookmarks, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime_usec)
+ has_many(:bookmarks, Bookmark)
has_many(:notifications, Notification)
has_many(:registrations, Registration)
embeds_one(:info, Pleroma.User.Info)
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
def user_info(%User{} = user) do
- oneself = if user.local, do: 1, else: 0
-
%{
- following_count: length(user.following) - oneself,
+ following_count: following_count(user),
note_count: user.info.note_count,
follower_count: user.info.follower_count,
locked: user.info.locked,
}
end
+ defp restrict_deactivated(query) do
+ from(u in query,
+ where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
+ )
+ end
+
+ def following_count(%User{following: []}), do: 0
+
+ def following_count(%User{following: following, id: id}) do
+ from(u in User,
+ where: u.follower_address in ^following,
+ where: u.id != ^id
+ )
+ |> restrict_deactivated()
+ |> Repo.aggregate(:count, :id)
+ end
+
def remote_user_creation(params) do
params =
params
Enum.map(
followed_identifiers,
fn followed_identifier ->
- with %User{} = followed <- get_or_fetch(followed_identifier),
+ with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed),
{:ok, _} <- ActivityPub.follow(follower, followed) do
followed
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
- Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
+
+ Cachex.fetch!(:user_cache, key, fn ->
+ user_result = get_or_fetch_by_nickname(nickname)
+
+ case user_result do
+ {:ok, user} -> {:commit, user}
+ {:error, _error} -> {:ignore, nil}
+ end
+ end)
end
def get_cached_by_nickname_or_id(nickname_or_id) do
def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do
- user
+ {:ok, user}
else
_e ->
with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end
- user
+ {:ok, user}
else
- _e -> nil
+ _e -> {:error, "not found " <> nickname}
end
end
end
where: fragment("? <@ ?", ^[follower_address], u.following),
where: u.id != ^id
)
+ |> restrict_deactivated()
end
def get_followers_query(user, page) do
where: u.follower_address in ^following,
where: u.id != ^id
)
+ |> restrict_deactivated()
end
def get_friends_query(user, page) do
info_cng = User.Info.set_note_count(user.info, note_count)
- cng =
- change(user)
- |> put_embed(:info, info_cng)
-
- update_and_set_cache(cng)
+ user
+ |> change()
+ |> put_embed(:info, info_cng)
+ |> update_and_set_cache()
end
def update_follower_count(%User{} = user) do
|> where([u], ^user.follower_address in u.following)
|> where([u], u.id != ^user.id)
|> select([u], %{count: count(u.id)})
+ |> restrict_deactivated()
User
|> where(id: ^user.id)
^processed_query
)
)
+ |> restrict_deactivated()
end
defp trigram_search_subquery(term) do
},
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
)
+ |> restrict_deactivated()
end
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
Enum.map(
blocked_identifiers,
fn blocked_identifier ->
- with %User{} = blocked <- get_or_fetch(blocked_identifier),
+ with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
{:ok, blocker} <- block(blocker, blocked),
{:ok, _} <- ActivityPub.block(blocker, blocked) do
blocked
)
end
+ def deactivate_async(user, status \\ true) do
+ PleromaJobQueue.enqueue(:user, __MODULE__, [:deactivate_async, user, status])
+ end
+
+ def perform(:deactivate_async, user, status), do: deactivate(user, status)
+
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)
+ with {:ok, friends} <- User.get_friends(user),
+ {:ok, followers} <- User.get_followers(user),
+ {:ok, user} <-
+ user
+ |> change()
+ |> put_embed(:info, info_cng)
+ |> update_and_set_cache() do
+ Enum.each(followers, &invalidate_cache(&1))
+ Enum.each(friends, &update_follower_count(&1))
- update_and_set_cache(cng)
+ {:ok, user}
+ end
end
def update_notification_settings(%User{} = user, settings \\ %{}) do
|> update_and_set_cache()
end
- def delete(%User{} = user) do
+ @spec delete(User.t()) :: :ok
+ def delete(%User{} = user),
+ do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
+
+ @spec perform(atom(), User.t()) :: {:ok, User.t()}
+ def perform(:delete, %User{} = user) do
{:ok, user} = User.deactivate(user)
# Remove all relationships
end
def delete_user_activities(%User{ap_id: ap_id} = user) do
- Activity
- |> where(actor: ^ap_id)
- |> Activity.with_preloaded_object()
- |> Repo.all()
- |> Enum.each(fn
- %{data: %{"type" => "Create"}} = activity ->
- activity |> Object.normalize() |> ActivityPub.delete()
+ stream =
+ ap_id
+ |> Activity.query_by_actor()
+ |> Activity.with_preloaded_object()
+ |> Repo.stream()
- # TODO: Do something with likes, follows, repeats.
- _ ->
- "Doing nothing"
- end)
+ Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
{:ok, user}
end
+ defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
+ Object.normalize(activity) |> ActivityPub.delete()
+ end
+
+ defp delete_activity(_activity), do: "Doing nothing"
+
def html_filter_policy(%User{info: %{no_rich_text: true}}) do
Pleroma.HTML.Scrubber.TwitterText
end
case ap_try do
{:ok, user} ->
- user
+ {:ok, user}
_ ->
case OStatus.make_user(ap_id) do
- {:ok, user} -> user
+ {:ok, user} -> {:ok, user}
_ -> {:error, "Could not fetch by AP id"}
end
end
user = get_cached_by_ap_id(ap_id)
if !is_nil(user) and !User.needs_update?(user) do
- user
+ {:ok, user}
else
# Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
- user = fetch_by_ap_id(ap_id)
+ resp = fetch_by_ap_id(ap_id)
if should_fetch_initial do
- with %User{} = user do
+ with {:ok, %User{} = user} = resp do
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end
end
- user
+ resp
end
end
end
def get_public_key_for_ap_id(ap_id) do
- with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
+ with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key_from_info(user.info) do
{:ok, public_key}
else
end
end
- def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
- def parse_bio(nil, _user), do: ""
- def parse_bio(bio, _user) when bio == "", do: bio
+ def parse_bio(bio) when is_binary(bio) and bio != "" do
+ bio
+ |> CommonUtils.format_input("text/plain", mentions_format: :full)
+ |> elem(0)
+ end
- def parse_bio(bio, user) do
- 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)
+ def parse_bio(_), do: ""
+ def parse_bio(bio, user) when is_binary(bio) and bio != "" do
# TODO: get profile URLs other than user.ap_id
profile_urls = [user.ap_id]
rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
)
|> elem(0)
- |> Formatter.emojify(emoji)
end
+ def parse_bio(_, _), do: ""
+
def tag(user_identifiers, tags) when is_list(user_identifiers) do
Repo.transaction(fn ->
for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
updated_user
end
- def bookmark(%User{} = user, status_id) do
- bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
- update_bookmarks(user, bookmarks)
- end
-
- def unbookmark(%User{} = user, status_id) do
- bookmarks = Enum.uniq(user.bookmarks -- [status_id])
- update_bookmarks(user, bookmarks)
- end
-
- def update_bookmarks(%User{} = user, bookmarks) do
- user
- |> change(%{bookmarks: bookmarks})
- |> update_and_set_cache
- end
-
defp normalize_tags(tags) do
[tags]
|> List.flatten()
public = "https://www.w3.org/ns/activitystreams#Public"
if activity.data["type"] in ["Create", "Announce", "Delete"] do
- object = Object.normalize(activity)
Pleroma.Web.Streamer.stream("user", activity)
Pleroma.Web.Streamer.stream("list", activity)
end
if activity.data["type"] in ["Create"] do
+ object = Object.normalize(activity)
+
object.data
|> Map.get("tag", [])
|> Enum.filter(fn tag -> is_bitstring(tag) end)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(opts)
+ |> Activity.restrict_deactivated_users()
end
def fetch_activities(recipients, opts \\ %{}) do
def delete_account(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
{:ok, user} ->
- Task.start(fn -> User.delete(user) end)
+ User.delete(user)
json(conn, %{status: "success"})
{:error, msg} ->
end
end
+ def disable_account(%{assigns: %{user: user}} = conn, params) do
+ case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
+ {:ok, user} ->
+ User.deactivate_async(user)
+ json(conn, %{status: "success"})
+
+ {:error, msg} ->
+ json(conn, %{error: msg})
+ end
+ end
+
def captcha(conn, _params) do
json(conn, Pleroma.Captcha.new())
end
def get_user(user \\ nil, params) do
case params do
%{"user_id" => user_id} ->
- case target = User.get_cached_by_nickname_or_id(user_id) do
+ case User.get_cached_by_nickname_or_id(user_id) do
nil ->
{:error, "No user with such user_id"}
- _ ->
- {:ok, target}
+ %User{info: %{deactivated: true}} ->
+ {:error, "User has been disabled"}
+
+ user ->
+ {:ok, user}
end
%{"screen_name" => nickname} ->
end
def get_external_profile(for_user, uri) do
- with %User{} = user <- User.get_or_fetch(uri) do
+ with {:ok, %User{} = user} <- User.get_or_fetch(uri) do
{:ok, UserView.render("show.json", %{user: user, for: for_user})}
else
_e ->
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
use Pleroma.DataCase
test "fetches correct profile for nickname beginning with number" do
# Use old-style integer ID to try to reproduce the problem
user = insert(:user, %{id: 1080})
- userwithnumbers = insert(:user, %{nickname: "#{user.id}garbage"})
- assert userwithnumbers == User.get_cached_by_nickname_or_id(userwithnumbers.nickname)
+ user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"})
+ assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname)
end
describe "user registration" do
describe "get_or_fetch/1" do
test "gets an existing user by nickname" do
user = insert(:user)
- fetched_user = User.get_or_fetch(user.nickname)
+ {:ok, fetched_user} = User.get_or_fetch(user.nickname)
assert user == fetched_user
end
info: %{}
)
- fetched_user = User.get_or_fetch(ap_id)
+ {:ok, fetched_user} = User.get_or_fetch(ap_id)
freshed_user = refresh_record(user)
assert freshed_user == fetched_user
end
describe "fetching a user from nickname or trying to build one" do
test "gets an existing user" do
user = insert(:user)
- fetched_user = User.get_or_fetch_by_nickname(user.nickname)
+ {:ok, fetched_user} = User.get_or_fetch_by_nickname(user.nickname)
assert user == fetched_user
end
test "gets an existing user, case insensitive" do
user = insert(:user, nickname: "nick")
- fetched_user = User.get_or_fetch_by_nickname("NICK")
+ {:ok, fetched_user} = User.get_or_fetch_by_nickname("NICK")
assert user == fetched_user
end
test "gets an existing user by fully qualified nickname" do
user = insert(:user)
- fetched_user =
+ {:ok, fetched_user} =
User.get_or_fetch_by_nickname(user.nickname <> "@" <> Pleroma.Web.Endpoint.host())
assert user == fetched_user
user = insert(:user, nickname: "nick")
casing_altered_fqn = String.upcase(user.nickname <> "@" <> Pleroma.Web.Endpoint.host())
- fetched_user = User.get_or_fetch_by_nickname(casing_altered_fqn)
+ {:ok, fetched_user} = User.get_or_fetch_by_nickname(casing_altered_fqn)
assert user == fetched_user
end
test "fetches an external user via ostatus if no user exists" do
- fetched_user = User.get_or_fetch_by_nickname("shp@social.heldscal.la")
+ {:ok, fetched_user} = User.get_or_fetch_by_nickname("shp@social.heldscal.la")
assert fetched_user.nickname == "shp@social.heldscal.la"
end
test "returns nil if no user could be fetched" do
- fetched_user = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
- assert fetched_user == nil
+ {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
+ assert fetched_user == "not found nonexistant@social.heldscal.la"
end
test "returns nil for nonexistant local user" do
- fetched_user = User.get_or_fetch_by_nickname("nonexistant")
- assert fetched_user == nil
+ {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant")
+ assert fetched_user == "not found nonexistant"
end
test "updates an existing user, if stale" do
assert orig_user.last_refreshed_at == a_week_ago
- user = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
+ {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
assert user.info.source_data["endpoints"]
refute user.last_refreshed_at == orig_user.last_refreshed_at
assert addressed in recipients
end
- test ".deactivate can de-activate then re-activate a user" do
- user = insert(:user)
- assert false == user.info.deactivated
- {:ok, user} = User.deactivate(user)
- assert true == user.info.deactivated
- {:ok, user} = User.deactivate(user, false)
- assert false == user.info.deactivated
+ describe ".deactivate" do
+ test "can de-activate then re-activate a user" do
+ user = insert(:user)
+ assert false == user.info.deactivated
+ {:ok, user} = User.deactivate(user)
+ assert true == user.info.deactivated
+ {:ok, user} = User.deactivate(user, false)
+ assert false == user.info.deactivated
+ end
+
+ test "hide a user from followers " do
+ user = insert(:user)
+ user2 = insert(:user)
+
+ {:ok, user} = User.follow(user, user2)
+ {:ok, _user} = User.deactivate(user)
+
+ info = User.get_cached_user_info(user2)
+
+ assert info.follower_count == 0
+ assert {:ok, []} = User.get_followers(user2)
+ end
+
+ test "hide a user from friends" do
+ user = insert(:user)
+ user2 = insert(:user)
+
+ {:ok, user2} = User.follow(user2, user)
+ assert User.following_count(user2) == 1
+
+ {:ok, _user} = User.deactivate(user)
+
+ info = User.get_cached_user_info(user2)
+
+ assert info.following_count == 0
+ assert User.following_count(user2) == 0
+ assert {:ok, []} = User.get_friends(user2)
+ end
+
+ test "hide a user's statuses from timelines and notifications" do
+ user = insert(:user)
+ user2 = insert(:user)
+
+ {:ok, user2} = User.follow(user2, user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}"})
+
+ [notification] = Pleroma.Notification.for_user(user2)
+ assert notification.activity.id == activity.id
+
+ assert [activity] == ActivityPub.fetch_public_activities(%{})
+
+ assert [activity] ==
+ ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
+ |> ActivityPub.contain_timeline(user2)
+
+ {:ok, _user} = User.deactivate(user)
+
+ assert [] == ActivityPub.fetch_public_activities(%{})
+ assert [] == Pleroma.Notification.for_user(user2)
+
+ assert [] ==
+ ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
+ |> ActivityPub.contain_timeline(user2)
+ end
end
test ".delete_user_activities deletes all create activities" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"})
- {:ok, _} = User.delete_user_activities(user)
- # TODO: Remove favorites, repeats, delete activities.
- refute Activity.get_by_id(activity.id)
+ Ecto.Adapters.SQL.Sandbox.unboxed_run(Repo, fn ->
+ {:ok, _} = User.delete_user_activities(user)
+ # TODO: Remove favorites, repeats, delete activities.
+ refute Activity.get_by_id(activity.id)
+ end)
end
test ".delete deactivates a user, all follow relationships and all create activities" do
expected_text =
"A.k.a. <span class='h-card'><a data-user='#{remote_user.id}' class='u-url mention' href='#{
remote_user.ap_id
- }'>" <> "@<span>nick@domain.com</span></a></span>"
+ }'>@<span>nick@domain.com</span></a></span>"
assert expected_text == User.parse_bio(bio, user)
end
end
end
- test "bookmarks" do
- user = insert(:user)
-
- {:ok, activity1} =
- CommonAPI.post(user, %{
- "status" => "heweoo!"
- })
-
- id1 = Object.normalize(activity1).data["id"]
-
- {:ok, activity2} =
- CommonAPI.post(user, %{
- "status" => "heweoo!"
- })
-
- id2 = Object.normalize(activity2).data["id"]
-
- assert {:ok, user_state1} = User.bookmark(user, id1)
- assert user_state1.bookmarks == [id1]
-
- assert {:ok, user_state2} = User.unbookmark(user, id1)
- assert user_state2.bookmarks == []
-
- assert {:ok, user_state3} = User.bookmark(user, id2)
- assert user_state3.bookmarks == [id2]
- end
-
test "follower count is updated when a follower is blocked" do
user = insert(:user)
follower = insert(:user)