私人语音通信

本用例介绍如何实现私人语音通信用例中描述的想法。它教您如何使用 Vonage 的 Node Server SDK 来构建语音代理,该代理使用虚拟号码隐藏参与者的真实电话号码。完整的源代码也可以在我们的 GitHub 存储库中找到。

概述

有时您希望两个用户能够在不透露私人电话号码的情况下互相通话。

例如,如果您正在经营共享出行服务,那么您希望用户能够互相交谈,协调接载时间和地点。但是,您不想透露客户的电话号码 - 毕竟,您有义务保护他们的隐私。而且,您也不希望他们不使用您的服务就能直接安排共享出行,因为这意味着您会损失业务收入。

借助 Vonage API,您可以为通话中的每个参与者提供一个临时号码,以掩盖他们的真实号码。在通话期间,每个主叫方看到的只是临时号码。当他们不再需要通信时,临时号码就会被撤消。

您可以从我们的 GitHub 存储库下载源代码。

先决条件

为了完成本用例,您需要:

代码存储库

有一个包含代码的 GitHub 存储库

步骤

要构建该应用程序,请执行以下步骤:

配置

你需要创建一个 .env包含配置的文件。GitHub Readme 中说明了如何做到这一点。在完成本用例时,可以使用变量(例如 API 密钥、API 密码、应用程序 ID、调试模式和预配的号码)的必需值填充配置文件。

创建语音 API 应用程序

语音 API 应用程序是一种 Vonage 构造,不应与您要编写的应用程序混淆。它是使用 API 所需的身份验证和配置设置的“容器”。

您可以使用 Nexmo CLI 创建语音 API 应用程序。您必须提供应用程序的名称以及两个 Webhook 端点的 URL:第一个是 Vonage API 在您的虚拟号码收到呼入电话时向其发出请求的端点,第二个是 API 可以在其中发布事件数据的端点。

将以下 Nexmo CLI 命令中的域名替换为您的 ngrok 域名(如何运行 ngrok),然后在项目的根目录中运行它:

nexmo app:create "voice-proxy" --capabilities=voice --voice-answer-url=https://example.com/proxy-call --voice-event-url=https://example.com/event --keyfile=private.key

此命令下载包含身份验证信息的 private.key 文件,并返回唯一的应用程序 ID。记下该 ID,因为您在后续步骤中会用到它。

创建 Web 应用程序

该应用使用 Express 框架进行路由,使用 Vonage Node Server SDK 来处理语音 API。我们使用 dotenv,以便通过 .env 文本文件配置应用

server.js 中,该代码初始化应用程序的依赖项并启动 web 服务器。为应用的主页 (/) 实现一个路由处理程序,以便您可以通过运行 node server.js 并在浏览器中访问 http://localhost:3000 来测试服务器是否正在运行:

"use strict";

const express = require('express');
const bodyParser = require('body-parser');

const app = express();
app.set('port', (process.env.PORT || 3000));
app.use(bodyParser.urlencoded({{ extended: false }}));

const config = require(__dirname + '/../config');

const VoiceProxy = require('./VoiceProxy');
const voiceProxy = new VoiceProxy(config);

app.listen(app.get('port'), function() {{
  console.log('Voice Proxy App listening on port', app.get('port'));
}});

请注意,该代码实例化 VoiceProxy 类的对象,以处理发送到虚拟号码的消息到目标收件人的真实号码的路由。代理呼叫中介绍了代理过程,但现在只需要注意,该类使用您在下一步中配置的 API 密钥和密码初始化 Vonage Server SDK。这使您的应用程序可以拨打和接听语音电话:

const VoiceProxy = function(config) {{
  this.config = config;

  this.nexmo = new Nexmo({{
      apiKey: this.config.VONAGE_API_KEY,
      apiSecret: this.config.VONAGE_API_SECRET
    }},{{
      debug: this.config.VONAGE_DEBUG
    }});

  // 待分配给 UserA 和 UserB 的虚拟号码
  this.provisionedNumbers = [].concat(this.config.PROVISIONED_NUMBERS);

  // 正在进行的通话
  this.conversations = [];
}};

预配虚拟号码

虚拟号码用于向应用程序用户隐藏真实电话号码。

以下工作流图表显示了预配和配置虚拟号码的过程:

sequenceDiagram 参与者应用 参与者 Vonage 参与者 UserA 参与者 UserB Note over App,Vonage: 初始化 应用->>Vonage:搜索号码 Vonage->>应用:找到的号码 应用->>Vonage:提供号码 Vonage- ->>应用:预配的号码 应用->> Vonage:配置号码 Vonage->>应用:已配置的号码

要预配虚拟号码,请搜索符合条件的可用号码。例如,特定国家/地区中具有语音功能的电话号码:



const Nexmo = require('nexmo');

/**
 * Create a new VoiceProxy
 */
const VoiceProxy = function(config) {
  this.config = config;

  this.nexmo = new Nexmo({
      apiKey: this.config.NEXMO_API_KEY,
      apiSecret: this.config.NEXMO_API_SECRET
    },{
      debug: this.config.NEXMO_DEBUG
    });

  // Virtual Numbers to be assigned to UserA and UserB
  this.provisionedNumbers = [].concat(this.config.PROVISIONED_NUMBERS);

  // In progress conversations
  this.conversations = [];
};

/**
 * Provision two virtual numbers. Would provision more in a real app.
 */
VoiceProxy.prototype.provisionVirtualNumbers = function() {
  // Buy a UK number with VOICE capabilities.
  // For this example we'll also get SMS so we can send them a text notification
  this.nexmo.number.search('GB', {features: 'VOICE,SMS'}, function(err, res) {
    if(err) {
      console.error(err);
    }
    else {
      const numbers = res.numbers;

      // For demo purposes:
      // - Assume that at least two numbers will be available
      // - Rent just two virtual numbers: one for each conversation participant
      this.rentNumber(numbers[0]);
      this.rentNumber(numbers[1]);
    }
  }.bind(this));
};

然后租用所需号码,并将其与应用程序关联。

注意: 某些类型的号码要求您具有邮政地址才能租用。如果无法通过编程方式获取号码,请访问 Dashboard,在这里,您可以根据需要租用号码。

当与应用程序关联的各个号码发生任何事件时,Vonage 会向您的 Webhook 端点发送一个请求,以请求事件相关信息。配置完成后,将电话号码存起来供以后使用:

/**
 * Rent the given numbers
 */
VoiceProxy.prototype.rentNumber = function(number) {
  this.nexmo.number.buy(number.country, number.msisdn, function(err, res) {
    if(err) {
      console.error(err);
    }
    else {
      this.configureNumber(number);
    }
  }.bind(this));
};

/**
 * Configure the number to be associated with the Voice Proxy application.
 */
VoiceProxy.prototype.configureNumber = function(number) {
  const options = {
    voiceCallbackType: 'app',
    voiceCallbackValue: this.config.NEXMO_APP_ID,
  };
  this.nexmo.number.update(number.country, number.msisdn, options, function(err, res) {
    if(err) {
      console.error(err);
    }
    else {
      this.provisionedNumbers.push(number);
    }
  }.bind(this));
};

要预配虚拟号码,请在浏览器中访问http://localhost:3000/numbers/provision

现在,您已经拥有掩盖用户间通信所需的虚拟号码。

注意: 在生产应用程序中,可从虚拟号码池中进行选择。但是,您应该保留此功能,以便即时租用其他号码。

创建呼叫

创建呼叫的工作流为:

sequenceDiagram 参与者应用 参与者 Vonage 参与者 UserA 参与者 UserB Note over App,Vonage: 对话开始 应用->>Vonage:搜索号码 Vonage->>Basic Number Insight⏎ Vonage->>应用:Number Insight 响应 应用->>App: Map Real/Virtual Numbers
for 每个参与者 应用->>Vonage: 发短信给 UserA Vonage->>UserA: 短信 应用->>Vonage: 发短信给 UserB Vonage->>UserB: 短信

以下呼叫:

/**
 * Create a new tracked conversation so there is a real/virtual mapping of numbers.
 */
VoiceProxy.prototype.createConversation = function(userANumber, userBNumber, cb) {
  this.checkNumbers(userANumber, userBNumber)
    .then(this.saveConversation.bind(this))
    .then(this.sendSMS.bind(this))
    .then(function(conversation) {
      cb(null, conversation);
    })
    .catch(function(err) {
      cb(err);
    });
};

验证电话号码

当应用程序用户提供其电话号码时,请使用 Number Insight 来确保这些号码有效。您还可以查看电话号码是在哪个国家/地区注册的:

/**
 * Ensure the given numbers are valid and which country they are associated with.
 */
VoiceProxy.prototype.checkNumbers = function(userANumber, userBNumber) {
  const niGetPromise = (number) => new Promise ((resolve) => {
    this.nexmo.numberInsight.get(number, (error, result) => {
      if(error) {
        console.error('error',error);
      }
      else {
        return resolve(result);
      }
    })
  });

  const userAGet = niGetPromise({level: 'basic', number: userANumber});
  const userBGet = niGetPromise({level: 'basic', number: userBNumber});

  return Promise.all([userAGet, userBGet]);
};

将电话号码映射到真实号码

一旦确定电话号码有效,就将每个真实号码映射到一个虚拟号码并保存呼叫:

/**
 * Store the conversation information.
 */
VoiceProxy.prototype.saveConversation = function(results) {
  let userAResult = results[0];
  let userANumber = {
    msisdn: userAResult.international_format_number,
    country: userAResult.country_code
  };

  let userBResult = results[1];
  let userBNumber = {
    msisdn: userBResult.international_format_number,
    country: userBResult.country_code
  };

  // Create conversation object - for demo purposes:
  // - Use first indexed LVN for user A
  // - Use second indexed LVN for user B
  let conversation = {
    userA: {
      realNumber: userANumber,
      virtualNumber: this.provisionedNumbers[0]
    },
    userB: {
      realNumber: userBNumber,
      virtualNumber: this.provisionedNumbers[1]
    }
  };

  this.conversations.push(conversation);

  return conversation;
};

发送确认短信

在私人通信系统中,当一个用户与另一个用户联系时,主叫方用电话拨打虚拟号码。

发短信通知每个对话参与者他们需要拨打的虚拟号码:

/**
 * Send an SMS to each conversation participant so they know each other's
 * virtual number and can call either other via the proxy.
 */
VoiceProxy.prototype.sendSMS = function(conversation) {
  // Send UserA conversation information
  // From the UserB virtual number
  // To the UserA real number
  this.nexmo.message.sendSms(conversation.userB.virtualNumber.msisdn,
                             conversation.userA.realNumber.msisdn,
                             'Call this number to talk to UserB');

  // Send UserB conversation information
  // From the UserA virtual number
  // To the UserB real number
  this.nexmo.message.sendSms(conversation.userA.virtualNumber.msisdn,
                             conversation.userB.realNumber.msisdn,
                             'Call this number to talk to UserB');

  return conversation;
};

用户不能互发短信。要启用此功能,您需要设置私人短信通信

在本用例中,各用户通过短信收到了虚拟号码。在其他系统中,可以使用电子邮件、应用内通知或以预定义号码的形式提供虚拟号码。

处理呼入电话

当 Vonage 收到虚拟号码的呼入电话时,它会向您在创建语音应用程序时设置的 Webhook 端点发出请求:

sequenceDiagram 参与者应用 参与者 Vonage 参与用户 A 参与用户 B 用户 A 注释,Vonage: UserA calls UserB's
Vonage Number 用户 A->>Vonage: 拨打虚拟号码 Vonage->>应用:呼入电话(从,至)

从入站 Webhook 中提取 和 ,并将它们传递给语音代理业务逻辑:

app.get('/proxy-call', function(req, res) {{
  const from = req.query.from;
  const to = req.query.to;

  const ncco = voiceProxy.getProxyNCCO(from, to);
  res.json(ncco);
}});

将真实电话号码反向映射到虚拟号码

您已经知道拨打电话的电话号码和接收者的虚拟号码,现在将入站虚拟号码反向映射到出站真实电话号码:

sequenceDiagram 参与者应用 参与者 Vonage 参与者 UserA 参与者 UserB UserA->>Vonage: Vonage->>应用: Note right of App:查找 UserB 的真实号码
应用->>应用:号‑码映射查找

呼叫方向可以标识为:

  • 号码是 UserA 真实号码, 号码是 UserB Vonage 号码
  • 号码是 UserB 真实号码,号码是 UserA Vonage 号码
const fromUserAToUserB = function(from, to, conversation) {
  return (from === conversation.userA.realNumber.msisdn &&
          to === conversation.userB.virtualNumber.msisdn);
};
const fromUserBToUserA = function(from, to, conversation) {
  return (from === conversation.userB.realNumber.msisdn &&
          to === conversation.userA.virtualNumber.msisdn);
};

/**
 * Work out real number to virtual number mapping between users.
 */
VoiceProxy.prototype.getProxyRoute = function(from, to) {
  let proxyRoute = null;
  let conversation;
  for(let i = 0, l = this.conversations.length; i < l; ++i) {
    conversation = this.conversations[i];

    // Use to and from to determine the conversation
    const fromUserA = fromUserAToUserB(from, to, conversation);
    const fromUserB = fromUserBToUserA(from, to, conversation);

    if(fromUserA || fromUserB) {
      proxyRoute = {
        conversation: conversation,
        to: fromUserA? conversation.userB : conversation.userA,
        from: fromUserA? conversation.userA : conversation.userB
      };
      break;
    }
  }

  return proxyRoute;
};

完成号码查找后,剩下的就是代理呼叫了。

代理呼叫

将呼叫代理到虚拟号码所关联的电话号码。号码始终是虚拟号码,是真实电话号码。

sequenceDiagram 参与者应用 参与者 Vonage 参与者 UserA 参与者 UserB UserA->>Vonage: Vonage->>应用: 应用->>Vonage:连接(代理) Note right of App:将呼入电话代理到 UserB 的真实号码 Vonage->>UserB: 呼叫 Note over UserA,UserB:UserA 呼叫了 UserB。但 UserA 没有⏎ UserB 的真实号码, 反之亦然。

为此,请创建一个 NCCO(Nexmo 呼叫控制对象)。该 NCCO 使对话 操作来读出一些文本。对话 完成后,内容操作将呼叫转接到真实号码。

/**
 * Build the NCCO response to instruct Nexmo how to handle the inbound call.
 */
VoiceProxy.prototype.getProxyNCCO = function(from, to) {
  // Determine how the call should be routed
  const proxyRoute = this.getProxyRoute(from, to);

  if(proxyRoute === null) {
    const errorText = 'No conversation found' +
                    ' from: ' + from +
                    ' to: ' + to;
    throw new Error(errorText);
  }

  // Build the NCCO
  let ncco = [];

  const textAction = {
    action: 'talk',
    text: 'Please wait whilst we connect your call'
  };
  ncco.push(textAction);

  const connectAction = {
    action: 'connect',
    from: proxyRoute.from.virtualNumber.msisdn,
    endpoint: [{
      type: 'phone',
      number: proxyRoute.to.realNumber.msisdn
    }]
  };
  ncco.push(connectAction);

  return ncco;
};

Web 服务器将 NCCO 返回给 Vonage。

app.get('/proxy-call', function(req, res) {{
  const from = req.query.from;
  const to = req.query.to;

  const ncco = voiceProxy.getProxyNCCO(from, to);
  res.json(ncco);
}});

结语

您已经学习了如何为私人通信构建语音代理。您预配和配置了电话号码,执行了号码洞察,将真实号码映射到虚拟号码以确保匿名性,处理了呼入电话并将该呼叫代理到另一个用户。

更多信息