miaw-core 1.7.0 → 1.9.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli/commands/business.d.ts +20 -0
  3. package/dist/cli/commands/business.d.ts.map +1 -0
  4. package/dist/cli/commands/business.js +76 -0
  5. package/dist/cli/commands/business.js.map +1 -0
  6. package/dist/cli/commands/commands-index.d.ts +3 -0
  7. package/dist/cli/commands/commands-index.d.ts.map +1 -1
  8. package/dist/cli/commands/commands-index.js +3 -0
  9. package/dist/cli/commands/commands-index.js.map +1 -1
  10. package/dist/cli/commands/community.d.ts +78 -0
  11. package/dist/cli/commands/community.d.ts.map +1 -0
  12. package/dist/cli/commands/community.js +201 -0
  13. package/dist/cli/commands/community.js.map +1 -0
  14. package/dist/cli/commands/index.d.ts.map +1 -1
  15. package/dist/cli/commands/index.js +204 -0
  16. package/dist/cli/commands/index.js.map +1 -1
  17. package/dist/cli/commands/status.d.ts +24 -0
  18. package/dist/cli/commands/status.d.ts.map +1 -0
  19. package/dist/cli/commands/status.js +58 -0
  20. package/dist/cli/commands/status.js.map +1 -0
  21. package/dist/cli/repl.d.ts +7 -0
  22. package/dist/cli/repl.d.ts.map +1 -1
  23. package/dist/cli/repl.js +44 -4
  24. package/dist/cli/repl.js.map +1 -1
  25. package/dist/client/MiawClient.d.ts +118 -1
  26. package/dist/client/MiawClient.d.ts.map +1 -1
  27. package/dist/client/MiawClient.js +619 -14
  28. package/dist/client/MiawClient.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/types/index.d.ts +147 -0
  33. package/dist/types/index.d.ts.map +1 -1
  34. package/dist/types/index.js.map +1 -1
  35. package/docs/BAILEYS_VS_MIAW_COMPARISON.md +20 -20
  36. package/docs/USAGE.md +74 -0
  37. package/package.json +1 -1
@@ -491,7 +491,13 @@ export class MiawClient extends EventEmitter {
491
491
  });
492
492
  // Messages
493
493
  this.socket.ev.on("messages.upsert", async (m) => {
494
- if (m.type !== "notify")
494
+ // Baileys only ever emits type 'notify' (live) or 'append' (our own
495
+ // sends + offline/reconnect backlog). The type is a live-vs-backlog
496
+ // signal, NOT a direction signal, so we must process BOTH to capture
497
+ // outbound messages (sent by us or from the user's phone) and any
498
+ // messages that arrive in the offline backlog on reconnect. Direction is
499
+ // decided later via `fromMe`; storeMessage() dedups re-delivered messages.
500
+ if (m.type !== "notify" && m.type !== "append")
495
501
  return;
496
502
  for (const msg of m.messages) {
497
503
  // Debug: Log raw Baileys message structure
@@ -560,14 +566,16 @@ export class MiawClient extends EventEmitter {
560
566
  }
561
567
  }
562
568
  }
563
- // Store message in messagesStore
564
- const chatJid = normalized.from;
565
- if (!this.messagesStore.has(chatJid)) {
566
- this.messagesStore.set(chatJid, []);
569
+ // Store message in messagesStore (deduped by id). Outbound messages
570
+ // (fromMe) — our own sends and messages sent from the user's phone —
571
+ // are stored so getChatMessages / CLI `get messages` include them,
572
+ // but are NOT emitted on "message" so existing bots don't reply to
573
+ // their own sends. Emit only newly-stored INBOUND messages, so
574
+ // reconnect re-delivery of already-stored messages never re-fires.
575
+ const isNew = this.storeMessage(normalized);
576
+ if (isNew && !normalized.fromMe) {
577
+ this.emit("message", normalized);
567
578
  }
568
- this.messagesStore.get(chatJid).push(normalized);
569
- this.saveMessagesToFile();
570
- this.emit("message", normalized);
571
579
  }
572
580
  }
