defmodule Amarula.Protocol.Groups.Metadata do
@moduledoc """
Group metadata: build the `w:g2` query IQ and parse its result into the bits a
group send needs. Port of Baileys `groupQuery`/`extractGroupMetadata`
(`src/Socket/groups.ts`), trimmed to the send-relevant fields (participants +
addressing mode); description/community/etc. are skipped until a consumer needs
them.
Usage (the IQ goes through `Connection.query_iq/2`):
iq = Metadata.query_iq(group_jid)
{:ok, reply} = Connection.query_iq(conn, iq)
{:ok, meta} = Metadata.parse(reply)
"""
alias Amarula.Protocol.Binary.{JID, Node, NodeUtils}
@type participant :: %{
id: String.t(),
lid: String.t() | nil,
phone_number: String.t() | nil,
admin: String.t() | nil
}
@type t :: %{
id: String.t(),
subject: String.t() | nil,
owner: String.t() | nil,
addressing_mode: :pn | :lid,
size: non_neg_integer(),
participants: [participant()]
}
@doc """
Build the interactive group-metadata query IQ:
>
"""
@spec query_iq(String.t()) :: Node.t()
def query_iq(group_jid) do
%Node{
tag: "iq",
attrs: [{"type", "get"}, {"xmlns", "w:g2"}, {"to", group_jid}],
content: [%Node{tag: "query", attrs: %{"request" => "interactive"}, content: nil}]
}
end
@doc """
Parse a group-metadata IQ result. Returns `{:ok, metadata}` or
`{:error, reason}` (missing ``, surfaced ``, or missing id).
"""
@spec parse(Node.t()) :: {:ok, t()} | {:error, term()}
def parse(%Node{} = result) do
case NodeUtils.get_binary_node_child(result, "group") do
nil -> {:error, parse_error(result)}
group -> from_group_node(group)
end
end
@doc """
Build the "all participating groups" query IQ (Baileys
`groupFetchAllParticipating`):
"""
@spec query_all_iq() :: Node.t()
def query_all_iq do
%Node{
tag: "iq",
attrs: [{"type", "get"}, {"xmlns", "w:g2"}, {"to", "@g.us"}],
content: [
%Node{
tag: "participating",
attrs: %{},
content: [
%Node{tag: "participants", attrs: %{}, content: nil},
%Node{tag: "description", attrs: %{}, content: nil}
]
}
]
}
end
@doc """
Parse an "all participating" IQ result (`…`) into a
list of metadata maps. Unparseable group nodes are skipped.
"""
@spec parse_all(Node.t()) :: {:ok, [t()]}
def parse_all(%Node{} = result) do
groups =
case NodeUtils.get_binary_node_child(result, "groups") do
nil -> []
groups_node -> NodeUtils.get_binary_node_children(groups_node, "group")
end
metas =
Enum.flat_map(groups, fn group ->
case from_group_node(group) do
{:ok, meta} -> [meta]
{:error, _} -> []
end
end)
{:ok, metas}
end
# --- internals ---
defp from_group_node(group) do
case NodeUtils.get_attr(group, "id") do
nil ->
{:error, :missing_group_id}
id ->
participants = NodeUtils.get_binary_node_children(group, "participant")
{:ok,
%{
id: normalize_group_id(id),
subject: NodeUtils.get_attr(group, "subject"),
owner: NodeUtils.get_attr(group, "creator") || NodeUtils.get_attr(group, "owner"),
addressing_mode: addressing_mode(group),
size: size(group, participants),
participants: Enum.map(participants, &participant/1)
}}
end
end
# A participant's id may be a LID or a PN; carry the cross-mapped counterpart
# when present (Baileys keeps lid for pn ids and phone_number for lid ids).
defp participant(node) do
jid = NodeUtils.get_attr(node, "jid")
lid = NodeUtils.get_attr(node, "lid")
phone = NodeUtils.get_attr(node, "phone_number")
%{
id: jid,
lid: if(pn_user?(jid) and lid_user?(lid), do: lid),
phone_number: if(lid_user?(jid) and pn_user?(phone), do: phone),
admin: NodeUtils.get_attr(node, "type")
}
end
defp addressing_mode(group) do
if NodeUtils.get_attr(group, "addressing_mode") == "lid", do: :lid, else: :pn
end
defp size(group, participants) do
case Integer.parse(NodeUtils.get_attr(group, "size") || "") do
{s, _} -> s
:error -> length(participants)
end
end
# jidEncode(id, 'g.us') unless already a full jid.
defp normalize_group_id(id) do
if String.contains?(id, "@"), do: id, else: JID.encode(%{user: id, server: "g.us"})
end
defp parse_error(result) do
case NodeUtils.get_binary_node_child(result, "error") do
nil ->
:missing_group_node
error ->
{:group_query_failed, NodeUtils.get_attr(error, "code"),
NodeUtils.get_attr(error, "text")}
end
end
defp pn_user?(jid), do: is_binary(jid) and JID.jid_user?(jid) and not JID.lid_user?(jid)
defp lid_user?(jid), do: is_binary(jid) and JID.lid_user?(jid)
end