@whanext/core 0.19.2 → 0.19.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.4
4
+ ### Corrigido
5
+ - Corrigida a normalização de `timestampMs` dos eventos nativos de chamada; removida uma referência inválida ao helper privado `#number`.
6
+ - Chamadas no provider Zapo agora usam o evento nativo `call`, sem depender do plugin completo de VoIP.
7
+ - `rejectCall()` envia diretamente a sinalização mínima `<call><reject/></call>` pelo `client.lowlevel.sendNode()`, preservando `callId` e `callCreatorJid`.
8
+ - Removidas as dependências `@zapo-js/voip`, `@roamhq/wrtc` e `libmlow-wasm` do WhaNext Core; detectar/rejeitar chamadas não exige WebRTC nem binário nativo.
9
+ - O `callCreatorJid` técnico é mantido em cache limitado para que a rejeição use o endereço de protocolo correto mesmo quando o evento público expõe o `callerPnJid`.
10
+
11
+ ### Compatibilidade
12
+ - A API pública permanece igual: `app.on('call')`, `CallEvent` e `rejectCall(callId, from)` não mudam para consumidores.
13
+ - O suporte completo a aceitar/realizar chamadas VoIP não faz parte da API pública do WhaNext.
14
+
15
+ ## 0.19.3
16
+ ### Corrigido
17
+ - Carregamento lazy do plugin VoIP do Zapo para evitar importar o runtime WebRTC em consumidores e testes que não inicializam o provider.
18
+ - Ajustes de tipagem dos testes com `exactOptionalPropertyTypes`.
19
+ - Provider Zapo agora usa `@zapo-js/voip` como único caminho de chamadas: eventos `voip_*` e `client.voip.rejectCall()`, sem fallback silencioso.
20
+ - `messageEdited` também reconhece edições criptografadas descriptografadas pelo Zapo em `message_addon`, além de `message_protocol`.
21
+ - `messageDeleted` e `messageEdited` ignoram protocolo de backlog durante bootstrap quando `processOfflineMessages` está desativado.
22
+ - Mensagens recebidas antes de `connection: open` deixam de ser publicadas como mensagens ao vivo; ainda entram no cache limitado para permitir recuperação posterior.
23
+ - Eventos `message` duplicados no mesmo runtime são descartados antes de chegar aos consumidores, evitando downloads repetidos de mídia/ViewOnce.
24
+
25
+ ### Compatibilidade
26
+ - Mudança corretiva e aditiva; a API pública de `CallEvent`, `messageDeleted` e `messageEdited` permanece inalterada.
27
+
3
28
  ## 0.19.2
4
29
 
5
30
  - Corrigido o download real de mídias citadas no provider Zapo: `downloadMedia()` agora entrega o `Proto.IMessage` bruto diretamente ao `downloadBytes()`, como suportado pela API do Zapo.
@@ -58,7 +58,7 @@ O provider usa `@zapo-js/media-utils`. Para processamento completo de imagem, v
58
58
 
59
59
  ## Chamadas
60
60
 
61
- O suporte a `app.on('call')` e `rejectCall()` usa o plugin oficial `@zapo-js/voip`. As dependências de runtime do plugin fazem parte do pacote do WhaNext v0.18.
61
+ A partir da v0.19.4, `app.on('call')` usa o evento `call` nativo do Zapo e `rejectCall()` envia somente a sinalização de rejeição pelo `client.lowlevel`. O WhaNext não carrega o stack completo de VoIP/WebRTC para detectar ou rejeitar chamadas.
62
62
 
63
63
  ## Checklist de atualização
64
64
 
