Merge pull request #1578 from lamassu/feat/new-sms-plugins

LAM-899: Feat/new sms plugins
This commit is contained in:
Rafael Taranto 2023-08-22 13:00:53 +01:00 committed by GitHub
commit 115da7dff8
20 changed files with 1633 additions and 87 deletions

2
.vscode/launch.json vendored
View file

@ -10,7 +10,7 @@
"name": "Launch Program", "name": "Launch Program",
"program": "${workspaceRoot}/bin/lamassu-server", "program": "${workspaceRoot}/bin/lamassu-server",
"cwd": "${workspaceRoot}", "cwd": "${workspaceRoot}",
"args": ["--mockSms"] "args": [""]
}, },
{ {
"type": "node", "type": "node",

View file

@ -86,7 +86,7 @@ Go to all the required, unconfigured red fields and choose some values. Choose m
### Run lamassu-server ### Run lamassu-server
``` ```
node bin/lamassu-server --mockSms --mockScoring node bin/lamassu-server --mockScoring
``` ```
### Add a lamassu-machine ### 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: To start the Lamassu server run:
``` ```
node bin/lamassu-server --mockSms --mockScoring node bin/lamassu-server --mockScoring
``` ```
To start the Lamassu Admin run: To start the Lamassu Admin run:

View file

@ -104,7 +104,7 @@ Go to all the required, unconfigured red fields and choose some values. Choose m
### Run lamassu-server ### Run lamassu-server
``` ```
node bin/lamassu-server --mockSms --mockScoring node bin/lamassu-server --mockScoring
``` ```
### Add a lamassu-machine ### 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: To start the Lamassu server run:
``` ```
node bin/lamassu-server --mockSms --mockScoring node bin/lamassu-server --mockScoring
``` ```
To start the Lamassu Admin run: To start the Lamassu Admin run:

View file

@ -15,5 +15,5 @@ See [lamassu-remote-install/README.md](lamassu-remote-install/README.md).
## Running ## Running
```bash ```bash
node bin/lamassu-server --mockSms --mockScoring node bin/lamassu-server --mockScoring
``` ```

View file

@ -48,6 +48,8 @@ const ALL_ACCOUNTS = [
{ code: 'mock-sms', display: 'Mock SMS', class: SMS, dev: true }, { code: 'mock-sms', display: 'Mock SMS', class: SMS, dev: true },
{ code: 'mock-id-verify', display: 'Mock ID verifier', class: ID_VERIFIER, dev: true }, { code: 'mock-id-verify', display: 'Mock ID verifier', class: ID_VERIFIER, dev: true },
{ code: 'twilio', display: 'Twilio', class: SMS }, { 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: 'mailgun', display: 'Mailgun', class: EMAIL },
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS }, { code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] }, { code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },

View file

@ -23,7 +23,9 @@ const SECRET_FIELDS = [
'binanceus.privateKey', 'binanceus.privateKey',
'cex.privateKey', 'cex.privateKey',
'binance.privateKey', 'binance.privateKey',
'twilio.authToken' 'twilio.authToken',
'telnyx.apiKey',
'vonage.apiSecret'
] ]
/* /*

View file

@ -1,5 +1,4 @@
const _ = require('lodash/fp') const _ = require('lodash/fp')
const argv = require('minimist')(process.argv.slice(2))
const crypto = require('crypto') const crypto = require('crypto')
const pgp = require('pg-promise')() const pgp = require('pg-promise')()
const dateFormat = require('dateformat') const dateFormat = require('dateformat')
@ -163,9 +162,7 @@ function plugins (settings, deviceId) {
const virtualCassettes = [Math.max(...denominations) * 2] const virtualCassettes = [Math.max(...denominations) * 2]
const counts = argv.cassettes const counts = rec.counts
? argv.cassettes.split(',')
: rec.counts
if (rec.counts.length !== denominations.length) { if (rec.counts.length !== denominations.length) {
throw new Error('Denominations and respective counts do not match!') throw new Error('Denominations and respective counts do not match!')
@ -763,7 +760,9 @@ function plugins (settings, deviceId) {
} }
function getPhoneCode (phone) { function getPhoneCode (phone) {
const code = argv.mockSms const notifications = configManager.getNotifications(settings.config)
const code = notifications.thirdParty_sms === 'mock-sms'
? '123' ? '123'
: randomCode() : randomCode()

View 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
}

View 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
}

View 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
}

View file

@ -2,7 +2,6 @@
const dateFormat = require('dateformat') const dateFormat = require('dateformat')
const ph = require('./plugin-helper') const ph = require('./plugin-helper')
const argv = require('minimist')(process.argv.slice(2))
const { utils: coinUtils } = require('@lamassu/coins') const { utils: coinUtils } = require('@lamassu/coins')
const _ = require('lodash/fp') const _ = require('lodash/fp')
@ -25,7 +24,8 @@ function getSms (event, phone, content) {
} }
function getPlugin (settings) { 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 plugin = ph.load(ph.SMS, pluginCode)
const account = settings.accounts[pluginCode] const account = settings.accounts[pluginCode]

View file

@ -19,11 +19,19 @@ import CryptoBalanceOverrides from './sections/CryptoBalanceOverrides'
import FiatBalanceAlerts from './sections/FiatBalanceAlerts' import FiatBalanceAlerts from './sections/FiatBalanceAlerts'
import FiatBalanceOverrides from './sections/FiatBalanceOverrides' import FiatBalanceOverrides from './sections/FiatBalanceOverrides'
import Setup from './sections/Setup' import Setup from './sections/Setup'
import ThirdPartyProvider from './sections/ThirdPartyProvider'
import TransactionAlerts from './sections/TransactionAlerts' import TransactionAlerts from './sections/TransactionAlerts'
const GET_INFO = gql` const GET_INFO = gql`
query getData { query getData {
config config
accountsConfig {
code
display
class
cryptos
deprecated
}
machines { machines {
name name
deviceId deviceId
@ -59,6 +67,7 @@ const Notifications = ({
displayCryptoAlerts = true, displayCryptoAlerts = true,
displayOverrides = true, displayOverrides = true,
displayTitle = true, displayTitle = true,
displayThirdPartyProvider = true,
wizard = false wizard = false
}) => { }) => {
const [section, setSection] = useState(null) const [section, setSection] = useState(null)
@ -86,6 +95,7 @@ const Notifications = ({
const config = fromNamespace(SCREEN_KEY)(data?.config) const config = fromNamespace(SCREEN_KEY)(data?.config)
const machines = data?.machines const machines = data?.machines
const accountsConfig = data?.accountsConfig
const cryptoCurrencies = data?.cryptoCurrencies const cryptoCurrencies = data?.cryptoCurrencies
const twilioAvailable = R.has('twilio', data?.accounts || {}) const twilioAvailable = R.has('twilio', data?.accounts || {})
const mailgunAvailable = R.has('mailgun', data?.accounts || {}) const mailgunAvailable = R.has('mailgun', data?.accounts || {})
@ -136,6 +146,7 @@ const Notifications = ({
setEditing, setEditing,
setSection, setSection,
machines, machines,
accountsConfig,
cryptoCurrencies, cryptoCurrencies,
twilioAvailable, twilioAvailable,
setSmsSetupPopup, setSmsSetupPopup,
@ -148,6 +159,13 @@ const Notifications = ({
<> <>
<NotificationsCtx.Provider value={contextValue}> <NotificationsCtx.Provider value={contextValue}>
{displayTitle && <TitleSection title="Notifications" />} {displayTitle && <TitleSection title="Notifications" />}
{displayThirdPartyProvider && (
<Section
title="Third party providers"
error={error && !section === 'thirdParty'}>
<ThirdPartyProvider section="thirdParty" />
</Section>
)}
{displaySetup && ( {displaySetup && (
<Section title="Setup" error={error && !section}> <Section title="Setup" error={error && !section}>
<Setup forceDisable={!!editingKey} wizard={wizard} /> <Setup forceDisable={!!editingKey} wizard={wizard} />

View file

@ -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

View file

@ -9,7 +9,9 @@ import infura from './infura'
import itbit from './itbit' import itbit from './itbit'
import kraken from './kraken' import kraken from './kraken'
import mailgun from './mailgun' import mailgun from './mailgun'
import telnyx from './telnyx'
import twilio from './twilio' import twilio from './twilio'
import vonage from './vonage'
export default { export default {
[bitgo.code]: bitgo, [bitgo.code]: bitgo,
@ -19,6 +21,8 @@ export default {
[itbit.code]: itbit, [itbit.code]: itbit,
[kraken.code]: kraken, [kraken.code]: kraken,
[mailgun.code]: mailgun, [mailgun.code]: mailgun,
[telnyx.code]: telnyx,
[vonage.code]: vonage,
[twilio.code]: twilio, [twilio.code]: twilio,
[binanceus.code]: binanceus, [binanceus.code]: binanceus,
[cex.code]: cex, [cex.code]: cex,

View 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')
})
}
}

View 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

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
"name": "lamassu-server", "name": "lamassu-server",
"description": "bitcoin atm client server protocol module", "description": "bitcoin atm client server protocol module",
"keywords": [], "keywords": [],
"version": "8.1.3", "version": "8.1.4",
"license": "Unlicense", "license": "Unlicense",
"author": "Lamassu (https://lamassu.is)", "author": "Lamassu (https://lamassu.is)",
"dependencies": { "dependencies": {
@ -11,6 +11,8 @@
"@graphql-tools/merge": "^6.2.5", "@graphql-tools/merge": "^6.2.5",
"@lamassu/coins": "1.3.0", "@lamassu/coins": "1.3.0",
"@simplewebauthn/server": "^3.0.0", "@simplewebauthn/server": "^3.0.0",
"@vonage/auth": "^1.5.0",
"@vonage/sms": "^1.7.0",
"apollo-server-express": "2.25.1", "apollo-server-express": "2.25.1",
"argon2": "0.28.2", "argon2": "0.28.2",
"axios": "0.21.1", "axios": "0.21.1",
@ -79,6 +81,7 @@
"socket.io": "^2.0.3", "socket.io": "^2.0.3",
"socket.io-client": "^2.0.3", "socket.io-client": "^2.0.3",
"talisman": "^0.20.0", "talisman": "^0.20.0",
"telnyx": "^1.25.5",
"twilio": "^3.6.1", "twilio": "^3.6.1",
"uuid": "8.3.2", "uuid": "8.3.2",
"web3": "1.7.1", "web3": "1.7.1",
@ -121,7 +124,7 @@
"test": "mocha --recursive tests", "test": "mocha --recursive tests",
"jtest": "jest --detectOpenHandles", "jtest": "jest --detectOpenHandles",
"build-admin": "npm run build-admin:css && npm run build-admin:main && npm run build-admin:lamassu", "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", "admin-server": "nodemon bin/lamassu-admin-server --dev --logLevel silly",
"graphql-server": "nodemon bin/new-graphql-dev-insecure", "graphql-server": "nodemon bin/new-graphql-dev-insecure",
"watch": "concurrently \"npm:server\" \"npm:admin-server\" \"npm:graphql-server\"", "watch": "concurrently \"npm:server\" \"npm:admin-server\" \"npm:graphql-server\"",

View file

@ -1,16 +1,16 @@
with import (fetchTarball { with import (fetchTarball {
name = "nixpkgs-19.03"; name = "8ad5e8";
url = https://github.com/NixOS/nixpkgs/archive/0b8799ecaaf0dc6b4c11583a3c96ca5b40fcfdfb.tar.gz; url = https://github.com/NixOS/nixpkgs/archive/8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8.tar.gz;
sha256 = "11m4aig6cv0zi3gbq2xn9by29cfvnsxgzf9qsvz67qr0yq29ryyz"; sha256 = "17v6wigks04x1d63a2wcd7cc4z9ca6qr0f4xvw1pdw83f8a3c0nj";
}) {}; }) {};
stdenv.mkDerivation { stdenv.mkDerivation {
name = "node"; name = "node";
buildInputs = [ buildInputs = [
nodejs-14_x nodejs-14_x
python2Full python3
openssl openssl
postgresql_9_6 postgresql
]; ];
shellHook = '' shellHook = ''
export PATH="$PWD/node_modules/.bin/:$PATH" export PATH="$PWD/node_modules/.bin/:$PATH"

View file

@ -3,5 +3,5 @@ const cmd = require('./scripts')
process.on('message', async (msg) => { process.on('message', async (msg) => {
console.log('Message from parent:', 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`)
}) })