Packages

WhatsApp Web API client for Elixir. Full-featured port of Baileys with end-to-end Signal Protocol encryption, multi-device support, groups, communities, media, newsletters, and native BEAM fault tolerance.

Current section

Files

Jump to
baileys_ex lib baileys_ex message notification_handler.ex
Raw

lib/baileys_ex/message/notification_handler.ex

defmodule BaileysEx.Message.NotificationHandler do
@moduledoc """
Message-layer notification handling aligned with Baileys rc.9.
This module owns the non-auth notification cases that produce messaging,
group, newsletter, and account-side effects above the raw socket layer.
"""
require Logger
alias BaileysEx.BinaryNode
alias BaileysEx.Connection.EventEmitter
alias BaileysEx.Feature.TcToken
alias BaileysEx.Media.Retry, as: MediaRetry
alias BaileysEx.Message.StubSideEffects
alias BaileysEx.Protocol.BinaryNode, as: BinaryNodeUtil
alias BaileysEx.Protocol.JID, as: JIDUtil
alias BaileysEx.Protocol.MessageStubType
alias BaileysEx.Protocol.Proto.Message
@type context :: %{
optional(:event_emitter) => GenServer.server(),
optional(:me_id) => String.t(),
optional(:store_privacy_token_fun) => (String.t(), binary(), String.t() | nil ->
:ok | {:error, term()}),
optional(:handle_encrypt_notification_fun) => (BinaryNode.t() -> term()),
optional(:device_notification_fun) => (BinaryNode.t() -> term()),
optional(:resync_app_state_fun) => (String.t() -> term())
}
@doc """
Processes a raw envelope notification node and maps it to runtime events.
"""
@spec process_node(BinaryNode.t(), context()) :: :ok
def process_node(%BinaryNode{tag: "notification", attrs: %{"type" => type}} = node, context) do
dispatch_notification(type, node, context)
end
def process_node(%BinaryNode{}, _context), do: :ok
defp dispatch_notification("w:gp2", node, context), do: handle_group_notification(node, context)
defp dispatch_notification("encrypt", node, context),
do: handle_encrypt_notification(node, context)
defp dispatch_notification("devices", node, context),
do: handle_devices_notification(node, context)
defp dispatch_notification("picture", node, context),
do: handle_picture_notification(node, context)
defp dispatch_notification("account_sync", node, context),
do: handle_account_sync_notification(node, context)
defp dispatch_notification("server_sync", node, context),
do: handle_server_sync_notification(node, context)
defp dispatch_notification("mediaretry", node, context),
do: handle_media_retry_notification(node, context)
defp dispatch_notification("newsletter", node, context),
do: handle_newsletter_notification(node, context)
defp dispatch_notification("mex", node, context), do: handle_mex_notification(node, context)
defp dispatch_notification("link_code_companion_reg", _node, _context), do: :ok
defp dispatch_notification("privacy_token", node, context),
do: TcToken.handle_notification(node, context)
defp dispatch_notification(_type, _node, _context), do: :ok
defp handle_group_notification(%BinaryNode{} = node, context) do
case BinaryNodeUtil.children(node) do
[child | _rest] ->
emit_group_side_effects(node, child, context)
[] ->
:ok
end
end
defp emit_group_side_effects(
%BinaryNode{attrs: %{"from" => group_jid, "participant" => author} = attrs},
%BinaryNode{tag: "create", attrs: create_attrs},
context
) do
emit(context, :chats_upsert, [
%{
id: create_attrs["id"] || group_jid,
name: create_attrs["subject"],
conversation_timestamp: parse_integer(create_attrs["creation"])
}
])
emit(context, :groups_upsert, [
%{
id: create_attrs["id"] || group_jid,
subject: create_attrs["subject"],
creation: parse_integer(create_attrs["creation"]),
owner: create_attrs["creator"],
author: author
}
])
emit_group_message(context, attrs, %{
message_stub_type: :GROUP_CREATE,
message_stub_parameters: [create_attrs["subject"]]
})
end
defp emit_group_side_effects(
%BinaryNode{attrs: attrs},
%BinaryNode{tag: "ephemeral", attrs: child_attrs},
context
) do
emit_group_message(context, attrs, %{
message: %{
protocol_message: %{
type: :EPHEMERAL_SETTING,
ephemeral_expiration: parse_integer(child_attrs["expiration"]) || 0
}
}
})
end
defp emit_group_side_effects(
%BinaryNode{attrs: attrs},
%BinaryNode{tag: "not_ephemeral"},
context
) do
emit_group_message(context, attrs, %{
message: %{protocol_message: %{type: :EPHEMERAL_SETTING, ephemeral_expiration: 0}}
})
end
defp emit_group_side_effects(
%BinaryNode{attrs: attrs},
%BinaryNode{tag: tag} = child,
context
) do
stub_type = group_stub_type(tag)
stub_parameters = group_stub_parameters(tag, child, attrs)
message =
%{}
|> Map.put(:message_stub_type, stub_type)
|> maybe_put(:message_stub_parameters, stub_parameters)
|> maybe_put(:participant, attrs["participant"])
emit_group_message(context, attrs, message)
emit_stub_side_effects(context, %{
stub_type: stub_type,
stub_parameters: stub_parameters || [],
group_jid: attrs["from"],
author: attrs["participant"],
author_pn: attrs["participant_pn"],
me_id: context[:me_id] || ""
})
end
@spec emit_stub_side_effects(context(), StubSideEffects.stub_input()) :: :ok
defp emit_stub_side_effects(context, input) do
input
|> StubSideEffects.derive()
|> Enum.each(fn {event, data} ->
emit(context, event, data)
end)
end
defp handle_encrypt_notification(%BinaryNode{} = node, context) do
case context[:handle_encrypt_notification_fun] do
fun when is_function(fun, 1) ->
_ = fun.(node)
:ok
_ ->
:ok
end
end
defp handle_devices_notification(%BinaryNode{} = node, context) do
case context[:device_notification_fun] do
fun when is_function(fun, 1) ->
_ = fun.(node)
:ok
_ ->
:ok
end
end
defp handle_picture_notification(%BinaryNode{attrs: attrs} = node, context) do
set_picture = BinaryNodeUtil.child(node, "set")
delete_picture = BinaryNodeUtil.child(node, "delete")
emit(context, :contacts_update, [
%{
id: attrs["from"],
img_url: if(set_picture, do: :changed, else: :removed)
}
])
if JIDUtil.group?(attrs["from"]) do
picture_node = set_picture || delete_picture
emit_group_message(context, attrs, %{
message_stub_type: :GROUP_CHANGE_ICON,
message_stub_parameters: if(set_picture, do: [set_picture.attrs["id"]], else: nil),
participant: picture_node && picture_node.attrs["author"]
})
else
:ok
end
end
defp handle_account_sync_notification(%BinaryNode{} = node, context) do
case BinaryNodeUtil.children(node) do
[%BinaryNode{tag: "disappearing_mode", attrs: attrs}] ->
emit(context, :creds_update, %{
account_settings: %{
default_disappearing_mode: %{
ephemeral_expiration: parse_integer(attrs["duration"]) || 0,
ephemeral_setting_timestamp: parse_integer(attrs["t"])
}
}
})
[%BinaryNode{tag: "blocklist", content: items}] when is_list(items) ->
Enum.each(items, fn
%BinaryNode{tag: "item", attrs: %{"jid" => jid, "action" => action}} ->
emit(context, :blocklist_update, %{
blocklist: [jid],
type: if(action == "block", do: :add, else: :remove)
})
_other ->
:ok
end)
_ ->
:ok
end
end
defp handle_server_sync_notification(%BinaryNode{} = node, context) do
case context[:resync_app_state_fun] do
fun when is_function(fun, 1) ->
case BinaryNodeUtil.child(node, "collection") do
%BinaryNode{attrs: %{"name" => name}} ->
normalize_server_sync_result(name, fun.(name))
_ ->
:ok
end
_ ->
:ok
end
end
@spec normalize_server_sync_result(String.t(), term()) :: :ok
defp normalize_server_sync_result(_name, :ok), do: :ok
defp normalize_server_sync_result(name, {:error, reason}) do
Logger.warning("server_sync resync failed for #{name}: #{inspect(reason)}")
:ok
end
defp normalize_server_sync_result(_name, _result), do: :ok
defp handle_media_retry_notification(%BinaryNode{attrs: attrs} = node, context) do
_ = attrs
emit(context, :messages_media_update, [MediaRetry.decode_notification_event(node)])
end
defp handle_newsletter_notification(%BinaryNode{attrs: attrs} = node, context) do
case BinaryNodeUtil.children(node) do
[%BinaryNode{tag: "reaction", attrs: reaction_attrs} = child | _rest] ->
emit(context, :newsletter_reaction, %{
id: attrs["from"],
server_id: reaction_attrs["message_id"],
reaction: %{code: newsletter_reaction_code(child), count: 1}
})
[%BinaryNode{tag: "view", attrs: view_attrs, content: content} | _rest] ->
emit(context, :newsletter_view, %{
id: attrs["from"],
server_id: view_attrs["message_id"],
count: parse_integer(content) || 0
})
[%BinaryNode{tag: "participant", attrs: participant_attrs} | _rest] ->
emit(context, :newsletter_participants_update, %{
id: attrs["from"],
author: attrs["participant"],
user: participant_attrs["jid"],
action: participant_attrs["action"],
new_role: participant_attrs["role"]
})
[%BinaryNode{tag: "update"} = child | _rest] ->
emit(context, :newsletter_settings_update, %{
id: attrs["from"],
update: newsletter_settings(child)
})
[%BinaryNode{tag: "message", attrs: message_attrs} = child | _rest] ->
emit_newsletter_message(context, attrs["from"], child, message_attrs)
_ ->
:ok
end
end
defp handle_mex_notification(%BinaryNode{} = node, context) do
case BinaryNodeUtil.child(node, "mex") do
%BinaryNode{content: content} when is_binary(content) ->
case JSON.decode(content) do
{:ok, %{"operation" => operation, "updates" => updates}} when is_list(updates) ->
emit_mex_updates(context, operation, updates, node.attrs["from"])
_ ->
:ok
end
_ ->
:ok
end
end
defp emit_group_message(context, attrs, payload) do
emit(context, :messages_upsert, %{
type: :append,
messages: [
Map.merge(
%{
key: %{
remote_jid: attrs["from"],
from_me: false,
participant: attrs["participant"],
id: attrs["id"]
},
message_timestamp: parse_integer(attrs["t"]),
participant: attrs["participant"]
},
payload
)
]
})
end
defp emit_newsletter_message(context, newsletter_jid, %BinaryNode{} = child, message_attrs) do
case BinaryNodeUtil.child(child, "plaintext") do
%BinaryNode{content: {:binary, payload}} ->
emit_decoded_newsletter_message(context, newsletter_jid, payload, message_attrs)
%BinaryNode{content: payload} when is_binary(payload) ->
emit_decoded_newsletter_message(context, newsletter_jid, payload, message_attrs)
_ ->
:ok
end
end
defp emit_decoded_newsletter_message(context, newsletter_jid, payload, message_attrs) do
case Message.decode(payload) do
{:ok, %Message{} = message} ->
emit(context, :messages_upsert, %{
type: :append,
messages: [
%{
key: %{
remote_jid: newsletter_jid,
from_me: false,
id: message_attrs["message_id"] || message_attrs["server_id"]
},
message: message,
message_timestamp: parse_integer(message_attrs["t"])
}
]
})
_ ->
:ok
end
end
defp emit_mex_updates(context, "NotificationNewsletterUpdate", updates, _author) do
Enum.each(updates, fn
%{"jid" => jid, "settings" => settings} when is_map(settings) ->
emit(context, :newsletter_settings_update, %{id: jid, update: settings})
_other ->
:ok
end)
end
defp emit_mex_updates(context, "NotificationNewsletterAdminPromote", updates, author) do
Enum.each(updates, fn
%{"jid" => jid, "user" => user} ->
emit(context, :newsletter_participants_update, %{
id: jid,
author: author,
user: user,
new_role: "ADMIN",
action: "promote"
})
_other ->
:ok
end)
end
defp emit_mex_updates(_context, _operation, _updates, _author), do: :ok
defp newsletter_reaction_code(%BinaryNode{} = child) do
case BinaryNodeUtil.child(child, "reaction") do
%BinaryNode{content: content} when is_binary(content) -> content
%BinaryNode{content: {:binary, content}} -> content
_ -> nil
end
end
defp newsletter_settings(%BinaryNode{} = child) do
case BinaryNodeUtil.child(child, "settings") do
%BinaryNode{} = settings ->
%{}
|> maybe_put(:name, node_string(BinaryNodeUtil.child(settings, "name")))
|> maybe_put(:description, node_string(BinaryNodeUtil.child(settings, "description")))
_ ->
%{}
end
end
defp group_stub_type(tag), do: MessageStubType.from_string(tag)
defp group_stub_parameters("promote", child, _attrs), do: participant_parameters(child)
defp group_stub_parameters("demote", child, _attrs), do: participant_parameters(child)
defp group_stub_parameters("remove", child, _attrs), do: participant_parameters(child)
defp group_stub_parameters("add", child, _attrs), do: participant_parameters(child)
defp group_stub_parameters("leave", child, _attrs), do: participant_parameters(child)
defp group_stub_parameters("subject", %BinaryNode{attrs: attrs}, _attrs), do: [attrs["subject"]]
defp group_stub_parameters("description", %BinaryNode{} = child, _attrs),
do: [node_string(BinaryNodeUtil.child(child, "body"))]
defp group_stub_parameters("announcement", _child, _attrs), do: ["on"]
defp group_stub_parameters("not_announcement", _child, _attrs), do: ["off"]
defp group_stub_parameters("locked", _child, _attrs), do: ["on"]
defp group_stub_parameters("unlocked", _child, _attrs), do: ["off"]
defp group_stub_parameters("invite", %BinaryNode{attrs: attrs}, _node_attrs),
do: [attrs["code"]]
defp group_stub_parameters("member_add_mode", %BinaryNode{content: content}, _attrs),
do: [binary_content(content)]
defp group_stub_parameters("membership_approval_mode", %BinaryNode{} = child, _attrs) do
case BinaryNodeUtil.child(child, "group_join") do
%BinaryNode{attrs: join_attrs} -> [join_attrs["state"]]
_ -> nil
end
end
defp group_stub_parameters("created_membership_requests", _child, attrs) do
[JSON.encode!(%{lid: attrs["participant"], pn: attrs["participant_pn"]}), "created"]
end
defp group_stub_parameters("revoked_membership_requests", _child, attrs) do
[JSON.encode!(%{lid: attrs["participant"], pn: attrs["participant_pn"]}), "revoked"]
end
defp group_stub_parameters(_tag, _child, _attrs), do: nil
defp participant_parameters(%BinaryNode{} = child) do
child
|> BinaryNodeUtil.children("participant")
|> Enum.map(fn participant ->
JSON.encode!(%{
id: participant.attrs["jid"],
phone_number: participant.attrs["phone_number"],
lid: participant.attrs["lid"],
admin: participant.attrs["type"]
})
end)
end
defp parse_integer(nil), do: nil
defp parse_integer(value) when is_binary(value) do
case Integer.parse(value) do
{integer, ""} -> integer
_ -> nil
end
end
defp parse_integer(value) when is_integer(value), do: value
defp parse_integer(_value), do: nil
defp node_string(%BinaryNode{content: content}), do: binary_content(content)
defp node_string(_node), do: nil
defp binary_content({:binary, content}) when is_binary(content), do: content
defp binary_content(content) when is_binary(content), do: content
defp binary_content(_content), do: nil
defp emit(%{event_emitter: event_emitter}, event, data) when not is_nil(event_emitter) do
EventEmitter.emit(event_emitter, event, data)
end
defp emit(_context, _event, _data), do: :ok
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
end