defmodule GW2.ChatCode do
@moduledoc ~S"""
Encodes and decodes Guild Wars 2 chat codes.
Chat codes are the strings Guild Wars 2 places in chat when you link
game data, such as items or wardrobe templates. They are wrapped in the
familiar `[&...]` format and contain Base64-encoded binary payloads.
This module provides the public entry point for converting between those
chat code strings and typed Elixir structs.
## Supported chat codes
The library currently supports:
* `GW2.ChatCode.Item` for item links.
* `GW2.ChatCode.WardrobeTemplate` for wardrobe template links.
Guild Wars 2 has other chat code types, such as coins, maps, skills,
traits, recipes, outfits, build templates, achievements, and travel
templates. Those types are not implemented yet. Decoding an unsupported
chat code type returns an error tuple.
## Encoding
Build one of the supported structs and pass it to `encode/1`:
iex> GW2.ChatCode.encode(%GW2.ChatCode.Item{id: 43766, quantity: 9})
{:ok, "[&Agn2qgAA]"}
## Decoding
Pass a chat code string to `decode/1` to get a typed struct back:
iex> GW2.ChatCode.decode("[&Agn2qgAA]")
{:ok, %GW2.ChatCode.Item{id: 43766, quantity: 9, skin_id: nil, upgrade_ids: []}}
Unknown, malformed, or unsupported chat codes return an error tuple instead
of raising.
Common error reasons are:
* `:invalid_code` for invalid wrappers, Base64, unsupported chat code types,
or malformed payloads.
* `:quantity_out_of_range` when encoding an item quantity outside `0..255`.
* `:invalid_id` when encoding an id that cannot fit in the chat code format.
* `:invalid_dyes` when encoding a wardrobe skin with an invalid dye list.
"""
alias GW2.ChatCode.Header
@decoders %{
item: GW2.ChatCode.Item,
wardrobe_template: GW2.ChatCode.WardrobeTemplate
}
@typedoc "A supported decoded chat code struct."
@type parsed_struct :: GW2.ChatCode.Item.t() | GW2.ChatCode.WardrobeTemplate.t()
@doc """
Encodes a supported chat code struct into a Guild Wars 2 chat code string.
Returns `{:ok, code}` when the struct can be encoded, or `{:error, reason}`
when one of its fields cannot be represented in a chat code payload.
## Examples
iex> GW2.ChatCode.encode(%GW2.ChatCode.Item{id: 46762})
{:ok, "[&AgGqtgAA]"}
iex> GW2.ChatCode.encode(%GW2.ChatCode.Item{id: 1, quantity: 256})
{:error, :quantity_out_of_range}
"""
@spec encode(GW2.ChatCode.Item.t()) :: {:ok, binary()} | {:error, atom()}
def encode(%GW2.ChatCode.Item{} = item), do: do_encode(item, GW2.ChatCode.Item)
@spec encode(GW2.ChatCode.WardrobeTemplate.t()) :: {:ok, binary()} | {:error, atom()}
def encode(%GW2.ChatCode.WardrobeTemplate{} = wardrobe_template),
do: do_encode(wardrobe_template, GW2.ChatCode.WardrobeTemplate)
@spec encode(parsed_struct()) :: {:ok, binary()} | {:error, atom()}
defp do_encode(item, encoder) do
with {:ok, binary} <- encoder.encode(item),
encoded <- Base.encode64(binary) do
{:ok, wrap(encoded)}
else
{:error, _} = error -> error
_ -> {:error, :invalid_code}
end
end
@spec wrap(binary()) :: binary()
defp wrap(encoded) do
"[&#{encoded}]"
end
@doc """
Decodes a chat code string into a structured value.
Returns `{:ok, struct}` for supported chat code types. Invalid Base64,
missing wrappers, unknown chat code types, and malformed payloads return
an error tuple.
## Examples
iex> GW2.ChatCode.decode("[&AgGqtgAA]")
{:ok, %GW2.ChatCode.Item{id: 46762, quantity: 1, skin_id: nil, upgrade_ids: []}}
iex> GW2.ChatCode.decode("not a chat code")
{:error, :invalid_code}
"""
@spec decode(binary()) :: {:ok, parsed_struct()} | {:error, atom()}
def decode(code) do
with {:ok, binary} <- normalize_code(code),
{:ok, {type, rest}} <- Header.decode_type(binary),
{:ok, decoder} <- fetch_decoder(type) do
decoder.decode(rest)
end
end
@spec fetch_decoder(atom()) :: {:ok, module()} | {:error, :invalid_code}
defp fetch_decoder(type) do
case Map.fetch(@decoders, type) do
{:ok, parser} -> {:ok, parser}
:error -> {:error, :invalid_code}
end
end
@spec normalize_code(binary()) :: {:ok, binary()} | {:error, :invalid_code}
defp normalize_code("[&" <> code) do
case String.split(code, "]", parts: 2) do
[encoded, ""] -> Base.decode64(encoded)
_ -> {:error, :invalid_code}
end
end
defp normalize_code(_), do: {:error, :invalid_code}
end