From b43727ef8debb0cae2d9a50f2051d1e99bc1a373 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Thu, 29 Oct 2026 16:43:15 -0400 Subject: [PATCH] DX-3359 Update Sample --- .github/CODEOWNERS | 2 +- .github/actions/maven-test/Dockerfile | 11 -- .github/actions/maven-test/action.yml | 6 - .github/actions/maven-test/entrypoint.sh | 3 - .github/dependabot.yml | 19 ++ .github/workflows/on-pull-request.yaml | 16 -- .github/workflows/test.yml | 15 ++ LICENSE | 2 +- README.md | 30 +-- application.properties | 1 - icon-messaging.svg | 4 +- pom.xml | 47 ++--- src/main/java/com/bandwidth/Main.java | 16 +- .../controller/CallbacksController.java | 172 +++++++++--------- .../com/bandwidth/model/CreateMessage.java | 13 -- .../com/bandwidth/model/MessageReply.java | 12 -- 16 files changed, 170 insertions(+), 199 deletions(-) delete mode 100644 .github/actions/maven-test/Dockerfile delete mode 100644 .github/actions/maven-test/action.yml delete mode 100644 .github/actions/maven-test/entrypoint.sh create mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/on-pull-request.yaml create mode 100644 .github/workflows/test.yml delete mode 100644 application.properties delete mode 100644 src/main/java/com/bandwidth/model/CreateMessage.java delete mode 100644 src/main/java/com/bandwidth/model/MessageReply.java 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 - + Messaging Quick Start Guide @@ -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-8 UTF-8 - 11 - 2.2.6.RELEASE + 21 + 4.1.2 + + + + org.springframework.boot + spring-boot-dependencies + ${spring.version} + pom + import + + + + com.bandwidth.sdk bandwidth-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.plugins maven-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; -}