Merge branch 'develop' into tests/mastodon_api_controller.ex

This commit is contained in:
Maksim Pechnikov 2019-09-24 08:06:04 +03:00
commit d75d0ae134
102 changed files with 781 additions and 309 deletions

View File

@ -6,12 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added ### Added
- Refreshing poll results for remote polls - Refreshing poll results for remote polls
- Admin API: Add ability to require password reset
### Changed ### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7) - **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings) - Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler - Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
- Admin API: Return `total` when querying for reports - Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
### Fixed
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
## [1.1.0] - 2019-??-?? ## [1.1.0] - 2019-??-??
### Security ### Security
@ -39,6 +43,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses) - AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
- Improve digest email template - Improve digest email template
Pagination: (optional) return `total` alongside with `items` when paginating Pagination: (optional) return `total` alongside with `items` when paginating
- Add `rel="ugc"` to all links in statuses, to prevent SEO spam
### Fixed ### Fixed
- Following from Osada - Following from Osada

View File

@ -109,6 +109,7 @@
config :pleroma, Pleroma.Uploaders.S3, config :pleroma, Pleroma.Uploaders.S3,
bucket: nil, bucket: nil,
streaming_enabled: true,
public_endpoint: "https://s3.amazonaws.com" public_endpoint: "https://s3.amazonaws.com"
config :pleroma, Pleroma.Uploaders.MDII, config :pleroma, Pleroma.Uploaders.MDII,
@ -508,7 +509,7 @@
class: false, class: false,
strip_prefix: false, strip_prefix: false,
new_window: false, new_window: false,
rel: false rel: "ugc"
] ]
config :pleroma, :ldap, config :pleroma, :ldap,

View File

@ -110,6 +110,12 @@
description: description:
"If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc." <> "If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc." <>
" For example, when using CDN to S3 virtual host format, set \"\". At this time, write CNAME to CDN in public_endpoint." " For example, when using CDN to S3 virtual host format, set \"\". At this time, write CNAME to CDN in public_endpoint."
},
%{
key: :streaming_enabled,
type: :boolean,
description:
"Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems."
} }
] ]
}, },
@ -1900,7 +1906,7 @@
key: :rel, key: :rel,
type: [:string, false], type: [:string, false],
description: "override the rel attribute. false to clear", description: "override the rel attribute. false to clear",
suggestions: ["noopener noreferrer", false] suggestions: ["ugc", "noopener noreferrer", false]
}, },
%{ %{
key: :new_window, key: :new_window,

View File

@ -310,6 +310,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Params: none - Params: none
- Response: password reset token (base64 string) - Response: password reset token (base64 string)
## `/api/pleroma/admin/users/:nickname/force_password_reset`
### Force passord reset for a user with a given nickname
- Methods: `PATCH`
- Params: none
- Response: none (code `204`)
## `/api/pleroma/admin/reports` ## `/api/pleroma/admin/reports`
### Get a list of reports ### Get a list of reports
- Method `GET` - Method `GET`

View File

@ -23,6 +23,7 @@ Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
* `truncated_namespace`: If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or "" etc. * `truncated_namespace`: If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or "" etc.
For example, when using CDN to S3 virtual host format, set "". For example, when using CDN to S3 virtual host format, set "".
At this time, write CNAME to CDN in public_endpoint. At this time, write CNAME to CDN in public_endpoint.
* `streaming_enabled`: Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems.
## Pleroma.Upload.Filter.Mogrify ## Pleroma.Upload.Filter.Mogrify
@ -521,7 +522,7 @@ config :auto_linker,
class: false, class: false,
strip_prefix: false, strip_prefix: false,
new_window: false, new_window: false,
rel: false rel: "ugc"
] ]
``` ```

View File

@ -36,9 +36,9 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do
nickname_text = get_nickname_text(nickname, opts) nickname_text = get_nickname_text(nickname, opts)
link = link =
"<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{ap_id}'>@<span>#{ ~s(<span class="h-card"><a data-user="#{id}" class="u-url mention" href="#{ap_id}" rel="ugc">@<span>#{
nickname_text nickname_text
}</span></a></span>" }</span></a></span>)
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}} {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
@ -50,7 +50,7 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag) tag = String.downcase(tag)
url = "#{Pleroma.Web.base_url()}/tag/#{tag}" url = "#{Pleroma.Web.base_url()}/tag/#{tag}"
link = "<a class='hashtag' data-tag='#{tag}' href='#{url}' rel='tag'>#{tag_text}</a>" link = ~s(<a class="hashtag" data-tag="#{tag}" href="#{url}" rel="tag ugc">#{tag_text}</a>)
{link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}} {link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
end end

View File

@ -184,7 +184,8 @@ defmodule Pleroma.HTML.Scrubber.Default do
"tag", "tag",
"nofollow", "nofollow",
"noopener", "noopener",
"noreferrer" "noreferrer",
"ugc"
]) ])
Meta.allow_tag_with_these_attributes("a", ["name", "title"]) Meta.allow_tag_with_these_attributes("a", ["name", "title"])
@ -304,7 +305,8 @@ defmodule Pleroma.HTML.Scrubber.LinksOnly do
"nofollow", "nofollow",
"noopener", "noopener",
"noreferrer", "noreferrer",
"me" "me",
"ugc"
]) ])
Meta.allow_tag_with_these_attributes("a", ["name", "title"]) Meta.allow_tag_with_these_attributes("a", ["name", "title"])

View File

@ -31,6 +31,7 @@ defp maybe_reinject_internal_fields(data, %{data: %{} = old_data}) do
defp maybe_reinject_internal_fields(data, _), do: data defp maybe_reinject_internal_fields(data, _), do: data
@spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
defp reinject_object(struct, data) do defp reinject_object(struct, data) do
Logger.debug("Reinjecting object #{data["id"]}") Logger.debug("Reinjecting object #{data["id"]}")
@ -61,52 +62,54 @@ def refetch_object(%Object{data: %{"id" => id}} = object) do
# TODO: # TODO:
# This will create a Create activity, which we need internally at the moment. # This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id, options \\ []) do def fetch_object_from_id(id, options \\ []) do
if object = Object.get_cached_by_ap_id(id) do with {:fetch_object, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
{:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
{:normalize, nil} <- {:normalize, Object.normalize(data, false)},
params <- prepare_activity_params(data),
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
{:ok, activity} <- Transmogrifier.handle_incoming(params, options),
{:object, _data, %Object{} = object} <-
{:object, data, Object.normalize(activity, false)} do
{:ok, object} {:ok, object}
else else
Logger.info("Fetching #{id} via AP") {:containment, _} ->
{:error, "Object containment failed."}
with {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)}, {:error, {:reject, nil}} ->
{:normalize, nil} <- {:normalize, Object.normalize(data, false)}, {:reject, nil}
params <- %{
"type" => "Create", {:object, data, nil} ->
"to" => data["to"], reinject_object(%Object{}, data)
"cc" => data["cc"],
# Should we seriously keep this attributedTo thing? {:normalize, object = %Object{}} ->
"actor" => data["actor"] || data["attributedTo"],
"object" => data
},
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
{:ok, activity} <- Transmogrifier.handle_incoming(params, options),
{:object, _data, %Object{} = object} <-
{:object, data, Object.normalize(activity, false)} do
{:ok, object} {:ok, object}
else
{:containment, _} ->
{:error, "Object containment failed."}
{:error, {:reject, nil}} -> {:fetch_object, %Object{} = object} ->
{:reject, nil} {:ok, object}
{:object, data, nil} -> _e ->
reinject_object(%Object{}, data) # Only fallback when receiving a fetch/normalization error with ActivityPub
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
{:normalize, object = %Object{}} -> # FIXME: OStatus Object Containment?
{:ok, object} case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
_e -> e -> e
# Only fallback when receiving a fetch/normalization error with ActivityPub end
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
# FIXME: OStatus Object Containment?
case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
e -> e
end
end
end end
end end
defp prepare_activity_params(data) do
%{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
# Should we seriously keep this attributedTo thing?
"actor" => data["actor"] || data["attributedTo"],
"object" => data
}
end
def fetch_object_from_id!(id, options \\ []) do def fetch_object_from_id!(id, options \\ []) do
with {:ok, object} <- fetch_object_from_id(id, options) do with {:ok, object} <- fetch_object_from_id(id, options) do
object object

View File

@ -38,16 +38,26 @@ def get_file(file) do
def put_file(%Pleroma.Upload{} = upload) do def put_file(%Pleroma.Upload{} = upload) do
config = Config.get([__MODULE__]) config = Config.get([__MODULE__])
bucket = Keyword.get(config, :bucket) bucket = Keyword.get(config, :bucket)
streaming = Keyword.get(config, :streaming_enabled)
s3_name = strict_encode(upload.path) s3_name = strict_encode(upload.path)
op = op =
upload.tempfile if streaming do
|> ExAws.S3.Upload.stream_file() upload.tempfile
|> ExAws.S3.upload(bucket, s3_name, [ |> ExAws.S3.Upload.stream_file()
{:acl, :public_read}, |> ExAws.S3.upload(bucket, s3_name, [
{:content_type, upload.content_type} {:acl, :public_read},
]) {:content_type, upload.content_type}
])
else
{:ok, file_data} = File.read(upload.tempfile)
ExAws.S3.put_object(bucket, s3_name, file_data, [
{:acl, :public_read},
{:content_type, upload.content_type}
])
end
case ExAws.request(op) do case ExAws.request(op) do
{:ok, _} -> {:ok, _} ->

View File

@ -269,6 +269,7 @@ def password_update_changeset(struct, params) do
|> validate_required([:password, :password_confirmation]) |> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password) |> validate_confirmation(:password)
|> put_password_hash |> put_password_hash
|> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
end end
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
@ -285,6 +286,20 @@ def reset_password(%User{id: user_id} = user, data) do
end end
end end
def force_password_reset_async(user) do
BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
end
@spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def force_password_reset(user) do
info_cng = User.Info.set_password_reset_pending(user.info, true)
user
|> change()
|> put_embed(:info, info_cng)
|> update_and_set_cache()
end
def register_changeset(struct, params \\ %{}, opts \\ []) do def register_changeset(struct, params \\ %{}, opts \\ []) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
@ -1131,6 +1146,8 @@ def delete(%User{} = user) do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id}) BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end end
def perform(:force_password_reset, user), do: force_password_reset(user)
@spec perform(atom(), User.t()) :: {:ok, User.t()} @spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do def perform(:delete, %User{} = user) do
{:ok, _user} = ActivityPub.delete(user) {:ok, _user} = ActivityPub.delete(user)

