Skip to content

Commit

Permalink
Support reporting raids with SMS
Browse files Browse the repository at this point in the history
If a subscribed user sends an SMS starting with "report", the text after
"report" will be interpreted as an address and sent out as an alert to
all subscribers. This operates through the same code path as the alert
form on the site.

Right now, this only supports plain text addresses. We will be able to
integrate it with geocoding in the future to acquire coordinates, as
well as correct typos.
  • Loading branch information
binyomen committed Oct 29, 2017
1 parent 53d09a3 commit 0536be7
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 28 deletions.
13 changes: 2 additions & 11 deletions app/controllers/AlertController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ package controllers

import javax.inject.{Inject, Singleton}

import com.twilio.rest.api.v2010.account.Message
import com.twilio.`type`.PhoneNumber
import models.SubscriberRepository
import play.api.data.Form
import play.api.data.Forms._
import play.api.i18n.{Lang, MessagesApi}
import play.api.i18n.MessagesApi
import play.api.mvc.{AbstractController, ControllerComponents}

import scala.concurrent.{ExecutionContext, Future}
Expand All @@ -33,14 +31,7 @@ class AlertController @Inject()(cc: ControllerComponents, repo: SubscriberReposi
},
alert => {
repo.listActive().map { subscribers =>
val messages = subscribers.map { subscriber =>
implicit val lang: Lang = Lang.get("en").get
Message
.creator(new PhoneNumber(subscriber.phone), // to
new PhoneNumber(sys.env("TWILIO_PHONE")), // from
messagesApi("action_alert", alert.address))
.create()
}
val messages = alert.sendAlert(subscribers, messagesApi)
Redirect(routes.HomeController.index())
.flashing("success" -> ("Done! Messages sent with IDs " + messages.map(_.getSid()).mkString(",")))
}
Expand Down
18 changes: 17 additions & 1 deletion app/controllers/TwilioController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package controllers
import javax.inject.{Inject, Singleton}

import com.twilio.twiml.{Body, Message, MessagingResponse}
import models.{Alert}
import models.{SubscriberAction, AlertAction}
import models.{SubscriberRepository, SubscriberTransitions}
import play.api.data.Form
import play.api.data.Forms._
Expand All @@ -28,7 +30,11 @@ class TwilioController @Inject()(cc: ControllerComponents, repo: SubscriberRepos
repo.getOrCreate(twilioData.from).map { subscriber =>
implicit val lang: Lang = Lang.get("en").get
val subscriberTransition = new SubscriberTransitions(subscriber)
val (responseMessage, updatedSubscriber) = subscriberTransition.receiveInput(twilioData.body)
val action = subscriberTransition.action(twilioData.body)
if (action.isDefined) {
performAction(action.get)
}
val (responseMessage, updatedSubscriber) = subscriberTransition.transition(twilioData.body)
if (updatedSubscriber.isDefined) {
repo.update(updatedSubscriber.get)
}
Expand All @@ -41,4 +47,14 @@ class TwilioController @Inject()(cc: ControllerComponents, repo: SubscriberRepos
Ok(response.toXml()).as("application/xml")
}
}

def performAction(action: SubscriberAction) = {
action match {
case AlertAction(addr) =>
val alert = Alert(addr)
repo.listActive().map { subscribers =>
alert.sendAlert(subscribers, messagesApi)
}
}
}
}
19 changes: 18 additions & 1 deletion app/models/Alert.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
package models

case class Alert(address: String)
import com.twilio.rest.api.v2010.account.Message
import com.twilio.`type`.PhoneNumber
import play.api.i18n.{Lang, MessagesApi}

import scala.concurrent.ExecutionContext

case class Alert(address: String) {
def sendAlert(subscribers: Seq[Subscriber], messagesApi: MessagesApi) = {
subscribers.map { subscriber =>
implicit val lang: Lang = Lang.get("en").get
Message
.creator(new PhoneNumber(subscriber.phone), // to
new PhoneNumber(sys.env("TWILIO_PHONE")), // from
messagesApi("action_alert", address))
.create()
}
}
}
24 changes: 23 additions & 1 deletion app/models/SubscriberTransitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,28 @@ package models

import models.SubscriberTransitions.{Complete, SelectingLanguage, SubscriptionState, Unsubscribed}

sealed trait SubscriberAction
case class AlertAction(addr: String) extends SubscriberAction

class SubscriberTransitions(subscriber : Subscriber) {
def receiveInput(input : String):(String, Option[Subscriber]) = {
def action(input: String): Option[SubscriberAction] = {
val trimmedInput = input.trim
val currentstate = SubscriberTransitions.withName(subscriber.state);
currentstate match {
case Unsubscribed =>
None
case SelectingLanguage =>
None
case Complete =>
if (trimmedInput.length > 6 && trimmedInput.substring(0, 6).equalsIgnoreCase("report")) {
return Some(AlertAction(trimmedInput.substring(6).trim))
} else {
return None
}
}
}

def transition(input : String):(String, Option[Subscriber]) = {
val trimmedInput = input.trim
val currentstate = SubscriberTransitions.withName(subscriber.state);
currentstate match {
Expand All @@ -28,6 +48,8 @@ class SubscriberTransitions(subscriber : Subscriber) {
} else if (trimmedInput.equalsIgnoreCase("leave")) {
val newSubscriber = subscriber.copy(state = Unsubscribed.name)
return ("unsubscribed_msg", Some(newSubscriber))
} else if (trimmedInput.length > 6 && trimmedInput.substring(0, 6).equalsIgnoreCase("report")) {
return ("report_msg", None)
} else {
return ("error_msg", None)
}
Expand Down
1 change: 1 addition & 0 deletions conf/messages
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ subscribe_help_msg=We were unable to understand your message. Text "join" to sub
unsubscribed_msg=You have been unsubscribed from receiving text message alerts. You may re-join by texting "join" to this number.
unsupported_lang_msg=We were unable to understand your language choice.
welcome_msg=Thank you for joining the WA Immigrant Solidarity Network's Text Message alert system to provide alerts when there is immigration (ICE) activity in our state.
report_msg=Thank you for reporting ICE activity. An alert will be sent out to all subscribers.
79 changes: 65 additions & 14 deletions test/models/SubscriberTransitionsSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,85 +5,136 @@ import org.scalatest.{FunSpec, Matchers}
class SubscriberTransitionsSpec extends FunSpec with Matchers {
describe("The subscriber transitions") {
describe ("when a user is unsubscribed") {
val subscriber = Subscriber(1, "555-555-5555", None, SubscriberTransitions.Unsubscribed.name);
val subscriber = Subscriber(1, "555-555-5555", None, SubscriberTransitions.Unsubscribed.name)
val subscriberTransitions = new SubscriberTransitions(subscriber)

it ("should return the basic help message if an unknown message is passed") {
val (message, newSubscriber) = subscriberTransitions.receiveInput("Hi")
val (message, newSubscriber) = subscriberTransitions.transition("Hi")
message shouldEqual "subscribe_help_msg"
newSubscriber shouldBe None
}

it ("should transition the subscriber to selecting language if join is sent") {
val (message, newSubscriber) = subscriberTransitions.receiveInput("join")
val (message, newSubscriber) = subscriberTransitions.transition("join")
message shouldEqual "language_selection_msg"
newSubscriber.get.state shouldEqual SubscriberTransitions.SelectingLanguage.name
}

it ("should transition the subscriber to selecting language if join with surrounding whitespace is sent") {
val (message, newSubscriber) = subscriberTransitions.receiveInput(" \t\tjoin \n \n\t\n ")
val (message, newSubscriber) = subscriberTransitions.transition(" \t\tjoin \n \n\t\n ")
message shouldEqual "language_selection_msg"
newSubscriber.get.state shouldEqual SubscriberTransitions.SelectingLanguage.name
}
}

describe("when a user is choosing a language") {
val subscriber = Subscriber(1, "555-555-5555", None, SubscriberTransitions.SelectingLanguage.name);
val subscriber = Subscriber(1, "555-555-5555", None, SubscriberTransitions.SelectingLanguage.name)
val subscriberTransitions = new SubscriberTransitions(subscriber)

it("should return a help message if an unknown language is selected") {
val (message, newSubscriber) = subscriberTransitions.receiveInput("Hi")
val (message, newSubscriber) = subscriberTransitions.transition("Hi")
message shouldEqual "unsupported_lang_msg"
newSubscriber shouldBe None
}

it("should transition the subscriber to completed if a language is selected") {
val (message, newSubscriber) = subscriberTransitions.receiveInput("1")
val (message, newSubscriber) = subscriberTransitions.transition("1")
message shouldEqual "confirmation_msg"
newSubscriber.get.state shouldEqual SubscriberTransitions.Complete.name
newSubscriber.get.language shouldEqual Some("eng")
}

it("should transition the subscriber to completed if a language with surrounding whitespace is selected") {
val (message, newSubscriber) = subscriberTransitions.receiveInput(" \n\n1 \t\n ")
val (message, newSubscriber) = subscriberTransitions.transition(" \n\n1 \t\n ")
message shouldEqual "confirmation_msg"
newSubscriber.get.state shouldEqual SubscriberTransitions.Complete.name
newSubscriber.get.language shouldEqual Some("eng")
}
}

describe("when a user has completed the subscription process") {
val subscriber = Subscriber(1, "555-555-5555", None, SubscriberTransitions.Complete.name);
val subscriber = Subscriber(1, "555-555-5555", None, SubscriberTransitions.Complete.name)
val subscriberTransitions = new SubscriberTransitions(subscriber)

it ("should return information on how to unsubscribe or change languages if an unknown message is sent while subscribed") {
val (message, newSubscriber) = subscriberTransitions.receiveInput("Hi")
val (message, newSubscriber) = subscriberTransitions.transition("Hi")
message shouldEqual "error_msg"
newSubscriber shouldBe None
}

it ("should return to the select language state if the appropriate message is sent") {
val (message, newSubscriber) = subscriberTransitions.receiveInput("change language")
val (message, newSubscriber) = subscriberTransitions.transition("change language")
message shouldEqual "language_selection_msg"
newSubscriber.get.state shouldEqual SubscriberTransitions.SelectingLanguage.name
}

it ("should return to the select language state if the appropriate message with surrounding whitespace is sent") {
val (message, newSubscriber) = subscriberTransitions.receiveInput(" \t change language \n")
val (message, newSubscriber) = subscriberTransitions.transition(" \t change language \n")
message shouldEqual "language_selection_msg"
newSubscriber.get.state shouldEqual SubscriberTransitions.SelectingLanguage.name
}

it ("should unsubscribe the user if the appropriate message is sent") {
val (message, newSubscriber) = subscriberTransitions.receiveInput("leave")
val (message, newSubscriber) = subscriberTransitions.transition("leave")
message shouldEqual "unsubscribed_msg"
newSubscriber.get.state shouldEqual SubscriberTransitions.Unsubscribed.name
}

it ("should unsubscribe the user if the appropriate message with surrounding whitespace is sent") {
val (message, newSubscriber) = subscriberTransitions.receiveInput(" \n leave \t")
val (message, newSubscriber) = subscriberTransitions.transition(" \n leave \t")
message shouldEqual "unsubscribed_msg"
newSubscriber.get.state shouldEqual SubscriberTransitions.Unsubscribed.name
}

it ("should stay in the complete state if a report is sent") {
val (message, newSubscriber) = subscriberTransitions.transition("report 1111 My Address Lane")
message shouldEqual "report_msg"
newSubscriber shouldBe None
}

it ("should stay in the complete state if a report with surrounding whitespace is sent") {
val (message, newSubscriber) = subscriberTransitions.transition(" \t\n report 1111 My Address Lane\n\t\n ")
message shouldEqual "report_msg"
newSubscriber shouldBe None
}
}
}

describe("The subscriber actions") {
describe ("when a user is unsubscribed") {
val subscriber = Subscriber(1, "555-555-5555", None, SubscriberTransitions.Unsubscribed.name)
val subscriberTransitions = new SubscriberTransitions(subscriber)

it("should return None") {
val action = subscriberTransitions.action("Hi")
action shouldBe None
}
}

describe("when a user is choosing a language") {
val subscriber = Subscriber(1, "555-555-5555", None, SubscriberTransitions.SelectingLanguage.name)
val subscriberTransitions = new SubscriberTransitions(subscriber)

it("should return None") {
val action = subscriberTransitions.action("Hi")
action shouldBe None
}
}

describe("when a user has completed the subscription process") {
val subscriber = Subscriber(1, "555-555-5555", None, SubscriberTransitions.Complete.name)
val subscriberTransitions = new SubscriberTransitions(subscriber)

it ("should return None if a report is not sent") {
val action = subscriberTransitions.action("Hi")
action shouldBe None
}

it ("should return the alert action if a report is sent") {
val action = subscriberTransitions.action("report 1111 My Address Lane")
action shouldBe 'isDefined
action.get shouldEqual AlertAction("1111 My Address Lane")
}
}
}
}

0 comments on commit 0536be7

Please sign in to comment.