package/README.md CHANGED
@@ -994,6 +994,8 @@ app.on('call', async (call) => {
994
994
 
995
995
  `call.status` reflete o ciclo da chamada (`offer`, `ringing`, `preaccept`, `timeout`, `reject`, `accept`). `call.isVideo` e `call.isGroup` indicam o tipo. Somente chamadas em `offer` podem ser rejeitadas.
996
996
 
997
+ No provider Zapo, detecção e rejeição usam apenas a sinalização nativa de chamadas e `client.lowlevel.sendNode()`. O WhaNext não carrega o stack completo de VoIP/WebRTC para esse fluxo.
998
+
997
999
  ## Erros
998
1000
 
999
1001
  ```ts
package/dist/index.js CHANGED
@@ -2708,9 +2708,11 @@ var ZapoProvider = class {
2708
2708
  #logger;
2709
2709
  #messageStore = /* @__PURE__ */ new Map();
2710
2710
  #messageKeyStore = /* @__PURE__ */ new WeakMap();
2711
+ #deliveredMessageStore = /* @__PURE__ */ new Set();
2712
+ #handledProtocolStore = /* @__PURE__ */ new Set();
2713
+ #callCreatorStore = /* @__PURE__ */ new Map();
2711
2714
  #messageCacheSize;
2712
2715
  #client;
2713
- #voip;
2714
2716
  #intentionalClose = false;
2715
2717
  #reconnectAttempt = 0;
2716
2718
  #reconnectTimer;
@@ -2964,16 +2966,26 @@ var ZapoProvider = class {
2964
2966
  const presence = this.#requireClient().presence;
2965
2967
  await presence.sendChatstate(chatId, { state: value });
2966
2968
  }
2967
- async rejectCall(callId, _from) {
2968
- this.#requireClient();
2969
- if (!this.#voip) {
2970
- throw new WhaNextError(
2971
- "PROVIDER_ERROR",
2972
- "WhatsApp call support is unavailable because the Zapo VoIP plugin could not be loaded.",
2973
- { recoverable: true }
2974
- );
2975
- }
2976
- await this.#voip.rejectCall(callId);
2969
+ async rejectCall(callId, from) {
2970
+ const client = this.#requireClient();
2971
+ const creator = this.#callCreatorStore.get(callId) ?? from;
2972
+ if (!creator) {
2973
+ throw new WhaNextError("ARGUMENT_INVALID", "Call creator is required to reject a call.");
2974
+ }
2975
+ const lowlevel = client.lowlevel;
2976
+ await lowlevel.sendNode({
2977
+ tag: "call",
2978
+ attrs: { to: creator },
2979
+ content: [
2980
+ {
2981
+ tag: "reject",
2982
+ attrs: {
2983
+ "call-id": callId,
2984
+ "call-creator": creator
2985
+ }
2986
+ }
2987
+ ]
2988
+ });
2977
2989
  }
2978
2990
  async #ensureClient() {
2979
2991
  if (this.#client) return this.#client;
@@ -3002,15 +3014,6 @@ var ZapoProvider = class {
3002
3014
  messageSecret: "sqlite"
3003
3015
  }
3004
3016
  });
3005
- const plugins = [];
3006
- try {
3007
- const { voipPlugin } = await import("@zapo-js/voip");
3008
- plugins.push(voipPlugin({ logLevel: "warn" }));
3009
- } catch (error) {
3010
- this.#logger.warn("Zapo VoIP support is unavailable; call events and rejection are disabled.", {
3011
- error: error instanceof Error ? error.message : String(error)
3012
- });
3013
- }
3014
3017
  const client = new WaClient({
3015
3018
  store,
3016
3019
  sessionId: this.#options.sessionId ?? "default",
@@ -3022,11 +3025,9 @@ var ZapoProvider = class {
3022
3025
  autoDecrypt: true,
3023
3026
  persistAllSecrets: true
3024
3027
  },
3025
- media: { processor: createMediaProcessor() },
3026
- plugins
3028
+ media: { processor: createMediaProcessor() }
3027
3029
  }, new WhaNextZapoLogger(this.#logger));
3028
3030
  this.#client = client;
3029
- this.#voip = client.voip;
3030
3031
  this.#bind(client);
3031
3032
  return client;
3032
3033
  }
@@ -3058,49 +3059,46 @@ var ZapoProvider = class {
3058
3059
  client.on("message_protocol", (event) => {
3059
3060
  this.#handleProtocolEvent(event);
3060
3061
  });
3062
+ client.on("message_addon", (event) => {
3063
+ this.#handleAddonEvent(event);
3064
+ });
3061
3065
  client.on("group", (event) => {
3062
3066
  this.#handleGroupEvent(event);
3063
3067
  });
3064
- if (this.#voip) {
3065
- const voipClient = client;
3066
- voipClient.on("voip_call_incoming", (event) => {
3067
- const call = this.#normalizeVoipCall(event, "offer");
3068
- if (call) void this.#events.emit("call", call);
3069
- });
3070
- voipClient.on("voip_call_state", (event) => {
3071
- if (event.stateData?.state === "ended") return;
3072
- const call = this.#normalizeVoipCall(event);
3073
- if (call && call.status !== "offer") void this.#events.emit("call", call);
3074
- });
3075
- voipClient.on("voip_call_ended", (event) => {
3076
- const call = this.#normalizeVoipCall(
3077
- event,
3078
- this.#callEndStatus(event.stateData?.endReason)
3079
- );
3080
- if (call) void this.#events.emit("call", call);
3081
- });
3082
- }
3068
+ client.on("call", (event) => {
3069
+ const call = this.#normalizeCall(event);
3070
+ if (call) void this.#events.emit("call", call);
3071
+ });
3083
3072
  client.on("connection", (event) => {
3084
3073
  void this.#handleConnectionEvent(client, event);
3085
3074
  });
3086
3075
  }
3087
3076
  #handleMessage(event) {
3088
3077
  const stored = event;
3078
+ if (stored.key?.id && stored.message) this.#remember(stored);
3079
+ const quoted = extractQuotedZapoMessage(event);
3080
+ if (quoted?.key.id && quoted.message) {
3081
+ this.#remember(quoted);
3082
+ }
3089
3083
  if (this.#isOfflineMessage(stored)) {
3090
- this.#logger.debug("Ignored message queued before the current connection.", {
3084
+ this.#logger.debug("Ignored message queued before the current live connection.", {
3091
3085
  messageId: stored.key.id ?? void 0,
3092
3086
  chatId: stored.key.remoteJid ?? void 0,
3093
3087
  timestampSeconds: stored.timestampSeconds ?? void 0
3094
3088
  });
3095
3089
  return;
3096
3090
  }
3097
- if (stored.key?.id && stored.message) this.#remember(stored);
3098
- const quoted = extractQuotedZapoMessage(event);
3099
- if (quoted?.key.id && quoted.message) {
3100
- this.#remember(quoted);
3091
+ const deliveryKey = this.#messageDeliveryKey(stored.key);
3092
+ if (stored.key.id && this.#deliveredMessageStore.has(deliveryKey)) {
3093
+ this.#logger.debug("Ignored duplicate Zapo message event.", {
3094
+ messageId: stored.key.id,
3095
+ chatId: stored.key.remoteJid ?? void 0
3096
+ });
3097
+ return;
3101
3098
  }
3102
3099
  const message = normalizeZapoMessage(event);
3103
3100
  if (!message) return;
3101
+ if (stored.key.id) this.#rememberDeliveredMessage(deliveryKey);
3104
3102
  this.#messageKeyStore.set(message.keys, stored);
3105
3103
  if (message.quoted && quoted?.message) {
3106
3104
  this.#messageKeyStore.set(
@@ -3110,7 +3108,50 @@ var ZapoProvider = class {
3110
3108
  }
3111
3109
  void this.#events.emit("message", message);
3112
3110
  }
3111
+ #handleAddonEvent(event) {
3112
+ if (event.kind !== "message_edit" || !event.targetMessageId) return;
3113
+ const editedMessage = this.#addonEditedMessage(event.decrypted);
3114
+ if (!editedMessage) return;
3115
+ const target = {
3116
+ id: event.targetMessageId,
3117
+ ...event.key.remoteJid !== void 0 ? { remoteJid: event.key.remoteJid } : {},
3118
+ ...event.key.fromMe !== void 0 ? { fromMe: event.key.fromMe } : {},
3119
+ ...event.key.participant !== void 0 ? { participant: event.key.participant } : {},
3120
+ ...event.key.participantAlt !== void 0 ? { participantAlt: event.key.participantAlt } : {}
3121
+ };
3122
+ this.#handleProtocolEvent({
3123
+ key: event.key,
3124
+ ...event.offline !== void 0 ? { offline: event.offline } : {},
3125
+ protocolMessage: {
3126
+ type: proto.Message.ProtocolMessage.Type.MESSAGE_EDIT,
3127
+ key: target,
3128
+ editedMessage
3129
+ }
3130
+ });
3131
+ }
3132
+ #addonEditedMessage(value) {
3133
+ if (!value || typeof value !== "object") return void 0;
3134
+ const record = value;
3135
+ const protocol = record.protocolMessage;
3136
+ if (protocol && typeof protocol === "object") {
3137
+ const edited2 = protocol.editedMessage;
3138
+ if (edited2 && typeof edited2 === "object") return edited2;
3139
+ }
3140
+ const edited = record.editedMessage;
3141
+ if (edited && typeof edited === "object") return edited;
3142
+ const message = record.message;
3143
+ if (message && typeof message === "object") return message;
3144
+ return value;
3145
+ }
3113
3146
  #handleProtocolEvent(event) {
3147
+ if (this.#isOfflineMessage(event)) {
3148
+ this.#logger.debug("Ignored protocol event queued before the current live connection.", {
3149
+ messageId: event.key.id ?? void 0,
3150
+ chatId: event.key.remoteJid ?? void 0,
3151
+ timestampSeconds: event.timestampSeconds ?? void 0
3152
+ });
3153
+ return;
3154
+ }
3114
3155
  const protocol = event.protocolMessage ?? event.message?.protocolMessage;
3115
3156
  if (!protocol) return;
3116
3157
  const protocolKey = protocol.key;
@@ -3127,7 +3168,16 @@ var ZapoProvider = class {
3127
3168
  if (!target.remoteJid) return;
3128
3169
  const stored = this.#findStoredMessage(target);
3129
3170
  const type = protocol?.type;
3171
+ const mutationKey = this.#protocolDeliveryKey(event, target, type);
3172
+ if (mutationKey && this.#handledProtocolStore.has(mutationKey)) {
3173
+ this.#logger.debug("Ignored duplicate Zapo protocol event.", {
3174
+ messageId: event.key.id ?? void 0,
3175
+ targetMessageId: target.id ?? void 0
3176
+ });
3177
+ return;
3178
+ }
3130
3179
  if (type === proto.Message.ProtocolMessage.Type.REVOKE) {
3180
+ if (mutationKey) this.#rememberHandledProtocol(mutationKey);
3131
3181
  const previous2 = stored ? normalizeZapoMessage(stored) : void 0;
3132
3182
  const deletedByMe = event.key.fromMe === true;
3133
3183
  const deletedById = event.key.participant ?? event.key.participantAlt ?? (deletedByMe ? this.getCurrentUserIds()[0] : event.key.remoteJid ?? void 0);
@@ -3157,6 +3207,7 @@ var ZapoProvider = class {
3157
3207
  const message = normalizeZapoMessage(edited);
3158
3208
  if (!message) return;
3159
3209
  const previous = stored ? normalizeZapoMessage(stored) : void 0;
3210
+ if (mutationKey) this.#rememberHandledProtocol(mutationKey);
3160
3211
  this.#remember(edited);
3161
3212
  const editedByMe = event.key.fromMe === true;
3162
3213
  const editedById = event.key.participant ?? event.key.participantAlt ?? (editedByMe ? this.getCurrentUserIds()[0] : event.key.remoteJid ?? void 0);
@@ -3521,49 +3572,49 @@ var ZapoProvider = class {
3521
3572
  timestamp: /* @__PURE__ */ new Date()
3522
3573
  };
3523
3574
  }
3524
- #normalizeVoipCall(event, forcedStatus) {
3575
+ #normalizeCall(event) {
3525
3576
  const id = event.callId;
3526
- const from = event.callerPn ?? event.peerJid ?? event.callCreator;
3527
- const chatId = event.groupJid ?? event.peerJid ?? from;
3577
+ const creator = event.callCreatorJid ?? void 0;
3578
+ const from = event.callerPnJid ?? creator;
3579
+ const chatId = event.groupJid ?? from;
3528
3580
  if (!id || !from || !chatId) return void 0;
3581
+ if (creator) {
3582
+ this.#callCreatorStore.delete(id);
3583
+ this.#callCreatorStore.set(id, creator);
3584
+ while (this.#callCreatorStore.size > 256) {
3585
+ const oldest = this.#callCreatorStore.keys().next().value;
3586
+ if (!oldest) break;
3587
+ this.#callCreatorStore.delete(oldest);
3588
+ }
3589
+ }
3590
+ const timestampMs = toNumber2(event.timestampMs);
3529
3591
  return {
3530
3592
  id,
3531
3593
  chatId,
3532
3594
  from,
3533
- status: forcedStatus ?? this.#callStatus(event.stateData?.state),
3534
- isVideo: event.mediaType === "video",
3595
+ status: this.#callStatus(event.type),
3596
+ isVideo: event.isVideo === true,
3535
3597
  isGroup: Boolean(event.groupJid),
3536
- date: event.createdAt ?? /* @__PURE__ */ new Date()
3598
+ date: timestampMs && timestampMs > 0 ? new Date(timestampMs) : /* @__PURE__ */ new Date()
3537
3599
  };
3538
3600
  }
3539
3601
  #callStatus(status) {
3540
3602
  switch (status?.toLowerCase()) {
3541
3603
  case "offer":
3542
- case "initiating":
3543
3604
  return "offer";
3544
3605
  case "ringing":
3545
- case "incoming_ringing":
3546
3606
  return "ringing";
3547
3607
  case "preaccept":
3548
- case "connecting":
3549
3608
  return "preaccept";
3550
3609
  case "accept":
3551
- case "accepted":
3552
- case "active":
3553
3610
  return "accept";
3554
3611
  case "reject":
3555
- case "rejected":
3556
3612
  case "terminate":
3557
- case "terminated":
3558
- case "ended":
3559
3613
  return "reject";
3560
3614
  default:
3561
3615
  return "timeout";
3562
3616
  }
3563
3617
  }
3564
- #callEndStatus(reason) {
3565
- return reason?.toLowerCase() === "timeout" ? "timeout" : "reject";
3566
- }
3567
3618
  #groupAction(action) {
3568
3619
  const value = action?.toLowerCase() ?? "";
3569
3620
  if (value.includes("promote")) return "promote";
@@ -3607,9 +3658,33 @@ var ZapoProvider = class {
3607
3658
  #messageStoreKey(key) {
3608
3659
  return `${key.remoteJid ?? ""}:${key.id ?? ""}:${key.participant ?? key.participantAlt ?? ""}`;
3609
3660
  }
3661
+ #messageDeliveryKey(key) {
3662
+ return `${key.remoteJid ?? ""}:${key.id ?? ""}`;
3663
+ }
3664
+ #protocolDeliveryKey(event, target, type) {
3665
+ if (type == null || !event.key.id || !target.id) return void 0;
3666
+ return `${type}:${event.key.remoteJid ?? target.remoteJid ?? ""}:${event.key.id}:${target.id}`;
3667
+ }
3668
+ #rememberHandledProtocol(key) {
3669
+ this.#handledProtocolStore.delete(key);
3670
+ this.#handledProtocolStore.add(key);
3671
+ while (this.#handledProtocolStore.size > this.#messageCacheSize) {
3672
+ const oldest = this.#handledProtocolStore.values().next().value;
3673
+ if (oldest) this.#handledProtocolStore.delete(oldest);
3674
+ }
3675
+ }
3676
+ #rememberDeliveredMessage(key) {
3677
+ this.#deliveredMessageStore.delete(key);
3678
+ this.#deliveredMessageStore.add(key);
3679
+ while (this.#deliveredMessageStore.size > this.#messageCacheSize) {
3680
+ const oldest = this.#deliveredMessageStore.values().next().value;
3681
+ if (oldest) this.#deliveredMessageStore.delete(oldest);
3682
+ }
3683
+ }
3610
3684
  #isOfflineMessage(message) {
3611
3685
  if (this.#options.processOfflineMessages === true) return false;
3612
3686
  if (message.offline === true) return true;
3687
+ if (!this.#connected) return true;
3613
3688
  if (!this.#connectedAtSeconds || message.timestampSeconds == null) return false;
3614
3689
  const timestamp = toSeconds(message.timestampSeconds);
3615
3690
  if (timestamp === void 0) return false;
@@ -3699,6 +3774,12 @@ var WhaNextZapoLogger = class _WhaNextZapoLogger {
3699
3774
  return context ? { ...this.#context, ...context } : this.#context;
3700
3775
  }
3701
3776
  };
3777
+ function toNumber2(value) {
3778
+ if (typeof value === "number") return value;
3779
+ if (value?.toNumber) return value.toNumber();
3780
+ if (typeof value?.low === "number") return value.low;
3781
+ return void 0;
3782
+ }
3702
3783
  function toSeconds(value) {
3703
3784
  const raw = typeof value === "number" ? value : value?.toNumber ? value.toNumber() : value?.low;
3704
3785
  if (raw === void 0) return void 0;