View File

@ -20,6 +20,7 @@ defmodule Pleroma.User.Info do
field(:following_count, :integer, default: nil) field(:following_count, :integer, default: nil)
field(:locked, :boolean, default: false) field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false) field(:confirmation_pending, :boolean, default: false)
field(:password_reset_pending, :boolean, default: false)
field(:confirmation_token, :string, default: nil) field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public") field(:default_scope, :string, default: "public")
field(:blocks, {:array, :string}, default: []) field(:blocks, {:array, :string}, default: [])
@ -82,6 +83,14 @@ def set_activation_status(info, deactivated) do
|> validate_required([:deactivated]) |> validate_required([:deactivated])
end end
def set_password_reset_pending(info, pending) do
params = %{password_reset_pending: pending}
info
|> cast(params, [:password_reset_pending])
|> validate_required([:password_reset_pending])
end
def update_notification_settings(info, settings) do def update_notification_settings(info, settings) do
settings = settings =
settings settings

View File

@ -520,9 +520,10 @@ def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
end end
def fetch_public_activities(opts \\ %{}) do def fetch_public_activities(opts \\ %{}) do
q = fetch_activities_query([Pleroma.Constants.as_public()], opts) opts = Map.drop(opts, ["user"])
q [Pleroma.Constants.as_public()]
|> fetch_activities_query(opts)
|> restrict_unlisted() |> restrict_unlisted()
|> Pagination.fetch_paginated(opts) |> Pagination.fetch_paginated(opts)
|> Enum.reverse() |> Enum.reverse()

View File

