Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/main/Activity/UserActivity/NotifyIdPickupActivity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package Activity.UserActivity;

import Activity.Activity;
import java.util.ArrayList;
import java.util.List;

public class NotifyIdPickupActivity extends UserActivity {
@Override
public List<String> construct() {
List<String> a = new ArrayList<>();
a.add(Activity.class.getSimpleName());
a.add(UserActivity.class.getSimpleName());
a.add(NotifyIdPickupActivity.class.getSimpleName());
return a;
}

public NotifyIdPickupActivity(
String workerUsername, String clientUsername, String idToPickup) {
super(workerUsername, clientUsername, idToPickup);
}
}
7 changes: 7 additions & 0 deletions src/main/Config/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import Issue.IssueController;
import Mail.FileBackfillController;
import Mail.MailController;
import Notification.NotificationController;
import Notification.WindmillNotificationClient;
import Organization.Organization;
import Organization.OrganizationController;
import PDF.PdfController;
Expand Down Expand Up @@ -98,6 +100,8 @@ public static Javalin appFactory(DeploymentLevel deploymentLevel) {
FileBackfillController backfillController = new FileBackfillController(db, fileDao, userDao);
PdfControllerV2 pdfControllerV2 =
new PdfControllerV2(fileDao, formDao, activityDao, userDao, encryptionController);
WindmillNotificationClient notificationClient = new WindmillNotificationClient();
NotificationController notificationController = new NotificationController(activityDao, notificationClient);
// try { do not recommend this block of code, this will delete and regenerate our encryption
// key
// System.out.println("generating keyset");
Expand Down Expand Up @@ -200,6 +204,9 @@ public static Javalin appFactory(DeploymentLevel deploymentLevel) {
app.post("/get-all-activities", activityController.findMyActivities);
app.post("/get-org-activities", activityController.findOrganizationActivities);

/* --------------- NOTIFICATION ROUTES ------------- */
app.post("/notify-id-pickup", notificationController.notifyIdPickup);

/* --------------- FILE BACKFILL ROUTE ------------- */
// app.get("/backfill", backfillController.backfillSingleFile);

Expand Down
59 changes: 59 additions & 0 deletions src/main/Notification/NotificationController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package Notification;

import Config.Message;
import Database.Activity.ActivityDao;
import Notification.Services.NotifyIdPickupService;
import User.UserMessage;
import io.javalin.http.Handler;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONObject;

@Slf4j
public class NotificationController {
private ActivityDao activityDao;
private WindmillNotificationClient notificationClient;

public NotificationController(
ActivityDao activityDao, WindmillNotificationClient notificationClient) {
this.activityDao = activityDao;
this.notificationClient = notificationClient;
}

public Handler notifyIdPickup =
ctx -> {
JSONObject req = new JSONObject(ctx.body());

String sessionUsername = ctx.sessionAttribute("username");
if (sessionUsername == null) {
ctx.result(UserMessage.SESSION_TOKEN_FAILURE.toResponseString());
return;
}

String workerUsername = req.getString("workerUsername");

if (!sessionUsername.equals(workerUsername)) {
ctx.result(
UserMessage.INVALID_PARAMETER
.withMessage("Worker username does not match authenticated session")
.toResponseString());
return;
}

String clientUsername = req.getString("clientUsername");
String idToPickup = req.getString("idToPickup");
String clientPhoneNumber = req.getString("clientPhoneNumber");
String message = req.getString("message");

NotifyIdPickupService service =
new NotifyIdPickupService(
activityDao,
notificationClient,
workerUsername,
clientUsername,
idToPickup,
clientPhoneNumber,
message);
Message responseMessage = service.executeAndGetResponse();
ctx.result(responseMessage.toResponseString());
};
}
73 changes: 73 additions & 0 deletions src/main/Notification/Services/NotifyIdPickupService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package Notification.Services;

import Activity.UserActivity.NotifyIdPickupActivity;
import Config.Message;
import Config.Service;
import Database.Activity.ActivityDao;
import Notification.WindmillNotificationClient;
import User.UserMessage;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class NotifyIdPickupService implements Service {
private final ActivityDao activityDao;
private final WindmillNotificationClient notificationClient;
private final String workerUsername;
private final String clientUsername;
private final String idToPickup;
private final String clientPhoneNumber;
private final String message;

public NotifyIdPickupService(
ActivityDao activityDao,
WindmillNotificationClient notificationClient,
String workerUsername,
String clientUsername,
String idToPickup,
String clientPhoneNumber,
String message) {
this.activityDao = activityDao;
this.notificationClient = notificationClient;
this.workerUsername = workerUsername;
this.clientUsername = clientUsername;
this.idToPickup = idToPickup;
this.clientPhoneNumber = clientPhoneNumber;
this.message = message;
}

@Override
public Message executeAndGetResponse() {
if (workerUsername == null || workerUsername.isBlank()) {
return UserMessage.INVALID_PARAMETER.withMessage("Worker username is required");
}
if (clientUsername == null || clientUsername.isBlank()) {
return UserMessage.INVALID_PARAMETER.withMessage("Client username is required");
}
if (idToPickup == null || idToPickup.isBlank()) {
return UserMessage.INVALID_PARAMETER.withMessage("ID to pickup is required");
}
if (!notificationClient.isValidPhoneNumber(clientPhoneNumber)) {
return UserMessage.INVALID_PARAMETER.withMessage(
"Invalid phone number format. Expected +1XXXXXXXXXX");
}
if (message == null || message.isBlank()) {
return UserMessage.INVALID_PARAMETER.withMessage("Message is required");
}

notificationClient.sendSms(clientPhoneNumber, message);
recordNotifyIdPickupActivity();

log.info(
"ID pickup notification sent from {} to {} for ID: {}",
workerUsername,
clientUsername,
idToPickup);
return UserMessage.SUCCESS;
}

private void recordNotifyIdPickupActivity() {
NotifyIdPickupActivity activity =
new NotifyIdPickupActivity(workerUsername, clientUsername, idToPickup);
activityDao.save(activity);
}
}
112 changes: 112 additions & 0 deletions src/main/Notification/WindmillNotificationClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package Notification;

import okhttp3.*;
import com.google.gson.Gson;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;

@Slf4j
public class WindmillNotificationClient {
private final OkHttpClient client;
private final Gson gson;
private final String WINDMILL_URL;
private final String WINDMILL_TOKEN;
private final String TWILIO_PHONE_NUMBER;
private final Map<String, String> twilioResource;
private final Pattern PHONE_PATTERN = Pattern.compile("\\+1\\d{10}"); // +1 followed by 10 digits

public WindmillNotificationClient() {
this.client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
this.gson = new Gson();
this.WINDMILL_URL = System.getenv("WINDMILL_URL");
this.WINDMILL_TOKEN = System.getenv("WINDMILL_TOKEN");
this.TWILIO_PHONE_NUMBER = System.getenv("TWILIO_PHONE_NUMBER");
this.twilioResource = new HashMap<>();
String TWILIO_ACCOUNT_SID = System.getenv("ACCOUNT_SID");
String TWILIO_AUTH_TOKEN = System.getenv("AUTH_TOKEN_TWILIO");
this.twilioResource.put("accountSid", TWILIO_ACCOUNT_SID);
this.twilioResource.put("token", TWILIO_AUTH_TOKEN);
}

// Constructor for testing
public WindmillNotificationClient(String windmillUrl, String windmillToken, String twilioPhoneNumber,
String twilioAccountSid, String twilioAuthToken) {
this.client = new OkHttpClient();
this.gson = new Gson();
this.WINDMILL_URL = windmillUrl;
this.WINDMILL_TOKEN = windmillToken;
this.TWILIO_PHONE_NUMBER = twilioPhoneNumber;
this.twilioResource = new HashMap<>();
this.twilioResource.put("accountSid", twilioAccountSid);
this.twilioResource.put("token", twilioAuthToken);
}

public boolean isValidPhoneNumber(String phoneNumber) {
return phoneNumber != null && this.PHONE_PATTERN.matcher(phoneNumber).matches();
}

public void executeRequest(Request request, Callback callback) {
client.newCall(request).enqueue(callback);
}

public void sendSms(String to, String message) {
if (!isValidPhoneNumber(to)) {
log.error("sendSms failed: invalid phone number provided: {}", to);
return;
}
if (message == null || message.isBlank()) {
log.error("sendSms failed: empty message provided: {}", message);
return;
}

Map<String, Object> payload = Map.of(
"method", "sms",
"message", message,
"sms_config", Map.of(
"twilio_auth", twilioResource,
"to_phone_number", to,
"from_phone_number", this.TWILIO_PHONE_NUMBER
),
"email_config", Map.of()
);

Request request = new Request.Builder()
.url(this.WINDMILL_URL)
.post(RequestBody.create(gson.toJson(payload), MediaType.parse("application/json")))
.addHeader("Authorization", "Bearer " + this.WINDMILL_TOKEN)
.build();

log.info("Sending SMS to {} with message: {}", to, message);

Callback callback = new Callback() {
public void onFailure(@NotNull Call call, @NotNull IOException e) {
log.error("sendSms failed: " + e.getMessage());
}

public void onResponse(@NotNull Call call, @NotNull Response response) {
try (response) {
if (response.isSuccessful()) {
log.info("sent SMS successfully. Status: {}", response.code());
} else {
log.warn("SMS request completed but failed. Status: {}, Body: {}",
response.code(), response.body() != null ? response.body().string() : "");
}
} catch (IOException e) {
log.error("caught error reading SMS response: " + e.getMessage());
}
}
};
executeRequest(request, callback);
}
}
Loading