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

10 Versions

Jump to

Compare versions

29 files changed
+913 additions
-477 deletions
  @@ -1,5 +1,18 @@
1 1 # Changelog
2 2
3 + ## [0.1.0-alpha.3] - 2026-03-19
4 +
5 + ### Changed
6 +
7 + - Event emission no longer blocks callers on slow subscribers
8 + - Protocol and connection logging demoted from warning to debug level
9 + - Improved binary node encoding and decoding performance
10 +
11 + ### Fixed
12 +
13 + - Programmer errors in Noise protocol functions are no longer silently swallowed
14 + - Rust NIF error handling hardened to eliminate panic paths
15 +
3 16 ## [0.1.0-alpha.2] - 2026-03-19
4 17
5 18 Initial alpha release.
  @@ -5,7 +5,7 @@
5 5 <<"https://github.com/jeffhuen/baileys_ex/blob/main/CHANGELOG.md">>},
6 6 {<<"GitHub">>,<<"https://github.com/jeffhuen/baileys_ex">>}]}.
7 7 {<<"name">>,<<"baileys_ex">>}.
8 - {<<"version">>,<<"0.1.0-alpha.2">>}.
8 + {<<"version">>,<<"0.1.0-alpha.3">>}.
9 9 {<<"description">>,
10 10 <<"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.">>}.
11 11 {<<"elixir">>,<<"~> 1.19">>}.
  @@ -114,7 +114,8 @@
114 114 <<"lib/baileys_ex/util/lt_hash.ex">>,<<"lib/baileys_ex/wam.ex">>,
115 115 <<"lib/baileys_ex/wam/binary_info.ex">>,
116 116 <<"lib/baileys_ex/wam/definitions.ex">>,<<"lib/baileys_ex/wam/encoder.ex">>,
117 - <<"native/baileys_nif/src/lib.rs">>,<<"native/baileys_nif/src/noise.rs">>,
117 + <<"native/baileys_nif/src/error.rs">>,<<"native/baileys_nif/src/lib.rs">>,
118 + <<"native/baileys_nif/src/noise.rs">>,
118 119 <<"native/baileys_nif/src/xeddsa.rs">>,<<"priv/proto/WAProto.proto">>,
119 120 <<"priv/wam/definitions.json">>,
120 121 <<"user_docs/getting-started/first-connection.md">>,
  @@ -7,10 +7,170 @@ defmodule BaileysEx.Auth.FilePersistence do