@ -49,7 +49,8 @@ def user(conn, %{"nickname" => nickname}) do
{:ok, user} <- User.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> put_view(UserView)
|> render("user.json", %{user: user})
else else
nil -> {:error, :not_found} nil -> {:error, :not_found}
end end
@ -90,7 +91,8 @@ def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("likes.json", ap_id, likes, page)) |> put_view(ObjectView)
|> render("likes.json", %{ap_id: ap_id, likes: likes, page: page})
else else
{:public?, false} -> {:public?, false} ->
{:error, :not_found} {:error, :not_found}
@ -104,7 +106,8 @@ def object_likes(conn, %{"uuid" => uuid}) do
likes <- Utils.get_object_likes(object) do likes <- Utils.get_object_likes(object) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("likes.json", ap_id, likes)) |> put_view(ObjectView)
|> render("likes.json", %{ap_id: ap_id, likes: likes})
else else
{:public?, false} -> {:public?, false} ->
{:error, :not_found} {:error, :not_found}
@ -158,7 +161,8 @@ defp set_cache_ttl_for(conn, entity) do
def following(%{assigns: %{relay: true}} = conn, _params) do def following(%{assigns: %{relay: true}} = conn, _params) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: Relay.get_actor()})) |> put_view(UserView)
|> render("following.json", %{user: Relay.get_actor()})
end end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
@ -170,7 +174,8 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "p
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: user, page: page, for: for_user})) |> put_view(UserView)
|> render("following.json", %{user: user, page: page, for: for_user})
else else
{:show_follows, _} -> {:show_follows, _} ->
conn conn
@ -184,7 +189,8 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: user, for: for_user})) |> put_view(UserView)
|> render("following.json", %{user: user, for: for_user})
end end
end end
@ -192,7 +198,8 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
def followers(%{assigns: %{relay: true}} = conn, _params) do def followers(%{assigns: %{relay: true}} = conn, _params) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: Relay.get_actor()})) |> put_view(UserView)
|> render("followers.json", %{user: Relay.get_actor()})
end end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
@ -204,7 +211,8 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "p
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: user, page: page, for: for_user})) |> put_view(UserView)
|> render("followers.json", %{user: user, page: page, for: for_user})
else else
{:show_followers, _} -> {:show_followers, _} ->
conn conn
@ -218,7 +226,8 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: user, for: for_user})) |> put_view(UserView)
|> render("followers.json", %{user: user, for: for_user})
end end
end end
@ -227,7 +236,8 @@ def outbox(conn, %{"nickname" => nickname} = params) do
{:ok, user} <- User.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]})) |> put_view(UserView)
|> render("outbox.json", %{user: user, max_id: params["max_id"]})
end end
end end
@ -275,7 +285,8 @@ defp represent_service_actor(%User{} = user, conn) do
with {:ok, user} <- User.ensure_keys_present(user) do with {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> put_view(UserView)
|> render("user.json", %{user: user})
else else
nil -> {:error, :not_found} nil -> {:error, :not_found}
end end
@ -296,7 +307,8 @@ def internal_fetch(conn, _params) do
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> put_view(UserView)
|> render("user.json", %{user: user})
end end
def whoami(_conn, _params), do: {:error, :not_found} def whoami(_conn, _params), do: {:error, :not_found}

View File

@ -42,8 +42,7 @@ def fix_object(object, options \\ []) do
end end
def fix_summary(%{"summary" => nil} = object) do def fix_summary(%{"summary" => nil} = object) do
object Map.put(object, "summary", "")
|> Map.put("summary", "")
end end
def fix_summary(%{"summary" => _} = object) do def fix_summary(%{"summary" => _} = object) do
@ -51,10 +50,7 @@ def fix_summary(%{"summary" => _} = object) do
object object
end end
def fix_summary(object) do def fix_summary(object), do: Map.put(object, "summary", "")
object
|> Map.put("summary", "")
end
def fix_addressing_list(map, field) do def fix_addressing_list(map, field) do
cond do cond do
@ -74,13 +70,9 @@ def fix_explicit_addressing(
explicit_mentions, explicit_mentions,
follower_collection follower_collection
) do ) do
explicit_to = explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
to
|> Enum.filter(fn x -> x in explicit_mentions end)
explicit_cc = explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
to
|> Enum.filter(fn x -> x not in explicit_mentions end)
final_cc = final_cc =
(cc ++ explicit_cc) (cc ++ explicit_cc)
@ -98,13 +90,19 @@ def fix_explicit_addressing(object, _explicit_mentions, _followers_collection),
def fix_explicit_addressing(%{"directMessage" => true} = object), do: object def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
def fix_explicit_addressing(object) do def fix_explicit_addressing(object) do
explicit_mentions = explicit_mentions = Utils.determine_explicit_mentions(object)
%User{follower_address: follower_collection} =
object object
|> Utils.determine_explicit_mentions() |> Containment.get_actor()
|> User.get_cached_by_ap_id()
follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address explicit_mentions =
explicit_mentions ++
explicit_mentions = explicit_mentions ++ [Pleroma.Constants.as_public(), follower_collection] [
Pleroma.Constants.as_public(),
follower_collection
]
fix_explicit_addressing(object, explicit_mentions, follower_collection) fix_explicit_addressing(object, explicit_mentions, follower_collection)
end end
@ -148,48 +146,25 @@ def fix_addressing(object) do
end end
def fix_actor(%{"attributedTo" => actor} = object) do def fix_actor(%{"attributedTo" => actor} = object) do
object Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
|> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
end end
def fix_in_reply_to(object, options \\ []) def fix_in_reply_to(object, options \\ [])
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do when not is_nil(in_reply_to) do
in_reply_to_id = in_reply_to_id = prepare_in_reply_to(in_reply_to)
cond do
is_bitstring(in_reply_to) ->
in_reply_to
is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
in_reply_to["id"]
is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
Enum.at(in_reply_to, 0)
# Maybe I should output an error too?
true ->
""
end
object = Map.put(object, "inReplyToAtomUri", in_reply_to_id) object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
if Federator.allowed_incoming_reply_depth?(options[:depth]) do if Federator.allowed_incoming_reply_depth?(options[:depth]) do
case get_obj_helper(in_reply_to_id, options) do with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
{:ok, replied_object} -> %Activity{} = _ <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
with %Activity{} = _activity <- object
Activity.get_create_by_object_ap_id(replied_object.data["id"]) do |> Map.put("inReplyTo", replied_object.data["id"])
object |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("inReplyTo", replied_object.data["id"]) |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) |> Map.put("context", replied_object.data["context"] || object["conversation"])
|> Map.put("conversation", replied_object.data["context"] || object["conversation"]) else
|> Map.put("context", replied_object.data["context"] || object["conversation"])
else
e ->
Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
object
end
e -> e ->
Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}") Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
object object
@ -201,6 +176,22 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
def fix_in_reply_to(object, _options), do: object def fix_in_reply_to(object, _options), do: object
defp prepare_in_reply_to(in_reply_to) do
cond do
is_bitstring(in_reply_to) ->
in_reply_to
is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
in_reply_to["id"]
is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
Enum.at(in_reply_to, 0)
true ->
""
end
end
def fix_context(object) do def fix_context(object) do
context = object["context"] || object["conversation"] || Utils.generate_context_id() context = object["context"] || object["conversation"] || Utils.generate_context_id()
@ -211,11 +202,9 @@ def fix_context(object) do
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments = attachments =
attachment Enum.map(attachment, fn data ->
|> Enum.map(fn data ->
media_type = data["mediaType"] || data["mimeType"] media_type = data["mediaType"] || data["mimeType"]
href = data["url"] || data["href"] href = data["url"] || data["href"]
url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}] url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
data data
@ -223,30 +212,25 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
|> Map.put("url", url) |> Map.put("url", url)
end) end)
object Map.put(object, "attachment", attachments)
|> Map.put("attachment", attachments)
end end
def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
Map.put(object, "attachment", [attachment]) object
|> Map.put("attachment", [attachment])
|> fix_attachments() |> fix_attachments()
end end
def fix_attachments(object), do: object def fix_attachments(object), do: object
def fix_url(%{"url" => url} = object) when is_map(url) do def fix_url(%{"url" => url} = object) when is_map(url) do
object Map.put(object, "url", url["href"])
|> Map.put("url", url["href"])
end end
def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
first_element = Enum.at(url, 0) first_element = Enum.at(url, 0)
link_element = link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
url
|> Enum.filter(fn x -> is_map(x) end)
|> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
|> Enum.at(0)
object object
|> Map.put("attachment", [first_element]) |> Map.put("attachment", [first_element])
@ -264,36 +248,32 @@ def fix_url(%{"type" => object_type, "url" => url} = object)
true -> "" true -> ""
end end
object Map.put(object, "url", url_string)
|> Map.put("url", url_string)
end end
def fix_url(object), do: object def fix_url(object), do: object
def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
emoji = emoji =
emoji tags
|> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
|> Enum.reduce(%{}, fn data, mapping -> |> Enum.reduce(%{}, fn data, mapping ->
name = String.trim(data["name"], ":") name = String.trim(data["name"], ":")
mapping |> Map.put(name, data["icon"]["url"]) Map.put(mapping, name, data["icon"]["url"])
end) end)
# we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
emoji = Map.merge(object["emoji"] || %{}, emoji) emoji = Map.merge(object["emoji"] || %{}, emoji)
object Map.put(object, "emoji", emoji)
|> Map.put("emoji", emoji)
end end
def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
name = String.trim(tag["name"], ":") name = String.trim(tag["name"], ":")
emoji = %{name => tag["icon"]["url"]} emoji = %{name => tag["icon"]["url"]}
object Map.put(object, "emoji", emoji)
|> Map.put("emoji", emoji)
end end
def fix_emoji(object), do: object def fix_emoji(object), do: object
@ -304,17 +284,13 @@ def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end) |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn data -> String.slice(data["name"], 1..-1) end) |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
combined = tag ++ tags Map.put(object, "tag", tag ++ tags)
object
|> Map.put("tag", combined)
end end
def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
combined = [tag, String.slice(hashtag, 1..-1)] combined = [tag, String.slice(hashtag, 1..-1)]
object Map.put(object, "tag", combined)
|> Map.put("tag", combined)
end end
def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag]) def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
@ -326,8 +302,7 @@ def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map) content_groups = Map.to_list(content_map)
{_, content} = Enum.at(content_groups, 0) {_, content} = Enum.at(content_groups, 0)
object Map.put(object, "content", content)
|> Map.put("content", content)
end end
def fix_content_map(object), do: object def fix_content_map(object), do: object
@ -336,16 +311,11 @@ def fix_type(object, options \\ [])
def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options) def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do when is_binary(reply_id) do
reply = with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
with true <- Federator.allowed_incoming_reply_depth?(options[:depth]), {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
{:ok, object} <- get_obj_helper(reply_id, options) do
object
end
if reply && reply.data["type"] == "Question" do
Map.put(object, "type", "Answer") Map.put(object, "type", "Answer")
else else
object _ -> object
end end
end end
@ -377,6 +347,17 @@ defp get_follow_activity(follow_object, followed) do
end end
end end
# Reduce the object list to find the reported user.
defp get_reported(objects) do
Enum.reduce_while(objects, nil, fn ap_id, _ ->
with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
{:halt, user}
else
_ -> {:cont, nil}
end
end)
end
def handle_incoming(data, options \\ []) def handle_incoming(data, options \\ [])
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
@ -385,31 +366,19 @@ def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} =
with context <- data["context"] || Utils.generate_context_id(), with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "", content <- data["content"] || "",
%User{} = actor <- User.get_cached_by_ap_id(actor), %User{} = actor <- User.get_cached_by_ap_id(actor),
# Reduce the object list to find the reported user. # Reduce the object list to find the reported user.
%User{} = account <- %User{} = account <- get_reported(objects),
Enum.reduce_while(objects, nil, fn ap_id, _ ->
with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
{:halt, user}
else
_ -> {:cont, nil}
end
end),
# Remove the reported user from the object list. # Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
params = %{ %{
actor: actor, actor: actor,
context: context, context: context,
account: account, account: account,
statuses: statuses, statuses: statuses,
content: content, content: content,
additional: %{ additional: %{"cc" => [account.ap_id]}
"cc" => [account.ap_id]
}
} }
|> ActivityPub.flag()
ActivityPub.flag(params)
end end
end end
@ -756,8 +725,12 @@ def handle_incoming(
def handle_incoming(_, _), do: :error def handle_incoming(_, _), do: :error
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do def get_obj_helper(id, options \\ []) do
if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil case Object.normalize(id, true, options) do
%Object{} = object -> {:ok, object}
_ -> nil
end
end end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
@ -856,27 +829,24 @@ def prepare_outgoing(%{"type" => _type} = data) do
{:ok, data} {:ok, data}
end end
def maybe_fix_object_url(data) do def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do with false <- String.starts_with?(object, "http"),
case get_obj_helper(data["object"]) do {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
{:ok, relative_object} -> %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
if relative_object.data["external_url"] do relative_object do
_data = Map.put(data, "object", external_url)
data
|> Map.put("object", relative_object.data["external_url"])
else
data
end
e ->
Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
data
end
else else
data {:fetch, e} ->
Logger.error("Couldn't fetch #{object} #{inspect(e)}")
data
_ ->
data
end end
end end
def maybe_fix_object_url(data), do: data
def add_hashtags(object) do def add_hashtags(object) do
tags = tags =
(object["tag"] || []) (object["tag"] || [])
@ -894,53 +864,49 @@ def add_hashtags(object) do
tag tag
end) end)
object Map.put(object, "tag", tags)
|> Map.put("tag", tags)
end end
def add_mention_tags(object) do def add_mention_tags(object) do
mentions = mentions =
object object
|> Utils.get_notified_from_object() |> Utils.get_notified_from_object()
|> Enum.map(fn user -> |> Enum.map(&build_mention_tag/1)
%{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
end)
tags = object["tag"] || [] tags = object["tag"] || []
object Map.put(object, "tag", tags ++ mentions)
|> Map.put("tag", tags ++ mentions)
end end
def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
user_info = add_emoji_tags(user_info) %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
end
object def take_emoji_tags(%User{info: %{emoji: emoji} = _user_info} = _user) do
|> Map.put(:info, user_info) emoji
|> Enum.flat_map(&Map.to_list/1)
|> Enum.map(&build_emoji_tag/1)
end end
# TODO: we should probably send mtime instead of unix epoch time for updated # TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(%{"emoji" => emoji} = object) do def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || [] tags = object["tag"] || []
out = out = Enum.map(emoji, &build_emoji_tag/1)
emoji
|> Enum.map(fn {name, url} ->
%{
"icon" => %{"url" => url, "type" => "Image"},
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
"id" => url
}
end)
object Map.put(object, "tag", tags ++ out)
|> Map.put("tag", tags ++ out)
end end
def add_emoji_tags(object) do def add_emoji_tags(object), do: object
object
defp build_emoji_tag({name, url}) do
%{
"icon" => %{"url" => url, "type" => "Image"},
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
"id" => url
}
end end
def set_conversation(object) do def set_conversation(object) do
@ -960,9 +926,7 @@ def set_type(object), do: object
def add_attributed_to(object) do def add_attributed_to(object) do
attributed_to = object["attributedTo"] || object["actor"] attributed_to = object["attributedTo"] || object["actor"]
Map.put(object, "attributedTo", attributed_to)
object
|> Map.put("attributedTo", attributed_to)
end end
def prepare_attachments(object) do def prepare_attachments(object) do
@ -973,8 +937,7 @@ def prepare_attachments(object) do
%{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"} %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
end) end)
object Map.put(object, "attachment", attachments)
|> Map.put("attachment", attachments)
end end
defp strip_internal_fields(object) do defp strip_internal_fields(object) do
@ -983,12 +946,9 @@ defp strip_internal_fields(object) do
end end
defp strip_internal_tags(%{"tag" => tags} = object) do defp strip_internal_tags(%{"tag" => tags} = object) do
tags = tags = Enum.filter(tags, fn x -> is_map(x) end)
tags
|> Enum.filter(fn x -> is_map(x) end)
object Map.put(object, "tag", tags)
|> Map.put("tag", tags)
end end
defp strip_internal_tags(object), do: object defp strip_internal_tags(object), do: object
@ -1073,16 +1033,11 @@ def maybe_retire_websub(ap_id) do
end end
end end
def maybe_fix_user_url(data) do def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
if is_map(data["url"]) do Map.put(data, "url", url["href"])
Map.put(data, "url", data["url"]["href"])
else
data
end
end end
def maybe_fix_user_object(data) do def maybe_fix_user_url(data), do: data
data
|> maybe_fix_user_url def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
end
end end

