diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 7d56a71..12776e1 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,2 +1,2 @@
# Global rule:
-* @Bandwidth-Samples/band-swi
+* @Bandwidth-Samples/band-swi @Bandwidth-Samples/band-devx
diff --git a/.github/actions/maven-test/Dockerfile b/.github/actions/maven-test/Dockerfile
deleted file mode 100644
index 8f75cd0..0000000
--- a/.github/actions/maven-test/Dockerfile
+++ /dev/null
@@ -1,11 +0,0 @@
-# Container image that runs your code
-FROM maven:3.6.2-jdk-14
-
-# Copies your code file from your action repository to the filesystem path `/` of the container
-COPY entrypoint.sh /entrypoint.sh
-
-#Make entrypoint.sh exacutable
-RUN chmod +x /entrypoint.sh
-
-# Code file to execute when the docker container starts up (`entrypoint.sh`)
-ENTRYPOINT ["/entrypoint.sh"]
diff --git a/.github/actions/maven-test/action.yml b/.github/actions/maven-test/action.yml
deleted file mode 100644
index 598f220..0000000
--- a/.github/actions/maven-test/action.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-# action.yml
-name: 'Test'
-description: 'Runs Maven test command'
-runs:
- using: 'docker'
- image: 'Dockerfile'
diff --git a/.github/actions/maven-test/entrypoint.sh b/.github/actions/maven-test/entrypoint.sh
deleted file mode 100644
index 91f2160..0000000
--- a/.github/actions/maven-test/entrypoint.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-
-mvn test
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..78cbe5d
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,19 @@
+version: 2
+
+updates:
+ - package-ecosystem: maven
+ directory: /
+ schedule:
+ interval: monthly
+ open-pull-requests-limit: 5
+ ignore:
+ - dependency-name: "com.bandwidth.sdk:bandwidth-sdk"
+ update-types:
+ - version-update:semver-minor
+ - version-update:semver-patch
+ groups:
+ dependencies:
+ patterns:
+ - "*"
+ exclude-patterns:
+ - "com.bandwidth.sdk:bandwidth-sdk"
diff --git a/.github/workflows/on-pull-request.yaml b/.github/workflows/on-pull-request.yaml
deleted file mode 100644
index b7735f5..0000000
--- a/.github/workflows/on-pull-request.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-name: Test
-
-on:
- pull_request:
- branches:
- - main
-
-jobs:
- maven-test:
- name: Test
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- - name: Test
- uses: ./.github/actions/maven-test
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..b7543fe
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,15 @@
+name: Test
+
+on:
+ pull_request:
+ branches:
+ - main
+ schedule:
+ - cron: "0 9 * * 1"
+
+jobs:
+ contract-test:
+ name: Contract Test
+ uses: Bandwidth-Samples/sample-app-tooling/.github/workflows/java.yml@main
+ with:
+ sample: messaging-auto-respond
diff --git a/LICENSE b/LICENSE
index df9461d..ddc77f4 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2020 Bandwidth Samples
+Copyright (c) 2026 Bandwidth Samples
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 2aa7864..e992729 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# Auto-Respond to SMS
+# Messaging Auto Respond
-
+
@@ -11,15 +11,17 @@
* [Running the Application](#running-the-application)
* [Environmental Variables](#environmental-variables)
* [Callback URLs](#callback-urls)
- * [Ngrok](#ngrok)
+ * [Ngrok](#ngrok)
# Description
-This app automatically responds to texts sent to the associated Bandwidth number. Any texts sent to the `BW_NUMBER` will be auto-responded to using the auto response map. Valid words are: `STOP`, `QUIT`, `HELP`, and `INFO`.
+This sample app automatically replies to texts sent to your Bandwidth number. The reply depends on the text received: `STOP`, `QUIT`, `HELP` and `INFO` each get their own reply, and anything else gets a prompt to reply `HELP`. Keywords are matched against the whole message and are not case sensitive.
-To use this app, you must check the "Use multiple callback URLs" box on the application page in Dashboard. Then in Dashboard, set the INBOUND CALLBACK to `/callbacks/inbound/messaging` and the STATUS CALLBACK to `/callbacks/outbound/messaging/status`. The same can be accomplished via the Dashboard API by setting InboundCallbackUrl and OutboundCallbackUrl respectively.
+The `STOP` and `QUIT` replies are for demonstration only. This app doesn't keep track of opt-outs, so it will keep replying to a number that has sent `STOP`.
-Inbound callbacks are sent notifying you of a received message on a Bandwidth number, this app sends a custom response to those messages based on their content. Outbound callbacks are status updates for messages sent from a Bandwidth number, this app has a dedicated response for each type of status update.
+In order to use the correct endpoints, you must check the "Use multiple callback URLs" box on the application page in Dashboard. Then in Dashboard, set the INBOUND CALLBACK to `/callbacks/inbound/messaging` and the STATUS CALLBACK to `/callbacks/outbound/messaging/status`. The same can be accomplished via the Dashboard API by setting `InboundCallbackUrl` and `OutboundCallbackUrl` respectively.
+
+Inbound callbacks are sent notifying you of a received message on a Bandwidth number. This app prints the phone numbers involved and the text received, then sends the reply. Outbound callbacks are status updates for messages sent from a Bandwidth number, this app has a dedicated response for each type of status update.
# Pre-Requisites
@@ -31,10 +33,10 @@ For more information about API credentials see our [Account Credentials](https:/
# Running the Application
-To install the required packages for this app, run the command:
+To install the required packages and build the app, run the command:
```sh
-mvn clean install
+mvn package
```
Use the following command to run the application:
@@ -49,8 +51,8 @@ The sample app uses the below environmental variables.
```sh
BW_ACCOUNT_ID # Your Bandwidth Account Id
-BW_USERNAME # Your Bandwidth API Username
-BW_PASSWORD # Your Bandwidth API Password
+BW_CLIENT_ID # Your Bandwidth API OAuth Client Id
+BW_CLIENT_SECRET # Your Bandwidth API OAuth Client Secret
BW_NUMBER # The Bandwidth phone number involved with this application
BW_MESSAGING_APPLICATION_ID # Your Messaging Application Id created in the dashboard
LOCAL_PORT # The port number you wish to run the sample on
@@ -66,13 +68,11 @@ Below are the callback paths:
## Ngrok
-A simple way to set up a local callback URL for testing is to use the free tool [ngrok](https://ngrok.com/).
+A simple way to set up a local callback URL for testing is to use the free tool [ngrok](https://ngrok.com/).
After you have downloaded and installed `ngrok` run the following command to open a public tunnel to your port (`$LOCAL_PORT`)
-```cmd
+```sh
ngrok http $LOCAL_PORT
```
-You can view your public URL at `http://127.0.0.1:4040` after ngrok is running. You can also view the status of the tunnel and requests/responses here.
-
-Once your public ngrok url has been created, you can use it as the `BASE_CALLBACK_URL` environmental variable and set it in the messaging application created in the [pre-requisites](#pre-requisites) section.
+You can view your public URL at `http://127.0.0.1:4040` after ngrok is running. You can also view the status of the tunnel and requests/responses here. Once your public ngrok url has been created, use it as the base of the callback URLs in the messaging application created in the [Pre-Requisites](#pre-requisites) section.
diff --git a/application.properties b/application.properties
deleted file mode 100644
index e78beac..0000000
--- a/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.jackson.mapper.ACCEPT_CASE_INSENSITIVE_ENUMS = true
diff --git a/icon-messaging.svg b/icon-messaging.svg
index cc8cad7..fa130bd 100644
--- a/icon-messaging.svg
+++ b/icon-messaging.svg
@@ -1 +1,3 @@
-
+
diff --git a/pom.xml b/pom.xml
index 1562383..eaf70c7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,30 +11,29 @@
UTF-8UTF-8
- 11
- 2.2.6.RELEASE
+ 21
+ 4.1.2
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring.version}
+ pom
+ import
+
+
+
+
com.bandwidth.sdkbandwidth-sdk
- 9.1.2
-
-
-
-
- commons-io
- commons-io
- 2.8.0
-
-
-
- javax.activation
- javax.activation-api
- 1.2.0
+ 18.3.1
@@ -42,20 +41,6 @@
spring-boot-starter-web${spring.version}
-
-
- org.springframework.boot
- spring-boot-starter-test
- ${spring.version}
- test
-
-
-
- org.projectlombok
- lombok
- 1.18.30
- provided
-
@@ -68,7 +53,7 @@
org.apache.maven.pluginsmaven-compiler-plugin
- 3.10.1
+ 3.16.0${java.version}
diff --git a/src/main/java/com/bandwidth/Main.java b/src/main/java/com/bandwidth/Main.java
index c24bdae..23e0100 100644
--- a/src/main/java/com/bandwidth/Main.java
+++ b/src/main/java/com/bandwidth/Main.java
@@ -1,10 +1,11 @@
package com.bandwidth;
-
+import java.util.Collections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@@ -14,9 +15,18 @@ public class Main {
static Logger logger = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
- SpringApplication.run(Main.class, args);
+ String port = System.getenv("LOCAL_PORT");
+ if (System.getenv("BW_ACCOUNT_ID") == null || System.getenv("BW_CLIENT_ID") == null
+ || System.getenv("BW_CLIENT_SECRET") == null || System.getenv("BW_MESSAGING_APPLICATION_ID") == null
+ || System.getenv("BW_NUMBER") == null || port == null || !port.matches("\\d+")) {
+ System.err.println("Please set the environmental variables defined in the README");
+ System.exit(1);
+ }
+
+ SpringApplication application = new SpringApplication(Main.class);
+ application.setDefaultProperties(Collections.singletonMap("server.port", port));
+ application.run(args);
logger.info("Server has started...");
}
-
}
diff --git a/src/main/java/com/bandwidth/controller/CallbacksController.java b/src/main/java/com/bandwidth/controller/CallbacksController.java
index 313cb18..7196a1b 100644
--- a/src/main/java/com/bandwidth/controller/CallbacksController.java
+++ b/src/main/java/com/bandwidth/controller/CallbacksController.java
@@ -1,19 +1,23 @@
package com.bandwidth.controller;
-import com.bandwidth.Main;
import com.bandwidth.sdk.ApiClient;
-import com.bandwidth.sdk.ApiResponse;
-import com.bandwidth.sdk.ApiException;
-import com.bandwidth.sdk.ApiClient;
-import com.bandwidth.sdk.auth.HttpBasicAuth;
-import com.bandwidth.sdk.Configuration;
-import com.bandwidth.sdk.model.*;
+import com.bandwidth.sdk.JSON;
import com.bandwidth.sdk.api.MessagesApi;
-import com.bandwidth.model.CreateMessage;
-import com.bandwidth.model.MessageReply;
-import com.bandwidth.model.*;
+import com.bandwidth.sdk.model.InboundCallback;
+import com.bandwidth.sdk.model.InboundCallbackMessage;
+import com.bandwidth.sdk.model.InboundCallbackTypeEnum;
+import com.bandwidth.sdk.model.Message;
+import com.bandwidth.sdk.model.MessageRequest;
+import com.bandwidth.sdk.model.StatusCallback;
+import com.bandwidth.sdk.model.StatusCallbackTypeEnum;
+
+import java.util.Arrays;
+import java.util.Map;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -24,84 +28,82 @@ public class CallbacksController {
Logger logger = LoggerFactory.getLogger(CallbacksController.class);
- private String username = System.getenv("BW_USERNAME");
- private String password = System.getenv("BW_PASSWORD");
- private String accountId = System.getenv("BW_ACCOUNT_ID");
- private String bwNumber = System.getenv("BW_NUMBER");
- private String applicationId = System.getenv("BW_MESSAGING_APPLICATION_ID");
-
- ApiClient defaultClient = Configuration.getDefaultApiClient();
- HttpBasicAuth Basic = (HttpBasicAuth) defaultClient.getAuthentication("Basic");
- private final MessagesApi api = new MessagesApi(defaultClient);
- public MessageRequest messageRequest = new MessageRequest();
- public MessageReply messageReply = new MessageReply();
-
-
- @RequestMapping("/outbound/messaging/status")
- public void statusCallback(@RequestBody InboundMessageCallback[] callbacks) {
-
- for (InboundMessageCallback callback : callbacks) {
- logger.info(callback.getType());
- logger.info(callback.getDescription());
- switch (callback.getType()) {
- case "message-sending":
- logger.info("message-sending type is only for MMS");
- break;
- case "message-delivered":
- logger.info("your message has been handed off to the Bandwidth's MMSC network, but has not been confirmed at the downstream carrier");
- break;
- case "message-failed":
- logger.info("For MMS and Group Messages, you will only receive this callback if you have enabled delivery receipts on MMS. ");
- break;
- default:
- logger.info("Message type does not match endpoint. This endpoint is used for message status callbacks only.");
- break;
- }
+ private final String accountId = System.getenv("BW_ACCOUNT_ID");
+ private final String clientId = System.getenv("BW_CLIENT_ID");
+ private final String clientSecret = System.getenv("BW_CLIENT_SECRET");
+ private final String applicationId = System.getenv("BW_MESSAGING_APPLICATION_ID");
+ private final String number = System.getenv("BW_NUMBER");
+
+ private final MessagesApi messagesApi = new MessagesApi(new ApiClient(clientId, clientSecret, null));
+
+ private static final Map STATUS_MESSAGES = Map.of(
+ StatusCallbackTypeEnum.MESSAGE_SENDING, "message-sending type is only for MMS.",
+ StatusCallbackTypeEnum.MESSAGE_DELIVERED, "Your message has been handed off to the Bandwidth's MMSC network, but has not been confirmed at the downstream carrier.",
+ StatusCallbackTypeEnum.MESSAGE_FAILED, "For MMS and Group Messages, you will only receive this callback if you have enabled delivery receipts on MMS."
+ );
+
+ private static final Map REPLIES = Map.of(
+ "stop", "STOP: OK, you'll no longer receive messages from us.",
+ "quit", "QUIT: OK, you'll no longer receive messages from us.",
+ "help", "Valid words are: STOP, QUIT, HELP, and INFO. Reply STOP or QUIT to opt out.",
+ "info", "INFO: This is the test responder service. Reply STOP or QUIT to opt out."
+ );
+ private static final String DEFAULT_REPLY = "Please respond with a valid word. Reply HELP for help.";
+
+ // The SDK models are Gson-based, so callbacks are parsed with the SDK's Gson instead of Spring's Jackson
+ private static T[] parseCallbacks(String body, Class type) {
+ try {
+ return JSON.getGson().fromJson(body, type);
+ } catch (RuntimeException e) {
+ return null;
+ }
+ }
+
+ private static String autoReply(String text) {
+ return REPLIES.getOrDefault(text == null ? "" : text.strip().toLowerCase(), DEFAULT_REPLY);
+ }
+
+ private void sendReply(String to, String text) {
+ MessageRequest messageRequest = new MessageRequest()
+ .applicationId(applicationId)
+ .addToItem(to)
+ .from(number)
+ .text(text);
+
+ try {
+ Message message = messagesApi.createMessage(accountId, messageRequest);
+ logger.info("Sent reply " + message.getId() + "\nTo: " + to + "\nText: " + text);
+ } catch (Exception e) {
+ logger.error("Failed to send reply: " + e.getMessage());
}
}
- @RequestMapping("/inbound/messaging")
- public void inboundCallback(@RequestBody InboundMessageCallback[] callbacks) {
-
- Basic.setUsername(username);
- Basic.setPassword(password);
- messageRequest.applicationId(applicationId);
- messageRequest.priority(PriorityEnum.DEFAULT);
- messageRequest.from(bwNumber);
-
-
- for (InboundMessageCallback callback : callbacks) {
- logger.info(callback.getType());
- logger.info(callback.getDescription());
-
- messageRequest.addToItem(callback.getMessage().getFrom());
-
- logger.info(callback.getMessage().getText());
- switch (callback.getMessage().getText().toLowerCase()) {
- case "stop":
- messageRequest.text("STOP: OK, you'll no longer receive messages from us.");
- break;
- case "quit":
- messageRequest.text("QUIT: OK, you'll no longer receive messages from us.");
- break;
- case "info":
- messageRequest.text("INFO: This is the test responder service. Reply STOP or QUIT to opt out.");
- break;
- case "help":
- messageRequest.text("HELP: This is the test responder service. Reply STOP or QUIT to opt out.");
- break;
- default:
- messageRequest.text("AUTO-REPLY: Thank you for your message! Please respond with a valid word. Reply HELP for help.");
- break;
- }
- try {
- Message response = api.createMessage(accountId, messageRequest);
- messageReply.setSuccess(true);
- } catch (ApiException e) { // Bandwidth API response status not 2XX
- messageReply.setSuccess(false);
- messageReply.setError(e.getMessage());
- }
- }
+ @PostMapping("/outbound/messaging/status")
+ public ResponseEntity statusCallback(@RequestBody String body) {
+ StatusCallback[] callbacks = parseCallbacks(body, StatusCallback[].class);
+ if (callbacks == null || !Arrays.stream(callbacks).allMatch(callback -> STATUS_MESSAGES.containsKey(callback.getType()))) {
+ return ResponseEntity.badRequest().build();
+ }
+
+ for (StatusCallback callback : callbacks) {
+ logger.info(STATUS_MESSAGES.get(callback.getType()));
+ }
+ return ResponseEntity.noContent().build();
}
+ @PostMapping("/inbound/messaging")
+ public ResponseEntity inboundCallback(@RequestBody String body) {
+ InboundCallback[] callbacks = parseCallbacks(body, InboundCallback[].class);
+ if (callbacks == null || !Arrays.stream(callbacks).allMatch(callback -> callback.getType() == InboundCallbackTypeEnum.MESSAGE_RECEIVED)) {
+ return ResponseEntity.badRequest().build();
+ }
+
+ for (InboundCallback callback : callbacks) {
+ InboundCallbackMessage message = callback.getMessage();
+ logger.info(callback.getDescription() + "\nTo: " + message.getTo().iterator().next()
+ + "\nFrom: " + message.getFrom() + "\nText: " + message.getText());
+ sendReply(message.getFrom(), autoReply(message.getText()));
+ }
+ return ResponseEntity.noContent().build();
+ }
}
diff --git a/src/main/java/com/bandwidth/model/CreateMessage.java b/src/main/java/com/bandwidth/model/CreateMessage.java
deleted file mode 100644
index b137546..0000000
--- a/src/main/java/com/bandwidth/model/CreateMessage.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.bandwidth.model;
-
-import lombok.Getter;
-import lombok.Setter;
-
-@Getter
-@Setter
-public class CreateMessage {
-
- private String to;
- private String from;
- private String text;
-}
diff --git a/src/main/java/com/bandwidth/model/MessageReply.java b/src/main/java/com/bandwidth/model/MessageReply.java
deleted file mode 100644
index db0522b..0000000
--- a/src/main/java/com/bandwidth/model/MessageReply.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.bandwidth.model;
-
-import lombok.Getter;
-import lombok.Setter;
-
-@Getter
-@Setter
-public class MessageReply {
-
- private boolean success;
- private String error;
-}