Skip to content

Commit

Permalink
add webhook blacklist
Browse files Browse the repository at this point in the history
  • Loading branch information
clairton committed Jun 15, 2024
1 parent 56b640b commit 46a2769
Show file tree
Hide file tree
Showing 25 changed files with 307 additions and 39 deletions.
27 changes: 27 additions & 0 deletions __tests__/routes/blacklist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import request from 'supertest'
import { mock } from 'jest-mock-extended'
import { App } from '../../src/app'
import { Incoming } from '../../src/services/incoming'
import { Outgoing } from '../../src/services/outgoing'
import { defaultConfig, getConfig } from '../../src/services/config'
import { SessionStore } from '../../src/services/session_store'
import { OnNewLogin } from '../../src/services/socket'

const addToBlacklist = jest.fn().mockReturnValue(Promise.resolve(true))

const sessionStore = mock<SessionStore>()
const getConfigTest: getConfig = async (_phone: string) => {
return defaultConfig
}

describe('blacklist routes', () => {
test('update', async () => {
const incoming = mock<Incoming>()
const outgoing = mock<Outgoing>()
const onNewLogin = mock<OnNewLogin>()
const app: App = new App(incoming, outgoing, '', getConfigTest, sessionStore, onNewLogin, addToBlacklist)
const res = await request(app.server).post('/2/blacklist/1').send({ttl: 1, to: '3'})
expect(addToBlacklist).toHaveBeenCalledWith('2', '1', '3', 1);
expect(res.status).toEqual(200)
})
})
4 changes: 3 additions & 1 deletion __tests__/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { getConfig } from '../../src/services/config'
import { Outgoing } from '../../src/services/outgoing'
import { SessionStore } from '../../src/services/session_store'
import { OnNewLogin } from '../../src/services/socket'
import { addToBlacklist } from '../../src/services/blacklist'
const addToBlacklist = mock<addToBlacklist>()
const sessionStore = mock<SessionStore>()