View File

@ -37,12 +37,12 @@ def render("object.json", %{object: %Activity{} = activity}) do
Map.merge(base, additional) Map.merge(base, additional)
end end
def render("likes.json", ap_id, likes, page) do def render("likes.json", %{ap_id: ap_id, likes: likes, page: page}) do
collection(likes, "#{ap_id}/likes", page) collection(likes, "#{ap_id}/likes", page)
|> Map.merge(Pleroma.Web.ActivityPub.Utils.make_json_ld_header()) |> Map.merge(Pleroma.Web.ActivityPub.Utils.make_json_ld_header())
end end
def render("likes.json", ap_id, likes) do def render("likes.json", %{ap_id: ap_id, likes: likes}) do
%{ %{
"id" => "#{ap_id}/likes", "id" => "#{ap_id}/likes",
"type" => "OrderedCollection", "type" => "OrderedCollection",

View File

@ -75,10 +75,7 @@ def render("user.json", %{user: user}) do
endpoints = render("endpoints.json", %{user: user}) endpoints = render("endpoints.json", %{user: user})
user_tags = emoji_tags = Transmogrifier.take_emoji_tags(user)
user
|> Transmogrifier.add_emoji_tags()
|> Map.get("tag", [])
fields = fields =
user.info user.info
@ -110,7 +107,7 @@ def render("user.json", %{user: user}) do
}, },
"endpoints" => endpoints, "endpoints" => endpoints,
"attachment" => fields, "attachment" => fields,
"tag" => (user.info.source_data["tag"] || []) ++ user_tags "tag" => (user.info.source_data["tag"] || []) ++ emoji_tags
} }
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))

View File

@ -14,6 +14,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.AdminAPI.Config alias Pleroma.Web.AdminAPI.Config
alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.ModerationLogView
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -139,7 +140,8 @@ def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
def user_show(conn, %{"nickname" => nickname}) do def user_show(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
conn conn
|> json(AccountView.render("show.json", %{user: user})) |> put_view(AccountView)
|> render("show.json", %{user: user})
else else
_ -> {:error, :not_found} _ -> {:error, :not_found}
end end
@ -158,7 +160,8 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do
}) })
conn conn
|> json(StatusView.render("index.json", %{activities: activities, as: :activity})) |> put_view(StatusView)
|> render("index.json", %{activities: activities, as: :activity})
else else
_ -> {:error, :not_found} _ -> {:error, :not_found}
end end
@ -178,7 +181,8 @@ def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => ni
}) })
conn conn
|> json(AccountView.render("show.json", %{user: updated_user})) |> put_view(AccountView)
|> render("show.json", %{user: updated_user})
end end
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
@ -424,7 +428,8 @@ def invites(conn, _params) do
invites = UserInviteToken.list_invites() invites = UserInviteToken.list_invites()
conn conn
|> json(AccountView.render("invites.json", %{invites: invites})) |> put_view(AccountView)
|> render("invites.json", %{invites: invites})
end end
@doc "Revokes invite by token" @doc "Revokes invite by token"
@ -432,7 +437,8 @@ def revoke_invite(conn, %{"token" => token}) do
with {:ok, invite} <- UserInviteToken.find_by_token(token), with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
conn conn
|> json(AccountView.render("invite.json", %{invite: updated_invite})) |> put_view(AccountView)
|> render("invite.json", %{invite: updated_invite})
else else
nil -> {:error, :not_found} nil -> {:error, :not_found}
end end
@ -447,6 +453,15 @@ def get_password_reset(conn, %{"nickname" => nickname}) do
|> json(token.token) |> json(token.token)
end end
@doc "Force password reset for a given user"
def force_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
User.force_password_reset_async(user)
json_response(conn, :no_content, "")
end
def list_reports(conn, params) do def list_reports(conn, params) do
params = params =
params params
@ -465,7 +480,7 @@ def report_show(conn, %{"id" => id}) do
with %Activity{} = report <- Activity.get_by_id(id) do with %Activity{} = report <- Activity.get_by_id(id) do
conn conn
|> put_view(ReportView) |> put_view(ReportView)
|> render("show.json", %{report: report}) |> render("show.json", Report.extract_report_info(report))
else else
_ -> {:error, :not_found} _ -> {:error, :not_found}
end end
@ -481,7 +496,7 @@ def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state"
conn conn
|> put_view(ReportView) |> put_view(ReportView)
|> render("show.json", %{report: report}) |> render("show.json", Report.extract_report_info(report))
end end
end end

