From 9b096db784482776e501fb17b00e378a7e2246fe Mon Sep 17 00:00:00 2001 From: "Pedro S. Lopez" Date: Sun, 25 Oct 2020 22:44:37 -0400 Subject: [PATCH] feat: Send contacts (#395) Introduces the ability to send contact cards. You can send Contacts directly or send a vCard string and it will be automatically parsed. If you'd like to disable this autoparse functionality, you can set `parseVCards: false` as an option while sending a message. close #293 --- README.md | 2 +- index.d.ts | 4 +++- src/Client.js | 10 +++++++++- src/util/Injected.js | 37 ++++++++++++++++++++++++++++++++++++- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ec0a9e2..ec5396d 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Take a look at [example.js](https://github.com/pedroslopez/whatsapp-web.js/blob/ | Send media (video) | ✅ [(requires google chrome)](https://github.com/pedroslopez/whatsapp-web.js/issues/78#issuecomment-592723583) | | Send stickers | _pending_ | | Receive media (images/audio/video/documents) | ✅ | -| Send contact cards | _pending_ | +| Send contact cards | ✅ | | Send location | ✅ | | Receive location | ✅ | | Message replies | ✅ | diff --git a/index.d.ts b/index.d.ts index 2c59594..e96ed43 100644 --- a/index.d.ts +++ b/index.d.ts @@ -517,6 +517,8 @@ declare namespace WAWebJS { linkPreview?: boolean /** Send audio as voice message */ sendAudioAsVoice?: boolean + /** Automatically parse vCards and send them as contacts */ + parseVCards?: boolean /** Image or videos caption */ caption?: string /** Id of the message that is being quoted (or replied to) */ @@ -549,7 +551,7 @@ declare namespace WAWebJS { static fromFilePath: (filePath: string) => MessageMedia } - export type MessageContent = string | MessageMedia | Location + export type MessageContent = string | MessageMedia | Location | Contact | Contact[] /** * Represents a Contact on WhatsApp diff --git a/src/Client.js b/src/Client.js index 6eda30e..5463ce6 100644 --- a/src/Client.js +++ b/src/Client.js @@ -420,6 +420,7 @@ class Client extends EventEmitter { * @typedef {Object} MessageSendOptions * @property {boolean} [linkPreview=true] - Show links preview * @property {boolean} [sendAudioAsVoice=false] - Send audio as voice message + * @property {boolean} [parseVCards=true] - Automatically parse vCards and send them as contacts * @property {string} [caption] - Image or video caption * @property {string} [quotedMessageId] - Id of the message that is being quoted (or replied to) * @property {Contact[]} [mentions] - Contacts that are being mentioned in the message @@ -430,7 +431,7 @@ class Client extends EventEmitter { /** * Send a message to a specific chatId * @param {string} chatId - * @param {string|MessageMedia|Location} content + * @param {string|MessageMedia|Location|Contact|Array} content * @param {MessageSendOptions} [options] - Options used when sending the message * * @returns {Promise} Message that was just sent @@ -441,6 +442,7 @@ class Client extends EventEmitter { sendAudioAsVoice: options.sendAudioAsVoice, caption: options.caption, quotedMessageId: options.quotedMessageId, + parseVCards: options.parseVCards === false ? false : true, mentionedJidList: Array.isArray(options.mentions) ? options.mentions.map(contact => contact.id._serialized) : [] }; @@ -456,6 +458,12 @@ class Client extends EventEmitter { } else if (content instanceof Location) { internalOptions.location = content; content = ''; + } else if(content instanceof Contact) { + internalOptions.contactCard = content.id._serialized; + content = ''; + } else if(Array.isArray(content) && content.length > 0 && content[0] instanceof Contact) { + internalOptions.contactCardList = content.map(contact => contact.id._serialized); + content = ''; } const newMessage = await this.pupPage.evaluate(async (chatId, message, options, sendSeen) => { diff --git a/src/util/Injected.js b/src/util/Injected.js index 06dc381..81045be 100644 --- a/src/util/Injected.js +++ b/src/util/Injected.js @@ -23,6 +23,7 @@ exports.ExposeStore = (moduleRaidStr) => { window.Store.MediaUpload = window.mR.findModule('uploadMedia')[0]; window.Store.Cmd = window.mR.findModule('Cmd')[0].default; window.Store.MediaTypes = window.mR.findModule('msgToMediaType')[0]; + window.Store.VCard = window.mR.findModule('vcardFromContactModel')[0]; window.Store.UserConstructor = window.mR.findModule((module) => (module.default && module.default.prototype && module.default.prototype.isServer && module.default.prototype.isUser) ? module.default : null)[0].default; window.Store.Validators = window.mR.findModule('findLinks')[0]; window.Store.WidFactory = window.mR.findModule('createWid')[0]; @@ -78,6 +79,39 @@ exports.LoadUtils = () => { delete options.location; } + let vcardOptions = {}; + if (options.contactCard) { + let contact = window.Store.Contact.get(options.contactCard); + vcardOptions = { + body: window.Store.VCard.vcardFromContactModel(contact).vcard, + type: 'vcard', + vcardFormattedName: contact.formattedName + }; + delete options.contactCard; + } else if(options.contactCardList) { + let contacts = options.contactCardList.map(c => window.Store.Contact.get(c)); + let vcards = contacts.map(c => window.Store.VCard.vcardFromContactModel(c)); + vcardOptions = { + type: 'multi_vcard', + vcardList: vcards, + body: undefined + }; + delete options.contactCardList; + } else if(options.parseVCards && typeof(content) === 'string' && content.startsWith('BEGIN:VCARD')) { + delete options.parseVCards; + try { + const parsed = window.Store.VCard.parseVcard(content); + if(parsed) { + vcardOptions = { + type: 'vcard', + vcardFormattedName: window.Store.VCard.vcardGetNameFromParsed(parsed) + }; + } + } catch(_) { + // not a vcard + } + } + if (options.linkPreview) { delete options.linkPreview; const link = window.Store.Validators.findLink(content); @@ -109,7 +143,8 @@ exports.LoadUtils = () => { type: 'chat', ...locationOptions, ...attOptions, - ...quotedMsgOptions + ...quotedMsgOptions, + ...vcardOptions }; await window.Store.SendMessage.addAndSendMsgToChat(chat, message);