573
581
  });
@@ -1171,12 +1179,9 @@ export class MiawClient extends EventEmitter {
1171
1179
  if (normalized.participant) {
1172
1180
  normalized.participant = this.resolveLidToJid(normalized.participant);
1173
1181
  }
1174
- const chatJid = normalized.from;
1175
- if (!this.messagesStore.has(chatJid)) {
1176
- this.messagesStore.set(chatJid, []);
1177
- }
1178
- this.messagesStore.get(chatJid).push(normalized);
1179
- this.saveMessagesToFile();
1182
+ // Store (deduped by id). The history-sync path never emits "message"; it
1183
+ // only populates the store.
1184
+ this.storeMessage(normalized);
1180
1185
  }
1181
1186
  /**
1182
1187
  * Clear session data for this instance.
@@ -1745,6 +1750,78 @@ export class MiawClient extends EventEmitter {
1745
1750
  }
1746
1751
  this.emit("poll_vote", vote);
1747
1752
  }
1753
+ // ============================================
1754
+ // Status / Stories (v1.8.0) — via status@broadcast
1755
+ // ============================================
1756
+ /**
1757
+ * Resolve the audience (statusJidList) for a status post. When recipients are
1758
+ * given they are used (formatted to JIDs); otherwise it defaults to every
1759
+ * individual contact in the store (like the WhatsApp app's "all contacts").
1760
+ */
1761
+ getStatusAudience(recipients) {
1762
+ if (recipients && recipients.length > 0) {
1763
+ return recipients.map((r) => MessageHandler.formatPhoneToJid(r));
1764
+ }
1765
+ const jids = new Set();
1766
+ for (const contact of this.contactsStore.values()) {
1767
+ if (contact.jid?.endsWith("@s.whatsapp.net")) {
1768
+ jids.add(contact.jid);
1769
+ }
1770
+ }
1771
+ return [...jids];
1772
+ }
1773
+ /**
1774
+ * Post a text status / story.
1775
+ * @param text - The status text
1776
+ * @param recipients - Audience (phone numbers or JIDs). Omit to send to all contacts.
1777
+ * @param options - backgroundColor / font for the text status
1778
+ */
1779
+ async postTextStatus(text, recipients, options) {
1780
+ return this.postStatus({ text }, recipients, options);
1781
+ }
1782
+ /**
1783
+ * Post an image status / story.
1784
+ * @param image - Image source (file path, URL, or Buffer)
1785
+ * @param recipients - Audience (phone numbers or JIDs). Omit to send to all contacts.
1786
+ * @param options - caption for the image
1787
+ */
1788
+ async postImageStatus(image, recipients, options) {
1789
+ return this.postStatus({ image: Buffer.isBuffer(image) ? image : { url: image }, caption: options?.caption }, recipients, options);
1790
+ }
1791
+ /**
1792
+ * Post a video status / story.
1793
+ * @param video - Video source (file path, URL, or Buffer)
1794
+ * @param recipients - Audience (phone numbers or JIDs). Omit to send to all contacts.
1795
+ * @param options - caption for the video
1796
+ */
1797
+ async postVideoStatus(video, recipients, options) {
1798
+ return this.postStatus({ video: Buffer.isBuffer(video) ? video : { url: video }, caption: options?.caption }, recipients, options);
1799
+ }
1800
+ /** Shared status sender: builds statusJidList and posts to status@broadcast. */
1801
+ async postStatus(content, recipients, options) {
1802
+ try {
1803
+ if (!this.socket) {
1804
+ throw new Error("Not connected. Call connect() first.");
1805
+ }
1806
+ if (this.connectionState !== "connected") {
1807
+ throw new Error(`Cannot post status. Connection state: ${this.connectionState}`);
1808
+ }
1809
+ const statusJidList = this.getStatusAudience(recipients);
1810
+ if (statusJidList.length === 0 && this.options.debug) {
1811
+ this.logger.debug("[status] No audience (no recipients and empty contacts store); status will be visible only to you.");
1812
+ }
1813
+ const result = await this.socket.sendMessage("status@broadcast", content, {
1814
+ statusJidList,
1815
+ backgroundColor: options?.backgroundColor,
1816
+ font: options?.font,
1817
+ });
1818
+ return { success: true, messageId: result?.key?.id || undefined };
1819
+ }
1820
+ catch (error) {
1821
+ this.logger.error("Failed to post status:", error);
1822
+ return { success: false, error: error.message };
1823
+ }
1824
+ }
1748
1825
  /**
1749
1826
  * Download media from a received message
1750
1827
  * @param message - The MiawMessage containing media (must have raw field with original Baileys message)
@@ -2000,6 +2077,145 @@ export class MiawClient extends EventEmitter {
2000
2077
  return null;
2001
2078
  }
2002
2079
  }
2080
+ // ============================================
2081
+ // Business extras (v1.8.0) — WhatsApp Business only
2082
+ // ============================================
2083
+ /**
2084
+ * Update your own business profile (address, websites, email, description,
2085
+ * hours). Category is read-only and cannot be changed here.
2086
+ */
2087
+ async updateBusinessProfile(updates) {
2088
+ try {
2089
+ if (!this.socket) {
2090
+ throw new Error("Not connected. Call connect() first.");
2091
+ }
2092
+ if (this.connectionState !== "connected") {
2093
+ throw new Error(`Cannot update business profile. Connection state: ${this.connectionState}`);
2094
+ }
2095
+ const socket = this.socket;
2096
+ await socket.updateBussinesProfile(updates);
2097
+ return { success: true };
2098
+ }
2099
+ catch (error) {
2100
+ this.logger.error("Failed to update business profile:", error);
2101
+ return { success: false, error: error.message };
2102
+ }
2103
+ }
2104
+ /**
2105
+ * Set the business cover photo.
2106
+ * @param image - Image source (file path, URL, or Buffer)
2107
+ * @returns The uploaded cover photo id (pass to removeCoverPhoto)
2108
+ */
2109
+ async updateCoverPhoto(image) {
2110
+ try {
2111
+ if (!this.socket) {
2112
+ throw new Error("Not connected. Call connect() first.");
2113
+ }
2114
+ if (this.connectionState !== "connected") {
2115
+ throw new Error(`Cannot update cover photo. Connection state: ${this.connectionState}`);
2116
+ }
2117
+ const photo = (Buffer.isBuffer(image) ? image : { url: image });
2118
+ const id = await this.socket.updateCoverPhoto(photo);
2119
+ return { success: true, coverPhotoId: id != null ? String(id) : undefined };
2120
+ }
2121
+ catch (error) {
2122
+ this.logger.error("Failed to update cover photo:", error);
2123
+ return { success: false, error: error.message };
2124
+ }
2125
+ }
2126
+ /** Remove a business cover photo by its id. */
2127
+ async removeCoverPhoto(coverPhotoId) {
2128
+ try {
2129
+ if (!this.socket) {
2130
+ throw new Error("Not connected. Call connect() first.");
2131
+ }
2132
+ if (this.connectionState !== "connected") {
2133
+ throw new Error(`Cannot remove cover photo. Connection state: ${this.connectionState}`);
2134
+ }
2135
+ await this.socket.removeCoverPhoto(coverPhotoId);
2136
+ return { success: true };
2137
+ }
2138
+ catch (error) {
2139
+ this.logger.error("Failed to remove cover photo:", error);
2140
+ return { success: false, error: error.message };
2141
+ }
2142
+ }
2143
+ /**
2144
+ * Fetch details for a business order. The `tokenBase64` comes from a received
2145
+ * order message (`orderMessage.token`) — this is only usable after a customer
2146
+ * sends an order.
2147
+ */
2148
+ async getOrderDetails(orderId, tokenBase64) {
2149
+ try {
2150
+ if (!this.socket) {
2151
+ throw new Error("Not connected. Call connect() first.");
2152
+ }
2153
+ if (this.connectionState !== "connected") {
2154
+ throw new Error(`Cannot get order details. Connection state: ${this.connectionState}`);
2155
+ }
2156
+ const details = await this.socket.getOrderDetails(orderId, tokenBase64);
2157
+ if (!details) {
2158
+ return null;
2159
+ }
2160
+ const price = (details.price ?? {});
2161
+ return {
2162
+ currency: price.currency,
2163
+ total: price.total,
2164
+ products: (details.products ?? []).map((p) => ({
2165
+ id: p.id,
2166
+ name: p.name,
2167
+ imageUrl: p.imageUrl,
2168
+ quantity: p.quantity,
2169
+ currency: p.currency,
2170
+ price: p.price,
2171
+ })),
2172
+ };
2173
+ }
2174
+ catch (error) {
2175
+ this.logger.error("Failed to get order details:", error);
2176
+ return null;
2177
+ }
2178
+ }
2179
+ /** Add or edit a business quick reply. */
2180
+ async addQuickReply(quickReply) {
2181
+ try {
2182
+ if (!this.socket) {
2183
+ throw new Error("Not connected. Call connect() first.");
2184
+ }
2185
+ if (this.connectionState !== "connected") {
2186
+ throw new Error(`Cannot add quick reply. Connection state: ${this.connectionState}`);
2187
+ }
2188
+ await this.socket.addOrEditQuickReply({
2189
+ shortcut: quickReply.shortcut,
2190
+ message: quickReply.message,
2191
+ keywords: quickReply.keywords ?? [],
2192
+ count: 0,
2193
+ deleted: false,
2194
+ });
2195
+ return { success: true };
2196
+ }
2197
+ catch (error) {
2198
+ this.logger.error("Failed to add quick reply:", error);
2199
+ return { success: false, error: error.message };
2200
+ }
2201
+ }
2202
+ /** Remove a business quick reply by its timestamp. */
2203
+ async removeQuickReply(timestamp) {
2204
+ try {
2205
+ if (!this.socket) {
2206
+ throw new Error("Not connected. Call connect() first.");
2207
+ }
2208
+ if (this.connectionState !== "connected") {
2209
+ throw new Error(`Cannot remove quick reply. Connection state: ${this.connectionState}`);
2210
+ }
2211
+ await this.socket.removeQuickReply(timestamp);
2212
+ return { success: true };
2213
+ }
2214
+ catch (error) {
2215
+ this.logger.error("Failed to remove quick reply:", error);
2216
+ return { success: false, error: error.message };
2217
+ }
2218
+ }
2003
2219
  /**
2004
2220
  * Get profile picture URL for a contact or group
2005
2221
  * @param jidOrPhone - Contact's JID, phone number, or group JID
@@ -2552,6 +2768,33 @@ export class MiawClient extends EventEmitter {
2552
2768
  getMessagesFilePath() {
2553
2769
  return path.join(this.options.sessionPath, this.options.instanceId, "messages.json");
2554
2770
  }
2771
+ /**
2772
+ * Store a normalized message in the in-memory store (and persist to disk),
2773
+ * deduplicating by message id.
2774
+ *
2775
+ * Both the live `messages.upsert` handler and the history-sync path funnel
2776
+ * through here, so a message re-delivered via multiple routes (our own send
2777
+ * echo, offline/reconnect backlog, or history sync) is stored only once.
2778
+ *
2779
+ * @param normalized - Normalized message to store
2780
+ * @returns true if the message was newly stored; false if it was a duplicate
2781
+ */
2782
+ storeMessage(normalized) {
2783
+ const chatJid = normalized.from;
2784
+ const bucket = this.messagesStore.get(chatJid);
2785
+ if (bucket) {
2786
+ // Dedup by id. An empty id (missing key.id) can't be deduped -> append.
2787
+ if (normalized.id && bucket.some((m) => m.id === normalized.id)) {
2788
+ return false;
2789
+ }
2790
+ bucket.push(normalized);
2791
+ }
2792
+ else {
2793
+ this.messagesStore.set(chatJid, [normalized]);
2794
+ }
2795
+ this.saveMessagesToFile();
2796
+ return true;
2797
+ }
2555
2798
  /**
2556
2799
  * Save messages to disk for persistence across reconnections
2557
2800
  * Baileys v7 history sync doesn't always fire, so we persist messages locally
@@ -3681,6 +3924,368 @@ export class MiawClient extends EventEmitter {
3681
3924
  }
3682
3925
  }
3683
3926
  // ============================================
3927
+ // Community Methods (v1.9.0)
3928
+ // ============================================
3929
+ /**
3930
+ * Map Baileys GroupMetadata (communities share the shape) to CommunityInfo.
3931
+ */
3932
+ mapToCommunityInfo(metadata) {
3933
+ const participants = (metadata.participants || []).map((p) => ({
3934
+ jid: this.resolveLidToJid(p.id),
3935
+ role: p.admin === "superadmin"
3936
+ ? "superadmin"
3937
+ : p.admin === "admin"
3938
+ ? "admin"
3939
+ : "member",
3940
+ }));
3941
+ return {
3942
+ jid: metadata.id,
3943
+ name: metadata.subject,
3944
+ description: metadata.desc || undefined,
3945
+ owner: metadata.owner ? this.resolveLidToJid(metadata.owner) : undefined,
3946
+ createdAt: metadata.creation,
3947
+ participantCount: (metadata.participants || []).length,
3948
+ participants,
3949
+ announce: metadata.announce,
3950
+ restrict: metadata.restrict,
3951
+ };
3952
+ }
3953
+ /**
3954
+ * Create a new community.
3955
+ * @param name - Community name/subject
3956
+ * @param description - Optional community description
3957
+ */
3958
+ async createCommunity(name, description) {
3959
+ try {
3960
+ if (!this.socket) {
3961
+ throw new Error("Not connected. Call connect() first.");
3962
+ }
3963
+ if (this.connectionState !== "connected") {
3964
+ throw new Error(`Cannot create community. Connection state: ${this.connectionState}`);
3965
+ }
3966
+ const nameValidation = validateGroupName(name);
3967
+ if (!nameValidation.valid) {
3968
+ return { success: false, error: nameValidation.error };
3969
+ }
3970
+ const metadata = await this.socket.communityCreate(name, description || "");
3971
+ if (!metadata) {
3972
+ return { success: true };
3973
+ }
3974
+ const info = this.mapToCommunityInfo(metadata);
3975
+ return { success: true, communityJid: info.jid, communityInfo: info };
3976
+ }
3977
+ catch (error) {
3978
+ this.logger.error("Failed to create community:", error);
3979
+ return { success: false, error: error.message };
3980
+ }
3981
+ }
3982
+ /** Get community metadata/information. */
3983
+ async getCommunityInfo(communityJid) {
3984
+ try {
3985
+ if (!this.socket) {
3986
+ throw new Error("Not connected. Call connect() first.");
3987
+ }
3988
+ if (this.connectionState !== "connected") {
3989
+ throw new Error(`Cannot get community info. Connection state: ${this.connectionState}`);
3990
+ }
3991
+ const metadata = await this.socket.communityMetadata(communityJid);
3992
+ return metadata ? this.mapToCommunityInfo(metadata) : null;
3993
+ }
3994
+ catch (error) {
3995
+ this.logger.error("Failed to get community info:", error);
3996
+ return null;
3997
+ }
3998
+ }
3999
+ /** Get a community's participants. */
4000
+ async getCommunityParticipants(communityJid) {
4001
+ const info = await this.getCommunityInfo(communityJid);
4002
+ return info?.participants || null;
4003
+ }
4004
+ /** Fetch all communities the account participates in. */
4005
+ async getAllCommunities() {
4006
+ try {
4007
+ if (!this.socket) {
4008
+ throw new Error("Not connected. Call connect() first.");
4009
+ }
4010
+ if (this.connectionState !== "connected") {
4011
+ throw new Error(`Cannot fetch communities. Connection state: ${this.connectionState}`);
4012
+ }
4013
+ const all = await this.socket.communityFetchAllParticipating();
4014
+ return Object.values(all || {}).map((m) => this.mapToCommunityInfo(m));
4015
+ }
4016
+ catch (error) {
4017
+ this.logger.error("Failed to fetch communities:", error);
4018
+ return [];
4019
+ }
4020
+ }
4021
+ /** Update a community's name/subject. */
4022
+ async updateCommunityName(communityJid, name) {
4023
+ try {
4024
+ if (!this.socket) {
4025
+ throw new Error("Not connected. Call connect() first.");
4026
+ }
4027
+ if (this.connectionState !== "connected") {
4028
+ throw new Error(`Cannot update community. Connection state: ${this.connectionState}`);
4029
+ }
4030
+ await this.socket.communityUpdateSubject(communityJid, name);
4031
+ return { success: true };
4032
+ }
4033
+ catch (error) {
4034
+ this.logger.error("Failed to update community name:", error);
4035
+ return { success: false, error: error.message };
4036
+ }
4037
+ }
4038
+ /** Update a community's description. */
4039
+ async updateCommunityDescription(communityJid, description) {
4040
+ try {
4041
+ if (!this.socket) {
4042
+ throw new Error("Not connected. Call connect() first.");
4043
+ }
4044
+ if (this.connectionState !== "connected") {
4045
+ throw new Error(`Cannot update community. Connection state: ${this.connectionState}`);
4046
+ }
4047
+ await this.socket.communityUpdateDescription(communityJid, description);
4048
+ return { success: true };
4049
+ }
4050
+ catch (error) {
4051
+ this.logger.error("Failed to update community description:", error);
4052
+ return { success: false, error: error.message };
4053
+ }
4054
+ }
4055
+ /** Leave a community. */
4056
+ async leaveCommunity(communityJid) {
4057
+ try {
4058
+ if (!this.socket) {
4059
+ throw new Error("Not connected. Call connect() first.");
4060
+ }
4061
+ if (this.connectionState !== "connected") {
4062
+ throw new Error(`Cannot leave community. Connection state: ${this.connectionState}`);
4063
+ }
4064
+ await this.socket.communityLeave(communityJid);
4065
+ return { success: true };
4066
+ }
4067
+ catch (error) {
4068
+ this.logger.error("Failed to leave community:", error);
4069
+ return { success: false, error: error.message };
4070
+ }
4071
+ }
4072
+ /**
4073
+ * Create a new group inside a community.
4074
+ * @param communityJid - Parent community JID
4075
+ * @param name - Group name/subject
4076
+ * @param participants - Initial members (phone numbers or JIDs)
4077
+ */
4078
+ async createCommunityGroup(communityJid, name, participants = []) {
4079
+ try {
4080
+ if (!this.socket) {
4081
+ throw new Error("Not connected. Call connect() first.");
4082
+ }
4083
+ if (this.connectionState !== "connected") {
4084
+ throw new Error(`Cannot create community group. Connection state: ${this.connectionState}`);
4085
+ }
4086
+ const nameValidation = validateGroupName(name);
4087
+ if (!nameValidation.valid) {
4088
+ return { success: false, error: nameValidation.error };
4089
+ }
4090
+ const formatted = participants.map((p) => MessageHandler.formatPhoneToJid(p));
4091
+ const metadata = await this.socket.communityCreateGroup(name, formatted, communityJid);
4092
+ if (!metadata) {
4093
+ return { success: true };
4094
+ }
4095
+ const info = this.mapToCommunityInfo(metadata);
4096
+ return {
4097
+ success: true,
4098
+ groupJid: info.jid,
4099
+ groupInfo: {
4100
+ jid: info.jid,
4101
+ name: info.name,
4102
+ description: info.description,
4103
+ owner: info.owner,
4104
+ createdAt: info.createdAt,
4105
+ participantCount: info.participantCount,
4106
+ participants: info.participants,
4107
+ announce: info.announce,
4108
+ restrict: info.restrict,
4109
+ },
4110
+ };
4111
+ }
4112
+ catch (error) {
4113
+ this.logger.error("Failed to create community group:", error);
4114
+ return { success: false, error: error.message };
4115
+ }
4116
+ }
4117
+ /** Link an existing group to a community. */
4118
+ async linkGroupToCommunity(groupJid, communityJid) {
4119
+ try {
4120
+ if (!this.socket) {
4121
+ throw new Error("Not connected. Call connect() first.");
4122
+ }
4123
+ if (this.connectionState !== "connected") {
4124
+ throw new Error(`Cannot link group. Connection state: ${this.connectionState}`);
4125
+ }
4126
+ await this.socket.communityLinkGroup(groupJid, communityJid);
4127
+ return { success: true };
4128
+ }
4129
+ catch (error) {
4130
+ this.logger.error("Failed to link group to community:", error);
4131
+ return { success: false, error: error.message };
4132
+ }
4133
+ }
4134
+ /** Unlink a group from a community. */
4135
+ async unlinkGroupFromCommunity(groupJid, communityJid) {
4136
+ try {
4137
+ if (!this.socket) {
4138
+ throw new Error("Not connected. Call connect() first.");
4139
+ }
4140
+ if (this.connectionState !== "connected") {
4141
+ throw new Error(`Cannot unlink group. Connection state: ${this.connectionState}`);
4142
+ }
4143
+ await this.socket.communityUnlinkGroup(groupJid, communityJid);
4144
+ return { success: true };
4145
+ }
4146
+ catch (error) {
4147
+ this.logger.error("Failed to unlink group from community:", error);
4148
+ return { success: false, error: error.message };
4149
+ }
4150
+ }
4151
+ /** List the groups linked inside a community. */
4152
+ async getLinkedGroups(communityJid) {
4153
+ try {
4154
+ if (!this.socket) {
4155
+ throw new Error("Not connected. Call connect() first.");
4156
+ }
4157
+ if (this.connectionState !== "connected") {
4158
+ throw new Error(`Cannot fetch linked groups. Connection state: ${this.connectionState}`);
4159
+ }
4160
+ const result = await this.socket.communityFetchLinkedGroups(communityJid);
4161
+ return (result?.linkedGroups || []).map((g) => ({
4162
+ id: g.id,
4163
+ subject: g.subject,
4164
+ creation: g.creation,
4165
+ owner: g.owner ? this.resolveLidToJid(g.owner) : undefined,
4166
+ size: g.size,
4167
+ }));
4168
+ }
4169
+ catch (error) {
4170
+ this.logger.error("Failed to fetch linked groups:", error);
4171
+ return [];
4172
+ }
4173
+ }
4174
+ /** Shared helper for community participant operations. */
4175
+ async communityParticipantsOperation(communityJid, participants, action) {
4176
+ if (!this.socket) {
4177
+ throw new Error("Not connected. Call connect() first.");
4178
+ }
4179
+ if (this.connectionState !== "connected") {
4180
+ throw new Error(`Cannot perform community operation. Connection state: ${this.connectionState}`);
4181
+ }
4182
+ const formatted = participants.map((p) => MessageHandler.formatPhoneToJid(p));
4183
+ const results = await this.socket.communityParticipantsUpdate(communityJid, formatted, action);
4184
+ return results.map((r) => ({
4185
+ jid: r.jid || "",
4186
+ status: r.status,
4187
+ success: r.status === "200",
4188
+ }));
4189
+ }
4190
+ /** Add members to a community. */
4191
+ async addCommunityMembers(communityJid, participants) {
4192
+ return this.communityParticipantsOperation(communityJid, participants, "add");
4193
+ }
4194
+ /** Remove members from a community. */
4195
+ async removeCommunityMembers(communityJid, participants) {
4196
+ return this.communityParticipantsOperation(communityJid, participants, "remove");
4197
+ }
4198
+ /** Promote members to community admin. */
4199
+ async promoteCommunityMembers(communityJid, participants) {
4200
+ return this.communityParticipantsOperation(communityJid, participants, "promote");
4201
+ }
4202
+ /** Demote community admins to members. */
4203
+ async demoteCommunityMembers(communityJid, participants) {
4204
+ return this.communityParticipantsOperation(communityJid, participants, "demote");
4205
+ }
4206
+ /** Get the community invite link. */
4207
+ async getCommunityInviteLink(communityJid) {
4208
+ try {
4209
+ if (!this.socket) {
4210
+ throw new Error("Not connected. Call connect() first.");
4211
+ }
4212
+ if (this.connectionState !== "connected") {
4213
+ throw new Error(`Cannot get invite link. Connection state: ${this.connectionState}`);
4214
+ }
4215
+ const code = await this.socket.communityInviteCode(communityJid);
4216
+ return code ? `https://chat.whatsapp.com/${code}` : null;
4217
+ }
4218
+ catch (error) {
4219
+ this.logger.error("Failed to get community invite link:", error);
4220
+ return null;
4221
+ }
4222
+ }
4223
+ /** Revoke the community invite link and get a new one. */
4224
+ async revokeCommunityInvite(communityJid) {
4225
+ try {
4226
+ if (!this.socket) {
4227
+ throw new Error("Not connected. Call connect() first.");
4228
+ }
4229
+ if (this.connectionState !== "connected") {
4230
+ throw new Error(`Cannot revoke invite. Connection state: ${this.connectionState}`);
4231
+ }
4232
+ const code = await this.socket.communityRevokeInvite(communityJid);
4233
+ return code ? `https://chat.whatsapp.com/${code}` : null;
4234
+ }
4235
+ catch (error) {
4236
+ this.logger.error("Failed to revoke community invite:", error);
4237
+ return null;
4238
+ }
4239
+ }
4240
+ /** Accept a community invite and join. */
4241
+ async acceptCommunityInvite(inviteCode) {
4242
+ try {
4243
+ if (!this.socket) {
4244
+ throw new Error("Not connected. Call connect() first.");
4245
+ }
4246
+ if (this.connectionState !== "connected") {
4247
+ throw new Error(`Cannot accept invite. Connection state: ${this.connectionState}`);
4248
+ }
4249
+ let code = inviteCode;
4250
+ if (inviteCode.includes("chat.whatsapp.com/")) {
4251
+ code = inviteCode.split("chat.whatsapp.com/")[1];
4252
+ }
4253
+ const jid = await this.socket.communityAcceptInvite(code);
4254
+ return jid || null;
4255
+ }
4256
+ catch (error) {
4257
+ this.logger.error("Failed to accept community invite:", error);
4258
+ return null;
4259
+ }
4260
+ }
4261
+ /** Preview a community from an invite code without joining. */
4262
+ async getCommunityInviteInfo(inviteCode) {
4263
+ try {
4264
+ if (!this.socket) {
4265
+ throw new Error("Not connected. Call connect() first.");
4266
+ }
4267
+ if (this.connectionState !== "connected") {
4268
+ throw new Error(`Cannot get invite info. Connection state: ${this.connectionState}`);
4269
+ }
4270
+ let code = inviteCode;
4271
+ if (inviteCode.includes("chat.whatsapp.com/")) {
4272
+ code = inviteCode.split("chat.whatsapp.com/")[1];
4273
+ }
4274
+ const metadata = await this.socket.communityGetInviteInfo(code);
4275
+ return {
4276
+ jid: metadata.id,
4277
+ name: metadata.subject,
4278
+ description: metadata.desc || undefined,
4279
+ participantCount: metadata.size || metadata.participants?.length || 0,
4280
+ createdAt: metadata.creation,
4281
+ };
4282
+ }
4283
+ catch (error) {
4284
+ this.logger.error("Failed to get community invite info:", error);
4285
+ return null;
4286
+ }
4287
+ }
4288
+ // ============================================
3684
4289
  // Profile Management Methods (v0.8.0)
3685
4290
  // ============================================
3686
4291
  /**