View File

@ -0,0 +1,22 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.Report do
alias Pleroma.Activity
alias Pleroma.User
def extract_report_info(
%{data: %{"actor" => actor, "object" => [account_ap_id | status_ap_ids]}} = report
) do
user = User.get_cached_by_ap_id(actor)
account = User.get_cached_by_ap_id(account_ap_id)
statuses =
Enum.map(status_ap_ids, fn ap_id ->
Activity.get_by_ap_id_with_object(ap_id)
end)
%{report: report, user: user, account: account, statuses: statuses}
end
end

View File

@ -4,27 +4,26 @@
defmodule Pleroma.Web.AdminAPI.ReportView do defmodule Pleroma.Web.AdminAPI.ReportView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{reports: reports}) do def render("index.json", %{reports: reports}) do
%{ %{
reports: reports:
render_many(reports[:items], __MODULE__, "show.json", as: :report) |> Enum.reverse(), reports[:items]
|> Enum.map(&Report.extract_report_info(&1))
|> Enum.map(&render(__MODULE__, "show.json", &1))
|> Enum.reverse(),
total: reports[:total] total: reports[:total]
} }
end end
def render("show.json", %{report: report}) do def render("show.json", %{report: report, user: user, account: account, statuses: statuses}) do
user = User.get_cached_by_ap_id(report.data["actor"])
created_at = Utils.to_masto_date(report.data["published"]) created_at = Utils.to_masto_date(report.data["published"])
[account_ap_id | status_ap_ids] = report.data["object"]
account = User.get_cached_by_ap_id(account_ap_id)
content = content =
unless is_nil(report.data["content"]) do unless is_nil(report.data["content"]) do
HTML.filter_tags(report.data["content"]) HTML.filter_tags(report.data["content"])
@ -32,11 +31,6 @@ def render("show.json", %{report: report}) do
nil nil
end end
statuses =
Enum.map(status_ap_ids, fn ap_id ->
Activity.get_by_ap_id_with_object(ap_id)
end)
%{ %{
id: report.id, id: report.id,
account: merge_account_views(account), account: merge_account_views(account),

View File

@ -382,7 +382,6 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do
|> Map.put("local_only", local_only) |> Map.put("local_only", local_only)
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("muting_user", user) |> Map.put("muting_user", user)
|> Map.put("user", user)
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
|> Enum.reverse() |> Enum.reverse()

View File

@ -19,9 +19,10 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, search_options(params, user)) accounts = User.search(query, search_options(params, user))
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res) conn
|> put_view(AccountView)
|> render("accounts.json", users: accounts, for: user, as: :user)
end end
def search2(conn, params), do: do_search(:v2, conn, params) def search2(conn, params), do: do_search(:v2, conn, params)

View File

