The file type filters Discord shows the user when picking a file.
An ATTACHMENT command option — and, once it exists, the File Upload component — takes up
to ten filters. Each is one of the group names image, video or audio, or a
dot-prefixed extension such as .pdf.
import EDA.Command.Option
attachment("receipt", "Your receipt", required: true, file_types: [:image, ".pdf"])This is not validation
Discord matches the filename's extension and never looks inside the file. Renaming
payload.exe to payload.png passes the filter. Discord says so outright: "You are
still responsible for validating the actual contents of the file."
Treat it as a convenience that narrows the file picker, and check the bytes yourself when
it matters — EDA.ImageData.type/1 does that for images.
Discord reorders them
A registered command does not read back in the order it was sent: ["image", ".pdf"]
comes back as [".pdf", "image"]. Comparing the two lists directly — when diffing a
deployed command against its definition to decide whether to re-register — therefore says
"changed" forever. Compare expand/1 of each instead, which is sorted and deduplicated.
Checking a file yourself
expand/1 turns the filters into the concrete extensions Discord would have allowed, and
matches?/2 answers whether a filename is one of them — for re-checking an upload against
the same rule the picker used.
iex> EDA.FileType.matches?("holiday.JPG", [:image])
true
iex> EDA.FileType.matches?("notes.txt", [:image, ".pdf"])
false
Summary
Functions
Returns true if two filter lists mean the same thing.
Expands filters into the extensions Discord would accept, lowercased and deduplicated.
The group names Discord accepts, and the extensions each stands for.
Returns true if a filename passes the filters.
The most filters Discord accepts on one option.
Validates a list of filters and returns what should go on the wire.
Types
@type filter() :: :image | :video | :audio | String.t()
A group name, or a dot-prefixed extension such as ".pdf".
Functions
Returns true if two filter lists mean the same thing.
Order-insensitive and duplicate-insensitive, because Discord returns filters in an order of its own. This is the comparison to use when deciding whether a registered command still matches its definition.
Examples
iex> EDA.FileType.equivalent?(["image", ".pdf"], [".pdf", :image])
true
iex> EDA.FileType.equivalent?([:image], [:image, ".pdf"])
false
Expands filters into the extensions Discord would accept, lowercased and deduplicated.
Examples
iex> EDA.FileType.expand([:audio])
[".flac", ".m4a", ".mp3", ".ogg", ".opus", ".wav"]
iex> EDA.FileType.expand([".PDF", ".pdf"])
[".pdf"]
iex> EDA.FileType.expand([])
[]
The group names Discord accepts, and the extensions each stands for.
Examples
iex> EDA.FileType.groups()[:video]
[".mp4", ".mov", ".qt", ".webm"]
Returns true if a filename passes the filters.
Compares the extension only, case-insensitively — the same shallow check Discord makes, so
this re-applies the picker's rule rather than adding one. An empty filter list accepts
everything, which is what an option without :file_types means.
Remember that a matching extension says nothing about the contents.
Examples
iex> EDA.FileType.matches?("clip.MP4", [:video])
true
iex> EDA.FileType.matches?("report.pdf", [:image, ".pdf"])
true
iex> EDA.FileType.matches?("README", [:image])
false
iex> EDA.FileType.matches?("anything.xyz", [])
true
@spec max_filters() :: pos_integer()
The most filters Discord accepts on one option.
Validates a list of filters and returns what should go on the wire.
Group names may be given as atoms or strings; extensions are lowercased, since Discord
treats .PDF and .pdf alike.
Raises ArgumentError for more than 10 filters, for a name that is not a
group, or for an extension written without its dot — "pdf" is not a group Discord knows
and would silently filter nothing.
Examples
iex> EDA.FileType.normalize!([:image, ".PDF"])
["image", ".pdf"]
iex> EDA.FileType.normalize!(["audio"])
["audio"]