7 7
8 8 alias BaileysEx.Auth.KeyStore
9 9 alias BaileysEx.Auth.State
10 + alias BaileysEx.Protocol.Proto.ADVSignedDeviceIdentity
11 + alias BaileysEx.Protocol.Proto.MessageKey
12 + alias BaileysEx.Signal.Group.SenderChainKey
13 + alias BaileysEx.Signal.Group.SenderKeyRecord
14 + alias BaileysEx.Signal.Group.SenderKeyState
15 + alias BaileysEx.Signal.Group.SenderMessageKey
16 + alias BaileysEx.Signal.SessionRecord
10 17
11 18 @buffer_tag "Buffer"
12 19 @default_dir "baileys_auth_info"
13 20
21 + # Persistence decoding is intentionally bounded to explicit atoms/modules instead of
22 + # relying on generic atom reconstruction from disk. This keeps auth loading
23 + # deterministic across fresh and warm VMs and avoids reopening unbounded atom
24 + # creation from persisted files.
25 + #
26 + # Top-level struct fields are derived from the owning structs so they track schema
27 + # changes automatically. Nested map atoms still need explicit maintenance because
28 + # they are persisted as atom keys without dedicated structs. When adding persisted
29 + # nested fields, update the relevant decode context below and extend the fresh-VM
30 + # regressions in `test/baileys_ex/auth/file_persistence_test.exs`.
31 + @empty_decode_context %{atoms: %{}, modules: %{}}
32 +
33 + @credential_decode_context %{
34 + atoms:
35 + State.__struct__()
36 + |> Map.keys()
37 + |> Enum.reject(&(&1 == :__struct__))
38 + |> Kernel.++(
39 + ADVSignedDeviceIdentity.__struct__()
40 + |> Map.keys()
41 + |> Enum.reject(&(&1 == :__struct__))
42 + )
43 + |> Kernel.++(
44 + MessageKey.__struct__()
45 + |> Map.keys()
46 + |> Enum.reject(&(&1 == :__struct__))
47 + )
48 + |> Kernel.++([
49 + :public,
50 + :private,
51 + :key_pair,
52 + :key_id,
53 + :signature,
54 + :identifier,
55 + :identifier_key,
56 + :name,
57 + :device_id,
58 + :lid,
59 + :key,
60 + :message_timestamp,
61 + :unarchive_chats,
62 + :default_disappearing_mode,
63 + # processed_history_messages message key atoms
64 + :id,
65 + :remote_jid,
66 + :remote_jid_alt,
67 + :participant,
68 + :participant_alt,
69 + :from_me,
70 + :addressing_mode,
71 + :server_id,
72 + # addressing_mode atom values
73 + :pn,
74 + :lid
75 + ])
76 + |> Enum.uniq()
77 + |> Map.new(&{Atom.to_string(&1), &1}),
78 + modules:
79 + [State, ADVSignedDeviceIdentity, MessageKey]
80 + |> Map.new(&{Atom.to_string(&1), &1})
81 + }
82 +
83 + @session_decode_context %{
84 + atoms:
85 + SessionRecord.__struct__()
86 + |> Map.keys()
87 + |> Enum.reject(&(&1 == :__struct__))
88 + |> Kernel.++([
89 + :current_ratchet,
90 + :root_key,
91 + :ephemeral_key_pair,
92 + :public,
93 + :private,
94 + :last_remote_ephemeral,
95 + :previous_counter,
96 + :index_info,
97 + :remote_identity_key,
98 + :local_identity_key,
99 + :base_key,
100 + :base_key_type,
101 + :closed,
102 + :chains,
103 + :pending_pre_key,
104 + :registration_id,
105 + :chain_key,
106 + :counter,
107 + :key,
108 + :chain_type,
109 + :message_keys,
110 + :pre_key_id,
111 + :signed_pre_key_id,
112 + :sending,
113 + :receiving
114 + ])
115 + |> Enum.uniq()
116 + |> Map.new(&{Atom.to_string(&1), &1}),
117 + modules:
118 + [SessionRecord]
119 + |> Map.new(&{Atom.to_string(&1), &1})
120 + }
121 +
122 + @sender_key_decode_context %{
123 + atoms:
124 + SenderKeyRecord.__struct__()
125 + |> Map.keys()
126 + |> Enum.reject(&(&1 == :__struct__))
127 + |> Kernel.++(
128 + SenderKeyState.__struct__()
129 + |> Map.keys()
130 + |> Enum.reject(&(&1 == :__struct__))
131 + )
132 + |> Kernel.++(
133 + SenderChainKey.__struct__()
134 + |> Map.keys()
135 + |> Enum.reject(&(&1 == :__struct__))
136 + )
137 + |> Kernel.++(
138 + SenderMessageKey.__struct__()
139 + |> Map.keys()
140 + |> Enum.reject(&(&1 == :__struct__))
141 + )
142 + |> Kernel.++([:public, :private])
143 + |> Enum.uniq()
144 + |> Map.new(&{Atom.to_string(&1), &1}),
145 + modules:
146 + [SenderKeyRecord, SenderKeyState, SenderChainKey, SenderMessageKey]
147 + |> Map.new(&{Atom.to_string(&1), &1})
148 + }
149 +
150 + @pre_key_decode_context %{
151 + atoms: %{Atom.to_string(:public) => :public, Atom.to_string(:private) => :private},
152 + modules: %{}
153 + }
154 +
155 + @app_state_sync_key_decode_context %{
156 + atoms: %{Atom.to_string(:key_data) => :key_data},
157 + modules: %{}
158 + }
159 +
160 + @app_state_sync_version_decode_context %{
161 + atoms:
162 + [:version, :hash, :index_value_map, :value_mac]
163 + |> Map.new(&{Atom.to_string(&1), &1}),
164 + modules: %{}
165 + }
166 +
167 + @tc_token_decode_context %{
168 + atoms:
169 + [:token, :timestamp]
170 + |> Map.new(&{Atom.to_string(&1), &1}),
171 + modules: %{}
172 + }
173 +
14 174 @type multi_file_auth_state :: %{
15 175 required(:state) => State.t(),
16 176 required(:connect_opts) => keyword(),
  @@ -49,7 +209,7 @@ defmodule BaileysEx.Auth.FilePersistence do
49 209 @spec load_credentials(Path.t()) :: {:ok, State.t()} | {:error, term()}
50 210 def load_credentials(path) when is_binary(path) do
51 211 with :ok <- ensure_directory(path) do
52 - case read_data(path, "creds.json") do
212 + case read_data(path, "creds.json", &decode_credentials/1) do
53 213 {:ok, nil} ->
54 214 {:ok, State.new()}
55 215
  @@ -76,7 +236,8 @@ defmodule BaileysEx.Auth.FilePersistence do
76 236 """
77 237 @spec save_credentials(Path.t(), State.t()) :: :ok | {:error, term()}
78 238 def save_credentials(path, %State{} = state) when is_binary(path) do
79 - with :ok <- ensure_directory(path) do
239 + with :ok <- validate_additional_data(state.additional_data),
240 + :ok <- ensure_directory(path) do
80 241 write_data(path, "creds.json", state)
81 242 end
82 243 end
  @@ -95,7 +256,7 @@ defmodule BaileysEx.Auth.FilePersistence do
95 256 with :ok <- ensure_directory(path) do
96 257 file_name = data_file_name(type, id)
97 258
98 - case read_data(path, file_name) do
259 + case read_data(path, file_name, &decode_key_data(type, &1)) do
99 260 {:ok, nil} -> {:error, :not_found}
100 261 {:ok, value} -> {:ok, value}
101 262 {:error, _reason} = error -> error
  @@ -135,6 +296,59 @@ defmodule BaileysEx.Auth.FilePersistence do
135 296 end
136 297 end
137 298
299 + defp validate_additional_data(nil), do: :ok
300 + defp validate_additional_data(data), do: validate_json_safe(data, [:additional_data])
301 +
302 + defp validate_json_safe(nil, _path), do: :ok
303 + defp validate_json_safe(value, _path) when is_binary(value), do: :ok
304 + defp validate_json_safe(value, _path) when is_number(value), do: :ok
305 + defp validate_json_safe(value, _path) when is_boolean(value), do: :ok
306 +
307 + defp validate_json_safe(list, path) when is_list(list) do
308 + list
309 + |> Enum.with_index()
310 + |> Enum.reduce_while(:ok, fn {item, i}, :ok ->
311 + case validate_json_safe(item, [i | path]) do
312 + :ok -> {:cont, :ok}
313 + error -> {:halt, error}
314 + end
315 + end)
316 + end
317 +
318 + defp validate_json_safe(map, path) when is_map(map) do
319 + Enum.reduce_while(map, :ok, fn {key, value}, :ok ->
320 + with :ok <- validate_json_safe_key(key, path),
321 + :ok <- validate_json_safe(value, [key | path]) do
322 + {:cont, :ok}
323 + else
324 + error -> {:halt, error}
325 + end
326 + end)
327 + end
328 +
329 + defp validate_json_safe(value, path) do
330 + {:error,
331 + {:invalid_additional_data,
332 + "additional_data contains a #{type_name(value)} at path #{inspect(Enum.reverse(path))}; " <>
333 + "only strings, numbers, booleans, lists, and string-keyed maps are supported"}}
334 + end
335 +
336 + defp validate_json_safe_key(key, _path) when is_binary(key), do: :ok
337 +
338 + defp validate_json_safe_key(key, path) do
339 + {:error,
340 + {:invalid_additional_data,
341 + "additional_data contains a #{type_name(key)} map key at path #{inspect(Enum.reverse(path))}; " <>
342 + "only string keys are supported"}}
343 + end
344 +
345 + defp type_name(value) when is_atom(value), do: "atom (#{inspect(value)})"
346 + defp type_name(value) when is_tuple(value), do: "tuple"
347 + defp type_name(value) when is_pid(value), do: "pid"
348 + defp type_name(value) when is_reference(value), do: "reference"
349 + defp type_name(value) when is_function(value), do: "function"
350 + defp type_name(_value), do: "unsupported term"
351 +
138 352 defp default_path do
139 353 Application.get_env(:baileys_ex, __MODULE__, [])
140 354 |> Keyword.get(:path, Path.join(File.cwd!(), @default_dir))
  @@ -158,13 +372,13 @@ defmodule BaileysEx.Auth.FilePersistence do
158 372 |> String.replace(":", "-")
159 373 end
160 374
161 - defp read_data(path, file_name) do
375 + defp read_data(path, file_name, decoder_fun) do
162 376 file_path = Path.join(path, sanitize_file_name(file_name))
163 377
164 378 with_file_lock(file_path, fn ->
165 379 case File.read(file_path) do
166 380 {:ok, contents} ->
167 - {:ok, JSON.decode!(contents) |> decode_term()}
381 + {:ok, contents |> JSON.decode!() |> decoder_fun.()}
168 382
169 383 {:error, :enoent} ->
170 384 {:ok, nil}
  @@ -271,33 +485,65 @@ defmodule BaileysEx.Auth.FilePersistence do
271 485
272 486 defp encode_term(other), do: other
273 487
274 - defp decode_term(%{"__type__" => "struct", "module" => module_name, "value" => value}) do
275 - module = String.to_existing_atom(module_name)
276 - fields = decode_term(value)
488 + defp decode_credentials(value) do
489 + case decode_term(value, @credential_decode_context) do
490 + %State{} = state -> state
491 + %{} = fields -> struct(State, fields)
492 + other -> other
493 + end
494 + end
495 +
496 + defp decode_key_data(:session, value), do: decode_term(value, @session_decode_context)
497 + defp decode_key_data(:"pre-key", value), do: decode_term(value, @pre_key_decode_context)
498 + defp decode_key_data(:"sender-key", value), do: decode_term(value, @sender_key_decode_context)
499 + defp decode_key_data(:"sender-key-memory", value), do: decode_term(value, @empty_decode_context)
500 +
501 + defp decode_key_data(:"app-state-sync-key", value),
502 + do: decode_term(value, @app_state_sync_key_decode_context)
503 +
504 + defp decode_key_data(:"app-state-sync-version", value),
505 + do: decode_term(value, @app_state_sync_version_decode_context)
506 +
507 + defp decode_key_data(:"lid-mapping", value), do: decode_term(value, @empty_decode_context)
508 + defp decode_key_data(:"device-list", value), do: decode_term(value, @empty_decode_context)
509 + defp decode_key_data(:tctoken, value), do: decode_term(value, @tc_token_decode_context)
510 + defp decode_key_data(:"identity-key", value), do: decode_term(value, @empty_decode_context)
511 + defp decode_key_data(_type, value), do: decode_term(value)
512 +
513 + defp decode_term(value), do: decode_term(value, @empty_decode_context)
514 +
515 + defp decode_term(
516 + %{"__type__" => "struct", "module" => module_name, "value" => value},
517 + decode_context
518 + ) do
519 + module = decode_module(module_name, decode_context)
520 + fields = decode_term(value, decode_context)
277 521 struct(module, fields)
278 522 end
279 523
280 - defp decode_term(%{"__type__" => "map", "entries" => entries}) do
524 + defp decode_term(%{"__type__" => "map", "entries" => entries}, decode_context) do
281 525 entries
282 - |> Enum.map(fn [key, value] -> {decode_term(key), decode_term(value)} end)
526 + |> Enum.map(fn [key, value] ->
527 + {decode_term(key, decode_context), decode_term(value, decode_context)}
528 + end)
283 529 |> Map.new()
284 530 end
285 531
286 - defp decode_term(%{"__type__" => "tuple", "items" => items}) do
532 + defp decode_term(%{"__type__" => "tuple", "items" => items}, decode_context) do
287 533 items
288 - |> Enum.map(&decode_term/1)
534 + |> Enum.map(&decode_term(&1, decode_context))
289 535 |> List.to_tuple()
290 536 end
291 537
292 - defp decode_term(%{"__type__" => "atom", "value" => value}) when is_binary(value) do
293 - String.to_atom(value)
538 + defp decode_term(%{"__type__" => "atom", "value" => value}, decode_context)
539 + when is_binary(value) do
540 + decode_atom(value, decode_context)
294 541 end
295 542
296 - defp decode_term(%{"type" => @buffer_tag, "data" => data}) when is_list(data) do
297 - :erlang.list_to_binary(data)
298 - end
543 + defp decode_term(%{"type" => @buffer_tag, "data" => data}, _decode_context) when is_list(data),
544 + do: :erlang.list_to_binary(data)
299 545
300 - defp decode_term(%{} = map) do
546 + defp decode_term(%{} = map, decode_context) do
301 547 atom_keys = Map.get(map, "__atom_keys__", [])
302 548
303 549 map
  @@ -305,18 +551,44 @@ defmodule BaileysEx.Auth.FilePersistence do
305 551 |> Enum.map(fn {key, value} ->
306 552 decoded_key =
307 553 if key in atom_keys do
308 - String.to_atom(key)
554 + decode_atom(key, decode_context)
309 555 else
310 556 key
311 557 end
312 558
313 - {decoded_key, decode_term(value)}
559 + {decoded_key, decode_term(value, decode_context)}
314 560 end)
315 561 |> Map.new()
316 562 end
317 563
318 - defp decode_term(list) when is_list(list), do: Enum.map(list, &decode_term/1)
319 - defp decode_term(other), do: other
564 + defp decode_term(list, decode_context) when is_list(list),
565 + do: Enum.map(list, &decode_term(&1, decode_context))
566 +
567 + defp decode_term(other, _decode_context), do: other
568 +
569 + defp decode_atom(value, %{atoms: atoms}) do
570 + case Map.fetch(atoms, value) do
571 + {:ok, atom} ->
572 + atom
573 +
574 + :error ->
575 + raise ArgumentError,
576 + "unknown persisted atom #{inspect(value)}; " <>
577 + "add it to the relevant decode context in #{inspect(__MODULE__)}"
578 + end
579 + end
580 +
581 + defp decode_module(module_name, %{modules: modules}) do
582 + case Map.fetch(modules, module_name) do
583 + {:ok, module} ->
584 + module
585 +
586 + :error ->
587 + raise ArgumentError,
588 + "unknown persisted module #{inspect(module_name)}; " <>
589 + "add it to the relevant decode context in #{inspect(__MODULE__)}"
590 + end
591 + end
320 592
321 593 defp normalize_credentials_state(%State{} = state), do: state
  @@ -17,13 +17,18 @@ defmodule BaileysEx.Auth.Phone do
17 17 @pairing_bundle_info "link_code_pairing_key_bundle_encryption_key"
18 18 @adv_secret_info "adv_secret"
19 19
20 - @spec derive_pairing_code_key(binary(), binary()) :: {:ok, binary()} | {:error, term()}
21 - def derive_pairing_code_key(pairing_code, salt)
22 - when is_binary(pairing_code) and is_binary(salt) and byte_size(salt) == 32 do
23 - Crypto.pbkdf2_sha256(pairing_code, salt, @pairing_iterations, 32)
20 + @spec derive_pairing_code_key(binary(), binary(), pos_integer()) ::
21 + {:ok, binary()} | {:error, term()}
22 + def derive_pairing_code_key(pairing_code, salt, iterations \\ @pairing_iterations)
23 +
24 + def derive_pairing_code_key(pairing_code, salt, iterations)
25 + when is_binary(pairing_code) and is_binary(salt) and byte_size(salt) == 32 and
26 + is_integer(iterations) and iterations > 0 do
27 + Crypto.pbkdf2_sha256(pairing_code, salt, iterations, 32)
24 28 end
25 29
26 - def derive_pairing_code_key(_pairing_code, _salt), do: {:error, :invalid_pairing_key_material}
30 + def derive_pairing_code_key(_pairing_code, _salt, _iterations),
31 + do: {:error, :invalid_pairing_key_material}
27 32
28 33 @spec build_pairing_request(binary(), map(), Config.t(), keyword()) ::
29 34 {:ok, %{pairing_code: binary(), creds_update: map(), node: BinaryNode.t()}}
  @@ -61,7 +66,7 @@ defmodule BaileysEx.Auth.Phone do
61 66 {:ok, signed_identity_key} <- fetch_key_pair(auth_state, :signed_identity_key),
62 67 {:ok, me} <- fetch_me(auth_state),
63 68 {:ok, code_pairing_public_key} <-
64 - decipher_link_public_key(wrapped_primary_ephemeral_public_key, pairing_code),
69 + decipher_link_public_key(wrapped_primary_ephemeral_public_key, pairing_code, opts),
65 70 {:ok, companion_shared_key} <-
66 71 Curve.shared_key(pairing_ephemeral_key.private, code_pairing_public_key),
67 72 {:ok, finish_payload, adv_secret_key} <-
  @@ -96,8 +101,9 @@ defmodule BaileysEx.Auth.Phone do
96 101 defp generate_pairing_key(pairing_code, companion_ephemeral_public_key, opts) do
97 102 salt = random_bytes(opts, :pairing_key_salt, 32)
98 103 iv = random_bytes(opts, :pairing_key_iv, 16)
104 + iterations = Keyword.get(opts, :pairing_iterations, @pairing_iterations)
99 105
100 - with {:ok, key} <- derive_pairing_code_key(pairing_code, salt),
106 + with {:ok, key} <- derive_pairing_code_key(pairing_code, salt, iterations),
101 107 {:ok, encrypted_public_key} <-
102 108 Crypto.aes_ctr_encrypt(key, iv, companion_ephemeral_public_key) do
103 109 {:ok, salt <> iv <> encrypted_public_key}
  @@ -205,14 +211,17 @@ defmodule BaileysEx.Auth.Phone do
205 211
206 212 defp decipher_link_public_key(
207 213 <<salt::binary-size(32), iv::binary-size(16), payload::binary-size(32)>>,
208 - pairing_code
214 + pairing_code,
215 + opts
209 216 ) do
210 - with {:ok, secret_key} <- derive_pairing_code_key(pairing_code, salt) do
217 + iterations = Keyword.get(opts, :pairing_iterations, @pairing_iterations)
218 +
219 + with {:ok, secret_key} <- derive_pairing_code_key(pairing_code, salt, iterations) do
211 220 Crypto.aes_ctr_decrypt(secret_key, iv, payload)
212 221 end
213 222 end
214 223
215 - defp decipher_link_public_key(_wrapped_public_key, _pairing_code),
224 + defp decipher_link_public_key(_wrapped_public_key, _pairing_code, _opts),
216 225 do: {:error, :invalid_wrapped_primary_ephemeral_public_key}
217 226
218 227 defp random_bytes(opts, key, size) do
  @@ -21,6 +21,14 @@ defmodule BaileysEx.Auth.State do
21 21 default_disappearing_mode: map() | nil
22 22 }
23 23
24 + @type json_safe ::
25 + nil
26 + | boolean()
27 + | number()
28 + | binary()
29 + | [json_safe()]
30 + | %{optional(binary()) => json_safe()}
31 +
24 32 @type t :: %__MODULE__{
25 33 noise_key: key_pair(),
26 34 pairing_ephemeral_key: key_pair() | nil,
  @@ -41,7 +49,7 @@ defmodule BaileysEx.Auth.State do
41 49 last_prop_hash: binary() | nil,
42 50 routing_info: binary() | nil,
43 51 my_app_state_key_id: binary() | nil,
44 - additional_data: term() | nil
52 + additional_data: json_safe() | nil
45 53 }
46 54
47 55 defstruct [
  @@ -130,6 +138,24 @@ defmodule BaileysEx.Auth.State do
130 138
131 139 def get(_state, _key, default), do: default
132 140
141 + @doc """
142 + Returns the current account name from the auth state when present.
143 + """
144 + @spec me_name(t() | map()) :: binary() | nil
145 + def me_name(state), do: me_field(state, :name)
146 +
147 + @doc """
148 + Returns the current account JID from the auth state when present.
149 + """
150 + @spec me_id(t() | map()) :: binary() | nil
151 + def me_id(state), do: me_field(state, :id)
152 +
153 + @doc """
154 + Returns the current LID from the auth state when present.
155 + """
156 + @spec me_lid(t() | map()) :: binary() | nil
157 + def me_lid(state), do: me_field(state, :lid)
158 +
133 159 @doc """
134 160 Returns a `creds` viewing projection mapping suitable for saving standalone.
135 161 """
  @@ -181,6 +207,24 @@ defmodule BaileysEx.Auth.State do
181 207
182 208 defp nested_creds_get(_state, _key, default), do: default
183 209
210 + defp me_field(%{me: me}, field) when is_map(me), do: me_field(me, field)
211 + defp me_field(%{"me" => me}, field) when is_map(me), do: me_field(me, field)
212 +
213 + defp me_field(map, field) when is_map(map) do
214 + case Map.fetch(map, field) do
215 + {:ok, value} when is_binary(value) ->
216 + value
217 +
218 + _ ->
219 + case Map.get(map, Atom.to_string(field)) do
220 + value when is_binary(value) -> value
221 + _ -> nil
222 + end
223 + end
224 + end
225 +
226 + defp me_field(_state, _field), do: nil
227 +
184 228 defp merge_maps(left, right) when is_map(left) and is_map(right) do
185 229 Map.merge(left, right, fn _key, left_value, right_value ->
186 230 if is_map(left_value) and is_map(right_value) do
Loading more files…