diff options
| -rw-r--r-- | CHANGELOG.md | 8 | ||||
| -rw-r--r-- | docs/api/admin_api.md | 6 | ||||
| -rw-r--r-- | lib/pleroma/moderation_log.ex | 265 | ||||
| -rw-r--r-- | lib/pleroma/user/info.ex | 4 | ||||
| -rw-r--r-- | lib/pleroma/web/admin_api/admin_api_controller.ex | 10 | ||||
| -rw-r--r-- | lib/pleroma/web/admin_api/views/moderation_log_view.ex | 5 | ||||
| -rw-r--r-- | lib/pleroma/web/common_api/activity_draft.ex | 219 | ||||
| -rw-r--r-- | lib/pleroma/web/common_api/common_api.ex | 262 | ||||
| -rw-r--r-- | lib/pleroma/web/common_api/utils.ex | 138 | ||||
| -rw-r--r-- | lib/pleroma/web/controller_helper.ex | 2 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex | 176 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex | 136 | ||||
| -rw-r--r-- | lib/pleroma/web/router.ex | 10 | ||||
| -rw-r--r-- | test/moderation_log_test.exs | 36 | ||||
| -rw-r--r-- | test/web/admin_api/admin_api_controller_test.exs | 121 | ||||
| -rw-r--r-- | test/web/mastodon_api/controllers/timeline_controller_test.exs | 291 | ||||
| -rw-r--r-- | test/web/mastodon_api/mastodon_api_controller_test.exs | 283 | 
17 files changed, 1187 insertions, 785 deletions
| diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a76e6cf8..755b28d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).  - Improve digest email template  – Pagination: (optional) return `total` alongside with `items` when paginating  - Add `rel="ugc"` to all links in statuses, to prevent SEO spam -- ActivityPub: The first page in inboxes/outboxes is no longer embedded.  ### Fixed  - Following from Osada @@ -111,6 +110,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).  - Web response cache (currently, enabled for ActivityPub)  - Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)  - ActivityPub: Add ActivityPub actor's `discoverable` parameter. +- Admin API: Added moderation log filters (user/start date/end date/search/pagination)  ### Changed  - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text @@ -118,6 +118,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).  - RichMedia: parsers and their order are configured in `rich_media` config.  - RichMedia: add the rich media ttl based on image expiration time. +## [1.0.7] - 2019-09-26 +### Fixed +- Broken federation on Erlang 22 (previous versions of hackney http client were using an option that got deprecated) +### Changed +- ActivityPub: The first page in inboxes/outboxes is no longer embedded. +  ## [1.0.6] - 2019-08-14  ### Fixed  - MRF: fix use of unserializable keyword lists in describe() implementations diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md index d7ab808d5..fcdb33944 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -732,7 +732,11 @@ Compile time settings (need instance reboot):  - Method `GET`  - Params:    - *optional* `page`: **integer** page number -  - *optional* `page_size`: **integer** number of users per page (default is `50`) +  - *optional* `page_size`: **integer** number of log entries per page (default is `50`) +  - *optional* `start_date`: **datetime (ISO 8601)** filter logs by creation date, start from `start_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. `2005-08-09T18:31:42` +  - *optional* `end_date`: **datetime (ISO 8601)** filter logs by creation date, end by from `end_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. 2005-08-09T18:31:42 +  - *optional* `user_id`: **integer** filter logs by actor's id +  - *optional* `search`: **string** search logs by the log message  - Response:  ```json diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex index 1ef6fe67a..352cad433 100644 --- a/lib/pleroma/moderation_log.ex +++ b/lib/pleroma/moderation_log.ex @@ -14,61 +14,143 @@ defmodule Pleroma.ModerationLog do      timestamps()    end -  def get_all(page, page_size) do -    from(q in __MODULE__, -      order_by: [desc: q.inserted_at], +  def get_all(params) do +    base_query = +      get_all_query() +      |> maybe_filter_by_date(params) +      |> maybe_filter_by_user(params) +      |> maybe_filter_by_search(params) + +    query_with_pagination = base_query |> paginate_query(params) + +    %{ +      items: Repo.all(query_with_pagination), +      count: Repo.aggregate(base_query, :count, :id) +    } +  end + +  defp maybe_filter_by_date(query, %{start_date: nil, end_date: nil}), do: query + +  defp maybe_filter_by_date(query, %{start_date: start_date, end_date: nil}) do +    from(q in query, +      where: q.inserted_at >= ^parse_datetime(start_date) +    ) +  end + +  defp maybe_filter_by_date(query, %{start_date: nil, end_date: end_date}) do +    from(q in query, +      where: q.inserted_at <= ^parse_datetime(end_date) +    ) +  end + +  defp maybe_filter_by_date(query, %{start_date: start_date, end_date: end_date}) do +    from(q in query, +      where: q.inserted_at >= ^parse_datetime(start_date), +      where: q.inserted_at <= ^parse_datetime(end_date) +    ) +  end + +  defp maybe_filter_by_user(query, %{user_id: nil}), do: query + +  defp maybe_filter_by_user(query, %{user_id: user_id}) do +    from(q in query, +      where: fragment("(?)->'actor'->>'id' = ?", q.data, ^user_id) +    ) +  end + +  defp maybe_filter_by_search(query, %{search: search}) when is_nil(search) or search == "", +    do: query + +  defp maybe_filter_by_search(query, %{search: search}) do +    from(q in query, +      where: fragment("(?)->>'message' ILIKE ?", q.data, ^"%#{search}%") +    ) +  end + +  defp paginate_query(query, %{page: page, page_size: page_size}) do +    from(q in query,        limit: ^page_size,        offset: ^((page - 1) * page_size)      ) -    |> Repo.all()    end +  defp get_all_query do +    from(q in __MODULE__, +      order_by: [desc: q.inserted_at] +    ) +  end + +  defp parse_datetime(datetime) do +    {:ok, parsed_datetime, _} = DateTime.from_iso8601(datetime) + +    parsed_datetime +  end + +  @spec insert_log(%{actor: User, subject: User, action: String.t(), permission: String.t()}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          subject: %User{} = subject,          action: action,          permission: permission        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        subject: user_to_map(subject), -        action: action, -        permission: permission +        "actor" => user_to_map(actor), +        "subject" => user_to_map(subject), +        "action" => action, +        "permission" => permission, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{actor: User, subject: User, action: String.t()}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          action: "report_update",          subject: %Activity{data: %{"type" => "Flag"}} = subject        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: "report_update", -        subject: report_to_map(subject) +        "actor" => user_to_map(actor), +        "action" => "report_update", +        "subject" => report_to_map(subject), +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          action: "report_response",          subject: %Activity{} = subject,          text: text        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: "report_response", -        subject: report_to_map(subject), -        text: text +        "actor" => user_to_map(actor), +        "action" => "report_response", +        "subject" => report_to_map(subject), +        "text" => text, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{ +          actor: User, +          subject: Activity, +          action: String.t(), +          sensitive: String.t(), +          visibility: String.t() +        }) :: {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          action: "status_update", @@ -76,41 +158,49 @@ defmodule Pleroma.ModerationLog do          sensitive: sensitive,          visibility: visibility        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: "status_update", -        subject: status_to_map(subject), -        sensitive: sensitive, -        visibility: visibility +        "actor" => user_to_map(actor), +        "action" => "status_update", +        "subject" => status_to_map(subject), +        "sensitive" => sensitive, +        "visibility" => visibility, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          action: "status_delete",          subject_id: subject_id        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: "status_delete", -        subject_id: subject_id +        "actor" => user_to_map(actor), +        "action" => "status_delete", +        "subject_id" => subject_id, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end    @spec insert_log(%{actor: User, subject: User, action: String.t()}) ::            {:ok, ModerationLog} | {:error, any}    def insert_log(%{actor: %User{} = actor, subject: subject, action: action}) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: action, -        subject: user_to_map(subject) +        "actor" => user_to_map(actor), +        "action" => action, +        "subject" => user_to_map(subject), +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end    @spec insert_log(%{actor: User, subjects: [User], action: String.t()}) :: @@ -118,97 +208,128 @@ defmodule Pleroma.ModerationLog do    def insert_log(%{actor: %User{} = actor, subjects: subjects, action: action}) do      subjects = Enum.map(subjects, &user_to_map/1) -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: action, -        subjects: subjects +        "actor" => user_to_map(actor), +        "action" => action, +        "subjects" => subjects, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          followed: %User{} = followed,          follower: %User{} = follower,          action: "follow"        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: "follow", -        followed: user_to_map(followed), -        follower: user_to_map(follower) +        "actor" => user_to_map(actor), +        "action" => "follow", +        "followed" => user_to_map(followed), +        "follower" => user_to_map(follower), +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          followed: %User{} = followed,          follower: %User{} = follower,          action: "unfollow"        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: "unfollow", -        followed: user_to_map(followed), -        follower: user_to_map(follower) +        "actor" => user_to_map(actor), +        "action" => "unfollow", +        "followed" => user_to_map(followed), +        "follower" => user_to_map(follower), +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{ +          actor: User, +          action: String.t(), +          nicknames: [String.t()], +          tags: [String.t()] +        }) :: {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          nicknames: nicknames,          tags: tags,          action: action        }) do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        nicknames: nicknames, -        tags: tags, -        action: action +        "actor" => user_to_map(actor), +        "nicknames" => nicknames, +        "tags" => tags, +        "action" => action, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message()    end +  @spec insert_log(%{actor: User, action: String.t(), target: String.t()}) :: +          {:ok, ModerationLog} | {:error, any}    def insert_log(%{          actor: %User{} = actor,          action: action,          target: target        })        when action in ["relay_follow", "relay_unfollow"] do -    Repo.insert(%ModerationLog{ +    %ModerationLog{        data: %{ -        actor: user_to_map(actor), -        action: action, -        target: target +        "actor" => user_to_map(actor), +        "action" => action, +        "target" => target, +        "message" => ""        } -    }) +    } +    |> insert_log_entry_with_message() +  end + +  @spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any} + +  defp insert_log_entry_with_message(entry) do +    entry.data["message"] +    |> put_in(get_log_entry_message(entry)) +    |> Repo.insert()    end    defp user_to_map(%User{} = user) do      user      |> Map.from_struct()      |> Map.take([:id, :nickname]) -    |> Map.put(:type, "user") +    |> Map.new(fn {k, v} -> {Atom.to_string(k), v} end) +    |> Map.put("type", "user")    end    defp report_to_map(%Activity{} = report) do      %{ -      type: "report", -      id: report.id, -      state: report.data["state"] +      "type" => "report", +      "id" => report.id, +      "state" => report.data["state"]      }    end    defp status_to_map(%Activity{} = status) do      %{ -      type: "status", -      id: status.id +      "type" => "status", +      "id" => status.id      }    end diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index eef985d0d..ebd4ddebf 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -338,9 +338,7 @@ defmodule Pleroma.User.Info do      name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)      value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255) -    is_binary(name) && -      is_binary(value) && -      String.length(name) <= name_limit && +    is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&        String.length(value) <= value_limit    end diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index e9a048b9b..90aef99f7 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -556,7 +556,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    def list_log(conn, params) do      {page, page_size} = page_params(params) -    log = ModerationLog.get_all(page, page_size) +    log = +      ModerationLog.get_all(%{ +        page: page, +        page_size: page_size, +        start_date: params["start_date"], +        end_date: params["end_date"], +        user_id: params["user_id"], +        search: params["search"] +      })      conn      |> put_view(ModerationLogView) diff --git a/lib/pleroma/web/admin_api/views/moderation_log_view.ex b/lib/pleroma/web/admin_api/views/moderation_log_view.ex index b3fc7cfe5..e7752d1f3 100644 --- a/lib/pleroma/web/admin_api/views/moderation_log_view.ex +++ b/lib/pleroma/web/admin_api/views/moderation_log_view.ex @@ -8,7 +8,10 @@ defmodule Pleroma.Web.AdminAPI.ModerationLogView do    alias Pleroma.ModerationLog    def render("index.json", %{log: log}) do -    render_many(log, __MODULE__, "show.json", as: :log_entry) +    %{ +      items: render_many(log.items, __MODULE__, "show.json", as: :log_entry), +      total: log.count +    }    end    def render("show.json", %{log_entry: log_entry}) do diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex new file mode 100644 index 000000000..aa7c8c381 --- /dev/null +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -0,0 +1,219 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.CommonAPI.ActivityDraft do +  alias Pleroma.Activity +  alias Pleroma.Conversation.Participation +  alias Pleroma.Web.CommonAPI +  alias Pleroma.Web.CommonAPI.Utils + +  import Pleroma.Web.Gettext + +  defstruct valid?: true, +            errors: [], +            user: nil, +            params: %{}, +            status: nil, +            summary: nil, +            full_payload: nil, +            attachments: [], +            in_reply_to: nil, +            in_reply_to_conversation: nil, +            visibility: nil, +            expires_at: nil, +            poll: nil, +            emoji: %{}, +            content_html: nil, +            mentions: [], +            tags: [], +            to: [], +            cc: [], +            context: nil, +            sensitive: false, +            object: nil, +            preview?: false, +            changes: %{} + +  def create(user, params) do +    %__MODULE__{user: user} +    |> put_params(params) +    |> status() +    |> summary() +    |> full_payload() +    |> expires_at() +    |> poll() +    |> with_valid(&in_reply_to/1) +    |> with_valid(&attachments/1) +    |> with_valid(&in_reply_to_conversation/1) +    |> with_valid(&visibility/1) +    |> content() +    |> with_valid(&to_and_cc/1) +    |> with_valid(&context/1) +    |> sensitive() +    |> with_valid(&object/1) +    |> preview?() +    |> with_valid(&changes/1) +    |> validate() +  end + +  defp put_params(draft, params) do +    params = Map.put_new(params, "in_reply_to_status_id", params["in_reply_to_id"]) +    %__MODULE__{draft | params: params} +  end + +  defp status(%{params: %{"status" => status}} = draft) do +    %__MODULE__{draft | status: String.trim(status)} +  end + +  defp summary(%{params: params} = draft) do +    %__MODULE__{draft | summary: Map.get(params, "spoiler_text", "")} +  end + +  defp full_payload(%{status: status, summary: summary} = draft) do +    full_payload = String.trim(status <> summary) + +    case Utils.validate_character_limit(full_payload, draft.attachments) do +      :ok -> %__MODULE__{draft | full_payload: full_payload} +      {:error, message} -> add_error(draft, message) +    end +  end + +  defp attachments(%{params: params} = draft) do +    attachments = Utils.attachments_from_ids(params) +    %__MODULE__{draft | attachments: attachments} +  end + +  defp in_reply_to(draft) do +    case Map.get(draft.params, "in_reply_to_status_id") do +      "" -> draft +      nil -> draft +      id -> %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)} +    end +  end + +  defp in_reply_to_conversation(draft) do +    in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"]) +    %__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation} +  end + +  defp visibility(%{params: params} = draft) do +    case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do +      {visibility, "direct"} when visibility != "direct" -> +        add_error(draft, dgettext("errors", "The message visibility must be direct")) + +      {visibility, _} -> +        %__MODULE__{draft | visibility: visibility} +    end +  end + +  defp expires_at(draft) do +    case CommonAPI.check_expiry_date(draft.params["expires_in"]) do +      {:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at} +      {:error, message} -> add_error(draft, message) +    end +  end + +  defp poll(draft) do +    case Utils.make_poll_data(draft.params) do +      {:ok, {poll, poll_emoji}} -> +        %__MODULE__{draft | poll: poll, emoji: Map.merge(draft.emoji, poll_emoji)} + +      {:error, message} -> +        add_error(draft, message) +    end +  end + +  defp content(draft) do +    {content_html, mentions, tags} = +      Utils.make_content_html( +        draft.status, +        draft.attachments, +        draft.params, +        draft.visibility +      ) + +    %__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags} +  end + +  defp to_and_cc(draft) do +    addressed_users = +      draft.mentions +      |> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end) +      |> Utils.get_addressed_users(draft.params["to"]) + +    {to, cc} = +      Utils.get_to_and_cc( +        draft.user, +        addressed_users, +        draft.in_reply_to, +        draft.visibility, +        draft.in_reply_to_conversation +      ) + +    %__MODULE__{draft | to: to, cc: cc} +  end + +  defp context(draft) do +    context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation) +    %__MODULE__{draft | context: context} +  end + +  defp sensitive(draft) do +    sensitive = draft.params["sensitive"] || Enum.member?(draft.tags, {"#nsfw", "nsfw"}) +    %__MODULE__{draft | sensitive: sensitive} +  end + +  defp object(draft) do +    emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji) + +    object = +      Utils.make_note_data( +        draft.user.ap_id, +        draft.to, +        draft.context, +        draft.content_html, +        draft.attachments, +        draft.in_reply_to, +        draft.tags, +        draft.summary, +        draft.cc, +        draft.sensitive, +        draft.poll +      ) +      |> Map.put("emoji", emoji) + +    %__MODULE__{draft | object: object} +  end + +  defp preview?(draft) do +    preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"]) || false +    %__MODULE__{draft | preview?: preview?} +  end + +  defp changes(draft) do +    direct? = draft.visibility == "direct" + +    changes = +      %{ +        to: draft.to, +        actor: draft.user, +        context: draft.context, +        object: draft.object, +        additional: %{"cc" => draft.cc, "directMessage" => direct?} +      } +      |> Utils.maybe_add_list_data(draft.user, draft.visibility) + +    %__MODULE__{draft | changes: changes} +  end + +  defp with_valid(%{valid?: true} = draft, func), do: func.(draft) +  defp with_valid(draft, _func), do: draft + +  defp add_error(draft, message) do +    %__MODULE__{draft | valid?: false, errors: [message | draft.errors]} +  end + +  defp validate(%{valid?: true} = draft), do: {:ok, draft} +  defp validate(%{errors: [message | _]}), do: {:error, message} +end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 4a74dc16f..a00e4b0d8 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -6,7 +6,6 @@ defmodule Pleroma.Web.CommonAPI do    alias Pleroma.Activity    alias Pleroma.ActivityExpiration    alias Pleroma.Conversation.Participation -  alias Pleroma.Emoji    alias Pleroma.Object    alias Pleroma.ThreadMute    alias Pleroma.User @@ -18,14 +17,11 @@ defmodule Pleroma.Web.CommonAPI do    import Pleroma.Web.CommonAPI.Utils    def follow(follower, followed) do +    timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) +      with {:ok, follower} <- User.maybe_direct_follow(follower, followed),           {:ok, activity} <- ActivityPub.follow(follower, followed), -         {:ok, follower, followed} <- -           User.wait_and_refresh( -             Pleroma.Config.get([:activitypub, :follow_handshake_timeout]), -             follower, -             followed -           ) do +         {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do        {:ok, follower, followed, activity}      end    end @@ -76,8 +72,7 @@ defmodule Pleroma.Web.CommonAPI do           {:ok, delete} <- ActivityPub.delete(object) do        {:ok, delete}      else -      _ -> -        {:error, dgettext("errors", "Could not delete")} +      _ -> {:error, dgettext("errors", "Could not delete")}      end    end @@ -87,18 +82,16 @@ defmodule Pleroma.Web.CommonAPI do           nil <- Utils.get_existing_announce(user.ap_id, object) do        ActivityPub.announce(user, object)      else -      _ -> -        {:error, dgettext("errors", "Could not repeat")} +      _ -> {:error, dgettext("errors", "Could not repeat")}      end    end    def unrepeat(id_or_ap_id, user) do -    with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), -         object <- Object.normalize(activity) do +    with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do +      object = Object.normalize(activity)        ActivityPub.unannounce(user, object)      else -      _ -> -        {:error, dgettext("errors", "Could not unrepeat")} +      _ -> {:error, dgettext("errors", "Could not unrepeat")}      end    end @@ -108,30 +101,23 @@ defmodule Pleroma.Web.CommonAPI do           nil <- Utils.get_existing_like(user.ap_id, object) do        ActivityPub.like(user, object)      else -      _ -> -        {:error, dgettext("errors", "Could not favorite")} +      _ -> {:error, dgettext("errors", "Could not favorite")}      end    end    def unfavorite(id_or_ap_id, user) do -    with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), -         object <- Object.normalize(activity) do +    with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do +      object = Object.normalize(activity)        ActivityPub.unlike(user, object)      else -      _ -> -        {:error, dgettext("errors", "Could not unfavorite")} +      _ -> {:error, dgettext("errors", "Could not unfavorite")}      end    end -  def vote(user, object, choices) do -    with "Question" <- object.data["type"], -         {:author, false} <- {:author, object.data["actor"] == user.ap_id}, -         {:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)}, -         {options, max_count} <- get_options_and_max_count(object), -         option_count <- Enum.count(options), -         {:choice_check, {choices, true}} <- -           {:choice_check, normalize_and_validate_choice_indices(choices, option_count)}, -         {:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do +  def vote(user, %{data: %{"type" => "Question"}} = object, choices) do +    with :ok <- validate_not_author(object, user), +         :ok <- validate_existing_votes(user, object), +         {:ok, options, choices} <- normalize_and_validate_choices(choices, object) do        answer_activities =          Enum.map(choices, fn index ->            answer_data = make_answer_data(user, object, Enum.at(options, index)["name"]) @@ -150,33 +136,41 @@ defmodule Pleroma.Web.CommonAPI do        object = Object.get_cached_by_ap_id(object.data["id"])        {:ok, answer_activities, object} -    else -      {:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")} -      {:existing_votes, _} -> {:error, dgettext("errors", "Already voted")} -      {:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")} -      {:count_check, false} -> {:error, dgettext("errors", "Too many choices")}      end    end -  defp get_options_and_max_count(object) do -    if Map.has_key?(object.data, "anyOf") do -      {object.data["anyOf"], Enum.count(object.data["anyOf"])} +  defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}), +    do: {:error, dgettext("errors", "Poll's author can't vote")} + +  defp validate_not_author(_, _), do: :ok + +  defp validate_existing_votes(%{ap_id: ap_id}, object) do +    if Utils.get_existing_votes(ap_id, object) == [] do +      :ok      else -      {object.data["oneOf"], 1} +      {:error, dgettext("errors", "Already voted")}      end    end -  defp normalize_and_validate_choice_indices(choices, count) do -    Enum.map_reduce(choices, true, fn index, valid -> -      index = if is_binary(index), do: String.to_integer(index), else: index -      {index, if(valid, do: index < count, else: valid)} -    end) -  end +  defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)} +  defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1} + +  defp normalize_and_validate_choices(choices, object) do +    choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end) +    {options, max_count} = get_options_and_max_count(object) +    count = Enum.count(options) -  def get_visibility(_, _, %Participation{}) do -    {"direct", "direct"} +    with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))}, +         {_, true} <- {:count_check, Enum.count(choices) <= max_count} do +      {:ok, options, choices} +    else +      {:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")} +      {:count_check, _} -> {:error, dgettext("errors", "Too many choices")} +    end    end +  def get_visibility(_, _, %Participation{}), do: {"direct", "direct"} +    def get_visibility(%{"visibility" => visibility}, in_reply_to, _)        when visibility in ~w{public unlisted private direct},        do: {visibility, get_replied_to_visibility(in_reply_to)} @@ -197,13 +191,13 @@ defmodule Pleroma.Web.CommonAPI do    def get_replied_to_visibility(activity) do      with %Object{} = object <- Object.normalize(activity) do -      Pleroma.Web.ActivityPub.Visibility.get_visibility(object) +      Visibility.get_visibility(object)      end    end -  defp check_expiry_date({:ok, nil} = res), do: res +  def check_expiry_date({:ok, nil} = res), do: res -  defp check_expiry_date({:ok, in_seconds}) do +  def check_expiry_date({:ok, in_seconds}) do      expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)      if ActivityExpiration.expires_late_enough?(expiry) do @@ -213,107 +207,36 @@ defmodule Pleroma.Web.CommonAPI do      end    end -  defp check_expiry_date(expiry_str) do +  def check_expiry_date(expiry_str) do      Ecto.Type.cast(:integer, expiry_str)      |> check_expiry_date()    end -  def post(user, %{"status" => status} = data) do -    limit = Pleroma.Config.get([:instance, :limit]) - -    with status <- String.trim(status), -         attachments <- attachments_from_ids(data), -         in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]), -         in_reply_to_conversation <- Participation.get(data["in_reply_to_conversation_id"]), -         {visibility, in_reply_to_visibility} <- -           get_visibility(data, in_reply_to, in_reply_to_conversation), -         {_, false} <- -           {:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"}, -         {content_html, mentions, tags} <- -           make_content_html( -             status, -             attachments, -             data, -             visibility -           ), -         mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id), -         addressed_users <- get_addressed_users(mentioned_users, data["to"]), -         {poll, poll_emoji} <- make_poll_data(data), -         {to, cc} <- -           get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation), -         context <- make_context(in_reply_to, in_reply_to_conversation), -         cw <- data["spoiler_text"] || "", -         sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), -         {:ok, expires_at} <- check_expiry_date(data["expires_in"]), -         full_payload <- String.trim(status <> cw), -         :ok <- validate_character_limit(full_payload, attachments, limit), -         object <- -           make_note_data( -             user.ap_id, -             to, -             context, -             content_html, -             attachments, -             in_reply_to, -             tags, -             cw, -             cc, -             sensitive, -             poll -           ), -         object <- put_emoji(object, full_payload, poll_emoji) do -      preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false -      direct? = visibility == "direct" - -      result = -        %{ -          to: to, -          actor: user, -          context: context, -          object: object, -          additional: %{"cc" => cc, "directMessage" => direct?} -        } -        |> maybe_add_list_data(user, visibility) -        |> ActivityPub.create(preview?) - -      if expires_at do -        with {:ok, activity} <- result do -          {:ok, _} = ActivityExpiration.create(activity, expires_at) -        end -      end - -      result -    else -      {:private_to_public, true} -> -        {:error, dgettext("errors", "The message visibility must be direct")} - -      {:error, _} = e -> -        e - -      e -> -        {:error, e} +  def post(user, %{"status" => _} = data) do +    with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do +      draft.changes +      |> ActivityPub.create(draft.preview?) +      |> maybe_create_activity_expiration(draft.expires_at)      end    end -  # parse and put emoji to object data -  defp put_emoji(map, text, emojis) do -    Map.put( -      map, -      "emoji", -      Map.merge(Emoji.Formatter.get_emoji_map(text), emojis) -    ) +  defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do +    with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do +      {:ok, activity} +    end    end +  defp maybe_create_activity_expiration(result, _), do: result +    # Updates the emojis for a user based on their profile    def update(user) do      emoji = emoji_from_profile(user) -    source_data = user.info |> Map.get(:source_data, {}) |> Map.put("tag", emoji) +    source_data = user.info |> Map.get(:source_data, %{}) |> Map.put("tag", emoji)      user = -      with {:ok, user} <- User.update_info(user, &User.Info.set_source_data(&1, source_data)) do -        user -      else -        _e -> user +      case User.update_info(user, &User.Info.set_source_data(&1, source_data)) do +        {:ok, user} -> user +        _ -> user        end      ActivityPub.update(%{ @@ -328,14 +251,8 @@ defmodule Pleroma.Web.CommonAPI do    def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do      with %Activity{             actor: ^user_ap_id, -           data: %{ -             "type" => "Create" -           }, -           object: %Object{ -             data: %{ -               "type" => "Note" -             } -           } +           data: %{"type" => "Create"}, +           object: %Object{data: %{"type" => "Note"}}           } = activity <- get_by_id_or_ap_id(id_or_ap_id),           true <- Visibility.is_public?(activity),           {:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do @@ -372,51 +289,46 @@ defmodule Pleroma.Web.CommonAPI do    def thread_muted?(%{id: nil} = _user, _activity), do: false    def thread_muted?(user, activity) do -    with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do -      false -    else -      _ -> true -    end +    ThreadMute.check_muted(user.id, activity.data["context"]) != []    end -  def report(user, data) do -    with {:account_id, %{"account_id" => account_id}} <- {:account_id, data}, -         {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)}, +  def report(user, %{"account_id" => account_id} = data) do +    with {:ok, account} <- get_reported_account(account_id),           {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]), -         {:ok, statuses} <- get_report_statuses(account, data), -         {:ok, activity} <- -           ActivityPub.flag(%{ -             context: Utils.generate_context_id(), -             actor: user, -             account: account, -             statuses: statuses, -             content: content_html, -             forward: data["forward"] || false -           }) do -      {:ok, activity} -    else -      {:error, err} -> {:error, err} -      {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")} -      {:account, nil} -> {:error, dgettext("errors", "Account not found")} +         {:ok, statuses} <- get_report_statuses(account, data) do +      ActivityPub.flag(%{ +        context: Utils.generate_context_id(), +        actor: user, +        account: account, +        statuses: statuses, +        content: content_html, +        forward: data["forward"] || false +      }) +    end +  end + +  def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")} + +  defp get_reported_account(account_id) do +    case User.get_cached_by_id(account_id) do +      %User{} = account -> {:ok, account} +      _ -> {:error, dgettext("errors", "Account not found")}      end    end    def update_report_state(activity_id, state) do -    with %Activity{} = activity <- Activity.get_by_id(activity_id), -         {:ok, activity} <- Utils.update_report_state(activity, state) do -      {:ok, activity} +    with %Activity{} = activity <- Activity.get_by_id(activity_id) do +      Utils.update_report_state(activity, state)      else        nil -> {:error, :not_found} -      {:error, reason} -> {:error, reason}        _ -> {:error, dgettext("errors", "Could not update state")}      end    end    def update_activity_scope(activity_id, opts \\ %{}) do      with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), -         {:ok, activity} <- toggle_sensitive(activity, opts), -         {:ok, activity} <- set_visibility(activity, opts) do -      {:ok, activity} +         {:ok, activity} <- toggle_sensitive(activity, opts) do +      set_visibility(activity, opts)      else        nil -> {:error, :not_found}        {:error, reason} -> {:error, reason} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 52fbc162b..88a5f434a 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Web.CommonAPI.Utils do    import Pleroma.Web.Gettext +  import Pleroma.Web.ControllerHelper, only: [truthy_param?: 1]    alias Calendar.Strftime    alias Pleroma.Activity @@ -41,14 +42,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do        end    end -  def get_replied_to_activity(""), do: nil - -  def get_replied_to_activity(id) when not is_nil(id) do -    Activity.get_by_id(id) -  end - -  def get_replied_to_activity(_), do: nil -    def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do      attachments_from_ids_descs(ids, desc)    end @@ -159,70 +152,74 @@ defmodule Pleroma.Web.CommonAPI.Utils do    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 -    %{max_expiration: max_expiration, min_expiration: min_expiration} = -      limits = Pleroma.Config.get([:instance, :poll_limits]) - -    # XXX: There is probably a cleaner way of doing this -    try do -      # In some cases mastofe sends out strings instead of integers -      expires_in = if is_binary(expires_in), do: String.to_integer(expires_in), else: expires_in - -      if Enum.count(options) > limits.max_options do -        raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options" -      end +    limits = Pleroma.Config.get([:instance, :poll_limits]) -      {poll, emoji} = +    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 -> -          if String.length(option) > limits.max_option_chars do -            raise ArgumentError, -              message: -                "Poll options cannot be longer than #{limits.max_option_chars} characters each" -          end - -          {%{ -             "name" => option, -             "type" => "Note", -             "replies" => %{"type" => "Collection", "totalItems" => 0} -           }, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))} -        end) - -      case expires_in do -        expires_in when expires_in > max_expiration -> -          raise ArgumentError, message: "Expiration date is too far in the future" - -        expires_in when expires_in < min_expiration -> -          raise ArgumentError, message: "Expiration date is too soon" +          note = %{ +            "name" => option, +            "type" => "Note", +            "replies" => %{"type" => "Collection", "totalItems" => 0} +          } -        _ -> -          :noop -      end +          {note, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))} +        end)        end_time =          NaiveDateTime.utc_now()          |> NaiveDateTime.add(expires_in)          |> NaiveDateTime.to_iso8601() -      poll = -        if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do -          %{"type" => "Question", "anyOf" => poll, "closed" => end_time} -        else -          %{"type" => "Question", "oneOf" => poll, "closed" => end_time} -        end +      key = if truthy_param?(data["poll"]["multiple"]), do: "anyOf", else: "oneOf" +      poll = %{"type" => "Question", key => option_notes, "closed" => end_time} -      {poll, emoji} -    rescue -      e in ArgumentError -> e.message +      {:ok, {poll, emoji}}      end    end    def make_poll_data(%{"poll" => poll}) when is_map(poll) do -    "Invalid poll" +    {: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( @@ -234,7 +231,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do      no_attachment_links =        data        |> Map.get("no_attachment_links", Config.get([:instance, :no_attachment_links])) -      |> Kernel.in([true, "true"]) +      |> truthy_param?()      content_type = get_content_type(data["content_type"]) @@ -347,25 +344,25 @@ defmodule Pleroma.Web.CommonAPI.Utils do          attachments,          in_reply_to,          tags, -        cw \\ nil, +        summary \\ nil,          cc \\ [],          sensitive \\ false, -        merge \\ %{} +        extra_params \\ %{}        ) do      %{        "type" => "Note",        "to" => to,        "cc" => cc,        "content" => content_html, -      "summary" => cw, -      "sensitive" => !Enum.member?(["false", "False", "0", false], sensitive), +      "summary" => summary, +      "sensitive" => truthy_param?(sensitive),        "context" => context,        "attachment" => attachments,        "actor" => actor,        "tag" => Keyword.values(tags) |> Enum.uniq()      }      |> add_in_reply_to(in_reply_to) -    |> Map.merge(merge) +    |> Map.merge(extra_params)    end    defp add_in_reply_to(object, nil), do: object @@ -434,12 +431,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do      end    end -  def emoji_from_profile(%{info: _info} = user) do -    (Emoji.Formatter.get_emoji(user.bio) ++ Emoji.Formatter.get_emoji(user.name)) -    |> Enum.map(fn {shortcode, %Emoji{file: url}} -> +  def emoji_from_profile(%User{bio: bio, name: name}) do +    [bio, name] +    |> Enum.map(&Emoji.Formatter.get_emoji/1) +    |> Enum.concat() +    |> Enum.map(fn {shortcode, %Emoji{file: path}} ->        %{          "type" => "Emoji", -        "icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"}, +        "icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{path}"},          "name" => ":#{shortcode}:"        }      end) @@ -571,15 +570,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do      }    end -  def validate_character_limit(full_payload, attachments, limit) do +  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 = Pleroma.Config.get([:instance, :limit])      length = String.length(full_payload)      if length < limit do -      if length > 0 or Enum.count(attachments) > 0 do -        :ok -      else -        {:error, dgettext("errors", "Cannot post an empty status without attachments")} -      end +      :ok      else        {:error, dgettext("errors", "The status is over the character limit")}      end diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index b53a01955..e90bf842e 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.ControllerHelper do    use Pleroma.Web, :controller    # As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html -  @falsy_param_values [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"] +  @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"]    def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil    def truthy_param?(value), do: value not in @falsy_param_values diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex index 1e88ff7fe..e4ae63231 100644 --- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    use Pleroma.Web, :controller    import Pleroma.Web.ControllerHelper, -    only: [json_response: 3, add_link_headers: 2, add_link_headers: 3] +    only: [json_response: 3, add_link_headers: 2, truthy_param?: 1]    alias Ecto.Changeset    alias Pleroma.Activity @@ -44,7 +44,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    alias Pleroma.Web.OAuth.Token    alias Pleroma.Web.TwitterAPI.TwitterAPI -  alias Pleroma.Web.ControllerHelper    import Ecto.Query    require Logger @@ -156,7 +155,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        ]        |> Enum.reduce(%{}, fn key, acc ->          add_if_present(acc, params, to_string(key), key, fn value -> -          {:ok, ControllerHelper.truthy_param?(value)} +          {:ok, truthy_param?(value)}          end)        end)        |> add_if_present(params, "default_scope", :default_scope) @@ -344,43 +343,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      json(conn, mastodon_emoji)    end -  def home_timeline(%{assigns: %{user: user}} = conn, params) do -    params = -      params -      |> Map.put("type", ["Create", "Announce"]) -      |> Map.put("blocking_user", user) -      |> Map.put("muting_user", user) -      |> Map.put("user", user) - -    activities = -      [user.ap_id | user.following] -      |> ActivityPub.fetch_activities(params) -      |> Enum.reverse() - -    conn -    |> add_link_headers(activities) -    |> put_view(StatusView) -    |> render("index.json", %{activities: activities, for: user, as: :activity}) -  end - -  def public_timeline(%{assigns: %{user: user}} = conn, params) do -    local_only = params["local"] in [true, "True", "true", "1"] - -    activities = -      params -      |> Map.put("type", ["Create", "Announce"]) -      |> Map.put("local_only", local_only) -      |> Map.put("blocking_user", user) -      |> Map.put("muting_user", user) -      |> ActivityPub.fetch_public_activities() -      |> Enum.reverse() - -    conn -    |> add_link_headers(activities, %{"local" => local_only}) -    |> put_view(StatusView) -    |> render("index.json", %{activities: activities, for: user, as: :activity}) -  end -    def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do      with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do        params = @@ -400,25 +362,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end -  def dm_timeline(%{assigns: %{user: user}} = conn, params) do -    params = -      params -      |> Map.put("type", "Create") -      |> Map.put("blocking_user", user) -      |> Map.put("user", user) -      |> Map.put(:visibility, "direct") - -    activities = -      [user.ap_id] -      |> ActivityPub.fetch_activities_query(params) -      |> Pagination.fetch_paginated(params) - -    conn -    |> add_link_headers(activities) -    |> put_view(StatusView) -    |> render("index.json", %{activities: activities, for: user, as: :activity}) -  end -    def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do      limit = 100 @@ -575,14 +518,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end -  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do -    params = -      params -      |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) - -    scheduled_at = params["scheduled_at"] - -    if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do +  def post_status( +        %{assigns: %{user: user}} = conn, +        %{"status" => _, "scheduled_at" => scheduled_at} = params +      ) do +    if ScheduledActivity.far_enough?(scheduled_at) do        with {:ok, scheduled_activity} <-               ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do          conn @@ -590,24 +530,26 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          |> render("show.json", %{scheduled_activity: scheduled_activity})        end      else -      params = Map.drop(params, ["scheduled_at"]) - -      case CommonAPI.post(user, params) do -        {:error, message} -> -          conn -          |> put_status(:unprocessable_entity) -          |> json(%{error: message}) - -        {:ok, activity} -> -          conn -          |> put_view(StatusView) -          |> try_render("status.json", %{ -            activity: activity, -            for: user, -            as: :activity, -            with_direct_conversation_id: true -          }) -      end +      post_status(conn, Map.drop(params, ["scheduled_at"])) +    end +  end + +  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do +    case CommonAPI.post(user, params) do +      {:ok, activity} -> +        conn +        |> put_view(StatusView) +        |> try_render("status.json", %{ +          activity: activity, +          for: user, +          as: :activity, +          with_direct_conversation_id: true +        }) + +      {:error, message} -> +        conn +        |> put_status(:unprocessable_entity) +        |> json(%{error: message})      end    end @@ -822,45 +764,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end -  def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do -    local_only = params["local"] in [true, "True", "true", "1"] - -    tags = -      [params["tag"], params["any"]] -      |> List.flatten() -      |> Enum.uniq() -      |> Enum.filter(& &1) -      |> Enum.map(&String.downcase(&1)) - -    tag_all = -      params["all"] || -        [] -        |> Enum.map(&String.downcase(&1)) - -    tag_reject = -      params["none"] || -        [] -        |> Enum.map(&String.downcase(&1)) - -    activities = -      params -      |> Map.put("type", "Create") -      |> Map.put("local_only", local_only) -      |> Map.put("blocking_user", user) -      |> Map.put("muting_user", user) -      |> Map.put("user", user) -      |> Map.put("tag", tags) -      |> Map.put("tag_all", tag_all) -      |> Map.put("tag_reject", tag_reject) -      |> ActivityPub.fetch_public_activities() -      |> Enum.reverse() - -    conn -    |> add_link_headers(activities, %{"local" => local_only}) -    |> put_view(StatusView) -    |> render("index.json", %{activities: activities, for: user, as: :activity}) -  end -    def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do      with %User{} = user <- User.get_cached_by_id(id),           followers <- MastodonAPI.get_followers(user, params) do @@ -1173,31 +1076,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      json(conn, res)    end -  def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do -    with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do -      params = -        params -        |> Map.put("type", "Create") -        |> Map.put("blocking_user", user) -        |> Map.put("user", user) -        |> Map.put("muting_user", user) - -      # we must filter the following list for the user to avoid leaking statuses the user -      # does not actually have permission to see (for more info, peruse security issue #270). -      activities = -        following -        |> Enum.filter(fn x -> x in user.following end) -        |> ActivityPub.fetch_activities_bounded(following, params) -        |> Enum.reverse() - -      conn -      |> put_view(StatusView) -      |> render("index.json", %{activities: activities, for: user, as: :activity}) -    else -      _e -> render_error(conn, :forbidden, "Error.") -    end -  end -    def index(%{assigns: %{user: user}} = conn, _params) do      token = get_session(conn, :oauth_token) diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex new file mode 100644 index 000000000..bb8b0eb32 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -0,0 +1,136 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.TimelineController do +  use Pleroma.Web, :controller + +  import Pleroma.Web.ControllerHelper, +    only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1] + +  alias Pleroma.Pagination +  alias Pleroma.Web.ActivityPub.ActivityPub + +  plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) + +  # GET /api/v1/timelines/home +  def home(%{assigns: %{user: user}} = conn, params) do +    params = +      params +      |> Map.put("type", ["Create", "Announce"]) +      |> Map.put("blocking_user", user) +      |> Map.put("muting_user", user) +      |> Map.put("user", user) + +    recipients = [user.ap_id | user.following] + +    activities = +      recipients +      |> ActivityPub.fetch_activities(params) +      |> Enum.reverse() + +    conn +    |> add_link_headers(activities) +    |> render("index.json", activities: activities, for: user, as: :activity) +  end + +  # GET /api/v1/timelines/direct +  def direct(%{assigns: %{user: user}} = conn, params) do +    params = +      params +      |> Map.put("type", "Create") +      |> Map.put("blocking_user", user) +      |> Map.put("user", user) +      |> Map.put(:visibility, "direct") + +    activities = +      [user.ap_id] +      |> ActivityPub.fetch_activities_query(params) +      |> Pagination.fetch_paginated(params) + +    conn +    |> add_link_headers(activities) +    |> render("index.json", activities: activities, for: user, as: :activity) +  end + +  # GET /api/v1/timelines/public +  def public(%{assigns: %{user: user}} = conn, params) do +    local_only = truthy_param?(params["local"]) + +    activities = +      params +      |> Map.put("type", ["Create", "Announce"]) +      |> Map.put("local_only", local_only) +      |> Map.put("blocking_user", user) +      |> Map.put("muting_user", user) +      |> ActivityPub.fetch_public_activities() +      |> Enum.reverse() + +    conn +    |> add_link_headers(activities, %{"local" => local_only}) +    |> render("index.json", activities: activities, for: user, as: :activity) +  end + +  # GET /api/v1/timelines/tag/:tag +  def hashtag(%{assigns: %{user: user}} = conn, params) do +    local_only = truthy_param?(params["local"]) + +    tags = +      [params["tag"], params["any"]] +      |> List.flatten() +      |> Enum.uniq() +      |> Enum.filter(& &1) +      |> Enum.map(&String.downcase(&1)) + +    tag_all = +      params +      |> Map.get("all", []) +      |> Enum.map(&String.downcase(&1)) + +    tag_reject = +      params +      |> Map.get("none", []) +      |> Enum.map(&String.downcase(&1)) + +    activities = +      params +      |> Map.put("type", "Create") +      |> Map.put("local_only", local_only) +      |> Map.put("blocking_user", user) +      |> Map.put("muting_user", user) +      |> Map.put("user", user) +      |> Map.put("tag", tags) +      |> Map.put("tag_all", tag_all) +      |> Map.put("tag_reject", tag_reject) +      |> ActivityPub.fetch_public_activities() +      |> Enum.reverse() + +    conn +    |> add_link_headers(activities, %{"local" => local_only}) +    |> render("index.json", activities: activities, for: user, as: :activity) +  end + +  # GET /api/v1/timelines/list/:list_id +  def list(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do +    with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do +      params = +        params +        |> Map.put("type", "Create") +        |> Map.put("blocking_user", user) +        |> Map.put("user", user) +        |> Map.put("muting_user", user) + +      # we must filter the following list for the user to avoid leaking statuses the user +      # does not actually have permission to see (for more info, peruse security issue #270). +      activities = +        following +        |> Enum.filter(fn x -> x in user.following end) +        |> ActivityPub.fetch_activities_bounded(following, params) +        |> Enum.reverse() + +      render(conn, "index.json", activities: activities, for: user, as: :activity) +    else +      _e -> render_error(conn, :forbidden, "Error.") +    end +  end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 316c895ee..2575481ff 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -319,8 +319,8 @@ defmodule Pleroma.Web.Router do        get("/blocks", MastodonAPIController, :blocks)        get("/mutes", MastodonAPIController, :mutes) -      get("/timelines/home", MastodonAPIController, :home_timeline) -      get("/timelines/direct", MastodonAPIController, :dm_timeline) +      get("/timelines/home", TimelineController, :home) +      get("/timelines/direct", TimelineController, :direct)        get("/favourites", MastodonAPIController, :favourites)        get("/bookmarks", MastodonAPIController, :bookmarks) @@ -466,9 +466,9 @@ defmodule Pleroma.Web.Router do      scope [] do        pipe_through(:oauth_read_or_public) -      get("/timelines/public", MastodonAPIController, :public_timeline) -      get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline) -      get("/timelines/list/:list_id", MastodonAPIController, :list_timeline) +      get("/timelines/public", TimelineController, :public) +      get("/timelines/tag/:tag", TimelineController, :hashtag) +      get("/timelines/list/:list_id", TimelineController, :list)        get("/statuses", MastodonAPIController, :get_statuses)        get("/statuses/:id", MastodonAPIController, :get_status) diff --git a/test/moderation_log_test.exs b/test/moderation_log_test.exs index c78708471..a39a00e02 100644 --- a/test/moderation_log_test.exs +++ b/test/moderation_log_test.exs @@ -30,8 +30,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == -               "@#{moderator.nickname} deleted user @#{subject1.nickname}" +      assert log.data["message"] == "@#{moderator.nickname} deleted user @#{subject1.nickname}"      end      test "logging user creation by moderator", %{ @@ -48,7 +47,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} created users: @#{subject1.nickname}, @#{subject2.nickname}"      end @@ -63,7 +62,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{admin.nickname} made @#{subject2.nickname} follow @#{subject1.nickname}"      end @@ -78,7 +77,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{admin.nickname} made @#{subject2.nickname} unfollow @#{subject1.nickname}"      end @@ -100,8 +99,7 @@ defmodule Pleroma.ModerationLogTest do        tags = ["foo", "bar"] |> Enum.join(", ") -      assert ModerationLog.get_log_entry_message(log) == -               "@#{admin.nickname} added tags: #{tags} to users: #{users}" +      assert log.data["message"] == "@#{admin.nickname} added tags: #{tags} to users: #{users}"      end      test "logging user untagged by admin", %{admin: admin, subject1: subject1, subject2: subject2} do @@ -122,7 +120,7 @@ defmodule Pleroma.ModerationLogTest do        tags = ["foo", "bar"] |> Enum.join(", ") -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{admin.nickname} removed tags: #{tags} from users: #{users}"      end @@ -137,8 +135,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == -               "@#{moderator.nickname} made @#{subject1.nickname} moderator" +      assert log.data["message"] == "@#{moderator.nickname} made @#{subject1.nickname} moderator"      end      test "logging user revoke by moderator", %{moderator: moderator, subject1: subject1} do @@ -152,7 +149,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} revoked moderator role from @#{subject1.nickname}"      end @@ -166,7 +163,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} followed relay: https://example.org/relay"      end @@ -180,7 +177,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} unfollowed relay: https://example.org/relay"      end @@ -202,7 +199,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} updated report ##{report.id} with 'resolved' state"      end @@ -224,7 +221,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} responded with 'look at this' to report ##{report.id}"      end @@ -242,7 +239,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true'"      end @@ -260,7 +257,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} updated status ##{note.id}, set visibility: 'private'"      end @@ -278,7 +275,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == +      assert log.data["message"] ==                 "@#{moderator.nickname} updated status ##{note.id}, set sensitive: 'true', visibility: 'private'"      end @@ -294,8 +291,7 @@ defmodule Pleroma.ModerationLogTest do        log = Repo.one(ModerationLog) -      assert ModerationLog.get_log_entry_message(log) == -               "@#{moderator.nickname} deleted status ##{note.id}" +      assert log.data["message"] == "@#{moderator.nickname} deleted status ##{note.id}"      end    end  end diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 00e64692a..b5c355e66 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -2257,8 +2257,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do    describe "GET /api/pleroma/admin/moderation_log" do      setup %{conn: conn} do        admin = insert(:user, info: %{is_admin: true}) +      moderator = insert(:user, info: %{is_moderator: true}) -      %{conn: assign(conn, :user, admin), admin: admin} +      %{conn: assign(conn, :user, admin), admin: admin, moderator: moderator}      end      test "returns the log", %{conn: conn, admin: admin} do @@ -2291,9 +2292,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do        conn = get(conn, "/api/pleroma/admin/moderation_log")        response = json_response(conn, 200) -      [first_entry, second_entry] = response +      [first_entry, second_entry] = response["items"] -      assert response |> length() == 2 +      assert response["total"] == 2        assert first_entry["data"]["action"] == "relay_unfollow"        assert first_entry["message"] == @@ -2335,9 +2336,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do        conn1 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=1")        response1 = json_response(conn1, 200) -      [first_entry] = response1 +      [first_entry] = response1["items"] -      assert response1 |> length() == 1 +      assert response1["total"] == 2 +      assert response1["items"] |> length() == 1        assert first_entry["data"]["action"] == "relay_unfollow"        assert first_entry["message"] == @@ -2346,14 +2348,119 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do        conn2 = get(conn, "/api/pleroma/admin/moderation_log?page_size=1&page=2")        response2 = json_response(conn2, 200) -      [second_entry] = response2 +      [second_entry] = response2["items"] -      assert response2 |> length() == 1 +      assert response2["total"] == 2 +      assert response2["items"] |> length() == 1        assert second_entry["data"]["action"] == "relay_follow"        assert second_entry["message"] ==                 "@#{admin.nickname} followed relay: https://example.org/relay"      end + +    test "filters log by date", %{conn: conn, admin: admin} do +      first_date = "2017-08-15T15:47:06Z" +      second_date = "2017-08-20T15:47:06Z" + +      Repo.insert(%ModerationLog{ +        data: %{ +          actor: %{ +            "id" => admin.id, +            "nickname" => admin.nickname, +            "type" => "user" +          }, +          action: "relay_follow", +          target: "https://example.org/relay" +        }, +        inserted_at: NaiveDateTime.from_iso8601!(first_date) +      }) + +      Repo.insert(%ModerationLog{ +        data: %{ +          actor: %{ +            "id" => admin.id, +            "nickname" => admin.nickname, +            "type" => "user" +          }, +          action: "relay_unfollow", +          target: "https://example.org/relay" +        }, +        inserted_at: NaiveDateTime.from_iso8601!(second_date) +      }) + +      conn1 = +        get( +          conn, +          "/api/pleroma/admin/moderation_log?start_date=#{second_date}" +        ) + +      response1 = json_response(conn1, 200) +      [first_entry] = response1["items"] + +      assert response1["total"] == 1 +      assert first_entry["data"]["action"] == "relay_unfollow" + +      assert first_entry["message"] == +               "@#{admin.nickname} unfollowed relay: https://example.org/relay" +    end + +    test "returns log filtered by user", %{conn: conn, admin: admin, moderator: moderator} do +      Repo.insert(%ModerationLog{ +        data: %{ +          actor: %{ +            "id" => admin.id, +            "nickname" => admin.nickname, +            "type" => "user" +          }, +          action: "relay_follow", +          target: "https://example.org/relay" +        } +      }) + +      Repo.insert(%ModerationLog{ +        data: %{ +          actor: %{ +            "id" => moderator.id, +            "nickname" => moderator.nickname, +            "type" => "user" +          }, +          action: "relay_unfollow", +          target: "https://example.org/relay" +        } +      }) + +      conn1 = get(conn, "/api/pleroma/admin/moderation_log?user_id=#{moderator.id}") + +      response1 = json_response(conn1, 200) +      [first_entry] = response1["items"] + +      assert response1["total"] == 1 +      assert get_in(first_entry, ["data", "actor", "id"]) == moderator.id +    end + +    test "returns log filtered by search", %{conn: conn, moderator: moderator} do +      ModerationLog.insert_log(%{ +        actor: moderator, +        action: "relay_follow", +        target: "https://example.org/relay" +      }) + +      ModerationLog.insert_log(%{ +        actor: moderator, +        action: "relay_unfollow", +        target: "https://example.org/relay" +      }) + +      conn1 = get(conn, "/api/pleroma/admin/moderation_log?search=unfo") + +      response1 = json_response(conn1, 200) +      [first_entry] = response1["items"] + +      assert response1["total"] == 1 + +      assert get_in(first_entry, ["data", "message"]) == +               "@#{moderator.nickname} unfollowed relay: https://example.org/relay" +    end    end    describe "PATCH /users/:nickname/force_password_reset" do diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs new file mode 100644 index 000000000..d3652d964 --- /dev/null +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -0,0 +1,291 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do +  use Pleroma.Web.ConnCase + +  import Pleroma.Factory +  import Tesla.Mock + +  alias Pleroma.Config +  alias Pleroma.User +  alias Pleroma.Web.CommonAPI +  alias Pleroma.Web.OStatus + +  clear_config([:instance, :public]) + +  setup do +    mock(fn env -> apply(HttpRequestMock, :request, [env]) end) +    :ok +  end + +  test "the home timeline", %{conn: conn} do +    user = insert(:user) +    following = insert(:user) + +    {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) + +    conn = +      conn +      |> assign(:user, user) +      |> get("/api/v1/timelines/home") + +    assert Enum.empty?(json_response(conn, :ok)) + +    {:ok, user} = User.follow(user, following) + +    conn = +      build_conn() +      |> assign(:user, user) +      |> get("/api/v1/timelines/home") + +    assert [%{"content" => "test"}] = json_response(conn, :ok) +  end + +  describe "public" do +    @tag capture_log: true +    test "the public timeline", %{conn: conn} do +      following = insert(:user) + +      {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) + +      {:ok, [_activity]} = +        OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") + +      conn = get(conn, "/api/v1/timelines/public", %{"local" => "False"}) + +      assert length(json_response(conn, :ok)) == 2 + +      conn = get(build_conn(), "/api/v1/timelines/public", %{"local" => "True"}) + +      assert [%{"content" => "test"}] = json_response(conn, :ok) + +      conn = get(build_conn(), "/api/v1/timelines/public", %{"local" => "1"}) + +      assert [%{"content" => "test"}] = json_response(conn, :ok) +    end + +    test "the public timeline when public is set to false", %{conn: conn} do +      Config.put([:instance, :public], false) + +      assert %{"error" => "This resource requires authentication."} == +               conn +               |> get("/api/v1/timelines/public", %{"local" => "False"}) +               |> json_response(:forbidden) +    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 +  end + +  describe "direct" do +    test "direct timeline", %{conn: conn} do +      user_one = insert(:user) +      user_two = insert(:user) + +      {:ok, user_two} = User.follow(user_two, user_one) + +      {:ok, direct} = +        CommonAPI.post(user_one, %{ +          "status" => "Hi @#{user_two.nickname}!", +          "visibility" => "direct" +        }) + +      {:ok, _follower_only} = +        CommonAPI.post(user_one, %{ +          "status" => "Hi @#{user_two.nickname}!", +          "visibility" => "private" +        }) + +      # Only direct should be visible here +      res_conn = +        conn +        |> assign(:user, user_two) +        |> get("api/v1/timelines/direct") + +      [status] = json_response(res_conn, :ok) + +      assert %{"visibility" => "direct"} = status +      assert status["url"] != direct.data["id"] + +      # User should be able to see their own direct message +      res_conn = +        build_conn() +        |> assign(:user, user_one) +        |> get("api/v1/timelines/direct") + +      [status] = json_response(res_conn, :ok) + +      assert %{"visibility" => "direct"} = status + +      # Both should be visible here +      res_conn = +        conn +        |> assign(:user, user_two) +        |> get("api/v1/timelines/home") + +      [_s1, _s2] = json_response(res_conn, :ok) + +      # Test pagination +      Enum.each(1..20, fn _ -> +        {:ok, _} = +          CommonAPI.post(user_one, %{ +            "status" => "Hi @#{user_two.nickname}!", +            "visibility" => "direct" +          }) +      end) + +      res_conn = +        conn +        |> assign(:user, user_two) +        |> get("api/v1/timelines/direct") + +      statuses = json_response(res_conn, :ok) +      assert length(statuses) == 20 + +      res_conn = +        conn +        |> assign(:user, user_two) +        |> get("api/v1/timelines/direct", %{max_id: List.last(statuses)["id"]}) + +      [status] = json_response(res_conn, :ok) + +      assert status["url"] != direct.data["id"] +    end + +    test "doesn't include DMs from blocked users", %{conn: conn} do +      blocker = insert(:user) +      blocked = insert(:user) +      user = insert(:user) +      {:ok, blocker} = User.block(blocker, blocked) + +      {:ok, _blocked_direct} = +        CommonAPI.post(blocked, %{ +          "status" => "Hi @#{blocker.nickname}!", +          "visibility" => "direct" +        }) + +      {:ok, direct} = +        CommonAPI.post(user, %{ +          "status" => "Hi @#{blocker.nickname}!", +          "visibility" => "direct" +        }) + +      res_conn = +        conn +        |> assign(:user, user) +        |> get("api/v1/timelines/direct") + +      [status] = json_response(res_conn, :ok) +      assert status["id"] == direct.id +    end +  end + +  describe "list" do +    test "list timeline", %{conn: conn} do +      user = insert(:user) +      other_user = insert(:user) +      {:ok, _activity_one} = CommonAPI.post(user, %{"status" => "Marisa is cute."}) +      {:ok, activity_two} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."}) +      {:ok, list} = Pleroma.List.create("name", user) +      {:ok, list} = Pleroma.List.follow(list, other_user) + +      conn = +        conn +        |> assign(:user, user) +        |> get("/api/v1/timelines/list/#{list.id}") + +      assert [%{"id" => id}] = json_response(conn, :ok) + +      assert id == to_string(activity_two.id) +    end + +    test "list timeline does not leak non-public statuses for unfollowed users", %{conn: conn} do +      user = insert(:user) +      other_user = insert(:user) +      {:ok, activity_one} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."}) + +      {:ok, _activity_two} = +        CommonAPI.post(other_user, %{ +          "status" => "Marisa is cute.", +          "visibility" => "private" +        }) + +      {:ok, list} = Pleroma.List.create("name", user) +      {:ok, list} = Pleroma.List.follow(list, other_user) + +      conn = +        conn +        |> assign(:user, user) +        |> get("/api/v1/timelines/list/#{list.id}") + +      assert [%{"id" => id}] = json_response(conn, :ok) + +      assert id == to_string(activity_one.id) +    end +  end + +  describe "hashtag" do +    @tag capture_log: true +    test "hashtag timeline", %{conn: conn} do +      following = insert(:user) + +      {:ok, activity} = CommonAPI.post(following, %{"status" => "test #2hu"}) + +      {:ok, [_activity]} = +        OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") + +      nconn = get(conn, "/api/v1/timelines/tag/2hu") + +      assert [%{"id" => id}] = json_response(nconn, :ok) + +      assert id == to_string(activity.id) + +      # works for different capitalization too +      nconn = get(conn, "/api/v1/timelines/tag/2HU") + +      assert [%{"id" => id}] = json_response(nconn, :ok) + +      assert id == to_string(activity.id) +    end + +    test "multi-hashtag timeline", %{conn: conn} do +      user = insert(:user) + +      {:ok, activity_test} = CommonAPI.post(user, %{"status" => "#test"}) +      {:ok, activity_test1} = CommonAPI.post(user, %{"status" => "#test #test1"}) +      {:ok, activity_none} = CommonAPI.post(user, %{"status" => "#test #none"}) + +      any_test = get(conn, "/api/v1/timelines/tag/test", %{"any" => ["test1"]}) + +      [status_none, status_test1, status_test] = json_response(any_test, :ok) + +      assert to_string(activity_test.id) == status_test["id"] +      assert to_string(activity_test1.id) == status_test1["id"] +      assert to_string(activity_none.id) == status_none["id"] + +      restricted_test = +        get(conn, "/api/v1/timelines/tag/test", %{"all" => ["test1"], "none" => ["none"]}) + +      assert [status_test1] == json_response(restricted_test, :ok) + +      all_test = get(conn, "/api/v1/timelines/tag/test", %{"all" => ["none"]}) + +      assert [status_none] == json_response(all_test, :ok) +    end +  end +end diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index cd672132b..7f7a89516 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -20,12 +20,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do    alias Pleroma.Web.MastodonAPI.FilterView    alias Pleroma.Web.OAuth.App    alias Pleroma.Web.OAuth.Token -  alias Pleroma.Web.OStatus    alias Pleroma.Web.Push -  import Pleroma.Factory +    import ExUnit.CaptureLog -  import Tesla.Mock +  import Pleroma.Factory    import Swoosh.TestAssertions +  import Tesla.Mock    @image "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7" @@ -37,82 +37,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do    clear_config([:instance, :public])    clear_config([:rich_media, :enabled]) -  test "the home timeline", %{conn: conn} do -    user = insert(:user) -    following = insert(:user) - -    {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) - -    conn = -      conn -      |> assign(:user, user) -      |> get("/api/v1/timelines/home") - -    assert Enum.empty?(json_response(conn, 200)) - -    {:ok, user} = User.follow(user, following) - -    conn = -      build_conn() -      |> assign(:user, user) -      |> get("/api/v1/timelines/home") - -    assert [%{"content" => "test"}] = json_response(conn, 200) -  end - -  test "the public timeline", %{conn: conn} do -    following = insert(:user) - -    capture_log(fn -> -      {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) - -      {:ok, [_activity]} = -        OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") - -      conn = -        conn -        |> get("/api/v1/timelines/public", %{"local" => "False"}) - -      assert length(json_response(conn, 200)) == 2 - -      conn = -        build_conn() -        |> get("/api/v1/timelines/public", %{"local" => "True"}) - -      assert [%{"content" => "test"}] = json_response(conn, 200) - -      conn = -        build_conn() -        |> get("/api/v1/timelines/public", %{"local" => "1"}) - -      assert [%{"content" => "test"}] = json_response(conn, 200) -    end) -  end - -  test "the public timeline when public is set to false", %{conn: conn} do -    Config.put([:instance, :public], false) - -    assert conn -           |> get("/api/v1/timelines/public", %{"local" => "False"}) -           |> json_response(403) == %{"error" => "This resource requires authentication."} -  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      setup do        user = insert(:user) @@ -419,80 +343,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      end    end -  test "direct timeline", %{conn: conn} do -    user_one = insert(:user) -    user_two = insert(:user) - -    {:ok, user_two} = User.follow(user_two, user_one) - -    {:ok, direct} = -      CommonAPI.post(user_one, %{ -        "status" => "Hi @#{user_two.nickname}!", -        "visibility" => "direct" -      }) - -    {:ok, _follower_only} = -      CommonAPI.post(user_one, %{ -        "status" => "Hi @#{user_two.nickname}!", -        "visibility" => "private" -      }) - -    # Only direct should be visible here -    res_conn = -      conn -      |> assign(:user, user_two) -      |> get("api/v1/timelines/direct") - -    [status] = json_response(res_conn, 200) - -    assert %{"visibility" => "direct"} = status -    assert status["url"] != direct.data["id"] - -    # User should be able to see their own direct message -    res_conn = -      build_conn() -      |> assign(:user, user_one) -      |> get("api/v1/timelines/direct") - -    [status] = json_response(res_conn, 200) - -    assert %{"visibility" => "direct"} = status - -    # Both should be visible here -    res_conn = -      conn -      |> assign(:user, user_two) -      |> get("api/v1/timelines/home") - -    [_s1, _s2] = json_response(res_conn, 200) - -    # Test pagination -    Enum.each(1..20, fn _ -> -      {:ok, _} = -        CommonAPI.post(user_one, %{ -          "status" => "Hi @#{user_two.nickname}!", -          "visibility" => "direct" -        }) -    end) - -    res_conn = -      conn -      |> assign(:user, user_two) -      |> get("api/v1/timelines/direct") - -    statuses = json_response(res_conn, 200) -    assert length(statuses) == 20 - -    res_conn = -      conn -      |> assign(:user, user_two) -      |> get("api/v1/timelines/direct", %{max_id: List.last(statuses)["id"]}) - -    [status] = json_response(res_conn, 200) - -    assert status["url"] != direct.data["id"] -  end -    test "Conversations", %{conn: conn} do      user_one = insert(:user)      user_two = insert(:user) @@ -556,33 +406,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200)    end -  test "doesn't include DMs from blocked users", %{conn: conn} do -    blocker = insert(:user) -    blocked = insert(:user) -    user = insert(:user) -    {:ok, blocker} = User.block(blocker, blocked) - -    {:ok, _blocked_direct} = -      CommonAPI.post(blocked, %{ -        "status" => "Hi @#{blocker.nickname}!", -        "visibility" => "direct" -      }) - -    {:ok, direct} = -      CommonAPI.post(user, %{ -        "status" => "Hi @#{blocker.nickname}!", -        "visibility" => "direct" -      }) - -    res_conn = -      conn -      |> assign(:user, user) -      |> get("api/v1/timelines/direct") - -    [status] = json_response(res_conn, 200) -    assert status["id"] == direct.id -  end -    test "verify_credentials", %{conn: conn} do      user = insert(:user) @@ -955,50 +778,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      end    end -  describe "list timelines" do -    test "list timeline", %{conn: conn} do -      user = insert(:user) -      other_user = insert(:user) -      {:ok, _activity_one} = CommonAPI.post(user, %{"status" => "Marisa is cute."}) -      {:ok, activity_two} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."}) -      {:ok, list} = Pleroma.List.create("name", user) -      {:ok, list} = Pleroma.List.follow(list, other_user) - -      conn = -        conn -        |> assign(:user, user) -        |> get("/api/v1/timelines/list/#{list.id}") - -      assert [%{"id" => id}] = json_response(conn, 200) - -      assert id == to_string(activity_two.id) -    end - -    test "list timeline does not leak non-public statuses for unfollowed users", %{conn: conn} do -      user = insert(:user) -      other_user = insert(:user) -      {:ok, activity_one} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."}) - -      {:ok, _activity_two} = -        CommonAPI.post(other_user, %{ -          "status" => "Marisa is cute.", -          "visibility" => "private" -        }) - -      {:ok, list} = Pleroma.List.create("name", user) -      {:ok, list} = Pleroma.List.follow(list, other_user) - -      conn = -        conn -        |> assign(:user, user) -        |> get("/api/v1/timelines/list/#{list.id}") - -      assert [%{"id" => id}] = json_response(conn, 200) - -      assert id == to_string(activity_one.id) -    end -  end -    describe "reblogging" do      test "reblogs and returns the reblogged status", %{conn: conn} do        activity = insert(:note_activity) @@ -1554,62 +1333,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      assert url =~ "an_image"    end -  test "hashtag timeline", %{conn: conn} do -    following = insert(:user) - -    capture_log(fn -> -      {:ok, activity} = CommonAPI.post(following, %{"status" => "test #2hu"}) - -      {:ok, [_activity]} = -        OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873") - -      nconn = -        conn -        |> get("/api/v1/timelines/tag/2hu") - -      assert [%{"id" => id}] = json_response(nconn, 200) - -      assert id == to_string(activity.id) - -      # works for different capitalization too -      nconn = -        conn -        |> get("/api/v1/timelines/tag/2HU") - -      assert [%{"id" => id}] = json_response(nconn, 200) - -      assert id == to_string(activity.id) -    end) -  end - -  test "multi-hashtag timeline", %{conn: conn} do -    user = insert(:user) - -    {:ok, activity_test} = CommonAPI.post(user, %{"status" => "#test"}) -    {:ok, activity_test1} = CommonAPI.post(user, %{"status" => "#test #test1"}) -    {:ok, activity_none} = CommonAPI.post(user, %{"status" => "#test #none"}) - -    any_test = -      conn -      |> get("/api/v1/timelines/tag/test", %{"any" => ["test1"]}) - -    [status_none, status_test1, status_test] = json_response(any_test, 200) - -    assert to_string(activity_test.id) == status_test["id"] -    assert to_string(activity_test1.id) == status_test1["id"] -    assert to_string(activity_none.id) == status_none["id"] - -    restricted_test = -      conn -      |> get("/api/v1/timelines/tag/test", %{"all" => ["test1"], "none" => ["none"]}) - -    assert [status_test1] == json_response(restricted_test, 200) - -    all_test = conn |> get("/api/v1/timelines/tag/test", %{"all" => ["none"]}) - -    assert [status_none] == json_response(all_test, 200) -  end -    test "getting followers", %{conn: conn} do      user = insert(:user)      other_user = insert(:user) | 