@ -202,6 +202,8 @@ def token_exchange(
{:ok, app} <- Token.Utils.fetch_app(conn), {:ok, app} <- Token.Utils.fetch_app(conn),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)}, {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:user_active, true} <- {:user_active, !user.info.deactivated}, {:user_active, true} <- {:user_active, !user.info.deactivated},
{:password_reset_pending, false} <-
{:password_reset_pending, user.info.password_reset_pending},
{:ok, scopes} <- validate_scopes(app, params), {:ok, scopes} <- validate_scopes(app, params),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
@ -215,6 +217,9 @@ def token_exchange(
{:user_active, false} -> {:user_active, false} ->
render_error(conn, :forbidden, "Your account is currently disabled") render_error(conn, :forbidden, "Your account is currently disabled")
{:password_reset_pending, true} ->
render_error(conn, :forbidden, "Password reset is required")
_error -> _error ->
render_invalid_credentials_error(conn) render_invalid_credentials_error(conn)
end end

View File

@ -216,7 +216,8 @@ defp represent_activity(
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(ObjectView.render("object.json", %{object: object})) |> put_view(ObjectView)
|> render("object.json", %{object: object})
end end
defp represent_activity(_conn, "activity+json", _, _) do defp represent_activity(_conn, "activity+json", _, _) do

View File

@ -242,7 +242,7 @@ def create(conn, %{"name" => name}) do
File.write!( File.write!(
pack_file_p, pack_file_p,
Jason.encode!(%{pack: %{}, files: %{}}) Jason.encode!(%{pack: %{}, files: %{}}, pretty: true)
) )
conn |> json("ok") conn |> json("ok")

View File

@ -186,6 +186,7 @@ defmodule Pleroma.Web.Router do
post("/users/email_invite", AdminAPIController, :email_invite) post("/users/email_invite", AdminAPIController, :email_invite)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/:nickname/force_password_reset", AdminAPIController, :force_password_reset)
get("/users", AdminAPIController, :list_users) get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show) get("/users/:nickname", AdminAPIController, :user_show)

View File

@ -26,6 +26,11 @@ def perform(%{"op" => "delete_user", "user_id" => user_id}, _job) do
User.perform(:delete, user) User.perform(:delete, user)
end end
def perform(%{"op" => "force_password_reset", "user_id" => user_id}, _job) do
user = User.get_cached_by_id(user_id)
User.perform(:force_password_reset, user)
end
def perform( def perform(
%{ %{
"op" => "blocks_import", "op" => "blocks_import",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1 +1 @@
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=chunk-elementUI.e5cd8da6.css rel=stylesheet><link href=chunk-libs.4e8c4664.css rel=stylesheet><link href=app.34fc670f.css rel=stylesheet></head><body><script src=/pleroma/admin/static/tinymce4.7.5/tinymce.min.js></script><div id=app></div><script type=text/javascript src=static/js/runtime.f40c8ec4.js></script><script type=text/javascript src=static/js/chunk-elementUI.1911151b.js></script><script type=text/javascript src=static/js/chunk-libs.fb0b7f4a.js></script><script type=text/javascript src=static/js/app.8e186193.js></script></body></html> <!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=chunk-elementUI.f35d8ab1.css rel=stylesheet><link href=chunk-libs.00388c73.css rel=stylesheet><link href=app.40438ff5.css rel=stylesheet></head><body><script src=/pleroma/admin/static/tinymce4.7.5/tinymce.min.js></script><div id=app></div><script type=text/javascript src=static/js/runtime.e85850af.js></script><script type=text/javascript src=static/js/chunk-elementUI.708d6b68.js></script><script type=text/javascript src=static/js/chunk-libs.14514767.js></script><script type=text/javascript src=static/js/app.90c455c5.js></script></body></html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -19,7 +19,7 @@ test "turns hashtags into links" do
text = "I love #cofe and #2hu" text = "I love #cofe and #2hu"
expected_text = expected_text =
"I love <a class='hashtag' data-tag='cofe' href='http://localhost:4001/tag/cofe' rel='tag'>#cofe</a> and <a class='hashtag' data-tag='2hu' href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a>" ~s(I love <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe" rel="tag ugc">#cofe</a> and <a class="hashtag" data-tag="2hu" href="http://localhost:4001/tag/2hu" rel="tag ugc">#2hu</a>)
assert {^expected_text, [], _tags} = Formatter.linkify(text) assert {^expected_text, [], _tags} = Formatter.linkify(text)
end end
@ -28,7 +28,7 @@ test "does not turn html characters to tags" do
text = "#fact_3: pleroma does what mastodon't" text = "#fact_3: pleroma does what mastodon't"
expected_text = expected_text =
"<a class='hashtag' data-tag='fact_3' href='http://localhost:4001/tag/fact_3' rel='tag'>#fact_3</a>: pleroma does what mastodon't" ~s(<a class="hashtag" data-tag="fact_3" href="http://localhost:4001/tag/fact_3" rel="tag ugc">#fact_3</a>: pleroma does what mastodon't)
assert {^expected_text, [], _tags} = Formatter.linkify(text) assert {^expected_text, [], _tags} = Formatter.linkify(text)
end end
@ -39,21 +39,21 @@ test "turning urls into links" do
text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ." text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ."
expected = expected =
"Hey, check out <a href=\"https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla\">https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a> ." ~S(Hey, check out <a href="https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla" rel="ugc">https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a> .)
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://mastodon.social/@lambadalambda" text = "https://mastodon.social/@lambadalambda"
expected = expected =
"<a href=\"https://mastodon.social/@lambadalambda\">https://mastodon.social/@lambadalambda</a>" ~S(<a href="https://mastodon.social/@lambadalambda" rel="ugc">https://mastodon.social/@lambadalambda</a>)
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://mastodon.social:4000/@lambadalambda" text = "https://mastodon.social:4000/@lambadalambda"
expected = expected =
"<a href=\"https://mastodon.social:4000/@lambadalambda\">https://mastodon.social:4000/@lambadalambda</a>" ~S(<a href="https://mastodon.social:4000/@lambadalambda" rel="ugc">https://mastodon.social:4000/@lambadalambda</a>)
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
@ -63,55 +63,57 @@ test "turning urls into links" do
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "http://www.cs.vu.nl/~ast/intel/" text = "http://www.cs.vu.nl/~ast/intel/"
expected = "<a href=\"http://www.cs.vu.nl/~ast/intel/\">http://www.cs.vu.nl/~ast/intel/</a>"
expected =
~S(<a href="http://www.cs.vu.nl/~ast/intel/" rel="ugc">http://www.cs.vu.nl/~ast/intel/</a>)
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087" text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087"
expected = expected =
"<a href=\"https://forum.zdoom.org/viewtopic.php?f=44&t=57087\">https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>" "<a href=\"https://forum.zdoom.org/viewtopic.php?f=44&t=57087\" rel=\"ugc\">https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul" text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul"
expected = expected =
"<a href=\"https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul\">https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>" "<a href=\"https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul\" rel=\"ugc\">https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://www.google.co.jp/search?q=Nasim+Aghdam" text = "https://www.google.co.jp/search?q=Nasim+Aghdam"
expected = expected =
"<a href=\"https://www.google.co.jp/search?q=Nasim+Aghdam\">https://www.google.co.jp/search?q=Nasim+Aghdam</a>" "<a href=\"https://www.google.co.jp/search?q=Nasim+Aghdam\" rel=\"ugc\">https://www.google.co.jp/search?q=Nasim+Aghdam</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://en.wikipedia.org/wiki/Duff's_device" text = "https://en.wikipedia.org/wiki/Duff's_device"
expected = expected =
"<a href=\"https://en.wikipedia.org/wiki/Duff's_device\">https://en.wikipedia.org/wiki/Duff's_device</a>" "<a href=\"https://en.wikipedia.org/wiki/Duff's_device\" rel=\"ugc\">https://en.wikipedia.org/wiki/Duff's_device</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://pleroma.com https://pleroma.com/sucks" text = "https://pleroma.com https://pleroma.com/sucks"
expected = expected =
"<a href=\"https://pleroma.com\">https://pleroma.com</a> <a href=\"https://pleroma.com/sucks\">https://pleroma.com/sucks</a>" "<a href=\"https://pleroma.com\" rel=\"ugc\">https://pleroma.com</a> <a href=\"https://pleroma.com/sucks\" rel=\"ugc\">https://pleroma.com/sucks</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "xmpp:contact@hacktivis.me" text = "xmpp:contact@hacktivis.me"
expected = "<a href=\"xmpp:contact@hacktivis.me\">xmpp:contact@hacktivis.me</a>" expected = "<a href=\"xmpp:contact@hacktivis.me\" rel=\"ugc\">xmpp:contact@hacktivis.me</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = text =
"magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com" "magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com"
expected = "<a href=\"#{text}\">#{text}</a>" expected = "<a href=\"#{text}\" rel=\"ugc\">#{text}</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
end end
@ -135,13 +137,13 @@ test "gives a replacement for user links, using local nicknames in user links te
assert length(mentions) == 3 assert length(mentions) == 3
expected_text = expected_text =
"<span class='h-card'><a data-user='#{gsimg.id}' class='u-url mention' href='#{ ~s(<span class="h-card"><a data-user="#{gsimg.id}" class="u-url mention" href="#{
gsimg.ap_id gsimg.ap_id
}'>@<span>gsimg</span></a></span> According to <span class='h-card'><a data-user='#{ }" rel="ugc">@<span>gsimg</span></a></span> According to <span class="h-card"><a data-user="#{
archaeme.id archaeme.id
}' class='u-url mention' href='#{"https://archeme/@archa_eme_"}'>@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class='h-card'><a data-user='#{ }" class="u-url mention" href="#{"https://archeme/@archa_eme_"}" rel="ugc">@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class="h-card"><a data-user="#{
archaeme_remote.id archaeme_remote.id
}' class='u-url mention' href='#{archaeme_remote.ap_id}'>@<span>archaeme</span></a></span>" }" class="u-url mention" href="#{archaeme_remote.ap_id}" rel="ugc">@<span>archaeme</span></a></span>)
assert expected_text == text assert expected_text == text
end end
@ -156,7 +158,9 @@ test "gives a replacement for user links when the user is using Osada" do
assert length(mentions) == 1 assert length(mentions) == 1
expected_text = expected_text =
"<span class='h-card'><a data-user='#{mike.id}' class='u-url mention' href='#{mike.ap_id}'>@<span>mike</span></a></span> test" ~s(<span class="h-card"><a data-user="#{mike.id}" class="u-url mention" href="#{
mike.ap_id
}" rel="ugc">@<span>mike</span></a></span> test)
assert expected_text == text assert expected_text == text
end end
@ -170,7 +174,7 @@ test "gives a replacement for single-character local nicknames" do
assert length(mentions) == 1 assert length(mentions) == 1
expected_text = expected_text =
"<span class='h-card'><a data-user='#{o.id}' class='u-url mention' href='#{o.ap_id}'>@<span>o</span></a></span> hi" ~s(<span class="h-card"><a data-user="#{o.id}" class="u-url mention" href="#{o.ap_id}" rel="ugc">@<span>o</span></a></span> hi)
assert expected_text == text assert expected_text == text
end end
@ -192,13 +196,17 @@ test "given the 'safe_mention' option, it will only mention people in the beginn
assert mentions == [{"@#{user.nickname}", user}, {"@#{other_user.nickname}", other_user}] assert mentions == [{"@#{user.nickname}", user}, {"@#{other_user.nickname}", other_user}]
assert expected_text == assert expected_text ==
"<span class='h-card'><a data-user='#{user.id}' class='u-url mention' href='#{ ~s(<span class="h-card"><a data-user="#{user.id}" class="u-url mention" href="#{
user.ap_id user.ap_id
}'>@<span>#{user.nickname}</span></a></span> <span class='h-card'><a data-user='#{ }" rel="ugc">@<span>#{user.nickname}</span></a></span> <span class="h-card"><a data-user="#{
other_user.id other_user.id
}' class='u-url mention' href='#{other_user.ap_id}'>@<span>#{other_user.nickname}</span></a></span> hey dudes i hate <span class='h-card'><a data-user='#{ }" class="u-url mention" href="#{other_user.ap_id}" rel="ugc">@<span>#{
other_user.nickname
}</span></a></span> hey dudes i hate <span class="h-card"><a data-user="#{
third_user.id third_user.id
}' class='u-url mention' href='#{third_user.ap_id}'>@<span>#{third_user.nickname}</span></a></span>" }" class="u-url mention" href="#{third_user.ap_id}" rel="ugc">@<span>#{
third_user.nickname
}</span></a></span>)
end end
test "given the 'safe_mention' option, it will still work without any mention" do test "given the 'safe_mention' option, it will still work without any mention" do

View File

@ -1294,9 +1294,9 @@ test "preserves hosts in user links text" do
bio = "A.k.a. @nick@domain.com" bio = "A.k.a. @nick@domain.com"
expected_text = expected_text =
"A.k.a. <span class='h-card'><a data-user='#{remote_user.id}' class='u-url mention' href='#{ ~s(A.k.a. <span class="h-card"><a data-user="#{remote_user.id}" class="u-url mention" href="#{
remote_user.ap_id remote_user.ap_id
}'>@<span>nick@domain.com</span></a></span>" }" rel="ugc">@<span>nick@domain.com</span></a></span>)
assert expected_text == User.parse_bio(bio, user) assert expected_text == User.parse_bio(bio, user)
end end
@ -1690,4 +1690,21 @@ test "changes email", %{user: user} do
assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party") assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party")
end end
end end
describe "set_password_reset_pending/2" do
setup do
[user: insert(:user)]
end
test "sets password_reset_pending to true", %{user: user} do
%{password_reset_pending: password_reset_pending} = user.info
refute password_reset_pending
{:ok, %{info: %{password_reset_pending: password_reset_pending}}} =
User.force_password_reset(user)
assert password_reset_pending
end
end
end end

