Packages
parrot_platform
0.0.1-alpha.1
Elixir libraries and OTP behaviours for building telecom applications with SIP protocol and media handling
Current section
Files
Jump to
Current section
Files
lib/parrot/sip/message.ex
defmodule Parrot.Sip.Message do
@moduledoc """
Represents a SIP message (request or response).
This module provides a struct and functions for working with SIP messages as defined
in RFC 3261 and related specifications. It provides a pure functional implementation
that models both SIP requests and responses, along with utility functions for
manipulation, analysis, and conversion.
References:
- RFC 3261 Section 7: SIP Messages
- RFC 3261 Section 8.1: UAC Behavior
- RFC 3261 Section 8.2: UAS Behavior
- RFC 3261 Section 20: Header Fields
"""
require Logger
alias Parrot.Sip.DialogId
alias Parrot.Sip.Headers.{CSeq, From, To, Via, CallId, Contact}
alias Parrot.Sip.Method
defstruct [
# Atom like :invite, :register
:method,
# URI for requests
:request_uri,
# Integer for responses
:status_code,
# String for responses
:reason_phrase,
# String, typically "SIP/2.0"
:version,
# Map of header name to header struct
:headers,
# Binary string
:body,
# Source information for transport
:source,
# :request or :response
:type,
# :incoming or :outgoing
:direction,
# transaction_id
:transaction_id,
# dialog_id
:dialog_id
]
@type t :: %__MODULE__{
method: Method.t() | nil,
request_uri: String.t() | nil,
status_code: integer() | nil,
reason_phrase: String.t() | nil,
version: String.t(),
headers: map(),
body: String.t(),
source: map() | nil,
type: :request | :response | nil,
direction: :incoming | :outgoing | nil,
transaction_id: String.t() | nil,
dialog_id: String.t() | nil
}
@default_reason_phrases %{
100 => "Trying",
180 => "Ringing",
181 => "Call Is Being Forwarded",
182 => "Queued",
183 => "Session Progress",
199 => "Early Dialog Terminated",
200 => "OK",
202 => "Accepted",
204 => "No Notification",
300 => "Multiple Choices",
301 => "Moved Permanently",
302 => "Moved Temporarily",
305 => "Use Proxy",
380 => "Alternative Service",
400 => "Bad Request",
401 => "Unauthorized",
402 => "Payment Required",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
406 => "Not Acceptable",
407 => "Proxy Authentication Required",
408 => "Request Timeout",
409 => "Conflict",
410 => "Gone",
412 => "Conditional Request Failed",
413 => "Request Entity Too Large",
414 => "Request-URI Too Long",
415 => "Unsupported Media Type",
416 => "Unsupported URI Scheme",
417 => "Unknown Resource-Priority",
420 => "Bad Extension",
421 => "Extension Required",
422 => "Session Interval Too Small",
423 => "Interval Too Brief",
424 => "Bad Location Information",
428 => "Use Identity Header",
429 => "Provide Referrer Identity",
430 => "Flow Failed",
433 => "Anonymity Disallowed",
436 => "Bad Identity-Info",
437 => "Unsupported Certificate",
438 => "Invalid Identity Header",
439 => "First Hop Lacks Outbound Support",
440 => "Max-Breadth Exceeded",
470 => "Consent Needed",
480 => "Temporarily Unavailable",
481 => "Call/Transaction Does Not Exist",
482 => "Loop Detected",
483 => "Too Many Hops",
484 => "Address Incomplete",
485 => "Ambiguous",
486 => "Busy Here",
487 => "Request Terminated",
488 => "Not Acceptable Here",
489 => "Bad Event",
491 => "Request Pending",
493 => "Undecipherable",
494 => "Security Agreement Required",
500 => "Server Internal Error",
501 => "Not Implemented",
502 => "Bad Gateway",
503 => "Service Unavailable",
504 => "Server Time-out",
505 => "Version Not Supported",
513 => "Message Too Large",
580 => "Precondition Failure",
600 => "Busy Everywhere",
603 => "Decline",
604 => "Does Not Exist Anywhere",
606 => "Not Acceptable",
607 => "Unwanted",
608 => "Rejected"
}
@doc """
Returns the default reason phrase for a given SIP status code.
"""
@spec default_reason_phrase(integer()) :: String.t()
def default_reason_phrase(status_code) do
Map.get(@default_reason_phrases, status_code, "Unknown")
end
@doc """
Creates a new request message with the specified method, request URI, and headers.
This function is the main entry point for creating SIP request messages.
## Parameters
- method: The SIP method (atom) for the request
- request_uri: The request target URI
- headers: Optional map of initial headers
## Examples
iex> Parrot.Sip.Message.new_request(:invite, "sip:alice@example.com")
%Parrot.Sip.Message{
method: :invite,
request_uri: "sip:alice@example.com",
version: "SIP/2.0",
headers: %{},
body: "",
type: :request,
direction: :outgoing
}
"""
@spec new_request(Method.t(), String.t(), map(), keyword()) :: t()
def new_request(method, request_uri, headers \\ %{}, opts \\ []) do
%__MODULE__{
method: method,
request_uri: request_uri,
version: "SIP/2.0",
headers: headers,
body: "",
type: :request,
direction: :outgoing,
dialog_id: Keyword.get(opts, :dialog_id, nil),
transaction_id: Keyword.get(opts, :transaction_id, nil)
}
end
@doc """
Creates a new response message with the specified status code, reason phrase, and headers.
## Parameters
- status_code: The SIP response status code (100-699)
- reason_phrase: The reason phrase for the response
- headers: Optional map of initial headers
## Examples
iex> Parrot.Sip.Message.new_response(200, "OK")
%Parrot.Sip.Message{
status_code: 200,
reason_phrase: "OK",
version: "SIP/2.0",
headers: %{},
body: "",
type: :response,
direction: :outgoing
}
"""
@spec new_response(integer(), String.t(), map(), keyword()) :: t()
def new_response(status_code, reason_phrase, headers, opts) do
%__MODULE__{
status_code: status_code,
reason_phrase: reason_phrase,
version: "SIP/2.0",
headers: headers,
body: "",
type: :response,
direction: :outgoing,
dialog_id: Keyword.get(opts, :dialog_id, nil),
transaction_id: Keyword.get(opts, :transaction_id, nil)
}
end
@doc """
Creates a new response message with standard reason phrase based on status code.
If no reason phrase is provided, a standard one will be used based on the status code.
## Examples
iex> Parrot.Sip.Message.new_response(200)
%Parrot.Sip.Message{
status_code: 200,
reason_phrase: "OK",
version: "SIP/2.0",
headers: %{},
body: "",
type: :response,
direction: :outgoing
}
"""
@spec new_response(integer()) :: t()
def new_response(status_code) do
reason_phrase = Map.get(@default_reason_phrases, status_code, "Unknown")
new_response(status_code, reason_phrase, %{}, [])
end
@spec new_response(integer(), String.t()) :: t()
def new_response(status_code, reason_phrase) when is_binary(reason_phrase) do
new_response(status_code, reason_phrase, %{}, [])
end
@spec new_response(integer(), keyword()) :: t()
def new_response(status_code, opts) when is_list(opts) do
reason_phrase = Map.get(@default_reason_phrases, status_code, "Unknown")
new_response(status_code, reason_phrase, %{}, opts)
end
@spec new_response(integer(), String.t(), map()) :: t()
def new_response(status_code, reason_phrase, headers) do
new_response(status_code, reason_phrase, headers, [])
end
@doc """
Creates a response from a request, copying necessary headers and setting
the status code and reason phrase.
This function follows the requirements in RFC 3261 Section 8.2.6 for
copying headers from requests to responses.
## Parameters
- request: The SIP request message
- status_code: The response status code
- reason_phrase: The reason phrase for the response
## Examples
iex> request = Parrot.Sip.Message.new_request(:invite, "sip:alice@example.com")
iex> response = Parrot.Sip.Message.reply(request, 200, "OK")
iex> response.status_code
200
"""
@spec reply(t(), integer(), String.t()) :: t()
def reply(request, status_code, reason_phrase) when request.type == :request do
# Copy headers from request to response with proper manipulation
resp_headers = %{}
# Add via headers from request
resp_headers = Map.put(resp_headers, "via", request.headers["via"])
# Add to/from headers
resp_headers = Map.put(resp_headers, "to", request.headers["to"])
resp_headers = Map.put(resp_headers, "from", request.headers["from"])
# Add call-id
resp_headers = Map.put(resp_headers, "call-id", request.headers["call-id"])
# Add CSeq
resp_headers = Map.put(resp_headers, "cseq", request.headers["cseq"])
%__MODULE__{
method: request.method,
request_uri: request.request_uri,
status_code: status_code,
reason_phrase: reason_phrase,
version: request.version,
headers: resp_headers,
body: Map.get(request, :body, ""),
source: request.source,
type: :response,
direction: :outgoing,
dialog_id: Map.get(request, :dialog_id),
transaction_id: Map.get(request, :transaction_id)
}
end
@doc """
Creates a response from a request with standard reason phrase based on status code.
## Examples
iex> request = Parrot.Sip.Message.new_request(:invite, "sip:alice@example.com")
iex> response = Parrot.Sip.Message.reply(request, 200)
iex> response.reason_phrase
"OK"
"""
@spec reply(t(), integer()) :: t()
def reply(request, status_code) when request.type == :request do
reason_phrase = Map.get(@default_reason_phrases, status_code, "Unknown")
reply(request, status_code, reason_phrase)
end
@doc """
Gets a header from the message by name.
Header names are case-insensitive as per RFC 3261.
If the header is a map, it will be converted to the appropriate struct.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> message = Parrot.Sip.Message.set_header(message, "Via", %{protocol: "SIP", version: "2.0", transport: "udp"})
iex> Parrot.Sip.Message.get_header(message, "via")
%Parrot.Sip.Headers.Via{protocol: "SIP", version: "2.0", transport: :udp}
"""
@spec get_header(t(), String.t()) :: any()
# Get Via header using direct pattern matching
def get_header(message, "Via"), do: get_via_header(message)
def get_header(message, "via"), do: get_via_header(message)
def get_header(message, "v"), do: get_via_header(message)
# Regular get_header function for other headers
def get_header(message, name) do
downcased = String.downcase(name)
# Resolve compact headers to their full forms using the Serializer's function
full_name = Parrot.Sip.Serializer.expand_compact_header(downcased)
# Get the value
value = Map.get(message.headers, full_name)
# Return nil if not found
if is_nil(value) do
nil
else
# Special case for content-type when accessed directly
if full_name == "content-type" && is_struct(value, Parrot.Sip.Headers.ContentType) &&
downcased == "content-type" do
Parrot.Sip.Headers.ContentType.format(value)
else
# Return the value as-is (it should already be a proper struct or primitive value)
value
end
end
end
@doc """
Gets all headers of a given name. Returns a list, even if only one header is present.
Useful for headers that can appear multiple times like Record-Route or Via.
"""
@spec get_headers(t(), String.t()) :: [any()]
def get_headers(message, name) do
downcased = String.downcase(name)
full_name = Parrot.Sip.Serializer.expand_compact_header(downcased)
values = Map.get(message.headers, full_name)
cond do
is_nil(values) ->
[]
is_list(values) ->
Enum.map(values, fn
v when is_struct(v) ->
v
v when is_map(v) ->
struct_for_header(full_name, v)
v ->
v
end)
true ->
[get_header(message, name)]
end
end
# Extracted helper
# Headers are now parsed as structs during the initial parsing process,
# so this function is no longer needed. However, we keep it for backward compatibility
# with any code that might still rely on it.
defp struct_for_header("record-route", v) when is_map(v) and not is_struct(v),
do: struct(Parrot.Sip.Headers.RecordRoute, v)
defp struct_for_header("route", v) when is_map(v) and not is_struct(v),
do: struct(Parrot.Sip.Headers.Route, v)
defp struct_for_header("via", v) when is_map(v) and not is_struct(v),
do: struct(Parrot.Sip.Headers.Via, v)
defp struct_for_header("contact", v) when is_map(v) and not is_struct(v),
do: struct(Parrot.Sip.Headers.Contact, v)
defp struct_for_header(_, v), do: v
# Helper function to handle Via header logic
defp get_via_header(message) do
value = Map.get(message.headers, "via")
cond do
is_nil(value) ->
nil
is_struct(value) ->
value
is_binary(value) ->
Parrot.Sip.Headers.Via.parse(value)
is_list(value) && Enum.all?(value, &is_binary/1) ->
Enum.map(value, &Parrot.Sip.Headers.Via.parse/1)
is_list(value) && Enum.all?(value, &is_struct/1) ->
Enum.map(value, fn via_struct ->
if is_struct(via_struct, Parrot.Sip.Headers.Via) do
via_struct
else
struct(Parrot.Sip.Headers.Via, via_struct)
end
end)
is_list(value) ->
Enum.map(value, fn via ->
struct(Parrot.Sip.Headers.Via, via)
end)
true ->
struct(Parrot.Sip.Headers.Via, value)
end
end
@doc """
Sets a header in the message.
Header names are converted to lowercase for consistent storage and retrieval.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> from_header = %Parrot.Sip.Headers.From{}
iex> message = Parrot.Sip.Message.set_header(message, "From", from_header)
iex> message.headers["from"] == from_header
true
"""
@spec set_header(t(), String.t(), any()) :: t()
def set_header(message, name, value) do
downcased = String.downcase(name)
%{message | headers: Map.put(message.headers, downcased, value)}
end
@doc """
Sets the body of the message and updates the Content-Length header.
This function automatically calculates and sets the Content-Length header
based on the body's length.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> message = Parrot.Sip.Message.set_body(message, "Hello, world!")
iex> message.body
"Hello, world!"
iex> message.headers["content-length"]
13
"""
@spec set_body(t(), String.t()) :: t()
def set_body(message, body) do
headers = Map.put(message.headers, "content-length", byte_size(body))
%{message | body: body, headers: headers}
end
@doc """
Check if a message is a request.
"""
@spec is_request?(t()) :: boolean()
def is_request?(message), do: message.type == :request
@doc """
Check if a message is a response.
"""
@spec is_response?(t()) :: boolean()
def is_response?(message), do: message.type == :response
@doc """
Convert the message to a string representation.
"""
@spec to_s(t()) :: String.t()
def to_s(message) do
start_line =
if is_request?(message) do
"#{method_to_string(message.method)} #{message.request_uri} #{message.version}\r\n"
else
"#{message.version} #{message.status_code} #{message.reason_phrase}\r\n"
end
headers_str = headers_to_string(message.headers)
start_line <> headers_str <> "\r\n" <> (message.body || "")
end
defp method_to_string(method) when is_atom(method) do
Method.to_string(method)
end
defp headers_to_string(headers) do
# RFC 3261 Section 7.3.1: Header Field Order
# Via headers must appear before all other headers
# Then: Route, Record-Route, Proxy-Require, Max-Forwards,
# Proxy-Authorization, To, From, Contact, etc.
# Define header order priority (lower number = higher priority)
header_order = %{
"via" => 1,
"route" => 2,
"record-route" => 3,
"max-forwards" => 4,
"proxy-require" => 5,
"proxy-authorization" => 6,
"from" => 7,
"to" => 8,
"call-id" => 9,
"cseq" => 10,
"contact" => 11,
"expires" => 12,
"content-type" => 13,
"content-length" => 14
}
# Default priority for headers not in the list
default_priority = 100
headers
|> Enum.sort_by(fn {name, _value} ->
normalized_name = name |> to_string() |> String.downcase()
Map.get(header_order, normalized_name, default_priority)
end)
|> Enum.map(fn {name, value} -> format_header(name, value) end)
|> Enum.join("")
end
defp format_header(name, value) do
header_name =
name
|> to_string()
|> String.split("-")
|> Enum.map(&String.capitalize/1)
|> Enum.join("-")
"#{header_name}: #{format_header_value(name, value)}\r\n"
end
defp format_header_value(_name, value) when is_binary(value), do: value
defp format_header_value(_name, value) when is_integer(value), do: Integer.to_string(value)
defp format_header_value(_name, value) when is_list(value) do
# For lists, check if all elements are structs with format function
if Enum.all?(value, &(is_struct(&1) && function_exported?(&1.__struct__, :format, 1))) do
# Check if the module has a format_list function
first_struct = List.first(value)
if first_struct && function_exported?(first_struct.__struct__, :format_list, 1) do
first_struct.__struct__.format_list(value)
else
# Default: format each and join with comma
value
|> Enum.map(&(&1.__struct__.format(&1)))
|> Enum.join(", ")
end
else
inspect(value)
end
end
defp format_header_value(_name, value) do
# Try to call format function on header module
if is_struct(value) && function_exported?(value.__struct__, :format, 1) do
value.__struct__.format(value)
else
# Fall back to inspect for unhandled types
inspect(value)
end
end
@doc """
Gets the CSeq header from a message.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> cseq = %Parrot.Sip.Headers.CSeq{sequence: 1, method: :invite}
iex> message = Parrot.Sip.Message.set_header(message, "CSeq", cseq)
iex> Parrot.Sip.Message.cseq(message)
%Parrot.Sip.Headers.CSeq{sequence: 1, method: :invite}
"""
@spec cseq(t()) :: CSeq.t() | nil
def cseq(message) do
get_header(message, "cseq")
end
@doc """
Gets the From header from a message.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> from = %Parrot.Sip.Headers.From{uri: "sip:alice@example.com"}
iex> message = Parrot.Sip.Message.set_header(message, "From", from)
iex> Parrot.Sip.Message.from(message)
%Parrot.Sip.Headers.From{uri: "sip:alice@example.com"}
"""
@spec from(t()) :: From.t() | nil
def from(message) do
get_header(message, "from")
end
@doc """
Gets the To header from a message.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> to = %Parrot.Sip.Headers.To{uri: "sip:bob@example.com"}
iex> message = Parrot.Sip.Message.set_header(message, "To", to)
iex> Parrot.Sip.Message.to(message)
%Parrot.Sip.Headers.To{uri: "sip:bob@example.com"}
"""
@spec to(t()) :: To.t() | nil
def to(message) do
get_header(message, "to")
end
@doc """
Gets the Call-ID header from a message.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> message = Parrot.Sip.Message.set_header(message, "Call-ID", "abc123@example.com")
iex> Parrot.Sip.Message.call_id(message)
"abc123@example.com"
"""
@spec call_id(t()) :: String.t() | CallId.t() | nil
def call_id(message) do
get_header(message, "call-id")
end
@doc """
Gets the top Via header from a message.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> via = %Parrot.Sip.Headers.Via{host: "example.com", port: 5060}
iex> message = Parrot.Sip.Message.set_header(message, "Via", via)
iex> Parrot.Sip.Message.top_via(message)
%Parrot.Sip.Headers.Via{host: "example.com", port: 5060}
"""
@spec top_via(t()) :: Via.t() | nil
def top_via(message) do
case get_header(message, "via") do
nil -> nil
via when is_list(via) -> List.first(via)
via -> via
end
end
@doc """
Gets all Via headers from a message.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> via = %Parrot.Sip.Headers.Via{host: "example.com", port: 5060}
iex> message = Parrot.Sip.Message.set_header(message, "Via", via)
iex> Parrot.Sip.Message.all_vias(message)
[%Parrot.Sip.Headers.Via{host: "example.com", port: 5060}]
"""
@spec all_vias(t()) :: list(Via.t())
def all_vias(message) do
case get_header(message, "via") do
nil -> []
via when is_list(via) -> via
via -> [via]
end
end
@doc """
Gets the Contact header from a message.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> contact = %Parrot.Sip.Headers.Contact{uri: "sip:alice@example.com"}
iex> message = Parrot.Sip.Message.set_header(message, "Contact", contact)
iex> Parrot.Sip.Message.contact(message)
%Parrot.Sip.Headers.Contact{uri: "sip:alice@example.com"}
"""
@spec contact(t()) :: Contact.t() | nil
def contact(message) do
get_header(message, "contact")
end
@doc """
Gets the branch parameter from the top Via header.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> via = %Parrot.Sip.Headers.Via{parameters: %{"branch" => "z9hG4bK123"}}
iex> message = Parrot.Sip.Message.set_header(message, "Via", via)
iex> Parrot.Sip.Message.branch(message)
"z9hG4bK123"
"""
@spec branch(t()) :: String.t() | nil
def branch(message) do
case top_via(message) do
nil -> nil
via -> Map.get(via.parameters, "branch")
end
end
@doc """
Gets a dialog ID from a message.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> from = %Parrot.Sip.Headers.From{parameters: %{"tag" => "123"}}
iex> to = %Parrot.Sip.Headers.To{parameters: %{"tag" => "456"}}
iex> message = message |> Parrot.Sip.Message.set_header("From", from)
iex> message = message |> Parrot.Sip.Message.set_header("To", to)
iex> message = message |> Parrot.Sip.Message.set_header("Call-ID", "abc@example.com")
iex> dialog_id = Parrot.Sip.Message.dialog_id(message)
iex> dialog_id.call_id
"abc@example.com"
"""
@spec dialog_id(t()) :: DialogId.t()
def dialog_id(message) do
Logger.debug("Getting dialog_id from message")
DialogId.from_message(message)
end
@doc """
Determines if a message is within a dialog.
A message is within a dialog if it has a To tag and a From tag.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> from = %Parrot.Sip.Headers.From{parameters: %{"tag" => "123"}}
iex> to = %Parrot.Sip.Headers.To{parameters: %{"tag" => "456"}}
iex> message = message |> Parrot.Sip.Message.set_header("From", from)
iex> message = message |> Parrot.Sip.Message.set_header("To", to)
iex> Parrot.Sip.Message.in_dialog?(message)
true
"""
@spec in_dialog?(t()) :: boolean()
def in_dialog?(message) do
from_header = from(message)
to_header = to(message)
from_tag = if from_header, do: From.tag(from_header), else: nil
to_tag = if to_header, do: To.tag(to_header), else: nil
not is_nil(from_tag) and not is_nil(to_tag)
end
@doc """
Gets the status class of a response message (1xx, 2xx, etc.).
## Examples
iex> message = Parrot.Sip.Message.new_response(200, "OK")
iex> Parrot.Sip.Message.status_class(message)
2
"""
@spec status_class(t()) :: integer() | nil
def status_class(%__MODULE__{type: :response, status_code: status_code})
when is_integer(status_code) do
div(status_code, 100)
end
def status_class(_), do: nil
@doc """
Checks if a response message is provisional (1xx).
## Examples
iex> message = Parrot.Sip.Message.new_response(180, "Ringing")
iex> Parrot.Sip.Message.is_provisional?(message)
true
"""
@spec is_provisional?(t()) :: boolean()
def is_provisional?(message) do
status_class(message) == 1
end
@doc """
Checks if a response message is successful (2xx).
## Examples
iex> message = Parrot.Sip.Message.new_response(200, "OK")
iex> Parrot.Sip.Message.is_success?(message)
true
"""
@spec is_success?(t()) :: boolean()
def is_success?(message) do
status_class(message) == 2
end
@doc """
Checks if a response message is a redirection (3xx).
## Examples
iex> message = Parrot.Sip.Message.new_response(302, "Moved Temporarily")
iex> Parrot.Sip.Message.is_redirect?(message)
true
"""
@spec is_redirect?(t()) :: boolean()
def is_redirect?(message) do
status_class(message) == 3
end
@doc """
Checks if a response message is a client error (4xx).
## Examples
iex> message = Parrot.Sip.Message.new_response(404, "Not Found")
iex> Parrot.Sip.Message.is_client_error?(message)
true
"""
@spec is_client_error?(t()) :: boolean()
def is_client_error?(message) do
status_class(message) == 4
end
@doc """
Checks if a response message is a server error (5xx).
## Examples
iex> message = Parrot.Sip.Message.new_response(500, "Server Internal Error")
iex> Parrot.Sip.Message.is_server_error?(message)
true
"""
@spec is_server_error?(t()) :: boolean()
def is_server_error?(message) do
status_class(message) == 5
end
@doc """
Checks if a response message is a global error (6xx).
## Examples
iex> message = Parrot.Sip.Message.new_response(603, "Decline")
iex> Parrot.Sip.Message.is_global_error?(message)
true
"""
@spec is_global_error?(t()) :: boolean()
def is_global_error?(message) do
status_class(message) == 6
end
@doc """
Checks if a response message is a failure (4xx, 5xx, or 6xx).
## Examples
iex> message = Parrot.Sip.Message.new_response(404, "Not Found")
iex> Parrot.Sip.Message.is_failure?(message)
true
"""
@spec is_failure?(t()) :: boolean()
def is_failure?(message) do
class = status_class(message)
class in [4, 5, 6]
end
@doc """
Converts a message to binary format for transmission.
## Examples
iex> message = Parrot.Sip.Message.new_request(:invite, "sip:bob@example.com")
iex> binary = Parrot.Sip.Message.to_binary(message)
iex> String.starts_with?(binary, "INVITE sip:bob@example.com SIP/2.0")
true
"""
@spec to_binary(t()) :: binary()
def to_binary(message) do
to_s(message)
end
end