Skip to content

Commit

Permalink
Initial commit: version 0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
nafg committed Feb 5, 2018
0 parents commit a232b83
Show file tree
Hide file tree
Showing 23 changed files with 1,002 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
.idea/
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
jdk:
- oraclejdk8
language: scala
script: "sbt publish"
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## simpleivr

A Scala algebra for writing telephony applications, including an implementation using [asterisk-java](https://github.com/asterisk-java/asterisk-java).
72 changes: 72 additions & 0 deletions asterisk/src/main/scala/simpleivr/asterisk/AgiIvrApi.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package simpleivr.asterisk

import java.io.File

import cats.effect.IO
import org.asteriskjava.fastagi.{AgiChannel, AgiHangupException}
import simpleivr.IvrApi


/**
* Implements the IvrApi trait for Asterisk AGI
*/
class AgiIvrApi(channel: AgiChannel, val ami: Ami) extends IvrApi {
final val HangupReturnCode = -1.toChar

def hangupAndQuit(): Nothing = {
hangup()
throw new AgiHangupException
}

def dial(to: String, ringTimeout: Int, flags: String): Int = channel.exec("Dial", s"$to,$ringTimeout|$flags")

def amd() = channel.exec("AMD")

def getVar(name: String) = Option(channel.getFullVariable("${" + name + "}"))

def callerId: IO[String] = IO {
channel.getFullVariable("$" + "{CALLERID(num)}")
}

def waitForDigit(timeout: Int): IO[Option[Char]] = IO {
channel.waitForDigit(timeout) match {
case HangupReturnCode => hangupAndQuit()
case c: Char if c > 0 => Some(c)
case _ => None
}
}

def waitForSilence(ms: Int, repeat: Int = 1, timeoutSec: Option[Int] = None): Unit = {
channel.exec("WaitForSilence", s"$ms,$repeat" + timeoutSec.map("," + _).getOrElse(""))
()
}

def monitor(file: File): Unit = {
channel.exec("MixMonitor", file.getAbsolutePath)
()
}

def hangup(): Unit = channel.hangup()

def recordFile(pathAndName: String,
format: String,
interruptChars: String,
timeLimitMillis: Int,
offset: Int,
beep: Boolean,
maxSilenceSecs: Int): Char =
channel.recordFile(pathAndName, format, interruptChars, timeLimitMillis, offset, beep, maxSilenceSecs)

def setAutoHangup(seconds: Int): IO[Unit] = IO {
channel.setAutoHangup(seconds)
}

def streamFile(pathAndName: String, interruptChars: String): Char =
channel.streamFile(pathAndName, interruptChars) match {
case HangupReturnCode => hangupAndQuit()
case c => c
}

override def originate(dest: String, script: String, args: Seq[String]): Unit =
ami.originate(dest, script, args)
}
59 changes: 59 additions & 0 deletions asterisk/src/main/scala/simpleivr/asterisk/Ami.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package simpleivr.asterisk

import java.beans.PropertyChangeEvent
import java.time.Instant

import org.asteriskjava.live._


class Ami(settings: AmiSettings)
extends DefaultAsteriskServer(settings.asteriskHost, settings.amiUsername, settings.amiPassword) {

def originate(dest: String, script: String, args: Seq[String]): Unit = try {
val scriptAndArgs = script +: args
var done = false
val startTime = System.currentTimeMillis()
println(s"Executing call for $scriptAndArgs at ${Instant.now}")
var chan: Option[AsteriskChannel] = None
import scala.collection.JavaConverters._
originateToApplicationAsync(
s"SIP/${settings.peer}/1$dest",
"Agi",
s"agi://${settings.agiHost}/${scriptAndArgs.mkString(",")}",
60000,
new CallerId(settings.callerIdName, settings.callerIdNum),
Map("DEST_NUM" -> dest).asJava,
new OriginateCallback {
override def onDialing(channel: AsteriskChannel): Unit = {
println("Dialing " + dest)
}
override def onSuccess(channel: AsteriskChannel): Unit = {
println("Success! " + scriptAndArgs)
chan = Some(channel)
channel.addPropertyChangeListener(
"state",
(_: PropertyChangeEvent) => if (channel.getState == ChannelState.HUNGUP) done = true
)
}
override def onNoAnswer(channel: AsteriskChannel): Unit = {
println("No answer " + scriptAndArgs)
done = true
}
override def onBusy(channel: AsteriskChannel): Unit = {
println("Busy " + scriptAndArgs)
done = true
}
override def onFailure(cause: LiveException): Unit = {
println("Failure for " + scriptAndArgs + ": " + cause)
done = true
}
}
)

while (System.currentTimeMillis() - startTime < 120000 && !done) Thread.sleep(10000)

println("Finished executing call for " + scriptAndArgs + " at " + System.currentTimeMillis() + ", done = " + done + ", channel = " + chan)
} catch {
case e: Exception => e.printStackTrace()
}
}
11 changes: 11 additions & 0 deletions asterisk/src/main/scala/simpleivr/asterisk/AmiSettings.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package simpleivr.asterisk

trait AmiSettings {
def peer: String
def asteriskHost: String
def agiHost: String
def callerIdNum: String
def callerIdName: String
def amiUsername: String
def amiPassword: String
}
23 changes: 23 additions & 0 deletions asterisk/src/main/scala/simpleivr/asterisk/DefaultAgiScript.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package simpleivr.asterisk

import org.asteriskjava.fastagi._
import simpleivr.{IvrApi, IvrStep, IvrStepRunner, Sayables}


abstract class DefaultAgiScript(sayables: Sayables, api: IvrApi) extends AgiScript {
def run(request: AgiRequest): IvrStep[Unit]

def ivrStepRunner(request: AgiRequest) = new IvrStepRunner(api, sayables)

override def service(request: AgiRequest, channel: AgiChannel): Unit = {
channel.answer()
try {
val runner = ivrStepRunner(request)
runner.runIvrStep(run(request)).unsafeRunSync()
channel.hangup()
} catch {
case e: AgiHangupException =>
println("Caught hangup")
}
}
}
11 changes: 11 additions & 0 deletions bintray.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
publishMavenStyle in ThisBuild := true
publishTo in ThisBuild := Some("bintray" at "https://api.bintray.com/maven/naftoligug/maven/simpleivr")

sys.env.get("BINTRAYKEY").toSeq.map { key =>
credentials in ThisBuild += Credentials(
"Bintray API Realm",
"api.bintray.com",
"naftoligug",
key
)
}
30 changes: 30 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
ThisBuild / organization := "io.github.nafg.simpleivr"
ThisBuild / version := "0.1.0"

lazy val core = project
.settings(
name := "simpleivr-core",
libraryDependencies ++= Seq(
"com.lihaoyi" %% "sourcecode" % "0.1.4",
"org.typelevel" %% "cats-free" % "1.0.1",
"org.typelevel" %% "cats-effect" % "0.8"
)
)

lazy val testing = project
.dependsOn(core)
.settings(
name := "simpleivr-testing",
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5"
)

lazy val asterisk = project
.dependsOn(core)
.settings(
name := "simpleivr-asterisk",
libraryDependencies += "org.asteriskjava" % "asterisk-java" % "2.0.2"
)

publishArtifact := false
publish := ()
publishLocal := ()
26 changes: 26 additions & 0 deletions core/src/main/scala/simpleivr/AudioPath.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package simpleivr

import java.io.File


case class AudioPath(directory: File, name: String) {
lazy val supportedAudioFiles =
Seq("wav", "sln", "ulaw")
.map(ext => ext -> new File(directory, name + "." + ext))
.toMap

lazy val wavFile = supportedAudioFiles("wav")
lazy val slnFile = supportedAudioFiles("sln")

def pathAndName: String = directory.getAbsolutePath + File.separator + name

def existingFiles() = supportedAudioFiles.filter(_._2.exists())

def exists() = existingFiles().nonEmpty
}

object AudioPath {
private val removeExtensionRegex = """\.[^./\\]*$""".r
def fromFile(file: File) =
AudioPath(file.getParentFile, removeExtensionRegex.replaceAllIn(file.getName, ""))
}
66 changes: 66 additions & 0 deletions core/src/main/scala/simpleivr/Ivr.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package simpleivr

import cats.implicits._


class Ivr(sayables: Sayables) {

import sayables._


def record(desc: Sayable, path: AudioPath, timeLimitInSeconds: Int): IvrStep[Unit] =
(IvrStep.say(`Please say` & desc & `after the tone, and press pound when finished.`) *>
IvrStep.recordFile(path.pathAndName, "wav", "#", timeLimitInSeconds * 1000, 0, beep = true, 3))
.void

def confirmRecording(desc: Sayable, file: Sayable): IvrStep[Option[Boolean]] =
askYesNo(desc & `is` & file & `Is that correct?`)

def sayAndGetDigit(msgs: Sayable, wait: Int = 5000): IvrStep[Option[Char]] =
IvrStep.say(msgs, "0123456789#*").flatMap {
case Some(c) => IvrStep(Some(c))
case None => IvrStep.waitForDigit(wait)
}

/**
* None means * was pressed, signifying that inputting was canceled
*/
def sayAndHandleDigits[T](min: Int, max: Int, msgs: Sayable)
(handle: PartialFunction[String, T] = PartialFunction(identity[String])): IvrStep[Option[T]] = {
def validate(acc: String): Either[Sayable, T] =
if ((min == max) && (acc.length != min))
Left(`You must enter ` & numberWords(min) & (if (min == 1) `digit` else `digits`))
else if (acc.length < min)
Left(`You must enter at least` & numberWords(min) & (if (min == 1) `digit` else `digits`))
else if (acc.length > max)
Left(`You cannot enter more than` & numberWords(max) & (if (max == 1) `digit` else `digits`))
else
handle.andThen(Right(_)).applyOrElse(acc, (_: String) => Left(`That entry is not valid`))

def calcRes(acc: String = ""): Option[Char] => IvrStep[Either[Sayable, Option[T]]] = {
case Some(c) if acc.length + 1 < max && c.isDigit =>
IvrStep.waitForDigit(5000).flatMap(calcRes(acc + c))
case Some('*') =>
IvrStep(Right(None))
case x =>
val s = acc + x.filter(_ != '#').mkString
IvrStep(validate(s).right.map(Option(_)))
}

sayAndGetDigit(msgs)
.flatMap(calcRes())
.flatMap {
case Right(x) => IvrStep(x)
case Left(msg) => IvrStep.say(msg) *> sayAndHandleDigits(min, max, msgs)(handle)
}
}

def askYesNo(msgs: Sayable): IvrStep[Option[Boolean]] =
sayAndGetDigit(msgs & `Press 1 for yes, or 2 for no.`) flatMap {
case Some('1') => IvrStep(Some(true))
case Some('2') => IvrStep(Some(false))
case Some('*') => IvrStep(None)
case None => IvrStep.say(`Please make a selection`) *> askYesNo(msgs)
case _ => IvrStep.say(`That is not one of the choices.`) *> askYesNo(msgs)
}
}
21 changes: 21 additions & 0 deletions core/src/main/scala/simpleivr/IvrApi.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package simpleivr

import java.io.File

import cats.effect.IO


trait IvrApi {
def streamFile(pathAndName: String, interruptChars: String): Char
def recordFile(pathAndName: String, format: String, interruptChars: String, timeLimitMillis: Int, offset: Int, beep: Boolean, maxSilenceSecs: Int): Char
def waitForDigit(timeout: Int): IO[Option[Char]]
def dial(to: String, ringTimeout: Int, flags: String): Int
def amd(): Int
def getVar(name: String): Option[String]
def callerId: IO[String]
def waitForSilence(ms: Int, repeat: Int = 1, timeoutSec: Option[Int] = None): Unit
def monitor(file: File): Unit
def hangup(): Unit
def setAutoHangup(seconds: Int): IO[Unit]
def originate(dest: String, script: String, args: Seq[String])
}
Loading

0 comments on commit a232b83

Please sign in to comment.