View File

@ -1455,4 +1455,271 @@ test "removes recipient's follower collection from cc", %{user: user} do
refute recipient.follower_address in fixed_object["to"] refute recipient.follower_address in fixed_object["to"]
end end
end end
describe "fix_summary/1" do
test "returns fixed object" do
assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""}
assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"}
assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""}
end
end
describe "fix_in_reply_to/2" do
clear_config([:instance, :federation_incoming_replies_max_depth])
setup do
data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
[data: data]
end
test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do
assert Transmogrifier.fix_in_reply_to(data) == data
end
test "returns object with inReplyToAtomUri when denied incoming reply", %{data: data} do
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
object_with_reply =
Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873")
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873"
assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
object_with_reply =
Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"})
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"}
assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
object_with_reply =
Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"]
assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
object_with_reply = Map.put(data["object"], "inReplyTo", [])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == []
assert modified_object["inReplyToAtomUri"] == ""
end
test "returns modified object when allowed incoming reply", %{data: data} do
object_with_reply =
Map.put(
data["object"],
"inReplyTo",
"https://shitposter.club/notice/2827873"
)
Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 5)
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] ==
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
assert modified_object["conversation"] ==
"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
assert modified_object["context"] ==
"tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
end
end
describe "fix_url/1" do
test "fixes data for object when url is map" do
object = %{
"url" => %{
"type" => "Link",
"mimeType" => "video/mp4",
"href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
}
}
assert Transmogrifier.fix_url(object) == %{
"url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
}
end
test "fixes data for video object" do
object = %{
"type" => "Video",
"url" => [
%{
"type" => "Link",
"mimeType" => "video/mp4",
"href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
},
%{
"type" => "Link",
"mimeType" => "video/mp4",
"href" => "https://peertube46fb-ad81-2d4c2d1630e3-240.mp4"
},
%{
"type" => "Link",
"mimeType" => "text/html",
"href" => "https://peertube.-2d4c2d1630e3"
},
%{
"type" => "Link",
"mimeType" => "text/html",
"href" => "https://peertube.-2d4c2d16377-42"
}
]
}
assert Transmogrifier.fix_url(object) == %{
"attachment" => [
%{
"href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4",
"mimeType" => "video/mp4",
"type" => "Link"
}
],
"type" => "Video",
"url" => "https://peertube.-2d4c2d1630e3"
}
end
test "fixes url for not Video object" do
object = %{
"type" => "Text",
"url" => [
%{
"type" => "Link",
"mimeType" => "text/html",
"href" => "https://peertube.-2d4c2d1630e3"
},
%{
"type" => "Link",
"mimeType" => "text/html",
"href" => "https://peertube.-2d4c2d16377-42"
}
]
}
assert Transmogrifier.fix_url(object) == %{
"type" => "Text",
"url" => "https://peertube.-2d4c2d1630e3"
}
assert Transmogrifier.fix_url(%{"type" => "Text", "url" => []}) == %{
"type" => "Text",
"url" => ""
}
end
test "retunrs not modified object" do
assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
end
end
describe "get_obj_helper/2" do
test "returns nil when cannot normalize object" do
refute Transmogrifier.get_obj_helper("test-obj-id")
end
test "returns {:ok, %Object{}} for success case" do
assert {:ok, %Object{}} =
Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873")
end
end
describe "fix_attachments/1" do
test "returns not modified object" do
data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
assert Transmogrifier.fix_attachments(data) == data
end
test "returns modified object when attachment is map" do
assert Transmogrifier.fix_attachments(%{
"attachment" => %{
"mediaType" => "video/mp4",
"url" => "https://peertube.moe/stat-480.mp4"
}
}) == %{
"attachment" => [
%{
"mediaType" => "video/mp4",
"url" => [
%{
"href" => "https://peertube.moe/stat-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
]
}
]
}
end
test "returns modified object when attachment is list" do
assert Transmogrifier.fix_attachments(%{
"attachment" => [
%{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"},
%{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"}
]
}) == %{
"attachment" => [
%{
"mediaType" => "video/mp4",
"url" => [
%{
"href" => "https://pe.er/stat-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
]
},
%{
"href" => "https://pe.er/stat-480.mp4",
"mediaType" => "video/mp4",
"mimeType" => "video/mp4",
"url" => [
%{
"href" => "https://pe.er/stat-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
]
}
]
}
end
end
describe "fix_emoji/1" do
test "returns not modified object when object not contains tags" do
data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
assert Transmogrifier.fix_emoji(data) == data
end
test "returns object with emoji when object contains list tags" do
assert Transmogrifier.fix_emoji(%{
"tag" => [
%{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}},
%{"type" => "Hashtag"}
]
}) == %{
"emoji" => %{"bib" => "/test"},
"tag" => [
%{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"},
%{"type" => "Hashtag"}
]
}
end
test "returns object with emoji when object contains map tag" do
assert Transmogrifier.fix_emoji(%{
"tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}
}) == %{
"emoji" => %{"bib" => "/test"},
"tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}
}
end
end
end end

View File

@ -37,6 +37,22 @@ test "Renders profile fields" do
} = UserView.render("user.json", %{user: user}) } = UserView.render("user.json", %{user: user})
end end
test "Renders with emoji tags" do
user = insert(:user, %{info: %{emoji: [%{"bib" => "/test"}]}})
assert %{
"tag" => [
%{
"icon" => %{"type" => "Image", "url" => "/test"},
"id" => "/test",
"name" => ":bib:",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z"
}
]
} = UserView.render("user.json", %{user: user})
end
test "Does not add an avatar image if the user hasn't set one" do test "Does not add an avatar image if the user hasn't set one" do
user = insert(:user) user = insert(:user)
{:ok, user} = User.ensure_keys_present(user) {:ok, user} = User.ensure_keys_present(user)

View File

@ -4,11 +4,13 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.ModerationLog alias Pleroma.ModerationLog
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -2351,6 +2353,30 @@ test "returns the log with pagination", %{conn: conn, admin: admin} do
"@#{admin.nickname} followed relay: https://example.org/relay" "@#{admin.nickname} followed relay: https://example.org/relay"
end end
end end
describe "PATCH /users/:nickname/force_password_reset" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user)
%{conn: assign(conn, :user, admin), admin: admin, user: user}
end
test "sets password_reset_pending to true", %{admin: admin, user: user} do
assert user.info.password_reset_pending == false
conn =
build_conn()
|> assign(:user, admin)
|> patch("/api/pleroma/admin/users/#{user.nickname}/force_password_reset")
assert json_response(conn, 204) == ""
ObanHelpers.perform_all()
assert User.get_by_id(user.id).info.password_reset_pending == true
end
end
end end
# Needed for testing # Needed for testing

View File

@ -5,6 +5,7 @@
defmodule Pleroma.Web.AdminAPI.ReportViewTest do defmodule Pleroma.Web.AdminAPI.ReportViewTest do
use Pleroma.DataCase use Pleroma.DataCase
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
@ -34,7 +35,7 @@ test "renders a report" do
} }
result = result =
ReportView.render("show.json", %{report: activity}) ReportView.render("show.json", Report.extract_report_info(activity))
|> Map.delete(:created_at) |> Map.delete(:created_at)
assert result == expected assert result == expected
@ -66,7 +67,7 @@ test "includes reported statuses" do
} }
result = result =
ReportView.render("show.json", %{report: report_activity}) ReportView.render("show.json", Report.extract_report_info(report_activity))
|> Map.delete(:created_at) |> Map.delete(:created_at)
assert result == expected assert result == expected
@ -78,7 +79,9 @@ test "renders report's state" do
{:ok, activity} = CommonAPI.report(user, %{"account_id" => other_user.id}) {:ok, activity} = CommonAPI.report(user, %{"account_id" => other_user.id})
{:ok, activity} = CommonAPI.update_report_state(activity.id, "closed") {:ok, activity} = CommonAPI.update_report_state(activity.id, "closed")
assert %{state: "closed"} = ReportView.render("show.json", %{report: activity})
assert %{state: "closed"} =
ReportView.render("show.json", Report.extract_report_info(activity))
end end
test "renders report description" do test "renders report description" do
@ -92,7 +95,7 @@ test "renders report description" do
}) })
assert %{content: "posts are too good for this instance"} = assert %{content: "posts are too good for this instance"} =
ReportView.render("show.json", %{report: activity}) ReportView.render("show.json", Report.extract_report_info(activity))
end end
test "sanitizes report description" do test "sanitizes report description" do
@ -109,7 +112,7 @@ test "sanitizes report description" do
activity = Map.put(activity, :data, data) activity = Map.put(activity, :data, data)
refute "<script> alert('hecked :D:D:D:D:D:D:D') </script>" == refute "<script> alert('hecked :D:D:D:D:D:D:D') </script>" ==
ReportView.render("show.json", %{report: activity})[:content] ReportView.render("show.json", Report.extract_report_info(activity))[:content]
end end
test "doesn't error out when the user doesn't exists" do test "doesn't error out when the user doesn't exists" do
@ -125,6 +128,6 @@ test "doesn't error out when the user doesn't exists" do
Pleroma.User.delete(other_user) Pleroma.User.delete(other_user)
Pleroma.User.invalidate_cache(other_user) Pleroma.User.invalidate_cache(other_user)
assert %{} = ReportView.render("show.json", %{report: activity}) assert %{} = ReportView.render("show.json", Report.extract_report_info(activity))
end end
end end

