Packages

CLI to inspect, export, and delete Mastodon posts safely.

Current section

Files

Jump to
mastomation lib mastomation.ex
Raw

lib/mastomation.ex

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