From 315dbac1753c67c0c5622c3aee4caeedc320c969 Mon Sep 17 00:00:00 2001 From: canove Date: Sun, 4 Oct 2020 14:05:32 -0300 Subject: [PATCH] improvement: moved vcard parse logic to backend --- ...150008-add-contactId-column-to-messages.ts | 16 +++++ ...9-add-vcardContactId-column-to-messages.ts | 16 +++++ backend/src/helpers/ParseVcardToJson.ts | 59 ++++++++++++++++ backend/src/models/Message.ts | 15 ++++ .../MessageServices/ListMessagesService.ts | 1 + .../WbotServices/wbotMessageListener.ts | 36 +++++++--- backend/yarn-error.log | 30 +++++++- frontend/src/components/Ticket/index.js | 70 ++++--------------- 8 files changed, 175 insertions(+), 68 deletions(-) create mode 100644 backend/src/database/migrations/20201004150008-add-contactId-column-to-messages.ts create mode 100644 backend/src/database/migrations/20201004155719-add-vcardContactId-column-to-messages.ts create mode 100644 backend/src/helpers/ParseVcardToJson.ts diff --git a/backend/src/database/migrations/20201004150008-add-contactId-column-to-messages.ts b/backend/src/database/migrations/20201004150008-add-contactId-column-to-messages.ts new file mode 100644 index 0000000..4b8f111 --- /dev/null +++ b/backend/src/database/migrations/20201004150008-add-contactId-column-to-messages.ts @@ -0,0 +1,16 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Messages", "contactId", { + type: DataTypes.INTEGER, + references: { model: "Contacts", key: "id" }, + onUpdate: "CASCADE", + onDelete: "CASCADE" + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Messages", "contactId"); + } +}; diff --git a/backend/src/database/migrations/20201004155719-add-vcardContactId-column-to-messages.ts b/backend/src/database/migrations/20201004155719-add-vcardContactId-column-to-messages.ts new file mode 100644 index 0000000..d897363 --- /dev/null +++ b/backend/src/database/migrations/20201004155719-add-vcardContactId-column-to-messages.ts @@ -0,0 +1,16 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Messages", "vcardContactId", { + type: DataTypes.INTEGER, + references: { model: "Contacts", key: "id" }, + onUpdate: "CASCADE", + onDelete: "CASCADE" + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Messages", "vcardContactId"); + } +}; diff --git a/backend/src/helpers/ParseVcardToJson.ts b/backend/src/helpers/ParseVcardToJson.ts new file mode 100644 index 0000000..4045334 --- /dev/null +++ b/backend/src/helpers/ParseVcardToJson.ts @@ -0,0 +1,59 @@ +interface JCard { + [key: string]: any; +} + +interface Meta { + [key: string]: string; +} + +const ParseVcardToJson = (input: string): JCard => { + const Re1 = /^(version|fn|title|org):(.+)$/i; + const Re2 = /^([^:;]+);([^:]+):(.+)$/; + const ReKey = /item\d{1,2}\./; + const fields = {} as JCard; + + input.split(/\r\n|\r|\n/).forEach(line => { + let results; + let key; + + if (Re1.test(line)) { + results = line.match(Re1); + if (results) { + key = results[1].toLowerCase(); + const [, , res] = results; + fields[key] = res; + } + } else if (Re2.test(line)) { + results = line.match(Re2); + if (results) { + key = results[1].replace(ReKey, "").toLowerCase(); + + const meta = {} as Meta; + results[2] + .split(";") + .map((p, i) => { + const match = p.match(/([a-z]+)=(.*)/i); + if (match) { + return [match[1], match[2]]; + } + return [`TYPE${i === 0 ? "" : i}`, p]; + }) + .forEach(p => { + const [, m] = p; + meta[p[0]] = m; + }); + + if (!fields[key]) fields[key] = []; + + fields[key].push({ + meta, + value: results[3].split(";") + }); + } + } + }); + + return fields; +}; + +export default ParseVcardToJson; diff --git a/backend/src/models/Message.ts b/backend/src/models/Message.ts index bc2b03c..abcd04f 100644 --- a/backend/src/models/Message.ts +++ b/backend/src/models/Message.ts @@ -10,6 +10,7 @@ import { BelongsTo, ForeignKey } from "sequelize-typescript"; +import Contact from "./Contact"; import Ticket from "./Ticket"; @Table @@ -64,6 +65,20 @@ class Message extends Model { @BelongsTo(() => Ticket) ticket: Ticket; + + @ForeignKey(() => Contact) + @Column + contactId: number; + + @BelongsTo(() => Contact, "contactId") + contact: Contact; + + @ForeignKey(() => Contact) + @Column + vcardContactId: number; + + @BelongsTo(() => Contact, "vcardContactId") + vcardContact: Contact; } export default Message; diff --git a/backend/src/services/MessageServices/ListMessagesService.ts b/backend/src/services/MessageServices/ListMessagesService.ts index 4d5f694..362cc5a 100644 --- a/backend/src/services/MessageServices/ListMessagesService.ts +++ b/backend/src/services/MessageServices/ListMessagesService.ts @@ -44,6 +44,7 @@ const ListMessagesService = async ({ const { count, rows: messages } = await Message.findAndCountAll({ where: whereCondition, limit, + include: ["contact", "vcardContact"], offset, order: [["createdAt", "DESC"]] }); diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 2dbfbda..a55e913 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -19,6 +19,7 @@ import { getIO } from "../../libs/socket"; import { getWbot } from "../../libs/wbot"; import AppError from "../../errors/AppError"; import ShowTicketService from "../TicketServices/ShowTicketService"; +import ParseVcardToJson from "../../helpers/ParseVcardToJson"; const writeFileAsync = promisify(writeFile); @@ -161,9 +162,8 @@ const handlMedia = async ( const newMessage: Message = await ticket.$create("message", { id: msg.id.id, - body: msg.fromMe - ? `${msg.body ? msg.body : media.filename}` - : `${contact.name}: ${msg.body ? msg.body : media.filename}`, + contactId: msg.fromMe ? null : contact.id, + body: msg.body || media.filename, fromMe: msg.fromMe, mediaUrl: media.filename, mediaType: media.mimetype.split("/")[0] @@ -175,21 +175,28 @@ const handlMedia = async ( const handleMessage = async ( msg: WbotMessage, ticket: Ticket, - contact: Contact + contact: Contact, + vcardContact?: Contact ) => { - let newMessage: Message; + let newMessage: Message | null; if (msg.hasMedia) { newMessage = await handlMedia(msg, ticket, contact); } else { - newMessage = await ticket.$create("message", { + const { id } = await ticket.$create("message", { id: msg.id.id, - body: msg.fromMe ? msg.body : `${contact.name}: ${msg.body}`, + contactId: msg.fromMe ? null : contact.id, + vcardContactId: vcardContact ? vcardContact.id : null, + body: msg.body, fromMe: msg.fromMe, mediaType: msg.type, read: msg.fromMe }); await ticket.update({ lastMessage: msg.body }); + + newMessage = await Message.findByPk(id, { + include: ["contact", "vcardContact"] + }); } const io = getIO(); @@ -234,13 +241,15 @@ const wbotMessageListener = (whatsapp: Whatsapp): void => { try { let msgContact: WbotContact; let groupContact: Contact | undefined; + let vcardContact: Contact | undefined; if (msg.fromMe) { msgContact = await wbot.getContactById(msg.to); // return if it's a media message, it will be handled by media_uploaded event - if (msg.hasMedia || msg.type !== "chat") return; + if (msg.hasMedia || (msg.type !== "chat" && msg.type !== "vcard")) + return; } else { msgContact = await msg.getContact(); } @@ -252,11 +261,20 @@ const wbotMessageListener = (whatsapp: Whatsapp): void => { groupContact = await verifyGroup(msgGroupContact); } + if (msg.type === "vcard") { + const { tel } = ParseVcardToJson(msg.body); + const vcardWaid = tel[0]?.meta?.waid; + const vcardMsgContact = await wbot.getContactById(`${vcardWaid}@c.us`); + const profilePicUrl = await vcardMsgContact.getProfilePicUrl(); + + vcardContact = await verifyContact(vcardMsgContact, profilePicUrl); + } + const profilePicUrl = await msgContact.getProfilePicUrl(); const contact = await verifyContact(msgContact, profilePicUrl); const ticket = await verifyTicket(contact, whatsappId, groupContact); - await handleMessage(msg, ticket, contact); + await handleMessage(msg, ticket, contact, vcardContact); } catch (err) { Sentry.captureException(err); console.log(err); diff --git a/backend/yarn-error.log b/backend/yarn-error.log index ff7a8d9..6754bc8 100644 --- a/backend/yarn-error.log +++ b/backend/yarn-error.log @@ -1,8 +1,8 @@ Arguments: - /usr/bin/node /usr/share/yarn/bin/yarn.js add -D @types/qrcode-terminal + /usr/bin/node /usr/share/yarn/bin/yarn.js add -D @types/vdata-parser PATH: - /home/canove/.vscode-server/bin/58bb7b2331731bf72587010e943852e13e6fd3cf/bin:/home/canove/.zinit/polaris/sbin:/home/canove/.zinit/polaris/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/mnt/c/Windows/system32:/mnt/c/Windows:/mnt/c/Windows/System32/Wbem:/mnt/c/Windows/System32/WindowsPowerShell/v1.0/:/mnt/c/Windows/System32/OpenSSH/:/mnt/c/ProgramData/chocolatey/bin:/mnt/c/Program Files/Microsoft VS Code/bin:/mnt/c/Program Files (x86)/GNU/GnuPG/pub:/mnt/c/Users/cassio/AppData/Local/Microsoft/WindowsApps:/mnt/c/Users/cassio/AppData/Local/GitHubDesktop/bin + /home/canove/.vscode-server/bin/e5e9e69aed6e1984f7499b7af85b3d05f9a6883a/bin:/home/canove/.zinit/polaris/sbin:/home/canove/.zinit/polaris/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/mnt/c/Windows/system32:/mnt/c/Windows:/mnt/c/Windows/System32/Wbem:/mnt/c/Windows/System32/WindowsPowerShell/v1.0/:/mnt/c/Windows/System32/OpenSSH/:/mnt/c/ProgramData/chocolatey/bin:/mnt/c/Program Files/Microsoft VS Code/bin:/mnt/c/Program Files (x86)/GNU/GnuPG/pub:/mnt/c/Program Files/Calibre2/:/mnt/c/Users/cassio/AppData/Local/Microsoft/WindowsApps:/mnt/c/Users/cassio/AppData/Local/GitHubDesktop/bin Yarn version: 1.22.4 @@ -14,7 +14,7 @@ Platform: linux x64 Trace: - Error: https://registry.yarnpkg.com/@types%2fqrcode-terminal: Not found + Error: https://registry.yarnpkg.com/@types%2fvdata-parser: Not found at Request.params.callback [as _callback] (/usr/share/yarn/lib/cli.js:66987:18) at Request.self.callback (/usr/share/yarn/lib/cli.js:140748:22) at Request.emit (events.js:315:20) @@ -34,6 +34,7 @@ npm manifest: "main": "index.js", "scripts": { "build": "tsc", + "watch": "tsc -w", "start": "nodemon dist/server.js", "dev:server": "ts-node-dev --respawn --transpile-only --ignore node_modules src/server.ts" }, @@ -42,6 +43,7 @@ npm manifest: "dependencies": { "@sentry/node": "5.22.3", "bcryptjs": "^2.4.3", + "cookie-parser": "^1.4.5", "cors": "^2.8.5", "date-fns": "^2.16.1", "dotenv": "^8.2.0", @@ -56,12 +58,14 @@ npm manifest: "sequelize-cli": "5", "sequelize-typescript": "^1.1.0", "socket.io": "^2.3.0", + "vdata-parser": "^0.1.5", "whatsapp-web.js": "^1.8.2", "yup": "^0.29.3" }, "devDependencies": { "@types/bcryptjs": "^2.4.2", "@types/bluebird": "^3.5.32", + "@types/cookie-parser": "^1.4.2", "@types/cors": "^2.8.7", "@types/express": "^4.17.8", "@types/jsonwebtoken": "^8.5.0", @@ -273,6 +277,13 @@ Lockfile: dependencies: "@types/node" "*" + "@types/cookie-parser@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.2.tgz#e4d5c5ffda82b80672a88a4281aaceefb1bd9df5" + integrity sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg== + dependencies: + "@types/express" "*" + "@types/cors@^2.8.7": version "2.8.7" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.7.tgz#ab2f47f1cba93bce27dfd3639b006cc0e5600889" @@ -1030,6 +1041,14 @@ Lockfile: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + cookie-parser@^1.4.5: + version "1.4.5" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.5.tgz#3e572d4b7c0c80f9c61daf604e4336831b5d1d49" + integrity sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw== + dependencies: + cookie "0.4.0" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -4149,6 +4168,11 @@ Lockfile: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + vdata-parser@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/vdata-parser/-/vdata-parser-0.1.5.tgz#e57cf30065cf69d29a9d822e85fd2734c686bcfd" + integrity sha1-5XzzAGXPadKanYIuhf0nNMaGvP0= + whatsapp-web.js@^1.8.2: version "1.8.2" resolved "https://registry.yarnpkg.com/whatsapp-web.js/-/whatsapp-web.js-1.8.2.tgz#b9059ca7cb70d6297c3cba954ccf1ce0c004d52e" diff --git a/frontend/src/components/Ticket/index.js b/frontend/src/components/Ticket/index.js index 7222593..a6eacfa 100644 --- a/frontend/src/components/Ticket/index.js +++ b/frontend/src/components/Ticket/index.js @@ -20,10 +20,8 @@ import { Button, Card, CardActions, - CardContent, CardHeader, IconButton, - Typography, } from "@material-ui/core"; import { Block, ExpandMore } from "@material-ui/icons"; @@ -233,8 +231,10 @@ const useStyles = makeStyles(theme => ({ }, vcard: { - // display: "flex", + display: "flex", + backgroundColor: "inherit", marginBottom: 10, + marginRight: 10, }, })); @@ -416,50 +416,6 @@ const Ticket = () => { } }; - const parseVcard = vcard => { - var Re1 = /^(version|fn|title|org):(.+)$/i; - var Re2 = /^([^:;]+);([^:]+):(.+)$/; - var ReKey = /item\d{1,2}\./; - var fields = {}; - - vcard.split(/\r\n|\r|\n/).forEach(function (line) { - var results, key; - - if (Re1.test(line)) { - results = line.match(Re1); - key = results[1].toLowerCase(); - fields[key] = results[2]; - } else if (Re2.test(line)) { - results = line.match(Re2); - key = results[1].replace(ReKey, "").toLowerCase(); - - var meta = {}; - results[2] - .split(";") - .map(function (p, i) { - var match = p.match(/([a-z]+)=(.*)/i); - if (match) { - return [match[1], match[2]]; - } else { - return ["TYPE" + (i === 0 ? "" : i), p]; - } - }) - .forEach(function (p) { - meta[p[0]] = p[1]; - }); - - if (!fields[key]) fields[key] = []; - - fields[key].push({ - meta: meta, - value: results[3].split(";"), - }); - } - }); - - return fields; - }; - const checkMessageMedia = message => { if (message.mediaType === "image") { return ( @@ -490,20 +446,22 @@ const Ticket = () => { ); } if (message.mediaType === "vcard") { - const contactVcard = parseVcard(message.body); - - console.log(contactVcard); - return ( } - // action={} - title={contactVcard.fn} - subheader={contactVcard.tel[0].meta.waid} + avatar={ + + } + title={message?.vcardContact?.name} + subheader={message?.vcardContact?.number} /> - + );