Page MenuHomePhorge

No OneTemporary

Size
97 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 47279dc0b..c7de08634 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -1,244 +1,244 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload do
@moduledoc """
Manage user uploads
Options:
* `:type`: presets for activity type (defaults to Document) and size limits from app configuration
* `:description`: upload alternative text
* `:base_url`: override base url
* `:uploader`: override uploader
* `:filters`: override filters
* `:size_limit`: override size limit
* `:activity_type`: override activity type
The `%Pleroma.Upload{}` struct: all documented fields are meant to be overwritten in filters:
* `:id` - the upload id.
* `:name` - the upload file name.
* `:path` - the upload path: set at first to `id/name` but can be changed. Keep in mind that the path
is once created permanent and changing it (especially in uploaders) is probably a bad idea!
* `:tempfile` - path to the temporary file. Prefer in-place changes on the file rather than changing the
path as the temporary file is also tracked by `Plug.Upload{}` and automatically deleted once the request is over.
Related behaviors:
* `Pleroma.Uploaders.Uploader`
* `Pleroma.Upload.Filter`
"""
alias Ecto.UUID
require Logger
@type source ::
Plug.Upload.t()
| (data_uri_string :: String.t())
| {:from_local, name :: String.t(), id :: String.t(), path :: String.t()}
| map()
@type option ::
{:type, :avatar | :banner | :background}
| {:description, String.t()}
| {:activity_type, String.t()}
| {:size_limit, nil | non_neg_integer()}
| {:uploader, module()}
| {:filters, [module()]}
@type t :: %__MODULE__{
id: String.t(),
name: String.t(),
tempfile: String.t(),
content_type: String.t(),
path: String.t()
}
defstruct [:id, :name, :tempfile, :content_type, :path]
defp get_description(opts, upload) do
case {opts[:description], Pleroma.Config.get([Pleroma.Upload, :default_description])} do
{description, _} when is_binary(description) -> description
{_, :filename} -> upload.name
{_, str} when is_binary(str) -> str
_ -> ""
end
end
@spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
@doc "Store a file. If using a `Plug.Upload{}` as the source, be sure to use `Majic.Plug` to ensure its content_type and filename is correct."
def store(upload, opts \\ []) do
opts = get_opts(opts)
with {:ok, upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
:ok <- check_filename_extension(upload.name, opts),
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
description = get_description(opts, upload),
{_, true} <-
{:description_limit,
String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
{:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok,
%{
"type" => opts.activity_type,
"mediaType" => upload.content_type,
"url" => [
%{
"type" => "Link",
"mediaType" => upload.content_type,
"href" => url_from_spec(upload, opts.base_url, url_spec)
}
],
"name" => description,
- "filename" => Map.get(opts, :filename) || upload.name
+ "filename" => Map.get(opts, :filename)
}}
else
{:description_limit, _} ->
{:error, :description_too_long}
{:error, error} ->
Logger.error(
"#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
)
{:error, error}
end
end
def char_unescaped?(char) do
URI.char_unreserved?(char) or char == ?/
end
defp get_opts(opts) do
{size_limit, activity_type} =
case Keyword.get(opts, :type) do
:banner ->
{Pleroma.Config.get!([:instance, :banner_upload_limit]), "Image"}
:avatar ->
{Pleroma.Config.get!([:instance, :avatar_upload_limit]), "Image"}
:background ->
{Pleroma.Config.get!([:instance, :background_upload_limit]), "Image"}
_ ->
{Pleroma.Config.get!([:instance, :upload_limit]), "Document"}
end
%{
activity_type: Keyword.get(opts, :activity_type, activity_type),
size_limit: Keyword.get(opts, :size_limit, size_limit),
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
filters: Keyword.get(opts, :filters, Pleroma.Config.get([__MODULE__, :filters])),
description: Keyword.get(opts, :description),
filename: Keyword.get(opts, :filename),
base_url:
Keyword.get(
opts,
:base_url,
Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url())
)
}
end
defp prepare_upload(%Plug.Upload{} = file, opts) do
with :ok <- check_file_size(file.path, opts.size_limit) do
{:ok,
%__MODULE__{
id: UUID.generate(),
name: file.filename,
tempfile: file.path,
content_type: file.content_type
}}
end
end
defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"], ignore: :whitespace)
hash = Base.encode16(:crypto.hash(:sha256, data), lower: true)
with :ok <- check_binary_size(data, opts.size_limit),
tmp_path <- tempfile_for_image(data),
{:ok, %{mime_type: content_type}} <-
Majic.perform({:bytes, data}, pool: Pleroma.MajicPool),
[ext | _] <- MIME.extensions(content_type) do
{:ok,
%__MODULE__{
id: UUID.generate(),
name: hash <> "." <> ext,
tempfile: tmp_path,
content_type: content_type
}}
end
end
# For Mix.Tasks.MigrateLocalUploads
defp prepare_upload(%__MODULE__{tempfile: path} = upload, _opts) do
with {:ok, %{mime_type: content_type}} <- Majic.perform(path, pool: Pleroma.MajicPool) do
{:ok, %__MODULE__{upload | content_type: content_type}}
end
end
defp check_binary_size(binary, size_limit)
when is_integer(size_limit) and size_limit > 0 and byte_size(binary) >= size_limit do
{:error, :file_too_large}
end
defp check_binary_size(_, _), do: :ok
defp check_file_size(path, size_limit) when is_integer(size_limit) and size_limit > 0 do
with {:ok, %{size: size}} <- File.stat(path),
true <- size <= size_limit do
:ok
else
false -> {:error, :file_too_large}
error -> error
end
end
defp check_file_size(_, _), do: :ok
defp check_filename_extension(name, %{filename: filename}) when is_binary(filename) do
if Path.extname(name) == Path.extname(filename) do
:ok
else
{:error, :invalid_filename_extension}
end
end
defp check_filename_extension(_, _), do: :ok
# Creates a tempfile using the Plug.Upload Genserver which cleans them up
# automatically.
defp tempfile_for_image(data) do
{:ok, tmp_path} = Plug.Upload.random_file("profile_pics")
{:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary])
IO.binwrite(tmp_file, data)
tmp_path
end
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
path =
URI.encode(path, &char_unescaped?/1) <>
if Pleroma.Config.get([__MODULE__, :link_name], false) do
"?name=#{URI.encode(name, &char_unescaped?/1)}"
else
""
end
prefix =
if is_nil(Pleroma.Config.get([__MODULE__, :base_url])) do
"media"
else
""
end
[base_url, prefix, path]
|> Path.join()
end
defp url_from_spec(_upload, _base_url, {:url, url}), do: url
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 1c74ea787..385414c41 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -1,535 +1,535 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.Utils do
import Pleroma.Web.Gettext
import Pleroma.Web.ControllerHelper, only: [truthy_param?: 1]
alias Calendar.Strftime
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI.ActivityDraft
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.Plugs.AuthenticationPlug
require Logger
require Pleroma.Constants
def attachments_from_ids(%{media_ids: ids, descriptions: desc}) do
attachments_from_ids_descs(ids, desc)
end
def attachments_from_ids(%{media_ids: ids}) do
attachments_from_ids_no_descs(ids)
end
def attachments_from_ids(_), do: []
def attachments_from_ids_no_descs([]), do: []
def attachments_from_ids_no_descs(ids) do
Enum.map(ids, fn media_id ->
case Repo.get(Object, media_id) do
%Object{data: data} -> data
_ -> nil
end
end)
|> Enum.reject(&is_nil/1)
end
def attachments_from_ids_descs([], _), do: []
def attachments_from_ids_descs(ids, descs_str) do
{_, descs} = Jason.decode(descs_str)
Enum.map(ids, fn media_id ->
with %Object{data: data} <- Repo.get(Object, media_id) do
Map.put(data, "name", descs[media_id])
end
end)
|> Enum.reject(&is_nil/1)
end
@spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
def get_to_and_cc(%{in_reply_to_conversation: %Participation{} = participation}) do
participation = Repo.preload(participation, :recipients)
{Enum.map(participation.recipients, & &1.ap_id), []}
end
def get_to_and_cc(%{visibility: visibility} = draft) when visibility in ["public", "local"] do
to =
case visibility do
"public" -> [Pleroma.Constants.as_public() | draft.mentions]
"local" -> [Pleroma.Constants.as_local_public() | draft.mentions]
end
cc = [draft.user.follower_address]
if draft.in_reply_to do
{Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc}
else
{to, cc}
end
end
def get_to_and_cc(%{visibility: "unlisted"} = draft) do
to = [draft.user.follower_address | draft.mentions]
cc = [Pleroma.Constants.as_public()]
if draft.in_reply_to do
{Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc}
else
{to, cc}
end
end
def get_to_and_cc(%{visibility: "private"} = draft) do
{to, cc} = get_to_and_cc(struct(draft, visibility: "direct"))
{[draft.user.follower_address | to], cc}
end
def get_to_and_cc(%{visibility: "direct"} = draft) do
# If the OP is a DM already, add the implicit actor.
if draft.in_reply_to && Visibility.is_direct?(draft.in_reply_to) do
{Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]), []}
else
{draft.mentions, []}
end
end
def get_to_and_cc(%{visibility: {:list, _}, mentions: mentions}), do: {mentions, []}
def get_addressed_users(_, to) when is_list(to) do
User.get_ap_ids_by_nicknames(to)
end
def get_addressed_users(mentioned_users, _), do: mentioned_users
def maybe_add_list_data(activity_params, user, {:list, list_id}) do
case Pleroma.List.get(list_id, user) do
%Pleroma.List{} = list ->
activity_params
|> put_in([:additional, "bcc"], [list.ap_id])
|> put_in([:additional, "listMessage"], list.ap_id)
|> put_in([:object, "listMessage"], list.ap_id)
_ ->
activity_params
end
end
def maybe_add_list_data(activity_params, _, _), do: activity_params
def make_poll_data(%{"poll" => %{"expires_in" => expires_in}} = data)
when is_binary(expires_in) do
# In some cases mastofe sends out strings instead of integers
data
|> put_in(["poll", "expires_in"], String.to_integer(expires_in))
|> make_poll_data()
end
def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data)
when is_list(options) do
limits = Config.get([:instance, :poll_limits])
with :ok <- validate_poll_expiration(expires_in, limits),
:ok <- validate_poll_options_amount(options, limits),
:ok <- validate_poll_options_length(options, limits) do
{option_notes, emoji} =
Enum.map_reduce(options, %{}, fn option, emoji ->
note = %{
"name" => option,
"type" => "Note",
"replies" => %{"type" => "Collection", "totalItems" => 0}
}
{note, Map.merge(emoji, Pleroma.Emoji.Formatter.get_emoji_map(option))}
end)
end_time =
DateTime.utc_now()
|> DateTime.add(expires_in)
|> DateTime.to_iso8601()
key = if truthy_param?(data.poll[:multiple]), do: "anyOf", else: "oneOf"
poll = %{"type" => "Question", key => option_notes, "closed" => end_time}
{:ok, {poll, emoji}}
end
end
def make_poll_data(%{"poll" => poll}) when is_map(poll) do
{:error, "Invalid poll"}
end
def make_poll_data(_data) do
{:ok, {%{}, %{}}}
end
defp validate_poll_options_amount(options, %{max_options: max_options}) do
if Enum.count(options) > max_options do
{:error, "Poll can't contain more than #{max_options} options"}
else
:ok
end
end
defp validate_poll_options_length(options, %{max_option_chars: max_option_chars}) do
if Enum.any?(options, &(String.length(&1) > max_option_chars)) do
{:error, "Poll options cannot be longer than #{max_option_chars} characters each"}
else
:ok
end
end
defp validate_poll_expiration(expires_in, %{min_expiration: min, max_expiration: max}) do
cond do
expires_in > max -> {:error, "Expiration date is too far in the future"}
expires_in < min -> {:error, "Expiration date is too soon"}
true -> :ok
end
end
def make_content_html(%ActivityDraft{} = draft) do
attachment_links =
draft.params
|> Map.get("attachment_links", Config.get([:instance, :attachment_links]))
|> truthy_param?()
content_type = get_content_type(draft.params[:content_type])
options =
if draft.visibility == "direct" && Config.get([:instance, :safe_dm_mentions]) do
[safe_mention: true]
else
[]
end
draft.status
|> format_input(content_type, options)
|> maybe_add_attachments(draft.attachments, attachment_links)
|> maybe_add_nsfw_tag(draft.params)
end
defp get_content_type(content_type) do
if Enum.member?(Config.get([:instance, :allowed_post_formats]), content_type) do
content_type
else
"text/plain"
end
end
defp maybe_add_nsfw_tag({text, mentions, tags}, %{"sensitive" => sensitive})
when sensitive in [true, "True", "true", "1"] do
{text, mentions, [{"#nsfw", "nsfw"} | tags]}
end
defp maybe_add_nsfw_tag(data, _), do: data
def make_context(_, %Participation{} = participation) do
Repo.preload(participation, :conversation).conversation.ap_id
end
def make_context(%Activity{data: %{"context" => context}}, _), do: context
def make_context(_, _), do: Utils.generate_context_id()
def maybe_add_attachments(parsed, _attachments, false = _no_links), do: parsed
def maybe_add_attachments({text, mentions, tags}, attachments, _no_links) do
text = add_attachments(text, attachments)
{text, mentions, tags}
end
def add_attachments(text, attachments) do
attachment_text = Enum.map(attachments, &build_attachment_link/1)
Enum.join([text | attachment_text], "<br>")
end
defp build_attachment_link(%{"url" => [%{"href" => href} | _]} = attachment) do
- name = attachment["name"] || URI.decode(Path.basename(href))
+ name = attachment["filename"] || attachment["name"] || URI.decode(Path.basename(href))
href = MediaProxy.url(href)
"<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>"
end
defp build_attachment_link(_), do: ""
def format_input(text, format, options \\ [])
@doc """
Formatting text to plain text, BBCode, HTML, or Markdown
"""
def format_input(text, "text/plain", options) do
text
|> Formatter.html_escape("text/plain")
|> Formatter.linkify(options)
|> (fn {text, mentions, tags} ->
{String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
end).()
end
def format_input(text, "text/bbcode", options) do
text
|> String.replace(~r/\r/, "")
|> Formatter.html_escape("text/plain")
|> BBCode.to_html()
|> (fn {:ok, html} -> html end).()
|> Formatter.linkify(options)
end
def format_input(text, "text/html", options) do
text
|> Formatter.html_escape("text/html")
|> Formatter.linkify(options)
end
def format_input(text, "text/markdown", options) do
text
|> Formatter.mentions_escape(options)
|> Earmark.as_html!(%Earmark.Options{renderer: Pleroma.EarmarkRenderer})
|> Formatter.linkify(options)
|> Formatter.html_escape("text/html")
end
def make_note_data(%ActivityDraft{} = draft) do
%{
"type" => "Note",
"to" => draft.to,
"cc" => draft.cc,
"content" => draft.content_html,
"summary" => draft.summary,
"sensitive" => draft.sensitive,
"context" => draft.context,
"attachment" => draft.attachments,
"actor" => draft.user.ap_id,
"tag" => Keyword.values(draft.tags) |> Enum.uniq()
}
|> add_in_reply_to(draft.in_reply_to)
|> Map.merge(draft.extra)
end
defp add_in_reply_to(object, nil), do: object
defp add_in_reply_to(object, in_reply_to) do
with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do
Map.put(object, "inReplyTo", in_reply_to_object.data["id"])
else
_ -> object
end
end
def format_naive_asctime(date) do
date |> DateTime.from_naive!("Etc/UTC") |> format_asctime
end
def format_asctime(date) do
Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")
end
def date_to_asctime(date) when is_binary(date) do
with {:ok, date, _offset} <- DateTime.from_iso8601(date) do
format_asctime(date)
else
_e ->
Logger.warn("Date #{date} in wrong format, must be ISO 8601")
""
end
end
def date_to_asctime(date) do
Logger.warn("Date #{date} in wrong format, must be ISO 8601")
""
end
def to_masto_date(%NaiveDateTime{} = date) do
date
|> NaiveDateTime.to_iso8601()
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
end
def to_masto_date(date) when is_binary(date) do
with {:ok, date} <- NaiveDateTime.from_iso8601(date) do
to_masto_date(date)
else
_ -> ""
end
end
def to_masto_date(_), do: ""
defp shortname(name) do
with max_length when max_length > 0 <-
Config.get([Pleroma.Upload, :filename_display_max_length], 30),
true <- String.length(name) > max_length do
String.slice(name, 0..max_length) <> "…"
else
_ -> name
end
end
@spec confirm_current_password(User.t(), String.t()) :: {:ok, User.t()} | {:error, String.t()}
def confirm_current_password(user, password) do
with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do
{:ok, db_user}
else
_ -> {:error, dgettext("errors", "Invalid password.")}
end
end
def maybe_notify_to_recipients(
recipients,
%Activity{data: %{"to" => to, "type" => _type}} = _activity
) do
recipients ++ to
end
def maybe_notify_to_recipients(recipients, _), do: recipients
def maybe_notify_mentioned_recipients(
recipients,
%Activity{data: %{"to" => _to, "type" => type} = data} = activity
)
when type == "Create" do
object = Object.normalize(activity, false)
object_data =
cond do
not is_nil(object) ->
object.data
is_map(data["object"]) ->
data["object"]
true ->
%{}
end
tagged_mentions = maybe_extract_mentions(object_data)
recipients ++ tagged_mentions
end
def maybe_notify_mentioned_recipients(recipients, _), do: recipients
# Do not notify subscribers if author is making a reply
def maybe_notify_subscribers(recipients, %Activity{
object: %Object{data: %{"inReplyTo" => _ap_id}}
}) do
recipients
end
def maybe_notify_subscribers(
recipients,
%Activity{data: %{"actor" => actor, "type" => type}} = activity
)
when type == "Create" do
with %User{} = user <- User.get_cached_by_ap_id(actor) do
subscriber_ids =
user
|> User.subscriber_users()
|> Enum.filter(&Visibility.visible_for_user?(activity, &1))
|> Enum.map(& &1.ap_id)
recipients ++ subscriber_ids
else
_e -> recipients
end
end
def maybe_notify_subscribers(recipients, _), do: recipients
def maybe_notify_followers(recipients, %Activity{data: %{"type" => "Move"}} = activity) do
with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do
user
|> User.get_followers()
|> Enum.map(& &1.ap_id)
|> Enum.concat(recipients)
else
_e -> recipients
end
end
def maybe_notify_followers(recipients, _), do: recipients
def maybe_extract_mentions(%{"tag" => tag}) do
tag
|> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end)
|> Enum.map(fn x -> x["href"] end)
|> Enum.uniq()
end
def maybe_extract_mentions(_), do: []
def make_report_content_html(nil), do: {:ok, {nil, [], []}}
def make_report_content_html(comment) do
max_size = Config.get([:instance, :max_report_comment_size], 1000)
if String.length(comment) <= max_size do
{:ok, format_input(comment, "text/plain")}
else
{:error,
dgettext("errors", "Comment must be up to %{max_size} characters", max_size: max_size)}
end
end
def get_report_statuses(%User{ap_id: actor}, %{status_ids: status_ids})
when is_list(status_ids) do
{:ok, Activity.all_by_actor_and_id(actor, status_ids)}
end
def get_report_statuses(_, _), do: {:ok, nil}
# DEPRECATED mostly, context objects are now created at insertion time.
def context_to_conversation_id(context) do
with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
id
else
_e ->
changeset = Object.context_mapping(context)
case Repo.insert(changeset) do
{:ok, %{id: id}} ->
id
# This should be solved by an upsert, but it seems ecto
# has problems accessing the constraint inside the jsonb.
{:error, _} ->
Object.get_cached_by_ap_id(context).id
end
end
end
def conversation_id_to_context(id) do
with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
context
else
_e ->
{:error, dgettext("errors", "No such conversation")}
end
end
def validate_character_limit("" = _full_payload, [] = _attachments) do
{:error, dgettext("errors", "Cannot post an empty status without attachments")}
end
def validate_character_limit(full_payload, _attachments) do
limit = Config.get([:instance, :limit])
length = String.length(full_payload)
if length <= limit do
:ok
else
{:error, dgettext("errors", "The status is over the character limit")}
end
end
end
diff --git a/test/pleroma/upload_test.exs b/test/pleroma/upload_test.exs
index fc5b0acc4..21bdb6ee8 100644
--- a/test/pleroma/upload_test.exs
+++ b/test/pleroma/upload_test.exs
@@ -1,307 +1,307 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UploadTest do
use Pleroma.DataCase
import ExUnit.CaptureLog
alias Pleroma.Upload
alias Pleroma.Uploaders.Uploader
@upload_file %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "image.jpg"
}
defmodule TestUploaderBase do
def put_file(%{path: path} = _upload, module_name) do
task_pid =
Task.async(fn ->
:timer.sleep(10)
{Uploader, path}
|> :global.whereis_name()
|> send({Uploader, self(), {:test}, %{}})
assert_receive {Uploader, {:test}}, 4_000
end)
Agent.start(fn -> task_pid end, name: module_name)
:wait_callback
end
end
describe "Tried storing a file when http callback response success result" do
defmodule TestUploaderSuccess do
def http_callback(conn, _params),
do: {:ok, conn, {:file, "post-process-file.jpg"}}
def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__)
end
setup do: [uploader: TestUploaderSuccess]
setup [:ensure_local_uploader]
test "it returns file" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
assert Upload.store(@upload_file) ==
{:ok,
%{
"name" => "image.jpg",
"type" => "Document",
"mediaType" => "image/jpeg",
"url" => [
%{
"href" => "http://localhost:4001/media/post-process-file.jpg",
"mediaType" => "image/jpeg",
"type" => "Link"
}
],
- "filename" => "image.jpg"
+ "filename" => nil
}}
Task.await(Agent.get(TestUploaderSuccess, fn task_pid -> task_pid end))
end
end
describe "Tried storing a file when http callback response error" do
defmodule TestUploaderError do
def http_callback(conn, _params), do: {:error, conn, "Errors"}
def put_file(upload), do: TestUploaderBase.put_file(upload, __MODULE__)
end
setup do: [uploader: TestUploaderError]
setup [:ensure_local_uploader]
test "it returns error" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
assert capture_log(fn ->
assert Upload.store(@upload_file) == {:error, "Errors"}
Task.await(Agent.get(TestUploaderError, fn task_pid -> task_pid end))
end) =~
"[error] Elixir.Pleroma.Upload store (using Pleroma.UploadTest.TestUploaderError) failed: \"Errors\""
end
end
describe "Tried storing a file when http callback doesn't response by timeout" do
defmodule(TestUploader, do: def(put_file(_upload), do: :wait_callback))
setup do: [uploader: TestUploader]
setup [:ensure_local_uploader]
test "it returns error" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
assert capture_log(fn ->
assert Upload.store(@upload_file) == {:error, "Uploader callback timeout"}
end) =~
"[error] Elixir.Pleroma.Upload store (using Pleroma.UploadTest.TestUploader) failed: \"Uploader callback timeout\""
end
end
describe "Storing a file with the Local uploader" do
setup [:ensure_local_uploader]
test "does not allow descriptions longer than the post limit" do
clear_config([:instance, :description_limit], 2)
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "image.jpg"
}
{:error, :description_too_long} = Upload.store(file, description: "123")
end
test "saves filename from opts" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
desc = "sample file"
filename = "test image.jpg"
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "image.jpg"
}
{:ok, data} = Upload.store(file, description: desc, filename: filename)
assert data["name"] == desc
assert data["filename"] == filename
end
- test "saves default filename if opts don't have one" do
+ test "sets filename to nil if opts don't have one" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
desc = "sample file"
filename = "image_tmp.jpg"
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: filename
}
{:ok, data} = Upload.store(file, description: desc)
assert data["name"] == desc
- assert data["filename"] == filename
+ refute data["filename"]
end
@tag capture_log: true
test "raise error when custom filename has different extension than original one" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
fake_name = "free_coins.exe"
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "image_tmp.jpg"
}
assert Upload.store(file, filename: fake_name) == {:error, :invalid_filename_extension}
end
test "returns a media url" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "image.jpg"
}
{:ok, data} = Upload.store(file)
assert %{"url" => [%{"href" => url}]} = data
assert String.starts_with?(url, Pleroma.Web.base_url() <> "/media/")
end
test "copies the file to the configured folder with deduping" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image.jpg"
}
{:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe])
assert List.first(data["url"])["href"] ==
Pleroma.Web.base_url() <>
"/media/e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg"
end
test "copies the file to the configured folder without deduping" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image.jpg"
}
{:ok, data} = Upload.store(file)
assert data["name"] == "an [image.jpg"
end
test "fixes incorrect content type when base64 is given" do
params = %{
img: "data:image/png;base64,#{Base.encode64(File.read!("test/fixtures/image.jpg"))}"
}
{:ok, data} = Upload.store(params)
assert hd(data["url"])["mediaType"] == "image/jpeg"
end
test "adds extension when base64 is given" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
params = %{
img: "data:image/png;base64,#{Base.encode64(File.read!("test/fixtures/image.jpg"))}"
}
{:ok, data} = Upload.store(params)
assert String.ends_with?(data["name"], ".jpg")
end
test "copies the file to the configured folder with anonymizing filename" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image.jpg"
}
{:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.AnonymizeFilename])
refute data["name"] == "an [image.jpg"
end
test "escapes invalid characters in url" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an… image.jpg"
}
{:ok, data} = Upload.store(file)
[attachment_url | _] = data["url"]
assert Path.basename(attachment_url["href"]) == "an%E2%80%A6%20image.jpg"
end
test "escapes reserved uri characters" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: ":?#[]@!$&\\'()*+,;=.jpg"
}
{:ok, data} = Upload.store(file)
[attachment_url | _] = data["url"]
assert Path.basename(attachment_url["href"]) ==
"%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg"
end
end
describe "Setting a custom base_url for uploaded media" do
setup do: clear_config([Pleroma.Upload, :base_url], "https://cache.pleroma.social")
test "returns a media url with configured base_url" do
base_url = Pleroma.Config.get([Pleroma.Upload, :base_url])
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "image.jpg"
}
{:ok, data} = Upload.store(file, base_url: base_url)
assert %{"url" => [%{"href" => url}]} = data
refute String.starts_with?(url, base_url <> "/media/")
end
end
end
diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
index 3dce0de27..12df4fce2 100644
--- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
@@ -1,1839 +1,1877 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
setup do: clear_config([:instance, :federating])
setup do: clear_config([:instance, :allow_relay])
setup do: clear_config([:rich_media, :enabled])
setup do: clear_config([:mrf, :policies])
setup do: clear_config([:mrf_keyword, :reject])
describe "posting statuses" do
setup do: oauth_access(["write:statuses"])
test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
Config.put([:instance, :federating], true)
Config.get([:instance, :allow_relay], true)
response =
conn
|> put_req_header("content-type", "application/json")
|> post("api/v1/statuses", %{
"content_type" => "text/plain",
"source" => "Pleroma FE",
"status" => "Hello world",
"visibility" => "public"
})
|> json_response_and_validate_schema(200)
assert response["reblogs_count"] == 0
ObanHelpers.perform_all()
response =
conn
|> get("api/v1/statuses/#{response["id"]}", %{})
|> json_response_and_validate_schema(200)
assert response["reblogs_count"] == 0
end
test "posting a status", %{conn: conn} do
idempotency_key = "Pikachu rocks!"
conn_one =
conn
|> put_req_header("content-type", "application/json")
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "0"
})
{:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key)
# Six hours
assert ttl > :timer.seconds(6 * 60 * 60 - 1)
assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
json_response_and_validate_schema(conn_one, 200)
assert Activity.get_by_id(id)
conn_two =
conn
|> put_req_header("content-type", "application/json")
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => 0
})
assert %{"id" => second_id} = json_response(conn_two, 200)
assert id == second_id
conn_three =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "False"
})
assert %{"id" => third_id} = json_response_and_validate_schema(conn_three, 200)
refute id == third_id
# An activity that will expire:
# 2 hours
expires_in = 2 * 60 * 60
expires_at = DateTime.add(DateTime.utc_now(), expires_in)
conn_four =
conn
|> put_req_header("content-type", "application/json")
|> post("api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200)
assert Activity.get_by_id(fourth_id)
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: fourth_id},
scheduled_at: expires_at
)
end
+ test "posting a status with an attachment", %{user: user, conn: conn} do
+ clear_config([:instance, :attachment_links], true)
+ filename = "an_image.jpg"
+ custom_filename = "look at this.jpg"
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: filename
+ }
+
+ {:ok, upload} =
+ ActivityPub.upload(file,
+ actor: user.ap_id,
+ description: "test image",
+ filename: custom_filename
+ )
+
+ response =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/statuses", %{
+ "status" => "cofe",
+ "spoiler_text" => "2hu",
+ "media_ids" => [to_string(upload.id)]
+ })
+ |> json_response_and_validate_schema(200)
+
+ assert String.ends_with?(response["content"], "#{filename}\">#{custom_filename}</a>")
+ assert length(response["media_attachments"]) == 1
+ end
+
test "it fails to create a status if `expires_in` is less or equal than an hour", %{
conn: conn
} do
# 1 minute
expires_in = 1 * 60
assert %{"error" => "Expiry date is too soon"} =
conn
|> put_req_header("content-type", "application/json")
|> post("api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
|> json_response_and_validate_schema(422)
# 5 minutes
expires_in = 5 * 60
assert %{"error" => "Expiry date is too soon"} =
conn
|> put_req_header("content-type", "application/json")
|> post("api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
|> json_response_and_validate_schema(422)
end
test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do
Config.put([:mrf_keyword, :reject], ["GNO"])
Config.put([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} =
conn
|> put_req_header("content-type", "application/json")
|> post("api/v1/statuses", %{"status" => "GNO/Linux"})
|> json_response_and_validate_schema(422)
end
test "posting an undefined status with an attachment", %{user: user, conn: conn} do
+ clear_config([:instance, :attachment_links], true)
+ filename = "an_image.jpg"
+ description = "test image"
+
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
- filename: "an_image.jpg"
+ filename: filename
}
- {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
+ {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id, description: description)
- conn =
+ response =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)]
})
+ |> json_response_and_validate_schema(200)
- assert json_response_and_validate_schema(conn, 200)
+ assert String.ends_with?(response["content"], "#{filename}\">#{description}</a>")
+ assert length(response["media_attachments"]) == 1
end
test "replying to a status", %{user: user, conn: conn} do
{:ok, replied_to} = CommonAPI.post(user, %{status: "cofe"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
activity = Activity.get_by_id(id)
assert activity.data["context"] == replied_to.data["context"]
assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
end
test "replying to a direct message with visibility other than direct", %{
user: user,
conn: conn
} do
{:ok, replied_to} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
Enum.each(["public", "private", "unlisted"], fn visibility ->
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "@#{user.nickname} hey",
"in_reply_to_id" => replied_to.id,
"visibility" => visibility
})
assert json_response_and_validate_schema(conn, 422) == %{
"error" => "The message visibility must be direct"
}
end)
end
test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a sensitive status", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
assert %{"content" => "cofe", "id" => id, "sensitive" => true} =
json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a fake status", %{conn: conn} do
real_conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" =>
"\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it"
})
real_status = json_response_and_validate_schema(real_conn, 200)
assert real_status
assert Object.get_by_ap_id(real_status["uri"])
real_status =
real_status
|> Map.put("id", nil)
|> Map.put("url", nil)
|> Map.put("uri", nil)
|> Map.put("created_at", nil)
|> Kernel.put_in(["pleroma", "conversation_id"], nil)
fake_conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" =>
"\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it",
"preview" => true
})
fake_status = json_response_and_validate_schema(fake_conn, 200)
assert fake_status
refute Object.get_by_ap_id(fake_status["uri"])
fake_status =
fake_status
|> Map.put("id", nil)
|> Map.put("url", nil)
|> Map.put("uri", nil)
|> Map.put("created_at", nil)
|> Kernel.put_in(["pleroma", "conversation_id"], nil)
assert real_status == fake_status
end
test "fake statuses' preview card is not cached", %{conn: conn} do
clear_config([:rich_media, :enabled], true)
Tesla.Mock.mock(fn
%{
method: :get,
url: "https://example.com/twitter-card"
} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}
env ->
apply(HttpRequestMock, :request, [env])
end)
conn1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "https://example.com/ogp",
"preview" => true
})
conn2 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "https://example.com/twitter-card",
"preview" => true
})
assert %{"card" => %{"title" => "The Rock"}} = json_response_and_validate_schema(conn1, 200)
assert %{"card" => %{"title" => "Small Island Developing States Photo Submission"}} =
json_response_and_validate_schema(conn2, 200)
end
test "posting a status with OGP link preview", %{conn: conn} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
clear_config([:rich_media, :enabled], true)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "https://example.com/ogp"
})
assert %{"id" => id, "card" => %{"title" => "The Rock"}} =
json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a direct status", %{conn: conn} do
user2 = insert(:user)
content = "direct cofe @#{user2.nickname}"
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"})
assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200)
assert response["visibility"] == "direct"
assert response["pleroma"]["direct_conversation_id"]
assert activity = Activity.get_by_id(id)
assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
assert activity.data["to"] == [user2.ap_id]
assert activity.data["cc"] == []
end
end
describe "posting scheduled statuses" do
setup do: oauth_access(["write:statuses"])
test "creates a scheduled activity", %{conn: conn} do
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "scheduled",
"scheduled_at" => scheduled_at
})
assert %{"scheduled_at" => expected_scheduled_at} =
json_response_and_validate_schema(conn, 200)
assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
assert [] == Repo.all(Activity)
end
test "ignores nil values", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "not scheduled",
"scheduled_at" => nil
})
assert result = json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(result["id"])
end
test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
scheduled_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(120), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} =
ActivityPub.upload(file,
actor: user.ap_id,
description: "sample image",
filename: "look at this.jpg"
)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)],
"status" => "scheduled",
"scheduled_at" => scheduled_at
})
assert %{"media_attachments" => [media_attachment]} =
json_response_and_validate_schema(conn, 200)
assert %{"description" => "sample image", "type" => "image"} = media_attachment
assert media_attachment["pleroma"]["filename"] == "look at this.jpg"
end
test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
%{conn: conn} do
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "not scheduled",
"scheduled_at" => scheduled_at
})
assert %{"content" => "not scheduled"} = json_response_and_validate_schema(conn, 200)
assert [] == Repo.all(ScheduledActivity)
end
test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
# TODO
|> Kernel.<>("Z")
attrs = %{params: %{}, scheduled_at: today}
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, attrs)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
assert %{"error" => "daily limit exceeded"} == json_response_and_validate_schema(conn, 422)
end
test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
tomorrow =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.hours(36), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
attrs = %{params: %{}, scheduled_at: today}
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
assert %{"error" => "total limit exceeded"} == json_response_and_validate_schema(conn, 422)
end
end
describe "posting polls" do
setup do: oauth_access(["write:statuses"])
test "posting a poll", %{conn: conn} do
time = NaiveDateTime.utc_now()
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "Who is the #bestgrill?",
"poll" => %{
"options" => ["Rei", "Asuka", "Misato"],
"expires_in" => 420
}
})
response = json_response_and_validate_schema(conn, 200)
assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
title in ["Rei", "Asuka", "Misato"]
end)
assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
refute response["poll"]["expred"]
question = Object.get_by_id(response["poll"]["id"])
# closed contains utc timezone
assert question.data["closed"] =~ "Z"
end
test "option limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_options])
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "desu~",
"poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll can't contain more than #{limit} options"
end
test "option character limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_option_chars])
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "...",
"poll" => %{
"options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
"expires_in" => 1
}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll options cannot be longer than #{limit} characters each"
end
test "minimal date limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :min_expiration])
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
"expires_in" => limit - 1
}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Expiration date is too soon"
end
test "maximum date limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_expiration])
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
"expires_in" => limit + 1
}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Expiration date is too far in the future"
end
end
test "get a status" do
%{conn: conn} = oauth_access(["read:statuses"])
activity = insert(:note_activity)
conn = get(conn, "/api/v1/statuses/#{activity.id}")
assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
assert id == to_string(activity.id)
end
defp local_and_remote_activities do
local = insert(:note_activity)
remote = insert(:note_activity, local: false)
{:ok, local: local, remote: remote}
end
describe "status with restrict unauthenticated activities for local and remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert json_response_and_validate_schema(res_conn, :not_found) == %{
"error" => "Record not found"
}
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert json_response_and_validate_schema(res_conn, :not_found) == %{
"error" => "Record not found"
}
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
end
describe "status with restrict unauthenticated activities for local" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert json_response_and_validate_schema(res_conn, :not_found) == %{
"error" => "Record not found"
}
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
end
describe "status with restrict unauthenticated activities for remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert json_response_and_validate_schema(res_conn, :not_found) == %{
"error" => "Record not found"
}
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
end
test "getting a status that doesn't exist returns 404" do
%{conn: conn} = oauth_access(["read:statuses"])
activity = insert(:note_activity)
conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
test "get a direct status" do
%{user: user, conn: conn} = oauth_access(["read:statuses"])
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "direct"})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}")
[participation] = Participation.for_user(user)
res = json_response_and_validate_schema(conn, 200)
assert res["pleroma"]["direct_conversation_id"] == participation.id
end
test "get statuses by IDs" do
%{conn: conn} = oauth_access(["read:statuses"])
%{id: id1} = insert(:note_activity)
%{id: id2} = insert(:note_activity)
query_string = "ids[]=#{id1}&ids[]=#{id2}"
conn = get(conn, "/api/v1/statuses/?#{query_string}")
assert [%{"id" => ^id1}, %{"id" => ^id2}] =
Enum.sort_by(json_response_and_validate_schema(conn, :ok), & &1["id"])
end
describe "getting statuses by ids with restricted unauthenticated for local and remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
assert json_response_and_validate_schema(res_conn, 200) == []
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
end
describe "getting statuses by ids with restricted unauthenticated for local" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
remote_id = remote.id
assert [%{"id" => ^remote_id}] = json_response_and_validate_schema(res_conn, 200)
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
end
describe "getting statuses by ids with restricted unauthenticated for remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
local_id = local.id
assert [%{"id" => ^local_id}] = json_response_and_validate_schema(res_conn, 200)
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
end
describe "deleting a status" do
test "when you created it" do
%{user: author, conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity, user: author)
object = Object.normalize(activity)
content = object.data["content"]
source = object.data["source"]
result =
conn
|> assign(:user, author)
|> delete("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert match?(%{"content" => ^content, "text" => ^source}, result)
refute Activity.get_by_id(activity.id)
end
test "when it doesn't exist" do
%{user: author, conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity, user: author)
conn =
conn
|> assign(:user, author)
|> delete("/api/v1/statuses/#{String.downcase(activity.id)}")
assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
end
test "when you didn't create it" do
%{conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity)
conn = delete(conn, "/api/v1/statuses/#{activity.id}")
assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
assert Activity.get_by_id(activity.id) == activity
end
test "when you're an admin or moderator", %{conn: conn} do
activity1 = insert(:note_activity)
activity2 = insert(:note_activity)
admin = insert(:user, is_admin: true)
moderator = insert(:user, is_moderator: true)
res_conn =
conn
|> assign(:user, admin)
|> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"]))
|> delete("/api/v1/statuses/#{activity1.id}")
assert %{} = json_response_and_validate_schema(res_conn, 200)
res_conn =
conn
|> assign(:user, moderator)
|> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"]))
|> delete("/api/v1/statuses/#{activity2.id}")
assert %{} = json_response_and_validate_schema(res_conn, 200)
refute Activity.get_by_id(activity1.id)
refute Activity.get_by_id(activity2.id)
end
end
describe "reblogging" do
setup do: oauth_access(["write:statuses"])
test "reblogs and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true
} = json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{String.downcase(activity.id)}/reblog")
assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404)
end
test "reblogs privately and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post(
"/api/v1/statuses/#{activity.id}/reblog",
%{"visibility" => "private"}
)
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true,
"visibility" => "private"
} = json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "reblogged status for another user" do
activity = insert(:note_activity)
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, _} = CommonAPI.favorite(user2, activity.id)
{:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
{:ok, reblog_activity1} = CommonAPI.repeat(activity.id, user1)
{:ok, _} = CommonAPI.repeat(activity.id, user2)
conn_res =
build_conn()
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
"reblog" => %{"id" => _id, "reblogged" => false, "reblogs_count" => 2},
"reblogged" => false,
"favourited" => false,
"bookmarked" => false
} = json_response_and_validate_schema(conn_res, 200)
conn_res =
build_conn()
|> assign(:user, user2)
|> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
"reblogged" => true,
"favourited" => true,
"bookmarked" => true
} = json_response_and_validate_schema(conn_res, 200)
assert to_string(activity.id) == id
end
end
describe "unreblogging" do
setup do: oauth_access(["write:statuses"])
test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
activity = insert(:note_activity)
{:ok, _} = CommonAPI.repeat(activity.id, user)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unreblog")
assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} =
json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "returns 404 error when activity does not exist", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/foo/unreblog")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
describe "favoriting" do
setup do: oauth_access(["write:favourites"])
test "favs a status and returns it", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "favoriting twice will just return 200", %{conn: conn} do
activity = insert(:note_activity)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
|> json_response_and_validate_schema(200)
end
test "returns 404 error for a wrong id", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/favourite")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
describe "unfavoriting" do
setup do: oauth_access(["write:favourites"])
test "unfavorites a status and returns it", %{user: user, conn: conn} do
activity = insert(:note_activity)
{:ok, _} = CommonAPI.favorite(user, activity.id)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "returns 404 error for a wrong id", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/unfavourite")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
describe "pinned statuses" do
setup do: oauth_access(["write:accounts"])
setup %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
%{activity: activity}
end
setup do: clear_config([:instance, :max_pinned_statuses], 1)
test "pin status", %{conn: conn, user: user, activity: activity} do
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "pinned" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id_str, "pinned" => true}] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response_and_validate_schema(200)
end
test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
{:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{dm.id}/pin")
assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not pin"}
end
test "unpin status", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.pin(activity.id, user)
user = refresh_record(user)
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "pinned" => false} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unpin")
|> json_response_and_validate_schema(200)
assert [] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response_and_validate_schema(200)
end
test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/unpin")
assert json_response_and_validate_schema(conn, 400) == %{"error" => "Could not unpin"}
end
test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
{:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
id_str_one = to_string(activity_one.id)
assert %{"id" => ^id_str_one, "pinned" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{id_str_one}/pin")
|> json_response_and_validate_schema(200)
user = refresh_record(user)
assert %{"error" => "You have already pinned the maximum number of statuses"} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity_two.id}/pin")
|> json_response_and_validate_schema(400)
end
test "on pin removes deletion job, on unpin reschedule deletion" do
%{conn: conn} = oauth_access(["write:accounts", "write:statuses"])
expires_in = 2 * 60 * 60
expires_at = DateTime.add(DateTime.utc_now(), expires_in)
assert %{"id" => id} =
conn
|> put_req_header("content-type", "application/json")
|> post("api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
|> json_response_and_validate_schema(200)
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: expires_at
)
assert %{"id" => ^id, "pinned" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{id}/pin")
|> json_response_and_validate_schema(200)
refute_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: expires_at
)
assert %{"id" => ^id, "pinned" => false} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{id}/unpin")
|> json_response_and_validate_schema(200)
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: expires_at
)
end
end
describe "cards" do
setup do
Config.put([:rich_media, :enabled], true)
oauth_access(["read:statuses"])
end
test "returns rich-media card", %{conn: conn, user: user} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp"})
card_data = %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"provider_name" => "example.com",
"provider_url" => "https://example.com",
"title" => "The Rock",
"type" => "link",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
"pleroma" => %{
"opengraph" => %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"title" => "The Rock",
"type" => "video.movie",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
}
}
}
response =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response_and_validate_schema(200)
assert response == card_data
# works with private posts
{:ok, activity} =
CommonAPI.post(user, %{status: "https://example.com/ogp", visibility: "direct"})
response_two =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response_and_validate_schema(200)
assert response_two == card_data
end
test "replaces missing description with an empty string", %{conn: conn, user: user} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp-missing-data"})
response =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response_and_validate_schema(:ok)
assert response == %{
"type" => "link",
"title" => "Pleroma",
"description" => "",
"image" => nil,
"provider_name" => "example.com",
"provider_url" => "https://example.com",
"url" => "https://example.com/ogp-missing-data",
"pleroma" => %{
"opengraph" => %{
"title" => "Pleroma",
"type" => "website",
"url" => "https://example.com/ogp-missing-data"
}
}
}
end
end
test "bookmarks" do
bookmarks_uri = "/api/v1/bookmarks"
%{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
author = insert(:user)
{:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"})
{:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"})
response1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity1.id}/bookmark")
assert json_response_and_validate_schema(response1, 200)["bookmarked"] == true
response2 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity2.id}/bookmark")
assert json_response_and_validate_schema(response2, 200)["bookmarked"] == true
bookmarks = get(conn, bookmarks_uri)
assert [
json_response_and_validate_schema(response2, 200),
json_response_and_validate_schema(response1, 200)
] ==
json_response_and_validate_schema(bookmarks, 200)
response1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity1.id}/unbookmark")
assert json_response_and_validate_schema(response1, 200)["bookmarked"] == false
bookmarks = get(conn, bookmarks_uri)
assert [json_response_and_validate_schema(response2, 200)] ==
json_response_and_validate_schema(bookmarks, 200)
end
describe "conversation muting" do
setup do: oauth_access(["write:mutes"])
setup do
post_user = insert(:user)
{:ok, activity} = CommonAPI.post(post_user, %{status: "HIE"})
%{activity: activity}
end
test "mute conversation", %{conn: conn, activity: activity} do
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "muted" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/mute")
|> json_response_and_validate_schema(200)
end
test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/mute")
assert json_response_and_validate_schema(conn, 400) == %{
"error" => "conversation is already muted"
}
end
test "unmute conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "muted" => false} =
conn
# |> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unmute")
|> json_response_and_validate_schema(200)
end
end
test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, replied_to} = CommonAPI.post(user1, %{status: "cofe"})
# Reply to status from another user
conn1 =
conn
|> assign(:user, user2)
|> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn1, 200)
activity = Activity.get_by_id_with_object(id)
assert Object.normalize(activity).data["inReplyTo"] == Object.normalize(replied_to).data["id"]
assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
# Reblog from the third user
conn2 =
conn
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
json_response_and_validate_schema(conn2, 200)
assert to_string(activity.id) == id
# Getting third user status
conn3 =
conn
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
|> get("api/v1/timelines/home")
[reblogged_activity] = json_response(conn3, 200)
assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
end
describe "GET /api/v1/statuses/:id/favourited_by" do
setup do: oauth_access(["read:accounts"])
setup %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
%{activity: activity}
end
test "returns users who have favorited the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been favorited yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not return users who have favorited the status but are blocked", %{
conn: %{assigns: %{user: user}} = conn,
activity: activity
} do
other_user = insert(:user)
{:ok, _user_relationship} = User.block(user, other_user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not fail on an unauthenticated request", %{activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
build_conn()
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "requires authentication for private posts", %{user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "@#{other_user.nickname} wanna get some #cofe together?",
visibility: "direct"
})
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
build_conn()
|> get(favourited_by_url)
|> json_response_and_validate_schema(404)
conn =
build_conn()
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
conn
|> assign(:token, nil)
|> get(favourited_by_url)
|> json_response_and_validate_schema(404)
response =
conn
|> get(favourited_by_url)
|> json_response_and_validate_schema(200)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do
clear_config([:instance, :show_reactions], false)
other_user = insert(:user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
end
describe "GET /api/v1/statuses/:id/reblogged_by" do
setup do: oauth_access(["read:accounts"])
setup %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
%{activity: activity}
end
test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been reblogged yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not return users who have reblogged the status but are blocked", %{
conn: %{assigns: %{user: user}} = conn,
activity: activity
} do
other_user = insert(:user)
{:ok, _user_relationship} = User.block(user, other_user)
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not return users who have reblogged the status privately", %{
conn: conn
} do
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "my secret post"})
{:ok, _} = CommonAPI.repeat(activity.id, other_user, %{visibility: "private"})
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not fail on an unauthenticated request", %{activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
response =
build_conn()
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "requires authentication for private posts", %{user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "@#{other_user.nickname} wanna get some #cofe together?",
visibility: "direct"
})
build_conn()
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(404)
response =
build_conn()
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(200)
assert [] == response
end
end
test "context" do
user = insert(:user)
{:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
{:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
{:ok, %{id: id3}} = CommonAPI.post(user, %{status: "3", in_reply_to_status_id: id2})
{:ok, %{id: id4}} = CommonAPI.post(user, %{status: "4", in_reply_to_status_id: id3})
{:ok, %{id: id5}} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4})
response =
build_conn()
|> get("/api/v1/statuses/#{id3}/context")
|> json_response_and_validate_schema(:ok)
assert %{
"ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
"descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
} = response
end
test "favorites paginate correctly" do
%{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user)
{:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id)
{:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id)
{:ok, third_favorite} = CommonAPI.favorite(user, second_post.id)
result =
conn
|> get("/api/v1/favourites?limit=1")
assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200)
assert post_id == second_post.id
# Using the header for pagination works correctly
[next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ")
[_, max_id] = Regex.run(~r/max_id=([^&]+)/, next)
assert max_id == third_favorite.id
result =
conn
|> get("/api/v1/favourites?max_id=#{max_id}")
assert [%{"id" => first_post_id}, %{"id" => third_post_id}] =
json_response_and_validate_schema(result, 200)
assert first_post_id == first_post.id
assert third_post_id == third_post.id
end
test "returns the favorites of a user" do
%{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user)
{:ok, _} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"})
{:ok, last_like} = CommonAPI.favorite(user, activity.id)
first_conn = get(conn, "/api/v1/favourites")
assert [status] = json_response_and_validate_schema(first_conn, 200)
assert status["id"] == to_string(activity.id)
assert [{"link", _link_header}] =
Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
# Honours query params
{:ok, second_activity} =
CommonAPI.post(other_user, %{
status: "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
})
{:ok, _} = CommonAPI.favorite(user, second_activity.id)
second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}")
assert [second_status] = json_response_and_validate_schema(second_conn, 200)
assert second_status["id"] == to_string(second_activity.id)
third_conn = get(conn, "/api/v1/favourites?limit=0")
assert [] = json_response_and_validate_schema(third_conn, 200)
end
test "expires_at is nil for another user" do
%{conn: conn, user: user} = oauth_access(["read:statuses"])
expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000})
assert %{"pleroma" => %{"expires_at" => a_expires_at}} =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:ok)
{:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at)
assert DateTime.diff(expires_at, a_expires_at) == 0
%{conn: conn} = oauth_access(["read:statuses"])
assert %{"pleroma" => %{"expires_at" => nil}} =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:ok)
end
test "posting a local only status" do
%{user: _user, conn: conn} = oauth_access(["write:statuses"])
conn_one =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "cofe",
"visibility" => "local"
})
local = Pleroma.Constants.as_local_public()
assert %{"content" => "cofe", "id" => id, "visibility" => "local"} =
json_response(conn_one, 200)
assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id)
end
describe "muted reactions" do
test "index" do
%{conn: conn, user: user} = oauth_access(["read:statuses"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
User.mute(user, other_user)
result =
conn
|> get("/api/v1/statuses/?ids[]=#{activity.id}")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => []
}
}
] = result
result =
conn
|> get("/api/v1/statuses/?ids[]=#{activity.id}&with_muted=true")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
}
}
] = result
end
test "show" do
# %{conn: conn, user: user, token: token} = oauth_access(["read:statuses"])
%{conn: conn, user: user, token: _token} = oauth_access(["read:statuses"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
User.mute(user, other_user)
result =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert %{
"pleroma" => %{
"emoji_reactions" => []
}
} = result
result =
conn
|> get("/api/v1/statuses/#{activity.id}?with_muted=true")
|> json_response_and_validate_schema(200)
assert %{
"pleroma" => %{
"emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
}
} = result
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Fri, Sep 18, 11:07 PM (4 h, 23 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768426
Default Alt Text
(97 KB)

Event Timeline