diff --git a/example.js b/example.js index bc73853..a674424 100644 --- a/example.js +++ b/example.js @@ -1,4 +1,4 @@ -const { Client } = require('./src') +const { Client } = require('./index') const client = new Client({puppeteer: {headless: false}}); @@ -16,16 +16,60 @@ client.on('ready', () => { console.log('READY'); }); -client.on('message', (msg) => { +client.on('message', async msg => { console.log('MESSAGE RECEIVED', msg); - if (msg.body == 'ping reply') { + if (msg.body == '!ping reply') { // Send a new message as a reply to the current one msg.reply('pong'); - } else if (msg.body == 'ping') { + } else if (msg.body == '!ping') { // Send a new message to the same chat client.sendMessage(msg.from, 'pong'); + + } else if (msg.body.startsWith('!subject ')) { + // Change the group subject + let chat = await msg.getChat(); + if(chat.isGroup) { + let newSubject = msg.body.slice(9); + chat.setSubject(newSubject); + } else { + msg.reply('This command can only be used in a group!'); + } + } else if (msg.body.startsWith('!echo ')) { + // Replies with the same message + msg.reply(msg.body.slice(6)); + } else if (msg.body.startsWith('!desc ')) { + // Change the group description + let chat = await msg.getChat(); + if(chat.isGroup) { + let newDescription = msg.body.slice(6); + chat.setDescription(newDescription); + } else { + msg.reply('This command can only be used in a group!'); + } + } else if (msg.body == '!leave') { + // Leave the group + let chat = await msg.getChat(); + if(chat.isGroup) { + chat.leave(); + } else { + msg.reply('This command can only be used in a group!'); + } + } else if(msg.body == '!groupinfo') { + let chat = await msg.getChat(); + if(chat.isGroup) { + msg.reply(` + *Group Details* + Name: ${chat.name} + Description: ${chat.description} + Created At: ${chat.createdAt.toString()} + Created By: ${chat.owner.user} + Participant count: ${chat.participants.length} + `); + } else { + msg.reply('This command can only be used in a group!'); + } } }); diff --git a/index.js b/index.js new file mode 100644 index 0000000..aa31ec8 --- /dev/null +++ b/index.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = { + Client: require('./src/Client'), + + version: require('./package.json').version, + + // Structures + Chat: require('./src/structures/Chat'), + PrivateChat: require('./src/structures/PrivateChat'), + GroupChat: require('./src/structures/GroupChat'), + Message: require('./src/structures/Message') +}; \ No newline at end of file diff --git a/package.json b/package.json index 904e409..46d62f3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "whatsapp-web.js", "version": "0.0.1", "description": "Library for interacting with the WhatsApp Web API ", - "main": "./src/index.js", + "main": "./index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/src/client/Client.js b/src/Client.js similarity index 69% rename from src/client/Client.js rename to src/Client.js index 9512f13..319d11e 100644 --- a/src/client/Client.js +++ b/src/Client.js @@ -2,11 +2,12 @@ const EventEmitter = require('events'); const puppeteer = require('puppeteer'); -const Util = require('../util/Util'); -const { WhatsWebURL, UserAgent, DefaultOptions, Events } = require('../util/Constants'); -const { ExposeStore, LoadExtraProps } = require('../util/Injected'); -const Chat = require('../models/Chat'); -const Message = require('../models/Message') +const Util = require('./util/Util'); +const { WhatsWebURL, UserAgent, DefaultOptions, Events } = require('./util/Constants'); +const { ExposeStore, LoadExtraProps, LoadCustomSerializers } = require('./util/Injected'); +const ChatFactory = require('./factories/ChatFactory'); +const Chat = require('./structures/Chat'); +const Message = require('./structures/Message') /** * Starting point for interacting with the WhatsApp Web API @@ -22,6 +23,9 @@ class Client extends EventEmitter { this.pupPage = null; } + /** + * Sets up events and requirements, kicks off authentication request + */ async initialize() { const browser = await puppeteer.launch(this.options.puppeteer); const page = await browser.newPage(); @@ -43,12 +47,14 @@ class Client extends EventEmitter { // Check Store Injection await page.waitForFunction('window.Store != undefined'); - // Load extra serialized props - const models = [Chat, Message]; + //Load extra serialized props + const models = [Message]; for (let model of models) { await page.evaluate(LoadExtraProps, model.WAppModel, model.extraFields); } + await page.evaluate(LoadCustomSerializers); + // Register events await page.exposeFunction('onAddMessageEvent', msg => { if (msg.id.fromMe || !msg.isNewMsg) return; @@ -77,26 +83,39 @@ class Client extends EventEmitter { await this.pupBrowser.close(); } + /** + * Send a message to a specific chatId + * @param {string} chatId + * @param {string} message + */ async sendMessage(chatId, message) { await this.pupPage.evaluate((chatId, message) => { Store.Chat.get(chatId).sendMessage(message); }, chatId, message) } + /** + * Get all current chat instances + */ async getChats() { - let chats = await this.pupPage.evaluate(() => { - return Store.Chat.serialize() - }); + // let chats = await this.pupPage.evaluate(() => { + // return Store.Chat.serialize() + // }); - return chats.map(chatData => new Chat(this, chatData)); + // return chats.map(chatData => ChatFactory.create(this, chatData)); + throw new Error('NOT IMPLEMENTED') } + /** + * Get chat instance by ID + * @param {string} chatId + */ async getChatById(chatId) { let chat = await this.pupPage.evaluate(chatId => { - return Store.Chat.get(chatId).serialize(); + return WWebJS.getChat(chatId); }, chatId); - return new Chat(this, chat); + return ChatFactory.create(this, chat); } diff --git a/src/factories/ChatFactory.js b/src/factories/ChatFactory.js new file mode 100644 index 0000000..524aea1 --- /dev/null +++ b/src/factories/ChatFactory.js @@ -0,0 +1,16 @@ +'use strict'; + +const PrivateChat = require('../structures/PrivateChat'); +const GroupChat = require('../structures/GroupChat'); + +class ChatFactory { + static create(client, data) { + if(data.isGroup) { + return new GroupChat(client, data); + } + + return new PrivateChat(client, data); + } +} + +module.exports = ChatFactory; \ No newline at end of file diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 0aa2c7b..0000000 --- a/src/index.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -module.exports = { - Client: require('./client/Client'), - version: require('../package.json').version, - - // Models - Chat: require('./models/Chat'), - Message: require('./models/Message') -}; \ No newline at end of file diff --git a/src/models/Base.js b/src/structures/Base.js similarity index 94% rename from src/models/Base.js rename to src/structures/Base.js index 4ad9f9c..4fb4bdd 100644 --- a/src/models/Base.js +++ b/src/structures/Base.js @@ -1,7 +1,7 @@ 'use strict'; /** - * Represents a data model + * Represents a WhatsApp data structure */ class Base { constructor(client) { diff --git a/src/models/Chat.js b/src/structures/Chat.js similarity index 79% rename from src/models/Chat.js rename to src/structures/Chat.js index 53aa888..ec5f430 100644 --- a/src/models/Chat.js +++ b/src/structures/Chat.js @@ -16,23 +16,18 @@ class Chat extends Base { _patch(data) { this.id = data.id; + this.name = data.formattedTitle; this.isGroup = data.isGroup; this.isReadOnly = data.isReadOnly; - this.name = data.name; this.unreadCount = data.unreadCount; this.timestamp = data.t; + + return super._patch(data); } sendMessage(message) { return this.client.sendMessage(this.id._serialized, message); } - - static get extraFields() { - return [ - 'formattedTitle', - 'isGroup' - ]; - } } module.exports = Chat; \ No newline at end of file diff --git a/src/structures/GroupChat.js b/src/structures/GroupChat.js new file mode 100644 index 0000000..99ce486 --- /dev/null +++ b/src/structures/GroupChat.js @@ -0,0 +1,166 @@ +'use strict'; + +const Chat = require('./Chat'); + +/** + * Represents a Group Chat on WhatsApp + * @extends {Chat} + */ +class GroupChat extends Chat { + _patch(data) { + this.groupMetadata = data.groupMetadata; + + return super._patch(data); + } + + /** + * Gets the group owner + */ + get owner() { + return this.groupMetadata.owner; + } + + /** + * Gets the date at which the group was created + */ + get createdAt() { + return new Date(this.groupMetadata.creation * 1000); + } + + /** + * Gets the group description + */ + get description() { + return this.groupMetadata.desc; + } + /** + * Gets the group participants + */ + get participants() { + return this.groupMetadata.participants; + } + + /** + * Adds a list of participants by ID to the group + * @param {Array[string]} participantIds + */ + async addParticipants(participantIds) { + return await this.client.pupPage.evaluate((chatId, participantIds) => { + return Store.Wap.addParticipants(chatId, participantIds); + }, this.id._serialized, participantIds); + } + + /** + * Removes a list of participants by ID to the group + * @param {Array[string]} participantIds + */ + async removeParticipants(participantIds) { + return await this.client.pupPage.evaluate((chatId, participantIds) => { + return Store.Wap.removeParticipants(chatId, participantIds); + }, this.id._serialized, participantIds); + } + + /** + * Promotes participants by IDs to admins + * @param {Array[string]} participantIds + */ + async promoteParticipants(participantIds) { + return await this.client.pupPage.evaluate((chatId, participantIds) => { + return Store.Wap.promoteParticipants(chatId, participantIds); + }, this.id._serialized, participantIds); + } + + /** + * Demotes participants by IDs to regular users + * @param {Array[string]} participantIds + */ + async demoteParticipants(participantIds) { + return await this.client.pupPage.evaluate((chatId, participantIds) => { + return Store.Wap.demoteParticipants(chatId, participantIds); + }, this.id._serialized, participantIds); + } + + /** + * Updates the group subject + * @param {string} subject + */ + async setSubject(subject) { + let res = await this.client.pupPage.evaluate((chatId, subject) => { + return Store.Chat.get(chatId).setSubject(subject); + }, this.id._serialized, subject); + + if(res.status == 200) { + this.name = subject; + } + } + + /** + * Updates the group description + * @param {string} description + */ + async setDescription(description) { + let res = await this.client.pupPage.evaluate((chatId, description) => { + return Store.Chat.get(chatId).setGroupDesc(description); + }, this.id._serialized, description); + + if (res.status == 200) { + this.groupMetadata.desc = description; + } + } + + /** + * Gets the invite code for a specific group + */ + async getInviteCode() { + let res = await this.client.pupPage.evaluate(chatId => { + return Store.Wap.groupInviteCode(chatId); + }, this.id._serialized); + + if (res.status == 200) { + return res.code; + } + + throw new Error('Not authorized') + } + + /** + * Invalidates the current group invite code and generates a new one + */ + async revokeInvite() { + return await this.client.pupPage.evaluate(chatId => { + return Store.Wap.revokeGroupInvite(chatId); + }, chatId); + } + + /** + * Returns an object with information about the invite code's group + * @param {string} inviteCode + */ + static async getInviteInfo(inviteCode) { + return await this.client.pupPage.evaluate(inviteCode => { + return Store.Wap.groupInviteInfo(inviteCode); + }, inviteCode); + } + + /** + * Joins a group with an invite code + * @param {string} inviteCode + */ + static async join(inviteCode) { + return await this.client.pupPage.evaluate(inviteCode => { + return Store.Wap.acceptGroupInvite(inviteCode); + }, inviteCode); + } + + /** + * Makes the bot leave the group + */ + async leave() { + return await this.client.pupPage.evaluate(chatId => { + return Store.Wap.leaveGroup(chatId); + }, this.id._serialized); + } + +} + +module.exports = GroupChat; \ No newline at end of file diff --git a/src/models/Message.js b/src/structures/Message.js similarity index 98% rename from src/models/Message.js rename to src/structures/Message.js index 3a95751..a7c0fd1 100644 --- a/src/models/Message.js +++ b/src/structures/Message.js @@ -23,6 +23,8 @@ class Message extends Base { this.author = data.author; this.isForwarded = data.isForwarded; this.broadcast = data.broadcast; + + return super._patch(data); } /** diff --git a/src/structures/PrivateChat.js b/src/structures/PrivateChat.js new file mode 100644 index 0000000..9296f86 --- /dev/null +++ b/src/structures/PrivateChat.js @@ -0,0 +1,13 @@ +'use strict'; + +const Chat = require('./Chat'); + +/** + * Represents a Private Chat on WhatsApp + * @extends {Chat} + */ +class PrivateChat extends Chat { + +} + +module.exports = PrivateChat; \ No newline at end of file diff --git a/src/util/Injected.js b/src/util/Injected.js index c52ef38..079f72e 100644 --- a/src/util/Injected.js +++ b/src/util/Injected.js @@ -56,6 +56,27 @@ exports.LoadExtraProps = (model, props) => { Store[model].models[0].__props = Store[model].models[0].__props.concat(props); } +exports.LoadCustomSerializers = () => { + window.WWebJS = {}; + + window.WWebJS.getChatModel = chat => { + let res = chat.serialize(); + res.isGroup = chat.isGroup; + res.formattedTitle = chat.formattedTitle; + + if(chat.groupMetadata) { + res.groupMetadata = chat.groupMetadata.serialize(); + } + + return res; + } + + window.WWebJS.getChat = chatId => { + const chat = Store.Chat.get(chatId); + return WWebJS.getChatModel(chat); + } +} + exports.MarkAllRead = () => { let Chats = Store.Chat.models;