describe('index routes', () => {
Expand All @@ -15,7 +17,7 @@ describe('index routes', () => {
const outgoing = mock<Outgoing>()
const getConfig = mock<getConfig>()
const onNewLogin = mock<OnNewLogin>()
const app: App = new App(incoming, outgoing, '', getConfig, sessionStore, onNewLogin)
const app: App = new App(incoming, outgoing, '', getConfig, sessionStore, onNewLogin, addToBlacklist)
const res = await request(app.server).get('/ping')
expect(res.text).toEqual('pong!')
})
Expand Down
4 changes: 3 additions & 1 deletion __tests__/routes/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { MediaStore } from '../../src/services/media_store'
import { getStore, Store } from '../../src/services/store'
import { SessionStore } from '../../src/services/session_store'
import { OnNewLogin } from '../../src/services/socket'
import { addToBlacklist } from '../../src/services/blacklist'
const addToBlacklist = mock<addToBlacklist>()

const sessionStore = mock<SessionStore>()

Expand Down Expand Up @@ -44,7 +46,7 @@ describe('media routes', () => {
incoming = mock<Incoming>()
outgoing = mock<Outgoing>()
const onNewLogin = mock<OnNewLogin>()
app = new App(incoming, outgoing, url, getConfigTest, sessionStore, onNewLogin)
app = new App(incoming, outgoing, url, getConfigTest, sessionStore, onNewLogin, addToBlacklist)
})

test('index', async () => {
Expand Down
4 changes: 3 additions & 1 deletion __tests__/routes/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Response } from '../../src/services/response'
import { getStore, Store } from '../../src/services/store'
import { SessionStore } from '../../src/services/session_store'
import { OnNewLogin } from '../../src/services/socket'
import { addToBlacklist } from '../../src/services/blacklist'
const addToBlacklist = mock<addToBlacklist>()

const sessionStore = mock<SessionStore>()
const store = mock<Store>()
Expand Down Expand Up @@ -36,7 +38,7 @@ describe('messages routes', () => {
outgoing = mock<Outgoing>()
incoming = mock<Incoming>()
const onNewLogin = mock<OnNewLogin>()
app = new App(incoming, outgoing, '', getConfigTest, sessionStore, onNewLogin)
app = new App(incoming, outgoing, '', getConfigTest, sessionStore, onNewLogin, addToBlacklist)
})

test('whatsapp with sucess', async () => {
Expand Down
4 changes: 3 additions & 1 deletion __tests__/routes/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Config, getConfig } from '../../src/services/config'
import { DataStore } from '../../src/services/data_store'
import { SessionStore } from '../../src/services/session_store'
import { OnNewLogin } from '../../src/services/socket'
import { addToBlacklist } from '../../src/services/blacklist'
const addToBlacklist = mock<addToBlacklist>()

const sessionStore = mock<SessionStore>()
const store = mock<Store>()
Expand All @@ -31,7 +33,7 @@ describe('templates routes', () => {
test('index', async () => {
const incoming = mock<Incoming>()
const outgoing = mock<Outgoing>()
const app: App = new App(incoming, outgoing, '', getConfigTest, sessionStore, onNewLogin)
const app: App = new App(incoming, outgoing, '', getConfigTest, sessionStore, onNewLogin, addToBlacklist)
const res = await request(app.server).get('/v15.0/123/message_templates')
expect(res.status).toEqual(200)
})
Expand Down
4 changes: 3 additions & 1 deletion __tests__/routes/webook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Outgoing } from '../../src/services/outgoing'
import { defaultConfig, getConfig } from '../../src/services/config'
import { SessionStore } from '../../src/services/session_store'
import { OnNewLogin } from '../../src/services/socket'
import { addToBlacklist } from '../../src/services/blacklist'
const addToBlacklist = mock<addToBlacklist>()

const sessionStore = mock<SessionStore>()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand All @@ -19,7 +21,7 @@ describe('webhook routes', () => {
const incoming = mock<Incoming>()
const outgoing = mock<Outgoing>()
const onNewLogin = mock<OnNewLogin>()
const app: App = new App(incoming, outgoing, '', getConfigTest, sessionStore, onNewLogin)
const app: App = new App(incoming, outgoing, '', getConfigTest, sessionStore, onNewLogin, addToBlacklist)
const res = await request(app.server).post('/webhooks/whatsapp/123')
expect(res.status).toEqual(200)
})
Expand Down
50 changes: 50 additions & 0 deletions __tests__/services/blacklist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@

jest.mock('../../src/services/redis')
import { extractDestinyPhone, isInBlacklistInMemory, addToBlacklistInMemory, cleanBlackList, isInBlacklistInRedis } from '../../src/services/blacklist'
import { redisGet, redisKeys, blacklist } from '../../src/services/redis'

const redisGetMock = redisGet as jest.MockedFunction<typeof redisGet>
const redisKeysMock = redisKeys as jest.MockedFunction<typeof redisKeys>
const blacklistMock = blacklist as jest.MockedFunction<typeof blacklist>

describe('service blacklist webhook', () => {
test('return empty extractDestinyPhone from webhook payload', async () => {
const payload = {
entry: [
{
changes: [
{
value: {
contacts: [{ wa_id: 'y' }],
},
},
],
},
],
}
expect(extractDestinyPhone(payload)).toBe('y')
})

test('return empty extractDestinyPhone from api payload', async () => {
expect(extractDestinyPhone({ to: 'y'})).toBe('y')
})

test('return false isInBlacklistInMemory', async () => {
await cleanBlackList()
expect(await isInBlacklistInMemory('x', 'y', { to: 'w' })).toBe('')
})

test('return addToBlacklistInMemory', async () => {
await cleanBlackList()
expect(await addToBlacklistInMemory('x', 'y', 'w', 100000)).toBe(true)
expect(await isInBlacklistInMemory('x', 'y', { to: 'w' })).toBe('w')
})

test('return false isInBlacklistInRedis', async () => {
await cleanBlackList()
redisKeysMock.mockReturnValue(Promise.resolve(['unoapi-webhook-blacklist:x:y:w']))
redisGetMock.mockReturnValue(Promise.resolve('1'))
blacklistMock.mockReturnValue('unoapi-webhook-blacklist:::')
expect(await isInBlacklistInRedis('x', 'y', { to: 'w' })).toBe('w')
})
})
28 changes: 19 additions & 9 deletions __tests__/services/outgoing_cloud_api.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { mock } from 'jest-mock-extended'
jest.mock('../../src/services/blacklist')
jest.mock('node-fetch')
import { OutgoingCloudApi } from '../../src/services/outgoing_cloud_api'
import { Outgoing } from '../../src/services/outgoing'
import { Store, getStore } from '../../src/services/store'
import fetch, { Response } from 'node-fetch'
import { DataStore } from '../../src/services/data_store'
import { MediaStore } from '../../src/services/media_store'
import { Config, getConfig, defaultConfig, getMessageMetadataDefault } from '../../src/services/config'
import { Config, getConfig, defaultConfig, getMessageMetadataDefault, Webhook } from '../../src/services/config'
import logger from '../../src/services/logger'
import { isInBlacklistInMemory } from '../../src/services/blacklist'

const mockFetch = fetch as jest.MockedFunction<typeof fetch>
const isInBlacklistMock = isInBlacklistInMemory as jest.MockedFunction<typeof isInBlacklistInMemory>
const webhook = mock<Webhook>()

let store: Store
let getConfig: getConfig
Expand All @@ -20,14 +24,11 @@ let phone
let service: Outgoing

const textPayload = {
key: {
remoteJid: 'askjhasd@kslkjasd.xom',
fromMe: false,
id: 'kasjhdkjhasjkshad',
},
message: {
conversation: 'skdfkdshf',
text: {
body: 'test'
},
type: 'text',
to: 'abc',
}

describe('service outgoing whatsapp cloud api', () => {
Expand All @@ -46,17 +47,26 @@ describe('service outgoing whatsapp cloud api', () => {
store.dataStore = mock<DataStore>()
store.mediaStore = mock<MediaStore>()
phone = `${new Date().getMilliseconds()}`
service = new OutgoingCloudApi(getConfig)
service = new OutgoingCloudApi(getConfig, isInBlacklistInMemory)
})

test('send text with success', async () => {
const mockUrl = `${url}/${phone}`
logger.debug(`Mock url ${mockUrl}`)
mockFetch.mockReset()
expect(fetch).toHaveBeenCalledTimes(0)
const response = new Response('ok', { status: 200 })
response.ok = true
mockFetch.mockResolvedValue(response)
await service.send(phone, textPayload)
expect(fetch).toHaveBeenCalledTimes(1)
})

test('not sendHttp in webhook when is in blacklist', async () => {
mockFetch.mockReset()
expect(mockFetch).toHaveBeenCalledTimes(0)
isInBlacklistMock.mockResolvedValue(Promise.resolve('1'))
await service.sendHttp(phone, webhook, textPayload)
expect(mockFetch).toHaveBeenCalledTimes(0)
})
})
24 changes: 20 additions & 4 deletions examples/typebot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ And click in submit on typebot ![image](prints/callback.png)

After, enable the integration on typebot and click in Publish. ![image](prints/publish.png)



# Lists with typebot

### Some observations before using list on Typebot, by default Typebot is not ready to work with lists, so has some limitations.
Expand All @@ -42,7 +40,25 @@ After, enable the integration on typebot and click in Publish. ![image](prints/p

## How to use


To use lists, you need to use the text bubble followed by button input. ![image](prints/lists.png)

![image](prints/exemple_list_typebot.png)
![image](prints/exemple_list_typebot.png)

## Config unoapi to not send message to type for some numbers
To work with this, set a unique id field in webhook json in redis or if use envs config, the id of webhook is a string `default`

For exemplo, if your session number is Y and you want do webhook with id W to never send more message to number X

ttl param is in milliseconds

To remove a phone number your black, send ttl with 0

```sh
curl -i -X POST \
http://localhost:9876/Y/blacklist/W \
-H 'Content-Type: application/json' \
-H 'Authorization: 1' \
-d '{
"ttl": -1,
"to": "X"
}'
7 changes: 5 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import middleware from './services/middleware'
import injectRoute from './services/inject_route'
import { OnNewLogin } from './services/socket'
import { Server } from 'socket.io'
import { addToBlacklist } from './services/blacklist'

export class App {
public readonly server: HttpServer
Expand All @@ -22,6 +23,7 @@ export class App {
getConfig: getConfig,
sessionStore: SessionStore,
onNewLogin: OnNewLogin,
addToBlacklist: addToBlacklist,
middleware: middleware = async (req: Request, res: Response, next: NextFunction) => next(),
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
injectRoute: injectRoute = async (router: Router) => {},
Expand All @@ -31,7 +33,7 @@ export class App {
this.app.use(express.urlencoded({ extended: true }))
this.server = createServer(this.app)
const socket: Server = new Server(this.server)
this.router(incoming, outgoing, baseUrl, getConfig, sessionStore, socket, onNewLogin, middleware, injectRoute)
this.router(incoming, outgoing, baseUrl, getConfig, sessionStore, socket, onNewLogin, addToBlacklist, middleware, injectRoute)
}

private router(
Expand All @@ -42,10 +44,11 @@ export class App {
sessionStore: SessionStore,
socket: Server,
onNewLogin: OnNewLogin,
addToBlacklist: addToBlacklist,
middleware: middleware,
injectRoute: injectRoute,
) {
const roter = router(incoming, outgoing, baseUrl, getConfig, sessionStore, socket, onNewLogin, middleware, injectRoute)
const roter = router(incoming, outgoing, baseUrl, getConfig, sessionStore, socket, onNewLogin, addToBlacklist, middleware, injectRoute)
this.app.use(roter)
}
}
22 changes: 22 additions & 0 deletions src/controllers/blacklist_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Request, Response } from 'express'
import logger from '../services/logger'
import { addToBlacklist } from '../services/blacklist'

export class BlacklistController {
private addToBlacklist: addToBlacklist

constructor(addToBlacklist: addToBlacklist) {
this.addToBlacklist = addToBlacklist
}

async update(req: Request, res: Response) {
logger.debug('blacklist method %s', req.method)
logger.debug('blacklist headers %s', JSON.stringify(req.headers))
logger.debug('blacklist params %s', JSON.stringify(req.params))
logger.debug('blacklist body %s', JSON.stringify(req.body))
const { to, ttl } = req.body
const { webhook_id, phone } = req.params
await this.addToBlacklist(phone, webhook_id, to, ttl)
res.status(200).send(`{"success": true}`)
}
}
12 changes: 5 additions & 7 deletions src/controllers/webhook_controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { Request, Response } from 'express'
import logger from '../services/logger'

class WebwookController {
export class WebhookController {
public whatsapp(req: Request, res: Response) {
logger.debug('webhook method %s', req.method)
logger.debug('webhook headers %s', JSON.stringify(req.headers))
logger.debug('webhook params %s', JSON.stringify(req.params))
logger.debug('webhook body %s', JSON.stringify(req.body))
logger.debug('webhook whatsapp method %s', req.method)
logger.debug('webhook whatsapp headers %s', JSON.stringify(req.headers))
logger.debug('webhook whatsapp params %s', JSON.stringify(req.params))
logger.debug('webhook whatsapp body %s', JSON.stringify(req.body))
res.status(200).send(`{"success": true}`)
}
}

export const webhookController = new WebwookController()
2 changes: 2 additions & 0 deletions src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export const UNOAPI_JOB_OUTGOING_PREFETCH = parseInt(process.env.UNOAPI_JOB_OUTG
export const UNOAPI_JOB_MEDIA = `${UNOAPI_QUEUE_NAME}.media`
export const UNOAPI_JOB_NOTIFICATION = `${UNOAPI_QUEUE_NAME}.notification`
export const UNOAPI_JOB_LISTENER = `${UNOAPI_QUEUE_NAME}.baileys.listener`
export const UNOAPI_JOB_BLACKLIST_ADD = `${UNOAPI_QUEUE_NAME}.webhooker.blacklist.add`
export const UNOAPI_JOB_BLACKLIST_RELOAD = `${UNOAPI_QUEUE_NAME}.webhooker.blacklist.reload`
export const UNOAPI_JOB_BIND = `${UNOAPI_QUEUE_NAME}.bind`
export const UNOAPI_JOB_OUTGOING = `${UNOAPI_QUEUE_NAME}.outgoing`
export const UNOAPI_JOB_CONTACT = `${UNOAPI_QUEUE_NAME}.contact`
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { autoConnect } from './services/auto_connect'
import { getConfigByEnv } from './services/config_by_env'
import { getClientBaileys } from './services/client_baileys'
import { onNewLoginAlert } from './services/on_new_login_alert'
import { isInBlacklistInMemory, addToBlacklistInMemory } from './services/blacklist'
import { version } from '../package.json'

import logger from './services/logger'
Expand All @@ -20,14 +21,14 @@ import { ListenerBaileys } from './services/listener_baileys'

import { BASE_URL, PORT } from './defaults'

const outgoingCloudApi: Outgoing = new OutgoingCloudApi(getConfigByEnv)
const outgoingCloudApi: Outgoing = new OutgoingCloudApi(getConfigByEnv, isInBlacklistInMemory)

const listenerBaileys: Listener = new ListenerBaileys(outgoingCloudApi, getConfigByEnv)
const onNewLoginn = onNewLoginAlert(listenerBaileys)
const incomingBaileys: Incoming = new IncomingBaileys(listenerBaileys, getConfigByEnv, getClientBaileys, onNewLoginn)
const sessionStore: SessionStore = new SessionStoreFile()

const app: App = new App(incomingBaileys, outgoingCloudApi, BASE_URL, getConfigByEnv, sessionStore, onNewLoginn)
const app: App = new App(incomingBaileys, outgoingCloudApi, BASE_URL, getConfigByEnv, sessionStore, onNewLoginn, addToBlacklistInMemory)

app.server.listen(PORT, '0.0.0.0', async () => {
logger.info('Unoapi Cloud version: %s, listening on port: %s', version, PORT)
Expand Down
8 changes: 8 additions & 0 deletions src/jobs/add_to_blacklist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { addToBlacklistInMemory } from '../services/blacklist'
import { setBlacklist } from '../services/redis'

export const addToBlacklist = async (_phone: string, data: object) => {
const { from, webhookId, to, ttl } = data as any
await setBlacklist(from, webhookId, to, ttl)
await addToBlacklistInMemory(from, webhookId, to, ttl)
}
Loading

0 comments on commit 46a2769

Please sign in to comment.