bizlin-chat 0.0.1-a1

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.
@@ -0,0 +1,54 @@
1
+ # Backend (Node/Express)
2
+
3
+ Run locally:
4
+
5
+ ```bash
6
+ cd backend
7
+ npm install
8
+ npm start
9
+ ```
10
+
11
+ API endpoints:
12
+
13
+ - `GET /api/messages` — list messages
14
+ - `POST /api/messages` — send message, body `{ text, sender? }`
15
+
16
+ Realtime: Socket.IO server on the same port (emits `message` events).
17
+
18
+ Example: http://localhost:4000/api/hello
19
+
20
+ Database (PostgreSQL)
21
+
22
+ The backend supports PostgreSQL. By default it connects to the `DATABASE_URL` env var or `postgresql://postgres:postgres@localhost:5432/bizchat`.
23
+
24
+ Quick start with Docker Compose:
25
+
26
+ ```bash
27
+ docker compose up -d
28
+ ```
29
+
30
+ This starts a Postgres instance on port 5432. Then run the backend as usual and it will auto-create the `messages` table.
31
+
32
+ WhatsApp integration
33
+
34
+ Set these environment variables for WhatsApp Cloud API integration:
35
+
36
+ - `WHATSAPP_PHONE_NUMBER_ID` — your phone number ID from the Meta App Dashboard
37
+ - `WHATSAPP_TOKEN` — system user permanent access token (keep secret)
38
+ - `WHATSAPP_VERIFY_TOKEN` — a token you set to verify webhook subscription
39
+ - `WHATSAPP_API_VERSION` — optional, defaults to `v15.0`
40
+
41
+ Endpoints:
42
+
43
+ - `GET /webhook` — verification endpoint used when subscribing to webhooks (uses `WHATSAPP_VERIFY_TOKEN`).
44
+ - `POST /webhook` — receives webhook events (incoming messages and statuses).
45
+ - `POST /api/send` — send a text message via WhatsApp Cloud API. Body: `{ "to": "<whatsapp_number>", "text": "Hello" }`.
46
+ - `POST /api/send` — send a text message via WhatsApp Cloud API. Body: `{ "to": "<whatsapp_number>", "text": "Hello" }`.
47
+ - `POST /api/sendMedia` — send media by URL. Body: `{ "to": "<whatsapp_number>", "mediaUrl": "https://...", "mediaType": "image|audio|video|document" }`.
48
+ - `POST /api/sendTemplate` — send a template message. Body: `{ "to": "<whatsapp_number>", "templateName": "template_name", "language": "en_US", "components": [...] }`.
49
+
50
+ The backend persists incoming messages to the `messages` table and emits realtime `message` events via Socket.IO.
51
+
52
+ Security
53
+ - Webhook requests are verified using `X-Hub-Signature-256` when `WHATSAPP_APP_SECRET` is set.
54
+
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "backend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "start": "node src/index.js",
8
+ "dev": "nodemon src/index.js"
9
+ },
10
+ "dependencies": {
11
+ "axios": "1.5.0",
12
+ "cors": "2.8.5",
13
+ "express": "4.18.2",
14
+ "socket.io": "4.7.2",
15
+ "uuid": "9.0.0",
16
+ "pg": "8.11.0"
17
+ }
18
+
19
+ }
@@ -0,0 +1,44 @@
1
+ const { Pool } = require('pg')
2
+
3
+ const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/bizchat'
4
+
5
+ const pool = new Pool({ connectionString: DATABASE_URL })
6
+
7
+ async function init() {
8
+ // Create messages table if it doesn't exist
9
+ const sql = `
10
+ CREATE TABLE IF NOT EXISTS messages (
11
+ id TEXT PRIMARY KEY,
12
+ text TEXT NOT NULL,
13
+ sender TEXT,
14
+ ts BIGINT NOT NULL,
15
+ wa_id TEXT,
16
+ direction TEXT,
17
+ type TEXT,
18
+ media_url TEXT,
19
+ status TEXT
20
+ );
21
+ `
22
+ await pool.query(sql)
23
+ }
24
+
25
+ async function getMessages() {
26
+ const res = await pool.query('SELECT id, text, sender, ts, wa_id, direction, type, media_url FROM messages ORDER BY ts ASC')
27
+ return res.rows
28
+ }
29
+
30
+ async function addMessage(message) {
31
+ const { id, text, sender, ts, wa_id, direction, type, media_url, status } = message
32
+ const sql = 'INSERT INTO messages(id, text, sender, ts, wa_id, direction, type, media_url, status) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9)'
33
+ await pool.query(sql, [id, text, sender, ts, wa_id || null, direction || null, type || null, media_url || null, status || null])
34
+ return message
35
+ }
36
+
37
+ async function updateMessageStatus(id, status) {
38
+ const sql = 'UPDATE messages SET status = $1 WHERE id = $2'
39
+ await pool.query(sql, [status, id])
40
+ }
41
+
42
+ module.exports = { init, getMessages, addMessage, updateMessageStatus, pool }
43
+
44
+ module.exports = { init, getMessages, addMessage, pool }
@@ -0,0 +1,176 @@
1
+ const express = require('express')
2
+ const cors = require('cors')
3
+ const http = require('http')
4
+ const { Server } = require('socket.io')
5
+ const { v4: uuidv4 } = require('uuid')
6
+ const db = require('./db')
7
+ const whatsapp = require('./whatsapp')
8
+ const crypto = require('crypto')
9
+
10
+ const app = express()
11
+ app.use(cors())
12
+ app.use(express.json())
13
+
14
+ app.get('/api/hello', (req, res) => {
15
+ res.json({ message: 'Hello from backend' })
16
+ })
17
+
18
+ app.get('/api/messages', async (req, res) => {
19
+ try {
20
+ const rows = await db.getMessages()
21
+ res.json(rows)
22
+ } catch (err) {
23
+ console.error(err)
24
+ res.status(500).json({ error: 'failed to load messages' })
25
+ }
26
+ })
27
+
28
+ app.post('/api/messages', async (req, res) => {
29
+ try {
30
+ const { text, sender } = req.body
31
+ if (!text) return res.status(400).json({ error: 'text is required' })
32
+ const message = { id: uuidv4(), text, sender: sender || 'agent', ts: Date.now() }
33
+ await db.addMessage(message)
34
+ io.emit('message', message)
35
+ res.status(201).json(message)
36
+ } catch (err) {
37
+ console.error(err)
38
+ res.status(500).json({ error: 'failed to save message' })
39
+ }
40
+ })
41
+
42
+ // Send outbound message via WhatsApp Cloud API
43
+ app.post('/api/send', async (req, res) => {
44
+ const { to, text } = req.body
45
+ if (!to || !text) return res.status(400).json({ error: 'to and text are required' })
46
+ try {
47
+ const resp = await whatsapp.sendText(to, text)
48
+ const waMessageId = resp && resp.messages && resp.messages[0] && resp.messages[0].id
49
+ const message = { id: waMessageId || uuidv4(), text, sender: 'us', ts: Date.now(), wa_id: to, direction: 'outbound', type: 'text' }
50
+ await db.addMessage(message)
51
+ io.emit('message', message)
52
+ res.status(200).json(message)
53
+ } catch (err) {
54
+ console.error('send failed', err?.response?.data || err.message || err)
55
+ res.status(500).json({ error: 'failed to send message' })
56
+ }
57
+ })
58
+
59
+ // Webhook verification endpoint (GET)
60
+ app.get('/webhook', (req, res) => {
61
+ const mode = req.query['hub.mode']
62
+ const token = req.query['hub.verify_token']
63
+ const challenge = req.query['hub.challenge']
64
+ if (mode && token) {
65
+ if (mode === 'subscribe' && token === process.env.WHATSAPP_VERIFY_TOKEN) {
66
+ console.log('WEBHOOK_VERIFIED')
67
+ res.status(200).send(challenge)
68
+ } else {
69
+ res.sendStatus(403)
70
+ }
71
+ } else {
72
+ res.sendStatus(400)
73
+ }
74
+ })
75
+
76
+ // Webhook receiver (POST)
77
+ // Use raw body parsing for webhook to allow signature verification
78
+ app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
79
+ const raw = req.body
80
+ const sig = req.headers['x-hub-signature-256']
81
+ const appSecret = process.env.WHATSAPP_APP_SECRET
82
+ if (appSecret && sig) {
83
+ const hmac = crypto.createHmac('sha256', appSecret).update(raw).digest('hex')
84
+ const expected = `sha256=${hmac}`
85
+ if (sig !== expected) {
86
+ console.warn('Invalid webhook signature')
87
+ return res.sendStatus(403)
88
+ }
89
+ }
90
+ let body
91
+ try {
92
+ body = JSON.parse(raw.toString())
93
+ } catch (err) {
94
+ console.error('invalid json', err)
95
+ return res.sendStatus(400)
96
+ }
97
+
98
+ if (body.object === 'whatsapp_business_account') {
99
+ try {
100
+ for (const entry of body.entry || []) {
101
+ for (const change of entry.changes || []) {
102
+ const value = change.value || {}
103
+ // Incoming messages
104
+ if (value.messages) {
105
+ for (const msg of value.messages) {
106
+ const wa_id = msg.from || (value.contacts && value.contacts[0] && value.contacts[0].wa_id)
107
+ const senderName = (value.contacts && value.contacts[0] && value.contacts[0].profile && value.contacts[0].profile.name) || wa_id
108
+ let text = ''
109
+ let media_url = null
110
+ if (msg.type === 'text') text = msg.text && msg.text.body
111
+ else if (['image', 'audio', 'video', 'document'].includes(msg.type)) {
112
+ const media = msg[msg.type]
113
+ // media.id is present — get downloadable url
114
+ if (media && media.id) {
115
+ try {
116
+ const url = await whatsapp.getMediaUrl(media.id)
117
+ media_url = url
118
+ } catch (e) {
119
+ console.warn('failed to fetch media url', e?.message || e)
120
+ }
121
+ }
122
+ } else if (msg.type === 'interactive') {
123
+ // buttons or list replies
124
+ if (msg.interactive && msg.interactive.type === 'button_reply') {
125
+ text = msg.interactive.button_reply && msg.interactive.button_reply.title
126
+ } else if (msg.interactive && msg.interactive.type === 'list_reply') {
127
+ text = msg.interactive.list_reply && msg.interactive.list_reply.title
128
+ }
129
+ } else if (msg.type === 'contacts') {
130
+ text = JSON.stringify(msg.contacts)
131
+ } else if (msg.type === 'location') {
132
+ text = JSON.stringify(msg.location)
133
+ }
134
+ const message = { id: msg.id || uuidv4(), text, sender: senderName, ts: Date.now(), wa_id, direction: 'inbound', type: msg.type, media_url }
135
+ await db.addMessage(message)
136
+ io.emit('message', message)
137
+ }
138
+ }
139
+ // Status updates
140
+ if (value.statuses) {
141
+ for (const st of value.statuses) {
142
+ const msgId = st.id
143
+ const status = st.status
144
+ if (msgId) await db.updateMessageStatus(msgId, status)
145
+ }
146
+ }
147
+ }
148
+ }
149
+ res.sendStatus(200)
150
+ } catch (err) {
151
+ console.error('webhook processing error', err)
152
+ res.sendStatus(500)
153
+ }
154
+ } else {
155
+ res.sendStatus(404)
156
+ }
157
+ })
158
+
159
+ const port = process.env.PORT || 4000
160
+ const server = http.createServer(app)
161
+ const io = new Server(server, { cors: { origin: '*' } })
162
+
163
+ io.on('connection', (socket) => {
164
+ console.log('socket connected', socket.id)
165
+ socket.on('disconnect', () => console.log('socket disconnected', socket.id))
166
+ })
167
+
168
+ ;(async () => {
169
+ try {
170
+ await db.init()
171
+ server.listen(port, () => console.log(`Backend listening on ${port}`))
172
+ } catch (err) {
173
+ console.error('Failed to initialize database', err)
174
+ process.exit(1)
175
+ }
176
+ })()
@@ -0,0 +1,67 @@
1
+ const axios = require('axios')
2
+
3
+ const PHONE_ID = process.env.WHATSAPP_PHONE_NUMBER_ID
4
+ const TOKEN = process.env.WHATSAPP_TOKEN
5
+ const API_VERSION = process.env.WHATSAPP_API_VERSION || 'v15.0'
6
+
7
+ if (!PHONE_ID || !TOKEN) {
8
+ // Not throwing here; some envs (local dev) may not have these set.
9
+ }
10
+
11
+ async function sendText(to, text) {
12
+ if (!PHONE_ID || !TOKEN) throw new Error('WHATSAPP_PHONE_NUMBER_ID or WHATSAPP_TOKEN not configured')
13
+ const url = `https://graph.facebook.com/${API_VERSION}/${PHONE_ID}/messages`
14
+ const body = {
15
+ messaging_product: 'whatsapp',
16
+ to,
17
+ type: 'text',
18
+ text: { body: text }
19
+ }
20
+ const res = await axios.post(url, body, {
21
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' }
22
+ })
23
+ return res.data
24
+ }
25
+
26
+ async function sendMedia(to, mediaUrl, mediaType = 'image') {
27
+ if (!PHONE_ID || !TOKEN) throw new Error('WHATSAPP_PHONE_NUMBER_ID or WHATSAPP_TOKEN not configured')
28
+ const url = `https://graph.facebook.com/${API_VERSION}/${PHONE_ID}/messages`
29
+ const body = {
30
+ messaging_product: 'whatsapp',
31
+ to,
32
+ type: mediaType,
33
+ [mediaType]: { link: mediaUrl }
34
+ }
35
+ const res = await axios.post(url, body, {
36
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' }
37
+ })
38
+ return res.data
39
+ }
40
+
41
+ async function sendTemplate(to, templateName, language = 'en_US', components = []) {
42
+ if (!PHONE_ID || !TOKEN) throw new Error('WHATSAPP_PHONE_NUMBER_ID or WHATSAPP_TOKEN not configured')
43
+ const url = `https://graph.facebook.com/${API_VERSION}/${PHONE_ID}/messages`
44
+ const body = {
45
+ messaging_product: 'whatsapp',
46
+ to,
47
+ type: 'template',
48
+ template: {
49
+ name: templateName,
50
+ language: { code: language },
51
+ components
52
+ }
53
+ }
54
+ const res = await axios.post(url, body, {
55
+ headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' }
56
+ })
57
+ return res.data
58
+ }
59
+
60
+ async function getMediaUrl(mediaId) {
61
+ if (!TOKEN) throw new Error('WHATSAPP_TOKEN not configured')
62
+ const url = `https://graph.facebook.com/${API_VERSION}/${mediaId}`
63
+ const res = await axios.get(url, { headers: { Authorization: `Bearer ${TOKEN}` } })
64
+ return res.data && res.data.url
65
+ }
66
+
67
+ module.exports = { sendText }
@@ -0,0 +1,11 @@
1
+ # Frontend (Next.js)
2
+
3
+ Run locally:
4
+
5
+ ```bash
6
+ cd frontend
7
+ npm install
8
+ npm run dev
9
+ ```
10
+
11
+ Open http://localhost:3000. The frontend connects to the backend at `http://localhost:4000` and listens for realtime `message` events from Socket.IO.
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev -p 3000",
7
+ "build": "next build",
8
+ "start": "next start -p 3000"
9
+ },
10
+ "dependencies": {
11
+ "next": "13.4.7",
12
+ "react": "18.2.0",
13
+ "react-dom": "18.2.0",
14
+ "socket.io-client": "4.7.2"
15
+ }
16
+ }
@@ -0,0 +1,98 @@
1
+ import { useEffect, useState, useRef } from 'react'
2
+ import { io } from 'socket.io-client'
3
+
4
+ export default function Home() {
5
+ const [messages, setMessages] = useState([])
6
+ const [text, setText] = useState('')
7
+ const [to, setTo] = useState('')
8
+ const [mediaUrl, setMediaUrl] = useState('')
9
+ const [templateName, setTemplateName] = useState('')
10
+ const [templateLang, setTemplateLang] = useState('en_US')
11
+ const socketRef = useRef(null)
12
+
13
+ useEffect(() => {
14
+ fetch('http://localhost:4000/api/messages')
15
+ .then((r) => r.json())
16
+ .then((d) => setMessages(d))
17
+ .catch(() => setMessages([]))
18
+
19
+ socketRef.current = io('http://localhost:4000')
20
+ socketRef.current.on('message', (msg) => {
21
+ setMessages((m) => [...m, msg])
22
+ })
23
+
24
+ return () => {
25
+ socketRef.current.disconnect()
26
+ }
27
+ }, [])
28
+
29
+ const send = async () => {
30
+ if (!text) return
31
+ // If `to` is provided, send via WhatsApp Cloud API through backend
32
+ if (to) {
33
+ await fetch('http://localhost:4000/api/send', {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json' },
36
+ body: JSON.stringify({ to, text })
37
+ })
38
+ } else {
39
+ await fetch('http://localhost:4000/api/messages', {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ text, sender: 'web' })
43
+ })
44
+ }
45
+ setText('')
46
+ }
47
+
48
+ const sendMedia = async () => {
49
+ if (!to || !mediaUrl) return
50
+ await fetch('http://localhost:4000/api/sendMedia', {
51
+ method: 'POST',
52
+ headers: { 'Content-Type': 'application/json' },
53
+ body: JSON.stringify({ to, mediaUrl, mediaType: 'image' })
54
+ })
55
+ setMediaUrl('')
56
+ }
57
+
58
+ const sendTemplate = async () => {
59
+ if (!to || !templateName) return
60
+ await fetch('http://localhost:4000/api/sendTemplate', {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ to, templateName, language: templateLang, components: [] })
64
+ })
65
+ setTemplateName('')
66
+ }
67
+
68
+ return (
69
+ <main style={{ fontFamily: 'system-ui, sans-serif', padding: 24 }}>
70
+ <h1>WhatsApp Team Inbox (Frontend)</h1>
71
+ <div style={{ maxWidth: 720 }}>
72
+ <div style={{ border: '1px solid #ddd', padding: 12, minHeight: 200 }}>
73
+ {messages.map((m) => (
74
+ <div key={m.id} style={{ padding: 8, borderBottom: '1px solid #eee' }}>
75
+ <div style={{ fontSize: 12, color: '#666' }}>{m.sender} • {new Date(m.ts).toLocaleTimeString()}</div>
76
+ <div>{m.text}</div>
77
+ </div>
78
+ ))}
79
+ </div>
80
+
81
+ <div style={{ display: 'flex', marginTop: 12, gap: 8 }}>
82
+ <input placeholder="To (WhatsApp number, optional)" value={to} onChange={(e) => setTo(e.target.value)} style={{ width: 220, padding: 8 }} />
83
+ <input value={text} onChange={(e) => setText(e.target.value)} style={{ flex: 1, padding: 8 }} />
84
+ <button onClick={send} style={{ padding: '8px 12px' }}>Send</button>
85
+ </div>
86
+ <div style={{ display: 'flex', marginTop: 8, gap: 8 }}>
87
+ <input placeholder="Media URL (optional)" value={mediaUrl} onChange={(e) => setMediaUrl(e.target.value)} style={{ flex: 1, padding: 8 }} />
88
+ <button onClick={sendMedia} style={{ padding: '8px 12px' }}>Send Media</button>
89
+ </div>
90
+ <div style={{ display: 'flex', marginTop: 8, gap: 8 }}>
91
+ <input placeholder="Template name" value={templateName} onChange={(e) => setTemplateName(e.target.value)} style={{ width: 240, padding: 8 }} />
92
+ <input placeholder="lang" value={templateLang} onChange={(e) => setTemplateLang(e.target.value)} style={{ width: 100, padding: 8 }} />
93
+ <button onClick={sendTemplate} style={{ padding: '8px 12px' }}>Send Template</button>
94
+ </div>
95
+ </div>
96
+ </main>
97
+ )
98
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "bizlin-chat",
3
+ "version": "0.0.1-a1",
4
+ "description": "zchat npm plugin (integrates backend and frontend)",
5
+ "main": "backend/index.js",
6
+ "files": [
7
+ "backend",
8
+ "frontend"
9
+ ],
10
+ "scripts": {
11
+ "start": "node backend/index.js",
12
+ "start:frontend": "node frontend/index.js",
13
+ "test": "echo \"No tests specified\" && exit 0"
14
+ },
15
+ "keywords": [
16
+ "zchat",
17
+ "plugin",
18
+ "chat"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/bizlinsolutions/zchat.git"
26
+ },
27
+ "author": "",
28
+ "license": "MIT",
29
+ "engines": {
30
+ "node": ">=14"
31
+ }
32
+ }