@whanext/core 0.5.0 → 0.7.0

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/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  // src/cache/memory-cache.ts
2
2
  var MemoryCache = class {
3
3
  #entries = /* @__PURE__ */ new Map();
4
+ #maxEntries;
5
+ constructor(options = {}) {
6
+ this.#maxEntries = Math.max(1, options.maxEntries ?? 1e3);
7
+ }
4
8
  async get(key) {
5
9
  const entry = this.#entries.get(key);
6
10
  if (!entry) {
@@ -10,6 +14,8 @@ var MemoryCache = class {
10
14
  this.#entries.delete(key);
11
15
  return void 0;
12
16
  }
17
+ this.#entries.delete(key);
18
+ this.#entries.set(key, entry);
13
19
  return entry.value;
14
20
  }
15
21
  async set(key, value, ttlMs) {
@@ -18,6 +24,11 @@ var MemoryCache = class {
18
24
  entry.expiresAt = Date.now() + ttlMs;
19
25
  }
20
26
  this.#entries.set(key, entry);
27
+ while (this.#entries.size > this.#maxEntries) {
28
+ const oldest = this.#entries.keys().next().value;
29
+ if (oldest === void 0) return;
30
+ this.#entries.delete(oldest);
31
+ }
21
32
  }
22
33
  async delete(key) {
23
34
  this.#entries.delete(key);
@@ -932,6 +943,7 @@ var GroupService = class {
932
943
  #provider;
933
944
  #cache;
934
945
  #ttlMs;
946
+ #requests = /* @__PURE__ */ new Map();
935
947
  constructor(provider, cache, ttlMs = 3e5) {
936
948
  this.#provider = provider;
937
949
  this.#cache = cache;
@@ -979,9 +991,17 @@ var GroupService = class {
979
991
  return cached;
980
992
  }
981
993
  }
982
- const group = await this.#provider.getGroup(groupId);
983
- await this.#cache.set(key, group, this.#ttlMs);
984
- return group;
994
+ const pending = this.#requests.get(key);
995
+ if (pending) {
996
+ return pending;
997
+ }
998
+ const request = this.#loadMetadata(key, groupId);
999
+ this.#requests.set(key, request);
1000
+ try {
1001
+ return await request;
1002
+ } finally {
1003
+ this.#requests.delete(key);
1004
+ }
985
1005
  }
986
1006
  async isAdmin(groupId, memberIds) {
987
1007
  if (!groupId.endsWith("@g.us")) {
@@ -1030,6 +1050,11 @@ var GroupService = class {
1030
1050
  #key(groupId) {
1031
1051
  return `group:${groupId}`;
1032
1052
  }
1053
+ async #loadMetadata(key, groupId) {
1054
+ const group = await this.#provider.getGroup(groupId);
1055
+ await this.#cache.set(key, group, this.#ttlMs);
1056
+ return group;
1057
+ }
1033
1058
  #matchesParticipant(participant, identities) {
1034
1059
  const participantIds = [participant.id, participant.lid, participant.phoneNumber].filter((identity) => identity !== void 0);
1035
1060
  return identities.some((identity) => participantIds.some((participantId) => identitiesMatch(identity, participantId)));
@@ -1051,6 +1076,10 @@ var MediaService = class {
1051
1076
  audio(chatId, content) {
1052
1077
  return this.#provider.sendMessage(chatId, content);
1053
1078
  }
1079
+ download(message) {
1080
+ const key = "keys" in message ? message.keys : message;
1081
+ return this.#provider.downloadMedia(key);
1082
+ }
1054
1083
  };
1055
1084
 
1056
1085
  // src/services/member-service.ts
@@ -1141,6 +1170,14 @@ var MessageService = class {
1141
1170
  const key = "keys" in message ? message.keys : message;
1142
1171
  return this.#provider.deleteMessage(key);
1143
1172
  }
1173
+ react(message, emoji) {
1174
+ const key = "keys" in message ? message.keys : message;
1175
+ return this.#provider.reactToMessage(key, emoji);
1176
+ }
1177
+ unreact(message) {
1178
+ const key = "keys" in message ? message.keys : message;
1179
+ return this.#provider.reactToMessage(key);
1180
+ }
1144
1181
  text(chatId, text, mentions) {
1145
1182
  const content = { text };
1146
1183
  if (mentions !== void 0) {
@@ -1199,7 +1236,9 @@ var WhaNextApp = class {
1199
1236
  this.#provider = provider;
1200
1237
  this.#phone = options.phone;
1201
1238
  this.logger = logger;
1202
- const cache = options.cache?.store ?? new MemoryCache();
1239
+ const cache = options.cache?.store ?? new MemoryCache(
1240
+ options.cache?.memoryMaxEntries === void 0 ? void 0 : { maxEntries: options.cache.memoryMaxEntries }
1241
+ );
1203
1242
  this.group = new GroupService(provider, cache, options.cache?.groupTtlMs);
1204
1243
  this.member = new MemberService(provider, this.group);
1205
1244
  this.message = new MessageService(provider);
@@ -1307,6 +1346,10 @@ var WhaNextApp = class {
1307
1346
  await this.#events.emit("connection", update);
1308
1347
  });
1309
1348
  this.#provider.on("groupChanged", ({ groupId }) => this.group.invalidate(groupId));
1349
+ this.#provider.on("groupParticipantsChanged", async (change) => {
1350
+ await this.group.invalidate(change.groupId);
1351
+ await this.#events.emit("groupParticipantsChanged", change);
1352
+ });
1310
1353
  this.#provider.on("call", async (call) => {
1311
1354
  this.logger.debug("Call received", {
1312
1355
  callId: call.id,
@@ -1401,6 +1444,7 @@ var Browser = /* @__PURE__ */ ((Browser2) => {
1401
1444
  import {
1402
1445
  Browsers,
1403
1446
  DisconnectReason,
1447
+ downloadMediaMessage,
1404
1448
  makeWASocket,
1405
1449
  proto,
1406
1450
  useMultiFileAuthState
@@ -1611,6 +1655,7 @@ var BaileysProvider = class {
1611
1655
  #events = new TypedEventEmitter();
1612
1656
  #logger;
1613
1657
  #messageStore = /* @__PURE__ */ new Map();
1658
+ #messageCacheSize;
1614
1659
  #socket;
1615
1660
  #saveCredentials;
1616
1661
  #saveQueue = Promise.resolve();
@@ -1621,6 +1666,7 @@ var BaileysProvider = class {
1621
1666
  constructor(options) {
1622
1667
  this.#options = options;
1623
1668
  this.#logger = options.logger ?? new Logger("silent");
1669
+ this.#messageCacheSize = Math.max(1, options.messageCacheSize ?? 1e3);
1624
1670
  }
1625
1671
  on(event, listener) {
1626
1672
  return this.#events.on(event, listener);
@@ -1645,7 +1691,7 @@ var BaileysProvider = class {
1645
1691
  markOnlineOnConnect: false,
1646
1692
  enableAutoSessionRecreation: true,
1647
1693
  enableRecentMessageCache: true,
1648
- getMessage: async (key) => key.id ? this.#messageStore.get(key.id) : void 0
1694
+ getMessage: async (key) => this.#messageStore.get(this.#messageStoreKey(key))?.message ?? void 0
1649
1695
  });
1650
1696
  this.#socket = socket;
1651
1697
  this.#bind(socket);
@@ -1724,6 +1770,39 @@ var BaileysProvider = class {
1724
1770
  const result = await socket.sendMessage(chatId, this.#toContent(content), options);
1725
1771
  return this.#sent(result);
1726
1772
  }
1773
+ async reactToMessage(key, emoji) {
1774
+ const result = await this.#requireSocket().sendMessage(key.chatId, {
1775
+ react: {
1776
+ text: emoji ?? "",
1777
+ key: this.#toWaKey(key)
1778
+ }
1779
+ });
1780
+ return this.#sent(result);
1781
+ }
1782
+ async downloadMedia(key) {
1783
+ const message = this.#messageStore.get(this.#messageStoreKey(key));
1784
+ if (!message?.message) {
1785
+ throw new WhaNextError(
1786
+ "MEDIA_NOT_AVAILABLE",
1787
+ "The media is unavailable. Download it from the received message event while it is cached.",
1788
+ { recoverable: true }
1789
+ );
1790
+ }
1791
+ const data = await downloadMediaMessage(message, "buffer", {}, {
1792
+ reuploadRequest: (current) => this.#requireSocket().updateMediaMessage(current),
1793
+ logger: createBaileysLogger(this.#logger.child("media"))
1794
+ });
1795
+ const media = normalizeBaileysMessage(message)?.media;
1796
+ if (!media) {
1797
+ throw new WhaNextError("MEDIA_NOT_AVAILABLE", "The selected message does not contain media.");
1798
+ }
1799
+ return {
1800
+ data,
1801
+ kind: media.kind,
1802
+ ...media.mimetype ? { mimetype: media.mimetype } : {},
1803
+ ...media.fileName ? { fileName: media.fileName } : {}
1804
+ };
1805
+ }
1727
1806
  async editMessage(key, content) {
1728
1807
  const result = await this.#requireSocket().sendMessage(key.chatId, {
1729
1808
  text: content,
@@ -1792,7 +1871,6 @@ var BaileysProvider = class {
1792
1871
  }
1793
1872
  async setPresence(chatId, state) {
1794
1873
  const socket = this.#requireSocket();
1795
- await socket.presenceSubscribe(chatId);
1796
1874
  const presence = state === "typing" ? "composing" : state === "recording" ? "recording" : "paused";
1797
1875
  await socket.sendPresenceUpdate(presence, chatId);
1798
1876
  }
@@ -1809,7 +1887,7 @@ var BaileysProvider = class {
1809
1887
  socket.ev.on("messages.upsert", ({ messages, type }) => {
1810
1888
  if (type !== "notify") return;
1811
1889
  for (const raw of messages) {
1812
- if (raw.key.id && raw.message) this.#remember(raw.key.id, raw.message);
1890
+ if (raw.key.id && raw.message) this.#remember(raw);
1813
1891
  const message = normalizeBaileysMessage(raw);
1814
1892
  if (message) void this.#events.emit("message", message);
1815
1893
  }
@@ -1819,7 +1897,10 @@ var BaileysProvider = class {
1819
1897
  if (group.id) void this.#events.emit("groupChanged", { groupId: group.id });
1820
1898
  }
1821
1899
  });
1822
- socket.ev.on("group-participants.update", ({ id }) => {
1900
+ socket.ev.on("group-participants.update", (update) => {
1901
+ const change = this.#groupParticipantsChanged(update);
1902
+ void this.#events.emit("groupParticipantsChanged", change);
1903
+ const { id } = update;
1823
1904
  void this.#events.emit("groupChanged", { groupId: id });
1824
1905
  });
1825
1906
  socket.ev.on("call", (calls) => {
@@ -1949,7 +2030,7 @@ var BaileysProvider = class {
1949
2030
  if (!message?.key.id || !message.key.remoteJid) {
1950
2031
  throw new WhaNextError("PROVIDER_ERROR", "WhatsApp did not confirm the sent message.");
1951
2032
  }
1952
- if (message.message) this.#remember(message.key.id, message.message);
2033
+ if (message.message) this.#remember(message);
1953
2034
  return {
1954
2035
  id: message.key.id,
1955
2036
  chatId: message.key.remoteJid,
@@ -1968,6 +2049,15 @@ var BaileysProvider = class {
1968
2049
  date: call.date ?? /* @__PURE__ */ new Date()
1969
2050
  };
1970
2051
  }
2052
+ #groupParticipantsChanged(change) {
2053
+ const participantIds = change.participants.map((participant) => participant.id).filter((id) => Boolean(id));
2054
+ return {
2055
+ groupId: change.id,
2056
+ action: change.action,
2057
+ participantIds,
2058
+ ...change.author ? { authorId: change.author } : {}
2059
+ };
2060
+ }
1971
2061
  #callStatus(status) {
1972
2062
  const known = [
1973
2063
  "offer",
@@ -1979,13 +2069,19 @@ var BaileysProvider = class {
1979
2069
  ];
1980
2070
  return known.find((value) => value === status) ?? "timeout";
1981
2071
  }
1982
- #remember(id, message) {
1983
- this.#messageStore.set(id, message);
1984
- if (this.#messageStore.size > 500) {
2072
+ #remember(message) {
2073
+ const key = this.#messageStoreKey(message.key);
2074
+ this.#messageStore.delete(key);
2075
+ this.#messageStore.set(key, message);
2076
+ while (this.#messageStore.size > this.#messageCacheSize) {
1985
2077
  const oldest = this.#messageStore.keys().next().value;
1986
2078
  if (oldest) this.#messageStore.delete(oldest);
1987
2079
  }
1988
2080
  }
2081
+ #messageStoreKey(key) {
2082
+ const chatId = "chatId" in key ? key.chatId : key.remoteJid;
2083
+ return `${chatId ?? ""}:${key.id ?? ""}`;
2084
+ }
1989
2085
  };
1990
2086
 
1991
2087
  // src/app/create.ts
@@ -1995,6 +2091,7 @@ async function create(options = {}) {
1995
2091
  auth: options.auth ?? "./session",
1996
2092
  browser: options.browser ?? "windows" /* Windows */,
1997
2093
  logger: logger.child("provider"),
2094
+ ...options.messageCacheSize !== void 0 ? { messageCacheSize: options.messageCacheSize } : {},
1998
2095
  ...options.reconnect ? { reconnect: options.reconnect } : {}
1999
2096
  });
2000
2097
  return new WhaNextApp(provider, {
@@ -2011,6 +2108,9 @@ async function create(options = {}) {
2011
2108
  function defineCommand(command) {
2012
2109
  return command;
2013
2110
  }
2111
+ function defineCommands(...commands) {
2112
+ return commands;
2113
+ }
2014
2114
 
2015
2115
  // src/commands/load-commands.ts
2016
2116
  import { readdir } from "fs/promises";
@@ -2023,16 +2123,20 @@ async function loadCommands(registrar, dirPath, options = {}) {
2023
2123
  const entries = await readEntries(dirPath, recursive);
2024
2124
  const loaded = [];
2025
2125
  const skipped = [];
2126
+ const commands = [];
2026
2127
  for (const filePath of entries) {
2027
2128
  if (!extensions.includes(path.extname(filePath))) {
2028
2129
  skipped.push(filePath);
2029
2130
  continue;
2030
2131
  }
2031
- const definition = await importCommand(filePath);
2032
- registrar.command(definition);
2132
+ const definitions = await importCommands(filePath);
2133
+ for (const definition of definitions) {
2134
+ registrar.command(definition);
2135
+ commands.push({ name: definition.name, filePath });
2136
+ }
2033
2137
  loaded.push(filePath);
2034
2138
  }
2035
- return { loaded, skipped };
2139
+ return { loaded, skipped, commands };
2036
2140
  }
2037
2141
  async function readEntries(dirPath, recursive) {
2038
2142
  let dirents;
@@ -2044,9 +2148,9 @@ async function readEntries(dirPath, recursive) {
2044
2148
  context: { dirPath }
2045
2149
  });
2046
2150
  }
2047
- return dirents.filter((dirent) => dirent.isFile()).map((dirent) => path.join(dirent.parentPath, dirent.name));
2151
+ return dirents.filter((dirent) => dirent.isFile()).map((dirent) => path.join(dirent.parentPath, dirent.name)).sort((left, right) => left.localeCompare(right));
2048
2152
  }
2049
- async function importCommand(filePath) {
2153
+ async function importCommands(filePath) {
2050
2154
  let module;
2051
2155
  try {
2052
2156
  module = await import(pathToFileURL(filePath).href);
@@ -2056,13 +2160,27 @@ async function importCommand(filePath) {
2056
2160
  context: { filePath }
2057
2161
  });
2058
2162
  }
2059
- const candidate = module.default ?? Object.values(module)[0];
2060
- if (!isCommandDefinition(candidate)) {
2061
- throw new WhaNextError("COMMAND_LOAD_FAILED", `The file "${filePath}" does not export a valid command.`, {
2163
+ const definitions = [];
2164
+ const seen = /* @__PURE__ */ new Set();
2165
+ const exports = [
2166
+ module.default,
2167
+ ...Object.entries(module).filter(([name]) => name !== "default").map(([, value]) => value)
2168
+ ];
2169
+ for (const value of exports) {
2170
+ const candidates = Array.isArray(value) ? value : [value];
2171
+ for (const candidate of candidates) {
2172
+ if (isCommandDefinition(candidate) && !seen.has(candidate)) {
2173
+ seen.add(candidate);
2174
+ definitions.push(candidate);
2175
+ }
2176
+ }
2177
+ }
2178
+ if (definitions.length === 0) {
2179
+ throw new WhaNextError("COMMAND_LOAD_FAILED", `The file "${filePath}" does not export any valid commands.`, {
2062
2180
  context: { filePath }
2063
2181
  });
2064
2182
  }
2065
- return candidate;
2183
+ return definitions;
2066
2184
  }
2067
2185
  function isCommandDefinition(value) {
2068
2186
  return typeof value === "object" && value !== null && typeof value.name === "string" && typeof value.execute === "function";
@@ -2080,6 +2198,7 @@ export {
2080
2198
  WhaNextError,
2081
2199
  create,
2082
2200
  defineCommand,
2201
+ defineCommands,
2083
2202
  loadCommands,
2084
2203
  toWhaNextError
2085
2204
  };