Packages

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

Current section

Files

Jump to
mastomation lib parser.ex
Raw

lib/parser.ex

defmodule Mastomation.Parser do
@moduledoc """
Centralized Optimus parser for Mastomation commands.
"""
@default_delay_ms 60_000
@doc """
Parses CLI arguments and returns an Optimus parse result.
"""
@spec parse([String.t()]) :: term()
def parse(args) do
args
|> normalize_args()
|> then(&Optimus.parse(parser(), &1))
end
@doc """
Parses CLI arguments and raises on invalid input.
"""
@spec parse!([String.t()]) :: term()
def parse!(args) do
args
|> normalize_args()
|> then(&Optimus.parse!(parser(), &1))
end
@doc """
Prints parser output/help and converts parser halts into tuples.
"""
@spec print_and_parse!([String.t()]) :: :ok | {:halt, non_neg_integer()}
def print_and_parse!(args) do
_ =
args
|> normalize_args()
|> then(&Optimus.parse!(parser(), &1, fn code -> throw({:optimus_halt, code}) end))
:ok
catch
{:optimus_halt, code} -> {:halt, code}
end
@doc """
Returns the Optimus parser configuration.
"""
@spec parser() :: term()
def parser do
Optimus.new!(
name: "mastomation",
about: "Inspect, export, and delete your Mastodon posts with safety defaults.",
allow_unknown_args: false,
parse_double_dash: true,
subcommands: [
delete: [
name: "delete",
about:
"Delete posts permanently. Use --dry-run first. Subcommands: all, thread <URL_OR_ID>.",
subcommands: [
all: [
name: "all",
about:
"Delete timeline statuses (excluding pinned/bookmarked/favourited by default).",
options:
common_auth_options() ++
[
delay_ms: [
value_name: "MILLISECONDS",
long: "--delay-ms",
short: "-d",
help: "Delay between deletions in milliseconds (default: 60000)",
parser: :integer,
default: @default_delay_ms,
required: false
],
keyword: [
value_name: "KEYWORD",
long: "--keyword",
help: "Delete only statuses containing this keyword (repeatable)",
parser: :string,
multiple: true,
default: []
]
],
flags: common_delete_flags()
],
thread: [
name: "thread",
about: "Delete your posts in a specific thread: delete thread <URL_OR_ID>.",
args: [
source: [
value_name: "STATUS_ID_OR_URL",
help: "Thread source status ID or full URL",
required: true,
parser: :string
]
],
options:
common_auth_options() ++
[
delay_ms: [
value_name: "MILLISECONDS",
long: "--delay-ms",
short: "-d",
help: "Delay between thread deletions in milliseconds (capped at 2000)",
parser: :integer,
default: @default_delay_ms,
required: false
],
keyword: [
value_name: "KEYWORD",
long: "--keyword",
help: "Delete only statuses containing this keyword (repeatable)",
parser: :string,
multiple: true,
default: []
]
],
flags: common_delete_flags()
]
]
],
thread: [
name: "thread",
about: "Thread operations. Use `see` to inspect and `export` to write files.",
subcommands: [
see: [
name: "see",
about: "Print a joined same-author thread to the terminal.",
args: [
source: [
value_name: "STATUS_ID_OR_URL",
help: "Thread source status ID or full URL",
required: true,
parser: :string
]
],
options: common_auth_options(),
flags: [
dry_run: [
long: "--dry-run",
help: "Explicit inspect mode (no files written)"
]
]
],
export: [
name: "export",
about: "Export joined thread markdown and media to ./export.",
args: [
source: [
value_name: "STATUS_ID_OR_URL",
help: "Thread source status ID or full URL",
required: true,
parser: :string
]
],
options: common_auth_options(),
flags: [
frontmatter: [
long: "--frontmatter",
help: "Include YAML frontmatter in exported markdown"
]
]
]
]
],
notes: [
name: "notes",
about: "Private profile notes operations.",
subcommands: [
download: [
name: "download",
about: "Download notes for followers and followed accounts.",
options:
common_auth_options() ++
[
path: [
value_name: "FILE_PATH",
long: "--path",
help: "Where to store notes JSON (default: XDG data path)",
parser: :string,
default: nil,
required: false
]
]
],
search: [
name: "search",
about: "Search locally downloaded notes.",
args: [
query: [
value_name: "QUERY",
help: "Search query text",
required: false,
parser: :string
]
],
options: [
path: [
value_name: "FILE_PATH",
long: "--path",
help: "Path to notes JSON file",
parser: :string,
default: nil,
required: false
],
term: [
value_name: "TERM",
long: "--term",
help: "Search term (repeatable)",
parser: :string,
multiple: true,
default: []
]
]
]
]
]
]
)
end
@spec common_auth_options() :: keyword()
defp common_auth_options do
[
instance_url: [
value_name: "INSTANCE_URL",
short: "-i",
long: "--instance-url",
help: "Mastodon instance URL (fallback: MASTODON_INSTANCE_URL)",
parser: :string,
default: nil,
required: false
],
access_token: [
value_name: "ACCESS_TOKEN",
short: "-t",
long: "--access-token",
help: "Access token (fallback: MASTODON_ACCESS_TOKEN)",
parser: :string,
default: nil,
required: false
],
override_instance_url: [
value_name: "INSTANCE_URL",
long: "--override-instance-url",
help: "Force instance URL for this run (highest priority)",
parser: :string,
default: nil,
required: false
],
override_access_token: [
value_name: "ACCESS_TOKEN",
long: "--override-access-token",
help: "Force access token for this run (highest priority)",
parser: :string,
default: nil,
required: false
]
]
end
@spec common_delete_flags() :: keyword()
defp common_delete_flags do
[
dry_run: [
long: "--dry-run",
help: "Only print what would be deleted; do not delete"
],
no_backup: [
long: "--no-backup",
help: "Disable backups (backups are ON by default)"
],
frontmatter: [
long: "--frontmatter",
help: "Include frontmatter in markdown backups"
]
]
end
# Normalizes legacy flags and help invocation forms.
@spec normalize_args([String.t()]) :: [String.t()]
defp normalize_args(args) do
args
|> normalize_legacy_command_forms()
|> Enum.map(fn
"--dry_run" -> "--dry-run"
"--instance_url" -> "--instance-url"
"--access_token" -> "--access-token"
"--delay_ms" -> "--delay-ms"
other -> other
end)
|> normalize_subcommand_help()
end
@spec normalize_legacy_command_forms([String.t()]) :: [String.t()]
defp normalize_legacy_command_forms(["thread", subcommand | _] = args)
when subcommand in ["see", "export", "--help", "-h"] do
args
end
defp normalize_legacy_command_forms(["thread", source]) when source not in ["--help", "-h"] do
["thread", "see", source]
end
defp normalize_legacy_command_forms(["thread" | rest]) do
["thread", "see" | rest]
end
defp normalize_legacy_command_forms(["delete", subcommand | _] = args)
when subcommand in ["all", "thread", "--help", "-h"] do
args
end
defp normalize_legacy_command_forms(["delete", "--thread-source", source | rest]) do
["delete", "thread", source | rest]
end
defp normalize_legacy_command_forms(["delete"]) do
["help", "delete"]
end
defp normalize_legacy_command_forms(["delete" | rest]) do
["delete", "all" | rest]
end
defp normalize_legacy_command_forms(args), do: args
# Rewrites subcommand help into Optimus-compatible help commands.
@spec normalize_subcommand_help([String.t()]) :: [String.t()]
defp normalize_subcommand_help(["thread", "--help"]), do: ["help", "thread"]
defp normalize_subcommand_help(["thread", "-h"]), do: ["help", "thread"]
defp normalize_subcommand_help(["thread", "see", "--help"]), do: ["help", "thread", "see"]
defp normalize_subcommand_help(["thread", "see", "-h"]), do: ["help", "thread", "see"]
defp normalize_subcommand_help(["thread", "export", "--help"]), do: ["help", "thread", "export"]
defp normalize_subcommand_help(["thread", "export", "-h"]), do: ["help", "thread", "export"]
defp normalize_subcommand_help(["delete", "--help"]), do: ["help", "delete"]
defp normalize_subcommand_help(["delete", "-h"]), do: ["help", "delete"]
defp normalize_subcommand_help(["delete", "all", "--help"]), do: ["help", "delete", "all"]
defp normalize_subcommand_help(["delete", "all", "-h"]), do: ["help", "delete", "all"]
defp normalize_subcommand_help(["delete", "thread", "--help"]), do: ["help", "delete", "thread"]
defp normalize_subcommand_help(["delete", "thread", "-h"]), do: ["help", "delete", "thread"]
defp normalize_subcommand_help(["notes", "--help"]), do: ["help", "notes"]
defp normalize_subcommand_help(["notes", "-h"]), do: ["help", "notes"]
defp normalize_subcommand_help(["notes", "download", "--help"]),
do: ["help", "notes", "download"]
defp normalize_subcommand_help(["notes", "download", "-h"]), do: ["help", "notes", "download"]
defp normalize_subcommand_help(["notes", "search", "--help"]), do: ["help", "notes", "search"]
defp normalize_subcommand_help(["notes", "search", "-h"]), do: ["help", "notes", "search"]
defp normalize_subcommand_help(args), do: args
end