defmodule Mastomation do @moduledoc """ CLI helpers to fetch and remove Mastodon statuses. """ require Logger alias Mastomation.CLI alias Mastomation.Delete alias Mastomation.Notes alias Mastomation.Threads @doc """ Gets the Mastodon instance URL from the environment. ## Returns - The instance URL as a string, or nil if not set. """ @spec instance_url() :: String.t() | nil def instance_url do case Application.get_env(:mastomation, :cache) do %{instance_url: cached} -> cached _ -> value = System.get_env("MASTODON_INSTANCE_URL") Application.put_env(:mastomation, :cache, Map.put(Application.get_env(:mastomation, :cache) || %{}, :instance_url, value)) value end end @doc """ Gets the Mastodon access token from the environment. ## Returns - The access token as a string, or nil if not set. """ @spec access_token() :: String.t() | nil def access_token do case Application.get_env(:mastomation, :cache) do %{access_token: cached} -> cached _ -> value = System.get_env("MASTODON_ACCESS_TOKEN") Application.put_env(:mastomation, :cache, Map.put(Application.get_env(:mastomation, :cache) || %{}, :access_token, value)) value end end @doc """ Gets the Mastodon UI setting from the environment. ## Returns - The UI setting as a string (default: "standard"). """ @spec ui() :: String.t() def ui do case Application.get_env(:mastomation, :cache) do %{ui: cached} -> cached _ -> value = System.get_env("MASTODON_UI") || "standard" Application.put_env(:mastomation, :cache, Map.put(Application.get_env(:mastomation, :cache) || %{}, :ui, value)) value end end @doc """ Gets the XDG data home directory. ## Returns - The XDG data home path as a string. """ @spec xdg_data_home() :: String.t() def xdg_data_home do case Application.get_env(:mastomation, :cache) do %{xdg_data_home: cached} -> cached _ -> value = System.get_env("XDG_DATA_HOME") || Path.join(System.user_home!(), ".local/share") Application.put_env(:mastomation, :cache, Map.put(Application.get_env(:mastomation, :cache) || %{}, :xdg_data_home, value)) value end end @doc """ Parses delete-related CLI options from raw arguments. """ @spec parse([String.t()]) :: map() def parse(args \\ []) do Delete.parse_options(args) end @doc """ Executes the CLI command router. """ @spec run([String.t()]) :: :ok | :error def run(args \\ []) do parse_args = if args == [], do: ["--help"], else: args dispatch(CLI.parse(parse_args), parse_args) end @doc """ Dispatches the parsed command to the appropriate handler. ## Parameters - parsed: The parsed command structure. - parse_args: The original arguments for help printing. ## Returns - :ok on success. - :error on failure. """ @spec dispatch(term(), [String.t()]) :: :ok | :error def dispatch({:ok, [:thread, :see], result}, _parse_args), do: Threads.run_thread_see(result) def dispatch({:ok, [:thread, :export], result}, _parse_args), do: Threads.run_thread_export(result) def dispatch({:ok, [:thread], result}, _parse_args), do: Threads.run_thread_see(result) def dispatch({:ok, [:delete, :all], result}, _parse_args), do: Delete.run_delete_all(result) def dispatch({:ok, [:delete, :thread], result}, _parse_args), do: Delete.run_delete_thread(result) def dispatch({:ok, [:delete], result}, _parse_args), do: Delete.run_delete(result) def dispatch({:ok, [:notes, :download], result}, _parse_args), do: run_notes_download(result) def dispatch({:ok, [:notes, :search], result}, _parse_args), do: run_notes_search(result) def dispatch(:help, parse_args), do: print_parser_help(parse_args, :ok) def dispatch({:help, _subcommand_path}, parse_args), do: print_parser_help(parse_args, :ok) def dispatch(:version, parse_args), do: print_parser_help(parse_args, :ok) def dispatch({:error, _errors}, parse_args), do: print_parser_help(parse_args, :error) def dispatch(_other, _parse_args), do: :ok @doc """ Prints parser help and returns the status. ## Parameters - parse_args: The arguments to parse. - status: The status to return. ## Returns - The status passed in. """ @spec print_parser_help([String.t()], :ok | :error) :: :ok | :error def print_parser_help(parse_args, status) do _ = CLI.print_and_parse!(parse_args) status end @doc """ Downloads notes for followers and followed accounts. ## Parameters - result: The parsed command result. ## Returns - :ok on success. """ @spec run_notes_download(map()) :: :ok def run_notes_download(result) do options = result.options instance = options.override_instance_url || options.instance_url || instance_url() token = options.override_access_token || options.access_token || access_token() path_opt = if options.path in [nil, ""], do: [], else: [path: options.path] {:ok, path, entries} = Notes.download(instance, token, path_opt) Logger.info("saved #{Enum.count(entries)} notes to #{path}") :ok end @doc """ Searches locally downloaded notes. ## Parameters - result: The parsed command result. ## Returns - :ok on success. - :error if the notes file is not found. """ @spec run_notes_search(map()) :: :ok | :error def run_notes_search(result) do options = result.options args = result.args path_opt = if options.path in [nil, ""], do: [], else: [path: options.path] terms = [args.query | options.term] |> Enum.reject(&(&1 in [nil, ""])) case Notes.search(terms, path_opt) do {:ok, entries} -> instance_url = System.get_env("MASTODON_INSTANCE_URL") Enum.each(entries, fn entry -> Logger.info("#{entry.username} (@#{entry.handle})") Logger.info("profile: #{profile_url(instance_url, entry.handle)}") Logger.info("note: #{entry.note}") end) Logger.info("found #{Enum.count(entries)} matches") :ok {:error, :notes_file_not_found} -> Logger.warning("notes file not found. run `mastomation notes download` first.") :error end end @doc """ Generates a profile URL for a given handle. ## Parameters - instance_url: The Mastodon instance URL. - handle: The user handle. ## Returns - The profile URL as a string. """ @spec profile_url(String.t() | nil, String.t()) :: String.t() def profile_url(nil, handle), do: "@#{handle}" def profile_url(instance_url, handle) do normalized = String.trim_trailing(instance_url, "/") ui = ui() case ui do "DECK" -> "#{normalized}/deck/@#{handle}" "deck" -> "#{normalized}/deck/@#{handle}" _ -> "#{normalized}/@#{handle}" end end @doc """ Decodes HTTP response bodies into maps/lists when needed. """ @spec decode!(binary() | map() | list()) :: map() | list() def decode!(result) do case result do binary when is_binary(binary) -> JSON.decode!(binary) %{} = map -> map list when is_list(list) -> list other -> raise "Unexpected body type: #{inspect(other)}" end end @doc """ Builds authorization/content headers for Mastodon API requests. """ @spec build_header(String.t()) :: keyword(String.t()) def build_header(token) do [ authorization: "Bearer #{token}", "content-type": "application/json" ] end end