-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit a232b83
Showing
23 changed files
with
1,002 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
target/ | ||
.idea/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
jdk: | ||
- oraclejdk8 | ||
language: scala | ||
script: "sbt publish" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
72
asterisk/src/main/scala/simpleivr/asterisk/AgiIvrApi.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
11
asterisk/src/main/scala/simpleivr/asterisk/AmiSettings.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
23
asterisk/src/main/scala/simpleivr/asterisk/DefaultAgiScript.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 := () |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, "")) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
} |
Oops, something went wrong.