View File

@ -157,11 +157,11 @@ test "works for text/markdown with mentions" do
text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*" text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*"
expected = expected =
"<p><strong>hello world</strong></p>\n<p><em>another <span class=\"h-card\"><a data-user=\"#{ ~s(<p><strong>hello world</strong></p>\n<p><em>another <span class="h-card"><a data-user="#{
user.id user.id
}\" class=\"u-url mention\" href=\"http://foo.com/user__test\">@<span>user__test</span></a></span> and <span class=\"h-card\"><a data-user=\"#{ }" class="u-url mention" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> and <span class="h-card"><a data-user="#{
user.id user.id
}\" class=\"u-url mention\" href=\"http://foo.com/user__test\">@<span>user__test</span></a></span> <a href=\"http://google.com\">google.com</a> paragraph</em></p>\n" }" class="u-url mention" href="http://foo.com/user__test" rel="ugc">@<span>user__test</span></a></span> <a href="http://google.com" rel="ugc">google.com</a> paragraph</em></p>\n)
{output, _, _} = Utils.format_input(text, "text/markdown") {output, _, _} = Utils.format_input(text, "text/markdown")

View File

@ -86,10 +86,9 @@ test "updates the user's bio", %{conn: conn} do
assert user = json_response(conn, 200) assert user = json_response(conn, 200)
assert user["note"] == assert user["note"] ==
~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe" rel="tag">#cofe</a> with <span class="h-card"><a data-user=") <> ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a data-user="#{
user2.id <> user2.id
~s(" class="u-url mention" href=") <> }" class="u-url mention" href="#{user2.ap_id}" rel="ugc">@<span>#{user2.nickname}</span></a></span>)
user2.ap_id <> ~s(">@<span>) <> user2.nickname <> ~s(</span></a></span>)
end end
test "updates the user's locking status", %{conn: conn} do test "updates the user's locking status", %{conn: conn} do
@ -334,7 +333,7 @@ test "update fields", %{conn: conn} do
assert account["fields"] == [ assert account["fields"] == [
%{"name" => "foo", "value" => "bar"}, %{"name" => "foo", "value" => "bar"},
%{"name" => "link", "value" => "<a href=\"http://cofe.io\">cofe.io</a>"} %{"name" => "link", "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)}
] ]
assert account["source"]["fields"] == [ assert account["source"]["fields"] == [

View File

@ -97,6 +97,22 @@ test "the public timeline when public is set to false", %{conn: conn} do
|> json_response(403) == %{"error" => "This resource requires authentication."} |> json_response(403) == %{"error" => "This resource requires authentication."}
end end
test "the public timeline includes only public statuses for an authenticated user" do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test"})
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "private"})
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "unlisted"})
{:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
res_conn = get(conn, "/api/v1/timelines/public")
assert length(json_response(res_conn, 200)) == 1
end
describe "posting statuses" do describe "posting statuses" do
setup do setup do
user = insert(:user) user = insert(:user)
@ -998,9 +1014,9 @@ test "list of notifications", %{conn: conn} do
|> get("/api/v1/notifications") |> get("/api/v1/notifications")
expected_response = expected_response =
"hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{ ~s(hi <span class="h-card"><a data-user="#{user.id}" class="u-url mention" href="#{
user.ap_id user.ap_id
}\">@<span>#{user.nickname}</span></a></span>" }" rel="ugc">@<span>#{user.nickname}</span></a></span>)
assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200) assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
assert response == expected_response assert response == expected_response
@ -1020,9 +1036,9 @@ test "getting a single notification", %{conn: conn} do
|> get("/api/v1/notifications/#{notification.id}") |> get("/api/v1/notifications/#{notification.id}")
expected_response = expected_response =
"hi <span class=\"h-card\"><a data-user=\"#{user.id}\" class=\"u-url mention\" href=\"#{ ~s(hi <span class="h-card"><a data-user="#{user.id}" class="u-url mention" href="#{
user.ap_id user.ap_id
}\">@<span>#{user.nickname}</span></a></span>" }" rel="ugc">@<span>#{user.nickname}</span></a></span>)
assert %{"status" => %{"content" => response}} = json_response(conn, 200) assert %{"status" => %{"content" => response}} = json_response(conn, 200)
assert response == expected_response assert response == expected_response

View File

@ -831,6 +831,33 @@ test "rejects token exchange for valid credentials belonging to deactivated user
refute Map.has_key?(resp, "access_token") refute Map.has_key?(resp, "access_token")
end end
test "rejects token exchange for user with password_reset_pending set to true" do
password = "testpassword"
user =
insert(:user,
password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
info: %{password_reset_pending: true}
)
app = insert(:oauth_app, scopes: ["read", "write"])
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "password",
"username" => user.nickname,
"password" => password,
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert resp = json_response(conn, 403)
assert resp["error"] == "Password reset is required"
refute Map.has_key?(resp, "access_token")
end
test "rejects an invalid authorization code" do test "rejects an invalid authorization code" do
app = insert(:oauth_app) app = insert(:oauth_app)

Some files were not shown because too many files have changed in this diff Show More