Skip to content
Browse files

Added Twilio support, validation, and general commenting/code cleanup

  • Loading branch information...
1 parent e768f4d commit 3187b655966cdd4f27ad1ccf08da75d90472766b @esseguin committed Mar 26, 2012
View
27 app/controllers/Application.java
@@ -1,24 +1,51 @@
package controllers;
import java.util.UUID;
+import java.util.concurrent.TimeUnit;
import models.Task;
+import models.TextMessageActor;
import play.data.Form;
+import play.libs.Akka;
import play.mvc.Controller;
import play.mvc.Result;
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import akka.util.Duration;
+
public class Application extends Controller {
static Form<Task> taskForm = form(Task.class);
+ static boolean initialized = false;
+
+ /**
+ * Initialize the application. This will only be called once for the life of the system.
+ * This function starts the scheduler for SMS notifications.
+ */
+ public static void init() {
+ if (!initialized) {
+ ActorRef txtActor = Akka.system().actorOf(new Props(TextMessageActor.class));
+ Akka.system().scheduler().schedule(
+ Duration.create(0, TimeUnit.MILLISECONDS),
+ Duration.create(30, TimeUnit.SECONDS),
+ txtActor,
+ "tick"
+ );
+ initialized = true;
+ }
+ }
public static Result index() {
+ init();
return redirect(routes.Application.tasks());
}
/**
* Main view for viewing tasks. Renders all of the tasks.
*/
public static Result tasks() {
+ init();
return ok(
views.html.index.render(Task.all(), taskForm)
);
View
58 app/models/Task.java
@@ -13,6 +13,10 @@
import siena.Model;
import siena.NotNull;
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
+
@Entity
public class Task extends Model {
@@ -49,17 +53,71 @@
@Max(32)
public String phone;
+ public boolean notificationSent;
+
+ /**
+ * Custom validation. This checks to make sure the phone number is a
+ * valid format.
+ *
+ * @return A validation error message if one exists. Null if validation passes.
+ */
+ public String validate() {
+ if (!phone.equals("")) {
+ PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
+ try {
+ PhoneNumber pn = phoneUtil.parse(phone, "US");
+ phone = String.valueOf(pn.getNationalNumber());
+ } catch (NumberParseException e) {
+ return "Invalid phone number";
+ }
+ }
+ return null;
+ }
+
public static List<Task> all() {
return Model.all(Task.class).fetch();
}
+ public static void update(Task task) {
+ task.update();
+ }
+
public static void create(Task task) {
+ if (!task.phone.equals("")) {
+ task.notificationSent = false;
+ } else {
+ task.notificationSent = true;
+ }
+
task.save();
+ forceSynchronousDBCall();
}
public static void delete(UUID id) {
Task task = new Task();
task.id = id;
task.delete();
+ forceSynchronousDBCall();
+ }
+
+ /**
+ * This is a workaround (note: hack) to make SimpleDB synchronous.
+ *
+ * SimpleDB has an eventually-consistent write model and is by default
+ * asynchronous. Siena's SimpleDB support is in alpha and they haven't
+ * yet added any way to ensure synchronous behavior.
+ *
+ * All this method does is add a 0.5 second sleep after a DB call. This
+ * doesn't ensure synchronicity, but will almost always provide it.
+ *
+ * TODO If this system were ever to move to production, this function
+ * should be replaced by something more certain.
+ */
+ private static void forceSynchronousDBCall() {
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
}
}
View
108 app/models/TextMessageActor.java
@@ -0,0 +1,108 @@
+package models;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import akka.actor.UntypedActor;
+
+import com.twilio.sdk.TwilioRestClient;
+import com.twilio.sdk.TwilioRestException;
+import com.twilio.sdk.resource.factory.SmsFactory;
+import com.twilio.sdk.resource.instance.Account;
+
+/**
+ * This Actor is responsible for sending SMS messages for tasks whose due date
+ * is approaching (80% of the total time has elapsed).
+ */
+public class TextMessageActor extends UntypedActor {
+
+ private SmsFactory smsFactory;
+
+ /**
+ * Constructor. Initializes Twilio SMS Factory.
+ */
+ public TextMessageActor() {
+ TwilioRestClient client = new TwilioRestClient("AC6d60af6100b144418d75852c3bd79d30",
+ "0639ed5b9beab4a9970e94c6fcce593a");
+
+ Account mainAccount = client.getAccount();
+ smsFactory = mainAccount.getSmsFactory();
+ }
+
+ /**
+ * Send an SMS message.
+ *
+ * @param toNumber The number to send the message to.
+ * If Twilio is in sandbox mode, this number must be a verified number.
+ * @param fromNumber The number to send the message from.
+ * This must be a valid Twilio (sandbox) number.
+ * @param message The text of the message.
+ *
+ * @throws TwilioRestException If there was a problem sending the SMS message
+ */
+ private void sendSMS(String toNumber, String fromNumber, String message) throws TwilioRestException {
+ Map<String, String> smsParams = new HashMap<String, String>();
+ smsParams.put("To", toNumber);
+ smsParams.put("From", fromNumber);
+ smsParams.put("Body", message);
+ smsFactory.create(smsParams);
+ }
+
+ @Override
+ public void onReceive(Object message) throws Exception {
+ if (message instanceof String && message.equals("tick")) {
+ sendNotifications();
+ } else {
+ unhandled(message);
+ }
+ }
+
+ /**
+ * Send notifications for tasks that are due soon.
+ */
+ private void sendNotifications() {
+ // Get all the tasks
+ List<Task> tasks = Task.all();
+
+ for (Task task : tasks) {
+ // Only send an SMS message if the task has a due date and phone number and we haven't
+ // sent a message yet.
+ if (!task.notificationSent && !task.dueDate.equals("") && !task.phone.equals("")) {
+ DateFormat df = new SimpleDateFormat("M/d/y k:m");
+ long addedSeconds = task.addedDate.getTime();
+ try {
+ Date dueDate = df.parse(task.dueDate);
+ long dueSeconds = dueDate.getTime();
+ double cutoffSeconds = (dueSeconds - addedSeconds) * 0.2;
+ double secondsLeft = dueSeconds - new Date().getTime();
+
+ // If more than 80% of the total time has elapsed, send the SMS.
+ if (secondsLeft < cutoffSeconds) {
+ DateFormat goalFormat = new SimpleDateFormat("M/d 'at' h:mm a");
+ String message = "Task \"" + task.label + "\" is due on "
+ + goalFormat.format(dueDate);
+
+ System.out.println("Sending Text Message");
+ sendSMS(task.phone, "4155992671", message);
+
+ // The message was successfully sent. Now, update the task so that we don't
+ // send any additional notifications.
+ task.notificationSent = true;
+ Task.update(task);
+ }
+ } catch (ParseException e) {
+ System.out.println("Error parsing date " + task.dueDate);
+ } catch (TwilioRestException e) {
+ System.out.println("Error Sending Text Message to number " + task.phone);
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+}
View
16 app/views/index.scala.html
@@ -33,6 +33,11 @@
}
}
)
+ @if(task.phone) {
+ <a href="#" title="Notifications will be sent to @task.phone">
+ <img src="@routes.Assets.at("images/phone.gif")" />
+ </a>
+ }
</span>
</div>
@@ -44,6 +49,17 @@
</div>
<h2>Add a new task</h2>
+ <div id ="errors">
+ @for(error <- taskForm.errors) {
+ @(error._1 match {
+ // Just display the first message from each general (i.e. keyless) error.
+ case "" => {
+ error._2(0) message
+ }
+ case _ =>
+ })
+ }
+ </div>
@form(routes.Application.newTask(), args = 'id -> "newTaskForm") {
View
1 app/views/main.scala.html
@@ -11,6 +11,7 @@
<script src="@routes.Assets.at("javascripts/jquery-1.7.1.min.js")" type="text/javascript"></script>
<script src="@routes.Assets.at("javascripts/jquery-ui-1.8.18.custom.min.js")" type="text/javascript"></script>
<script src="@routes.Assets.at("javascripts/jquery-ui-timepicker.js")" type="text/javascript"></script>
+ <script src="@routes.Assets.at("javascripts/tooltip.js")" type="text/javascript"></script>
</head>
<body>
<header>
View
5 conf/application.conf
@@ -18,7 +18,7 @@ application.langs="en"
# global=Global
# Database configuration
-# ~~~~~
+# ~~~~~
# You can declare as many datasources as you want.
# By convention, the default datasource is named `default`
#
@@ -57,3 +57,6 @@ logger.play=INFO
# Logger provided to your application:
logger.application=DEBUG
+akka.default-dispatcher.core-pool-size-max = 64
+akka.debug.receive = on
+
View
BIN lib/libphonenumber-4.7.jar
Binary file not shown.
View
BIN lib/twilio-client-3.3.7-jar-with-dependencies.jar
Binary file not shown.
View
BIN public/images/phone.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
5 public/stylesheets/main.css
@@ -1,6 +1,5 @@
body {
font-family: Helvetica, Arial;
- position: relative;
text-align: center;
}
@@ -69,3 +68,7 @@ div#tasklist div.task:nth-child(even) {
overflow:hidden;
}
+#errors {
+ color: red;
+ font-weight: bold;
+}

0 comments on commit 3187b65

Please sign in to comment.
Something went wrong with that request. Please try again.