defmodule ScrapyCloudEx.Endpoints.App.Comments do @moduledoc """ Wraps the [Comments](https://doc.scrapinghub.com/api/comments.html) endpoint. The comments API lets you add comments directly to scraped data, which can later be viewed on the items page. """ import ScrapyCloudEx.Endpoints.Guards alias ScrapyCloudEx.Endpoints.Helpers alias ScrapyCloudEx.HttpAdapter.RequestConfig @base_url "https://app.scrapinghub.com/api/comments" @typedoc """ A comment. Map with the following keys: * `"id"` - the comment id (`t:integer/0`). * `"text"` - the comment text (`t:String.t/0`). * `"created"` - the created date (`t:String.t/0`). * `"archived"` - the archived date (or `nil` if not archived) (`t:String.t/0`). * `"author"` - the comment author (`t:String.t/0`). * `"avatar"` - the gravatar URL for the author (`t:String.t/0`). * `"editable"` - a boolean value indicating whether the comment can be edited (`t:boolean/0`). """ @type comment :: %{required(String.t()) => integer() | boolean() | String.t()} @doc """ Retrieves comments for a job, optionally indexed by item or item/field. The `composite_id` must have at least 3 sections (i.e. refer to a job). When using an id with 4 sections (i.e. refering to an item), the comments for fields within that item will also be returned. The return values will be a map whose keys are strings indicating the item index/field identifier (e.g. `"11"`, `"11/logo"`). The `opts` value is documented [here](ScrapyCloudEx.Endpoints.html#module-options). See docs [here](https://doc.scrapinghub.com/api/comments.html#comments-project-id-spider-id-job-id) and [here](https://doc.scrapinghub.com/api/comments.html#comments-project-id-spider-id-job-id-item-no-field) (GET method). ## Examples ``` # Retrieve all comments for project 14, spider 13, job 12 ScrapyCloudEx.Endpoints.App.Comments.get("API_KEY", "14/13/12") # Retrieve comments for item at index 11 (including comments on its fields) # for project 14, spider 13, job 12 ScrapyCloudEx.Endpoints.App.Comments.get("API_KEY", "14/13/12/11") # As above, but retrieve only comment for field "logo" ScrapyCloudEx.Endpoints.App.Comments.get("API_KEY", "14/13/12/11/logo") ``` """ @spec get(String.t(), String.t(), Keyword.t()) :: ScrapyCloudEx.result(%{required(String.t()) => [comment()]}) def get(api_key, composite_id, opts \\ []) when is_api_key(api_key) when is_binary(composite_id) and composite_id != "" when is_list(opts) do case basic_comment_request(api_key, composite_id, [], opts, :get) do %RequestConfig{} = request -> request |> Helpers.make_request() error -> {:error, error} end end @doc """ Updates a single comment by id. The `id` is a numerical id, as returned e.g. by `get/3` or `post/4` and NOT a binary index/field identifier (such as `"11/logo"`). The following parameters are supported in the `params` argument: * `:text` - the comment text. The `opts` value is documented [here](ScrapyCloudEx.Endpoints.html#module-options). See [docs](https://doc.scrapinghub.com/api/comments.html#comments-comment-id) (PUT method). ## Example ``` ScrapyCloudEx.Endpoints.App.Comments.put("API_KEY", 123456, text: "foo bar") ``` """ @spec put(String.t(), String.t(), Keyword.t(), Keyword.t()) :: ScrapyCloudEx.result(comment()) def put(api_key, id, params \\ [], opts \\ []) when is_api_key(api_key) when is_binary(id) and id != "" when is_list(params) when is_list(opts) do case basic_comment_request(api_key, id, params, opts, :put) do %RequestConfig{} = request -> request |> Helpers.make_request() error -> {:error, error} end end @doc """ Creates a single comment. The `composite_id` must have at least 4 sections (i.e. refer to an item). The following parameters are supported in the `params` argument: * `:text` - the comment text. The `opts` value is documented [here](ScrapyCloudEx.Endpoints.html#module-options). See [docs](https://doc.scrapinghub.com/api/comments.html#comments-project-id-spider-id-job-id-item-no-field) (POST method). ## Examples ``` ScrapyCloudEx.Endpoints.App.Comments.post("API_KEY", "14/13/12/11", text: "some text") ScrapyCloudEx.Endpoints.App.Comments.post("API_KEY", "14/13/12/11/logo", text: "some text") ``` """ @spec post(String.t(), String.t(), Keyword.t(), Keyword.t()) :: ScrapyCloudEx.result(comment()) def post(api_key, composite_id, params \\ [], opts \\ []) when is_api_key(api_key) when is_binary(composite_id) and composite_id != "" when is_list(params) when is_list(opts) do case basic_comment_request(api_key, composite_id, params, opts, :post) do %RequestConfig{} = request -> request |> Helpers.make_request() error -> {:error, error} end end @doc """ Archives a single comment. The `id` must be a comment id, or have at least 4 sections (i.e. refer to an item). The `opts` value is documented [here](ScrapyCloudEx.Endpoints.html#module-options). See docs regarding deleting [by comment id](https://doc.scrapinghub.com/api/comments.html#comments-comment-id) (DELETE method) or [by item/field identifier](https://doc.scrapinghub.com/api/comments.html#comments-project-id-spider-id-job-id-item-no-field) (DELETE method). ## Examples ``` ScrapyCloudEx.Endpoints.App.Comments.delete("API_KEY", 456789) ScrapyCloudEx.Endpoints.App.Comments.delete("API_KEY", "14/13/12/11") ScrapyCloudEx.Endpoints.App.Comments.delete("API_KEY", "14/13/12/11/logo") ``` """ @spec delete(String.t(), integer | String.t(), Keyword.t()) :: ScrapyCloudEx.result(comment()) def delete(api_key, id, opts \\ []) when is_api_key(api_key) when is_integer(id) or (is_binary(id) and id != "") when is_list(opts) do case basic_comment_request(api_key, id, [], opts, :delete) do %RequestConfig{} = request -> request |> Helpers.make_request() error -> {:error, error} end end @doc """ Retrieves the number of items with unarchived comments by job. Returns a map containing job ids as keys, and unarchived comment counts as values. Only jobs with unarchived comments are present in the map. The `opts` value is documented [here](ScrapyCloudEx.Endpoints.html#module-options). See [docs](https://doc.scrapinghub.com/api/comments.html#comments-project-id-stats). ## Example ``` ScrapyCloudEx.Endpoints.App.Comments.stats("API_KEY", "123") # {:ok, %{"123/1/4" => 1}} ``` """ @spec stats(String.t(), String.t() | integer, Keyword.t()) :: ScrapyCloudEx.result(map()) def stats(api_key, project_id, opts \\ []) when is_api_key(api_key) when is_binary(project_id) when is_list(opts) do RequestConfig.new() |> RequestConfig.put(:api_key, api_key) |> RequestConfig.put(:opts, opts) |> RequestConfig.put(:url, [@base_url, project_id, "stats"] |> merge_sections()) |> Helpers.make_request() end @spec basic_comment_request(String.t(), String.t(), Keyword.t(), Keyword.t(), atom) :: RequestConfig.t() | ScrapyCloudEx.tagged_error_info() defp basic_comment_request(api_key, composite_id, params, opts, method) do with :ok <- Helpers.validate_params(params, [:text]), :ok <- check_constraints(method, composite_id, params) do RequestConfig.new() |> RequestConfig.put(:api_key, api_key) |> RequestConfig.put(:method, method) |> RequestConfig.put(:body, params) |> RequestConfig.put(:opts, opts) |> RequestConfig.put(:url, [@base_url, composite_id] |> Enum.join("/")) else {:invalid_param, _} = error -> error end end @spec check_constraints(atom, String.t(), Keyword.t()) :: :ok | ScrapyCloudEx.tagged_error_info() defp check_constraints(method, composite_id, params) when is_atom(method) when is_binary(composite_id) or is_integer(composite_id) when is_list(params) do do_check_constraints(method, section_count(composite_id), Keyword.has_key?(params, :text)) end @spec do_check_constraints(atom, integer, boolean) :: :ok | ScrapyCloudEx.tagged_error_info() # comments/:project_id/:spider_id/:job_id defp do_check_constraints(:get, 3, _), do: :ok # comments/:project_id/:spider_id/:job_id/:item_no[/:field] defp do_check_constraints(:get, count, _) when count > 3, do: :ok defp do_check_constraints(:get, _, _) do "expected `id` param to have at least 3 sections" |> Helpers.invalid_param_error(:id) end # comments/:comment_id defp do_check_constraints(:put, _, true), do: :ok defp do_check_constraints(:put, _, false), do: required_text_param_not_provided() defp do_check_constraints(:post, _count, false), do: required_text_param_not_provided() # comments/:project_id/:spider_id/:job_id/:item_no[/:field] defp do_check_constraints(:post, count, _) when count > 3, do: :ok defp do_check_constraints(:post, _, _) do "expected `id` param to have at least 4 sections" |> Helpers.invalid_param_error(:id) end # comments/:comment_id defp do_check_constraints(:delete, 1, _), do: :ok # comments/:project_id/:spider_id/:job_id/:item_no[/:field] defp do_check_constraints(:delete, count, _) when count > 3, do: :ok defp do_check_constraints(:delete, _, _) do "expected `id` param to have only 1 section, or at least 4 sections" |> Helpers.invalid_param_error(:id) end @spec required_text_param_not_provided() :: ScrapyCloudEx.tagged_error_info() defp required_text_param_not_provided() do "required `text` param not provided" |> Helpers.invalid_param_error(:text) end @spec merge_sections([String.t()]) :: String.t() defp merge_sections(sections), do: sections |> Enum.join("/") @spec section_count(String.t()) :: integer defp section_count(composite) when is_integer(composite), do: 1 defp section_count(composite) when is_binary(composite), do: composite |> String.split("/") |> length() end