Merge pull request #1578 from lamassu/feat/new-sms-plugins
LAM-899: Feat/new sms plugins
This commit is contained in:
commit
115da7dff8
20 changed files with 1633 additions and 87 deletions
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -10,7 +10,7 @@
|
|||
"name": "Launch Program",
|
||||
"program": "${workspaceRoot}/bin/lamassu-server",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"args": ["--mockSms"]
|
||||
"args": [""]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ Go to all the required, unconfigured red fields and choose some values. Choose m
|
|||
### Run lamassu-server
|
||||
|
||||
```
|
||||
node bin/lamassu-server --mockSms --mockScoring
|
||||
node bin/lamassu-server --mockScoring
|
||||
```
|
||||
|
||||
### Add a lamassu-machine
|
||||
|
|
@ -100,7 +100,7 @@ Now continue with lamassu-machine instructions from the ``INSTALL.md`` file in [
|
|||
To start the Lamassu server run:
|
||||
|
||||
```
|
||||
node bin/lamassu-server --mockSms --mockScoring
|
||||
node bin/lamassu-server --mockScoring
|
||||
```
|
||||
|
||||
To start the Lamassu Admin run:
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ Go to all the required, unconfigured red fields and choose some values. Choose m
|
|||
### Run lamassu-server
|
||||
|
||||
```
|
||||
node bin/lamassu-server --mockSms --mockScoring
|
||||
node bin/lamassu-server --mockScoring
|
||||
```
|
||||
|
||||
### Add a lamassu-machine
|
||||
|
|
@ -119,7 +119,7 @@ Now continue with lamassu-machine instructions from the ``INSTALL.md`` file in [
|
|||
To start the Lamassu server run:
|
||||
|
||||
```
|
||||
node bin/lamassu-server --mockSms --mockScoring
|
||||
node bin/lamassu-server --mockScoring
|
||||
```
|
||||
|
||||
To start the Lamassu Admin run:
|
||||
|
|
|
|||
|
|
@ -15,5 +15,5 @@ See [lamassu-remote-install/README.md](lamassu-remote-install/README.md).
|
|||
|
||||
## Running
|
||||
```bash
|
||||
node bin/lamassu-server --mockSms --mockScoring
|
||||
node bin/lamassu-server --mockScoring
|
||||
```
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ const ALL_ACCOUNTS = [
|
|||
{ code: 'mock-sms', display: 'Mock SMS', class: SMS, dev: true },
|
||||
{ code: 'mock-id-verify', display: 'Mock ID verifier', class: ID_VERIFIER, dev: true },
|
||||
{ code: 'twilio', display: 'Twilio', class: SMS },
|
||||
{ code: 'telnyx', display: 'Telnyx', class: SMS },
|
||||
{ code: 'vonage', display: 'Vonage', class: SMS },
|
||||
{ code: 'mailgun', display: 'Mailgun', class: EMAIL },
|
||||
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
|
||||
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ const SECRET_FIELDS = [
|
|||
'binanceus.privateKey',
|
||||
'cex.privateKey',
|
||||
'binance.privateKey',
|
||||
'twilio.authToken'
|
||||
'twilio.authToken',
|
||||
'telnyx.apiKey',
|
||||
'vonage.apiSecret'
|
||||
]
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
const _ = require('lodash/fp')
|
||||
const argv = require('minimist')(process.argv.slice(2))
|
||||
const crypto = require('crypto')
|
||||
const pgp = require('pg-promise')()
|
||||
const dateFormat = require('dateformat')
|
||||
|
|
@ -163,9 +162,7 @@ function plugins (settings, deviceId) {
|
|||
|
||||
const virtualCassettes = [Math.max(...denominations) * 2]
|
||||
|
||||
const counts = argv.cassettes
|
||||
? argv.cassettes.split(',')
|
||||
: rec.counts
|
||||
const counts = rec.counts
|
||||
|
||||
if (rec.counts.length !== denominations.length) {
|
||||
throw new Error('Denominations and respective counts do not match!')
|
||||
|
|
@ -763,7 +760,9 @@ function plugins (settings, deviceId) {
|
|||
}
|
||||
|
||||
function getPhoneCode (phone) {
|
||||
const code = argv.mockSms
|
||||
const notifications = configManager.getNotifications(settings.config)
|
||||
|
||||
const code = notifications.thirdParty_sms === 'mock-sms'
|
||||
? '123'
|
||||
: randomCode()
|
||||
|
||||
|
|
|
|||
27
lib/plugins/sms/telnyx/telnyx.js
Normal file
27
lib/plugins/sms/telnyx/telnyx.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
const Telnyx = require('telnyx')
|
||||
|
||||
const NAME = 'Telnyx'
|
||||
|
||||
function sendMessage (account, rec) {
|
||||
const telnyx = Telnyx(account.apiKey)
|
||||
|
||||
const from = account.fromNumber
|
||||
const text = rec.sms.body
|
||||
const to = rec.sms.toNumber || account.toNumber
|
||||
|
||||
return telnyx.messages.create({ from, to, text })
|
||||
.catch(err => {
|
||||
throw new Error(`Telnyx error: ${err.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
function getLookup () {
|
||||
throw new Error('Telnyx error: lookup not supported')
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
NAME,
|
||||
sendMessage,
|
||||
getLookup
|
||||
}
|
||||
31
lib/plugins/sms/vonage/vonage.js
Normal file
31
lib/plugins/sms/vonage/vonage.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
const { Auth } = require('@vonage/auth')
|
||||
const { SMS } = require('@vonage/sms')
|
||||
|
||||
const NAME = 'Vonage'
|
||||
|
||||
function sendMessage (account, rec) {
|
||||
const credentials = new Auth({
|
||||
apiKey: account.apiKey,
|
||||
apiSecret: account.apiSecret
|
||||
})
|
||||
|
||||
const from = account.fromNumber
|
||||
const text = rec.sms.body
|
||||
const to = rec.sms.toNumber || account.toNumber
|
||||
|
||||
const smsClient = new SMS(credentials)
|
||||
smsClient.send({ from, text, to })
|
||||
.catch(err => {
|
||||
throw new Error(`Vonage error: ${err.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
function getLookup () {
|
||||
throw new Error('Vonage error: lookup not supported')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NAME,
|
||||
sendMessage,
|
||||
getLookup
|
||||
}
|
||||
47
lib/plugins/sms/whatsapp/whatsapp.js
Normal file
47
lib/plugins/sms/whatsapp/whatsapp.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
const axios = require('axios')
|
||||
|
||||
const NAME = 'Whatsapp'
|
||||
|
||||
function sendMessage (account, rec) {
|
||||
const phoneId = account.phoneId
|
||||
const token = account.apiKey
|
||||
|
||||
const to = rec.sms.toNumber || account.toNumber
|
||||
const template = rec.sms.template
|
||||
|
||||
const url = `https://graph.facebook.com/v17.0/${phoneId}/messages`
|
||||
|
||||
const config = {
|
||||
headers:{
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
messaging_product: 'whatsapp',
|
||||
recipient_type: 'individual',
|
||||
type: 'template',
|
||||
to,
|
||||
template: {
|
||||
name: template,
|
||||
language: { code: 'en_US' }
|
||||
}
|
||||
}
|
||||
|
||||
axios.post(url, data, config)
|
||||
.catch(err => {
|
||||
// console.log(err)
|
||||
throw new Error(`Whatsapp error: ${err.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
function getLookup () {
|
||||
throw new Error('Whatsapp error: lookup not supported')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NAME,
|
||||
sendMessage,
|
||||
getLookup
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
const dateFormat = require('dateformat')
|
||||
|
||||
const ph = require('./plugin-helper')
|
||||
const argv = require('minimist')(process.argv.slice(2))
|
||||
const { utils: coinUtils } = require('@lamassu/coins')
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
|
|
@ -25,7 +24,8 @@ function getSms (event, phone, content) {
|
|||
}
|
||||
|
||||
function getPlugin (settings) {
|
||||
const pluginCode = argv.mockSms ? 'mock-sms' : 'twilio'
|
||||
const smsProvider = settings.config.notifications_thirdParty_sms
|
||||
const pluginCode = smsProvider ?? 'twilio'
|
||||
const plugin = ph.load(ph.SMS, pluginCode)
|
||||
const account = settings.accounts[pluginCode]
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,19 @@ import CryptoBalanceOverrides from './sections/CryptoBalanceOverrides'
|
|||
import FiatBalanceAlerts from './sections/FiatBalanceAlerts'
|
||||
import FiatBalanceOverrides from './sections/FiatBalanceOverrides'
|
||||
import Setup from './sections/Setup'
|
||||
import ThirdPartyProvider from './sections/ThirdPartyProvider'
|
||||
import TransactionAlerts from './sections/TransactionAlerts'
|
||||
|
||||
const GET_INFO = gql`
|
||||
query getData {
|
||||
config
|
||||
accountsConfig {
|
||||
code
|
||||
display
|
||||
class
|
||||
cryptos
|
||||
deprecated
|
||||
}
|
||||
machines {
|
||||
name
|
||||
deviceId
|
||||
|
|
@ -59,6 +67,7 @@ const Notifications = ({
|
|||
displayCryptoAlerts = true,
|
||||
displayOverrides = true,
|
||||
displayTitle = true,
|
||||
displayThirdPartyProvider = true,
|
||||
wizard = false
|
||||
}) => {
|
||||
const [section, setSection] = useState(null)
|
||||
|
|
@ -86,6 +95,7 @@ const Notifications = ({
|
|||
|
||||
const config = fromNamespace(SCREEN_KEY)(data?.config)
|
||||
const machines = data?.machines
|
||||
const accountsConfig = data?.accountsConfig
|
||||
const cryptoCurrencies = data?.cryptoCurrencies
|
||||
const twilioAvailable = R.has('twilio', data?.accounts || {})
|
||||
const mailgunAvailable = R.has('mailgun', data?.accounts || {})
|
||||
|
|
@ -136,6 +146,7 @@ const Notifications = ({
|
|||
setEditing,
|
||||
setSection,
|
||||
machines,
|
||||
accountsConfig,
|
||||
cryptoCurrencies,
|
||||
twilioAvailable,
|
||||
setSmsSetupPopup,
|
||||
|
|
@ -148,6 +159,13 @@ const Notifications = ({
|
|||
<>
|
||||
<NotificationsCtx.Provider value={contextValue}>
|
||||
{displayTitle && <TitleSection title="Notifications" />}
|
||||
{displayThirdPartyProvider && (
|
||||
<Section
|
||||
title="Third party providers"
|
||||
error={error && !section === 'thirdParty'}>
|
||||
<ThirdPartyProvider section="thirdParty" />
|
||||
</Section>
|
||||
)}
|
||||
{displaySetup && (
|
||||
<Section title="Setup" error={error && !section}>
|
||||
<Setup forceDisable={!!editingKey} wizard={wizard} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import * as R from 'ramda'
|
||||
import React, { useContext } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { Table as EditableTable } from 'src/components/editableTable'
|
||||
import Autocomplete from 'src/components/inputs/formik/Autocomplete'
|
||||
import { toNamespace, fromNamespace } from 'src/utils/config'
|
||||
|
||||
import NotificationsCtx from '../NotificationsContext'
|
||||
|
||||
const filterClass = type => R.filter(it => it.class === type)
|
||||
|
||||
const ThirdPartyProvider = () => {
|
||||
const { save, data: _data, error, accountsConfig } = useContext(
|
||||
NotificationsCtx
|
||||
)
|
||||
|
||||
const data = fromNamespace('thirdParty')(_data)
|
||||
|
||||
const filterOptions = type => filterClass(type)(accountsConfig || [])
|
||||
|
||||
const getDisplayName = type => it =>
|
||||
R.compose(
|
||||
R.prop('display'),
|
||||
R.find(R.propEq('code', it))
|
||||
)(filterOptions(type))
|
||||
|
||||
const innerSave = async value => {
|
||||
const config = toNamespace('thirdParty')(value?.thirdParty[0])
|
||||
await save('thirdParty', config)
|
||||
}
|
||||
|
||||
const ThirdPartySchema = Yup.object().shape({
|
||||
sms: Yup.string('The sms must be a string').required('The sms is required')
|
||||
})
|
||||
|
||||
const elements = [
|
||||
{
|
||||
name: 'sms',
|
||||
size: 'sm',
|
||||
view: getDisplayName('sms'),
|
||||
width: 175,
|
||||
input: Autocomplete,
|
||||
inputProps: {
|
||||
options: filterOptions('sms'),
|
||||
valueProp: 'code',
|
||||
labelProp: 'display'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<EditableTable
|
||||
name="thirdParty"
|
||||
initialValues={data?.thirdParty ?? { sms: 'twilio' }}
|
||||
data={R.of(data || [])}
|
||||
error={error?.message}
|
||||
enableEdit
|
||||
editWidth={174}
|
||||
save={innerSave}
|
||||
validationSchema={ThirdPartySchema}
|
||||
elements={elements}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThirdPartyProvider
|
||||
|
|
@ -9,7 +9,9 @@ import infura from './infura'
|
|||
import itbit from './itbit'
|
||||
import kraken from './kraken'
|
||||
import mailgun from './mailgun'
|
||||
import telnyx from './telnyx'
|
||||
import twilio from './twilio'
|
||||
import vonage from './vonage'
|
||||
|
||||
export default {
|
||||
[bitgo.code]: bitgo,
|
||||
|
|
@ -19,6 +21,8 @@ export default {
|
|||
[itbit.code]: itbit,
|
||||
[kraken.code]: kraken,
|
||||
[mailgun.code]: mailgun,
|
||||
[telnyx.code]: telnyx,
|
||||
[vonage.code]: vonage,
|
||||
[twilio.code]: twilio,
|
||||
[binanceus.code]: binanceus,
|
||||
[cex.code]: cex,
|
||||
|
|
|
|||
44
new-lamassu-admin/src/pages/Services/schemas/telnyx.js
Normal file
44
new-lamassu-admin/src/pages/Services/schemas/telnyx.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as Yup from 'yup'
|
||||
|
||||
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
||||
|
||||
import { secretTest } from './helper'
|
||||
|
||||
export default {
|
||||
code: 'telnyx',
|
||||
name: 'Telnyx',
|
||||
title: 'Telnyx (SMS)',
|
||||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API Key',
|
||||
component: SecretInputFormik
|
||||
},
|
||||
{
|
||||
code: 'fromNumber',
|
||||
display: 'Telnyx Number (international format)',
|
||||
component: TextInputFormik,
|
||||
face: true
|
||||
},
|
||||
{
|
||||
code: 'toNumber',
|
||||
display: 'Notifications Number (international format)',
|
||||
component: TextInputFormik,
|
||||
face: true
|
||||
}
|
||||
],
|
||||
getValidationSchema: account => {
|
||||
return Yup.object().shape({
|
||||
apiKey: Yup.string('The API key must be a string')
|
||||
.max(200, 'The API key is too long')
|
||||
.test(secretTest(account?.apiKey, 'API key')),
|
||||
fromNumber: Yup.string('The Telnyx number must be a string')
|
||||
.max(100, 'The Telnyx number is too long')
|
||||
.required('The Telnyx number is required'),
|
||||
toNumber: Yup.string('The notifications number must be a string')
|
||||
.max(100, 'The notifications number is too long')
|
||||
.required('The notifications number is required')
|
||||
})
|
||||
}
|
||||
}
|
||||
52
new-lamassu-admin/src/pages/Services/schemas/vonage.js
Normal file
52
new-lamassu-admin/src/pages/Services/schemas/vonage.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import * as Yup from 'yup'
|
||||
|
||||
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
||||
|
||||
import { secretTest } from './helper'
|
||||
|
||||
export default {
|
||||
code: 'vonage',
|
||||
name: 'Vonage',
|
||||
title: 'Vonage (SMS)',
|
||||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API Key',
|
||||
component: TextInputFormik
|
||||
},
|
||||
{
|
||||
code: 'apiSecret',
|
||||
display: 'API Secret',
|
||||
component: SecretInputFormik
|
||||
},
|
||||
{
|
||||
code: 'fromNumber',
|
||||
display: 'Vonage Number (international format)',
|
||||
component: TextInputFormik,
|
||||
face: true
|
||||
},
|
||||
{
|
||||
code: 'toNumber',
|
||||
display: 'Notifications Number (international format)',
|
||||
component: TextInputFormik,
|
||||
face: true
|
||||
}
|
||||
],
|
||||
getValidationSchema: account => {
|
||||
return Yup.object().shape({
|
||||
apiKey: Yup.string('The API key must be a string')
|
||||
.max(200, 'The API key is too long')
|
||||
.required('The Vonage number is required'),
|
||||
apiSecret: Yup.string('The API key must be a string')
|
||||
.max(200, 'The API secret is too long')
|
||||
.test(secretTest(account?.apiKey, 'API secret')),
|
||||
fromNumber: Yup.string('The Vonage number must be a string')
|
||||
.max(100, 'The Vonage number is too long')
|
||||
.required('The Vonage number is required'),
|
||||
toNumber: Yup.string('The notifications number must be a string')
|
||||
.max(100, 'The notifications number is too long')
|
||||
.required('The notifications number is required')
|
||||
})
|
||||
}
|
||||
}
|
||||
1380
package-lock.json
generated
1380
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,7 @@
|
|||
"name": "lamassu-server",
|
||||
"description": "bitcoin atm client server protocol module",
|
||||
"keywords": [],
|
||||
"version": "8.1.3",
|
||||
"version": "8.1.4",
|
||||
"license": "Unlicense",
|
||||
"author": "Lamassu (https://lamassu.is)",
|
||||
"dependencies": {
|
||||
|
|
@ -11,6 +11,8 @@
|
|||
"@graphql-tools/merge": "^6.2.5",
|
||||
"@lamassu/coins": "1.3.0",
|
||||
"@simplewebauthn/server": "^3.0.0",
|
||||
"@vonage/auth": "^1.5.0",
|
||||
"@vonage/sms": "^1.7.0",
|
||||
"apollo-server-express": "2.25.1",
|
||||
"argon2": "0.28.2",
|
||||
"axios": "0.21.1",
|
||||
|
|
@ -79,6 +81,7 @@
|
|||
"socket.io": "^2.0.3",
|
||||
"socket.io-client": "^2.0.3",
|
||||
"talisman": "^0.20.0",
|
||||
"telnyx": "^1.25.5",
|
||||
"twilio": "^3.6.1",
|
||||
"uuid": "8.3.2",
|
||||
"web3": "1.7.1",
|
||||
|
|
@ -121,7 +124,7 @@
|
|||
"test": "mocha --recursive tests",
|
||||
"jtest": "jest --detectOpenHandles",
|
||||
"build-admin": "npm run build-admin:css && npm run build-admin:main && npm run build-admin:lamassu",
|
||||
"server": "nodemon bin/lamassu-server --mockSms --mockScoring --logLevel silly",
|
||||
"server": "nodemon bin/lamassu-server --mockScoring --logLevel silly",
|
||||
"admin-server": "nodemon bin/lamassu-admin-server --dev --logLevel silly",
|
||||
"graphql-server": "nodemon bin/new-graphql-dev-insecure",
|
||||
"watch": "concurrently \"npm:server\" \"npm:admin-server\" \"npm:graphql-server\"",
|
||||
|
|
|
|||
10
shell.nix
10
shell.nix
|
|
@ -1,16 +1,16 @@
|
|||
with import (fetchTarball {
|
||||
name = "nixpkgs-19.03";
|
||||
url = https://github.com/NixOS/nixpkgs/archive/0b8799ecaaf0dc6b4c11583a3c96ca5b40fcfdfb.tar.gz;
|
||||
sha256 = "11m4aig6cv0zi3gbq2xn9by29cfvnsxgzf9qsvz67qr0yq29ryyz";
|
||||
name = "8ad5e8";
|
||||
url = https://github.com/NixOS/nixpkgs/archive/8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8.tar.gz;
|
||||
sha256 = "17v6wigks04x1d63a2wcd7cc4z9ca6qr0f4xvw1pdw83f8a3c0nj";
|
||||
}) {};
|
||||
|
||||
stdenv.mkDerivation {
|
||||
name = "node";
|
||||
buildInputs = [
|
||||
nodejs-14_x
|
||||
python2Full
|
||||
python3
|
||||
openssl
|
||||
postgresql_9_6
|
||||
postgresql
|
||||
];
|
||||
shellHook = ''
|
||||
export PATH="$PWD/node_modules/.bin/:$PATH"
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ const cmd = require('./scripts')
|
|||
process.on('message', async (msg) => {
|
||||
console.log('Message from parent:', msg)
|
||||
|
||||
await cmd.execCommand(`node --prof LAMASSU_DB=STRESS_TEST ../../bin/lamassu-server --mockSms`)
|
||||
await cmd.execCommand(`node --prof LAMASSU_DB=STRESS_TEST ../../bin/lamassu-server`)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue