使用Twilio和Java建立一个网络应用的语言(附代码)

284 阅读8分钟

生活已经够复杂的了,还需要在朋友和家人之间转发信息,以确保每个人都能了解最新的情况。在这篇文章中,我将分享我如何设置一个Twilio号码,我把它作为 "我 "的电话号码给我孩子的学校,这样,所有发自该号码的信息都会自动转发给我和我妻子,我们中的任何一个都可以回复。我希望你能想到在你自己的生活中,这可能是很方便的情况:包裹交付、聚会计划、约会提醒,这个名单很长。

在这篇文章中,我将使用Java,但同样的方法也适用于任何你可以建立一个网络应用的语言。如果你对JavaScript很熟悉,那么Code Exchange上的SMS Forwarding to Multiple Numbers将是一个很好的起点。

我们正在建立什么?

这里的一切都基于一个单一的Twilio电话号码,以及你和你的家人或朋友有手机的群体。你可以把Twilio号码给别人,就像你自己的号码一样。

我们将设置Twilio号码,以便当有人给它发短信时,信息将被转发给你的小组中的每个人。在这个例子中,我将使用两个小组成员,但代码的设计使你可以使用你想要的数量:

Diagram of a phone sending an SMS to a Twilio number which is forwarded on to two other phones

当你的小组以外的人(左边的手机)向Twilio号码发送短信时,该信息将被转发给小组中的每个人(右边的手机)。这些信息看起来是由Twilio号码发出的,所以真正的发件人的电话号码会被加在信息的开头。

当你的小组中的任何人回复Twilio号码时,他们应该把真正的目的地号码放在信息的开头,在信息被转发之前,这个号码会被删除。你群里的每个人也会得到一份消息的副本:

Diagram of one of the phones from the previous diagram replying to the SMS. The message is sent on to all participants.

请注意,如果你想把一个群组成员的信息发送给所有其他人,你可以通过在信息前加上自己的号码来实现。

使用Twilio的可编程消息系统

为了告诉Twilio如何响应传入的短信,我们将使用webhooks。当一条消息进入我们的电话号码时,Twilio将向我们提供的URL发出HTTP请求。我们将建立一个应用程序,在HTTP响应中发送指令,告诉Twilio接下来要做什么。

Same diagram as above with the addition of an HTTP request/response from Twilio to "your app"

HTTP响应中的指令是用TwiML写的,包括多个Message标签,告诉Twilio发送新的文本信息。这些消息的内容和目的地取决于谁发来的消息以及他们说了什么;这些属性是HTTP请求的一部分。继续阅读,看看如何使用Java和Spring Boot构建这个应用。

构建应用程序

Spring Boot是在Java中构建Web应用程序的最流行的框架。我喜欢用Spring Initializr开始新项目。如果你想跟着编码,可以使用这个链接,它设置的选项和我用的一样,或者在GitHub上找到成品项目

下载、解压并在你的IDE中打开生成的项目。在src/main/javacom.example.smsgroupbroadcast 包中会有一个单独的类,叫做SmsGroupBroadcastApplication 。你不需要编辑该类,但它有一个main 方法,可以用来运行应用程序。

在同一个包中,在一个名为SmsHandler.java 的新文件中创建一个新的类,名为SmsHandler 。为了不让事情变得太复杂,我们将把所有的代码放在这个类里。当我们完成时,它将有大约100行长。

从我们需要在启动时运行的代码开始:

@RestController
public class SmsHandler {

   private final Set<String> groupPhoneNumbers;

   public SmsHandler() {
       groupPhoneNumbers = Set.of(System.getenv("GROUP_PHONE_NUMBERS").split(","));
   }

// more code will go in here

}

@RestController 注解告诉Spring,这个类应该被扫描出可以处理HTTP请求的方法。我们很快就会写一个。

第4行的Set<String> groupPhoneNumbers ,在构造函数中通过读取一个环境变量进行初始化,该变量的值是一个以逗号分隔的E.164格式的电话号码字符串。这些号码应该是你的手机和你小组中其他人的手机(上图右边的所有人)。你可以直接在你的IDE中设置环境变量。

Screenshot showing how to set environment variables in IntelliJ IDEA

IntelliJ IDEA环境变量配置

一种处理HTTP请求的方法

为了更容易写出正确的TwiML,我们将使用Twilio的Java辅助库。在pom.xml ,即项目顶层的Maven配置文件的<dependencies> 部分中添加以下代码:

<dependency>
  <groupId>com.twilio.sdk</groupId>
  <artifactId>twilio</artifactId>
  <version>8.18.0</version>
</dependency>

我们一直建议使用最新版本的Twilio辅助库。在撰写本文时,最新版本是8.18.0 ,你可以在mvnrepository.com上查看最新版本

这时你可能需要告诉你的IDE重新加载Maven的变化。然后,在你的SmsHandler 类中加入这段代码:

@RequestMapping(
    value = "/sms",
    method = {RequestMethod.GET, RequestMethod.POST},
    produces = "application/xml")
@ResponseBody
public String handleSmsWebhook(
        @RequestParam("From") String fromNumber,
        @RequestParam("To")   String twilioNumber,
        @RequestParam("Body") String messageBody) {

    List<Message> outgoingMessages;

    if (groupPhoneNumbers.contains(fromNumber)) {
            outgoingMessages = messagesSentFromGroup(fromNumber, twilioNumber, messageBody);

    } else {
            outgoingMessages = messagesSentToGroup(fromNumber, twilioNumber, messageBody);
    }

    MessagingResponse.Builder responseBuilder = new MessagingResponse.Builder();
    outgoingMessages.forEach(responseBuilder::message);
    return responseBuilder.build().toXml();
}

这个方法的开头有很多注解,Spring会识别这些注解:

  • @RequestMapping 告诉Spring这个方法应该在 和 的请求中调用, ,并且响应中的 是 。GET POST /sms Content-type application/xml
  • @ResponseBody 告诉Spring,这个方法的返回值应该被用作HTTP响应的主体。
  • @RequestParam 注解告诉Spring从HTTP请求中提取命名的参数,并将它们作为参数传递给方法。这对GETPOST 都有效,尽管这些参数在HTTP请求的不同部分。

该方法的主体创建了一个Message 对象的列表,根据触发该webhook的传入短信是否来自一个组成员,该列表的填充方式不同(第12行)。我们稍后会定义messagesSentFromGroupmessagesSentToGroup 方法,但首先要注意在第19-21行,信息列表是如何通过forEach 添加到MessagingResponse

处理来自非小组成员的消息

如果上述方法中的groupPhoneNumbers.contains(fromNumber) 检查返回false ,那么我们就知道这个消息是来自我们组以外的人。在这种情况下,messagesSentToGroup 方法被调用,以获得一个Message 对象的列表,该列表代表将转发给每个组员的传入消息的副本:

private List<Message> messagesSentToGroup(String fromNumber, String twilioNumber, String messageBody) {

    List<Message> messages = new ArrayList<>();

    String finalMessage = "From " + fromNumber + " " + messageBody;
    groupPhoneNumbers.forEach(groupMemberNumber ->
                messages.add(createMessageTwiml(groupMemberNumber, twilioNumber, finalMessage))
    );

    return messages;
}

我们建立finalMessage ,并为每个组员添加一个消息到列表中。我创建了一个名为createMessageTwiml 的小型辅助方法,将Twilio辅助库的构建模式代码变成了一个单行代码。我觉得这样做是值得的,因为我们将在本课中多次建立Message对象。这个方法看起来像这样:

private Message createMessageTwiml(String to, String from, String body) {
    return new Message.Builder()
        .to(to)
        .from(from)
        .body(new Body.Builder(body).build())
        .build();
}

来自群组成员的消息

当小组成员向Twilio号码发送消息时,他们应该在前面加上真正的目的地号码。

An SMS saying "+4477xxxx Thank you!"

信息中的 "谢谢你!"部分将Twilio号码发送到信息开头的号码上。群里的每个人也会得到一份副本。要做到这一点,messagesSentFromGroup 方法必须拆分消息正文,检查它是否以电话号码开头(如果不是,则发送一个有用的提醒),并建立一个外发消息的列表,像这样:

private List<Message> messagesSentFromGroup(String fromNumber, String twilioNumber, String messageBody) {
    List<Message> messages = new ArrayList<>();

    String[] messageParts = messageBody.split("\\s+", 2);

    String e164Regex = "\\+[0-9]+";
    if (messageParts.length != 2 || !messageParts[0].matches(e164Regex)) {
        return List.of(createHowToMessage(fromNumber, twilioNumber));
    }

    String realToNumber = messageParts[0];
    String realMessageBody = messageParts[1];

    // add the message to the non-group recipient
    messages.add(
        createMessageTwiml(realToNumber, twilioNumber, realMessageBody)
    );

    // send a copy of the message to everyone in the group except the sender
    String groupCopyMessage = "To " + realToNumber + " " + realMessageBody;
    groupPhoneNumbers.forEach(groupMemberNumber -> {
        if (!groupMemberNumber.equals(fromNumber)) {
            messages.add(
                createMessageTwiml(groupMemberNumber, twilioNumber, groupCopyMessage));
        }
    });

    return messages;
}

private Message createHowToMessage(String fromNumber, String twilioNumber){
    return createMessageTwiml(fromNumber, twilioNumber,
        "To send a message to someone outside your group, " +
        "don't forget to include the destination phone number at the start, " +
        "eg '+44xxxx Ahoy!'");
}

messagesSentFromGroup 方法可能看起来有很多代码,但它大致上分成了两半。第4-9行处理拆分输入并检查是否以电话号码开头。e164Regex 测试+ 后面的数字,这与E.164电话号码的格式相对应。

messagesSentFromGroup 的其余部分建立了一个我们需要发送的所有信息的列表并将其返回。

最后,有一个单独的方法用于createHowToMessage ,我认为这个方法值得拆分,以保持较长的方法更易读。

在本地运行你的代码

启动该应用程序的最简单方法是使用你的IDE来运行我们之前看到的SmsGroupBroadcasterApplication 类中的main 方法。如果你喜欢使用命令行终端,那么从项目的顶层运行./mvnw spring-boot:run 。无论哪种方式,都要记得设置GROUP_PHONE_NUMBERS 环境变量。

一旦应用程序启动,你可以浏览到http://localhost:8080/sms?From=__from__&To=__to__&Body=__body__,你应该看到一个类似的响应:

<Response>
  <Message from="__to__" to="GROUP_MEMBER_1">
    <Body>From __from__ __body__</Body>
  </Message>
  <Message from="__to__" to="GROUP_MEMBER_2">
    <Body>From __from__ __body__</Body>
  </Message>
</Response>

GROUP_MEMBER_1 而 将是你的 环境变量中的数字。GROUP_MEMBER_2 GROUP_PHONE_NUMBERS

用Twilio使用你的代码

为了让Twilio能够使用你的应用程序进行网络勾选,它将需要一个公共的URL。有很多方法可以在线部署Java代码,但为了简单起见,当你在工作时,我推荐使用ngrok

安装ngrok后,你可以运行ngrok http 8080 ,你会看到一个https 转发URL,你需要在电话号码配置页面上为 "有消息进来 "时设置这个URL。不要忘记把/sms 的路径添加到URL上。

screenshot of setting the "when a message comes in" webhook in the Twilio console.

你也可以使用Twilio CLI为电话号码设置webhook URL。

twilio phone-numbers:update <PHONE_NUMBER> --sms-url=<URL>

如果URL是一个localhost 地址,Twilio CLI将为你创建一个ngrok隧道。

如果你想在真正给出号码之前测试一下工作情况,可以招募一些有电话的朋友,或者使用其他Twilio号码来试试。一旦你对它感到满意,就把应用移到永远在线的公共云上,这样你就不必让你的开发机器全天候运行。这不在这篇文章的范围内,但Spring的打包和部署文档有很多选择。你只需要为每条发送到Twilio号码的短信提供一个HTTP请求,所以要求非常低。