/////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2005 Gabriel Gunderson // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the: // // Free Software Foundation, Inc. // 59 Temple Place, Suite 330 // Boston, MA 02111-1307 USA /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// // About monotone.cs: // Provides a way for Mono or .NET applications to talk to the Asterisk // telephony server via AGI (Asterisk Gateway Interface) or FastAGI. // // Author: // Gabriel Gunderson <gabe(a)gundy.org>, gundy.org // // Contributers: // Michael Giagnocavo <mgg(a)telefinity.com> (FastAGI support) // // Comments: // I welcome suggestions and fixes. // Expect things to break between versions while still in the 0.0X stage. :) /////////////////////////////////////////////////////////////////////////////// using System; using System.IO; using System.Net.Sockets; using System.Text.RegularExpressions; // This namespace is where all of the core AGI and FastAGI functionality is found. namespace AGI { // Enums for each key on a phone pad. public enum Keypad { Zero = 48, One = 49, Two = 50, Three = 51, Four = 52, Five = 53, Six = 54, Seven = 55, Eight = 56, Nine = 57, Star = 42, Pound = 35, None = 0, Unknown = -1 } // Enums for each line status in Asterisk. public enum LineStatus { DownAvailable = 0, DownReserved = 1, OffHook = 2, DigitsDialed = 3, LineRinging = 4, RemoteEndRinging = 5, LineUp = 6, LineBusy = 7, StatusFailed = -1 } // The object that provides the main functionality. public class MonoTone : IDisposable { private const int ReplyOffset = 11; private bool debug; private bool disposed; private string request; private string channel; private string language; private string type; private string uniqueId; private string callerId; private string dnid; private string rdnis; private string context; private string extension; private string priority; private string enhanced; private string accountCode; private TextReader input; private TextWriter output; private TextWriter errorOutput; // Converts strings to the matching KeyPad enum. public static Keypad StringToKey(string keyPressed) { switch (keyPressed) { case "48": return Keypad.Zero; case "49": return Keypad.One; case "50": return Keypad.Two; case "51": return Keypad.Three; case "52": return Keypad.Four; case "53": return Keypad.Five; case "54": return Keypad.Six; case "55": return Keypad.Seven; case "56": return Keypad.Eight; case "57": return Keypad.Nine; case "42": return Keypad.Star; case "35": return Keypad.Pound; case "0": return Keypad.None; default: return Keypad.Unknown; } } // Converts KeyPad enums to strings. public static string KeyToString(Keypad keyPressed) { switch (keyPressed) { case Keypad.Zero: return "0"; case Keypad.One: return "1"; case Keypad.Two: return "2"; case Keypad.Three: return "3"; case Keypad.Four: return "4"; case Keypad.Five: return "5"; case Keypad.Six: return "6"; case Keypad.Seven: return "7"; case Keypad.Eight: return "8"; case Keypad.Nine: return "9"; case Keypad.Star: return "Star"; case Keypad.Pound: return "Pound"; case Keypad.None: return "None"; default: return "Unknown"; } } // Constructor for MonoTone that takes care of initialization. public MonoTone() { this.input = System.Console.In; this.output = System.Console.Out; this.output.Flush(); this.errorOutput = System.Console.Error; Initialize(); } // Constructor for MonoTone for FastAGI. public MonoTone(string outputIp, int outputPort) { TcpClient client = new TcpClient(); client.Connect(outputIp, outputPort); this.input = new StreamReader(client.GetStream()); this.output = new StreamWriter(client.GetStream()); this.output.Flush(); this.errorOutput = new StreamWriter(client.GetStream()); Initialize(); } // Constructor for MonoTone for FastAGI. public MonoTone(TcpClient client) { this.input = new StreamReader(client.GetStream()); this.output = new StreamWriter(client.GetStream()); //this.output.Flush(); this.errorOutput = new StreamWriter(client.GetStream()); Initialize(); } // Constructor for MonoTone for FastAGI. public MonoTone(string outputIp, int outputPort, string errorIp, int errorPort) { TcpClient client = new TcpClient(); client.Connect(outputIp, outputPort); TcpClient errorClient = new TcpClient(); client.Connect(errorIp, errorPort); this.input = new StreamReader(client.GetStream()); this.output = new StreamWriter(client.GetStream()); this.output.Flush(); this.output = new StreamWriter(errorClient.GetStream()); Initialize(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public void Dispose( bool disposing) { if (!this.disposed) { if (disposing) { if (this.output != null) { this.output.Flush(); this.output.Close(); } if (this.input != null) { this.input.Close(); } if (this.errorOutput != null) { this.errorOutput.Flush(); this.errorOutput.Close(); } } } this.disposed = true; } // Constructor for MonoTone for FastAGI. public MonoTone(System.IO.StreamReader input, System.IO.StreamWriter output ) { this.input = input; this.output = output; Initialize(); } // Overloaded constructor for MonoTone with options for initialization and debugging. public MonoTone(bool init, bool debugInfo) { if (init) { Initialize(); } if (debugInfo) { debug = debugInfo; } } // Property for debugging. public bool Debug { get { return debug; } set { debug = value; } } // Read only property for the request variable read in during initialization. public string Request { get { return request; } } // Read only property for the channel variable read in during initialization. public string Channel { get { return channel; } } // Read only property for the language variable read in during initialization. public string Language { get { return language; } } // Read only property for the type variable read in during initialization. public string Type { get { return type; } } // Read only property for the uniqueid variable read in during initialization. public string UniqueId { get { return uniqueId; } } // Read only property for the callerid variable read in during initialization. public string CallerId { get { return callerId; } } // Read only property for the dnid variable read in during initialization. public string Dnid { get { return dnid; } } // Read only property for the rdnis variable read in during initialization. public string Rdns { get { return rdnis; } } // Read only property for the context variable read in during initialization. public string Context { get { return context; } } // Read only property for the extension variable read in during initialization. public string Extension { get { return extension; } } // Read only property for the priority variable read in during initialization. public string Priority { get { return priority; } } // Read only property for the enhanced variable read in during initialization. public string Enhanced { get { return enhanced; } } // Read only property for the accountcode variable read in during initialization. public string AccountCode { get { return accountCode; } } // Reads in the variables that are available at run time. public void Initialize() { // Read all initialization variables from Asterisk. do { } while (processInitialVariable(this.input.ReadLine())); } private bool processInitialVariable(string line) { int colon = line.IndexOf(':'); if (colon < 0) { // End of initial variables. return false; } // Asterisk formats the variables as "name: value". // There's gotta be a more elegant way of dealing with all these variables... string name = line.Substring(0, colon).ToLowerInvariant(); string value = line.Substring(colon + 1).Trim(); switch (name) { case "agi_request": this.request = value; break; case "agi_channel": this.channel = value; break; case "agi_language": this.language = value; break; case "agi_type": this.type = value; break; case "agi_uniqueid": this.uniqueId = value; break; case "agi_callerid": this.callerId = value; break; case "agi_dnid": this.dnid = value; break; case "agi_rdnis": this.rdnis = value; break; case "agi_context": this.context = value; break; case "agi_extension": this.extension = value; break; case "agi_priority": this.priority = value; break; case "agi_enhanced": this.enhanced = value; break; case "agi_accountcode": this.accountCode = value; break; } return true; } // Writes text to the Asterisk console. public void Output(string format, params object[] args) { format = String.Concat("MONOTONE: ", format); this.output.WriteLine(format, args); this.output.Flush(); } // Writes text to the Asterisk console. public void Output(string message) { this.output.WriteLine("MONOTONE: {0}", message); this.output.Flush(); } // Writes text to the Asterisk console if Debugging in on. public void BugInfo(string message) { if (debug) { this.output.WriteLine("BUG INFO: {0}", message); this.output.Flush(); } } // Writes text to the Asterisk console if Debugging in on. public void BugInfo(string format, params object[] args) { format = String.Concat("BUG INFO: ", format); if (debug) { this.errorOutput.WriteLine(format, args); this.errorOutput.Flush(); } } // Does all of the reading and writing to STDIN and STDOUT. Trims common results. private string ProcessAgi(string command) { BugInfo("ProcessAGI-Command = {0}", command); // This change needed to be made because of the difference between .NET's and Mono's WriteLine(). // this.output.WriteLine(command); this.output.Write(command + "\n"); this.output.Flush(); string reply = this.input.ReadLine(); BugInfo("ProcessAGI-reply = {0}", reply); string result = null; Regex replyResult = new Regex("^200 result="); if (replyResult.IsMatch(reply)) { result = reply.Substring(ReplyOffset, reply.Length - ReplyOffset); } BugInfo("ProcessAGI-result = {0}", result); return result; } // Provides additional processing for replies that fit. private bool ProcessKeyPress(string preProcess, out Keypad keyPressed) { BugInfo("ProcessKeyPress-PreProcess = {0}", preProcess); switch (preProcess) { case "0": keyPressed = Keypad.None; return true; case "-1": keyPressed = Keypad.None; return false; default: keyPressed = StringToKey(preProcess); return true; } } // Provides additional processing for replies that fit. private bool ProcessParen(string preProcess, out string value) { BugInfo("ProcessParen-PreProcess = {0}", preProcess); string returned = preProcess.Substring(0, 1); if (returned == "0") { value = null; return false; } else { value = TrimParen(preProcess); return true; } } // Provides additional processing for replies that fit. private bool ProcessOneZerro(string preProcess) { BugInfo("ProcessOneZerro-PreProcess = {0}", preProcess); switch (preProcess) { case "1": return true; case "0": return false; default: throw new UnexpectedOutputException(preProcess); } } // Provides additional processing for replies that fit. private bool ProcessOneNegOne(string preProcess) { BugInfo("ProcessOneNegOne-PreProcess = {0}", preProcess); switch (preProcess) { case "1": return true; case "-1": return false; default: throw new UnexpectedOutputException(preProcess); } } // Provides additional processing for replies that fit. private bool ProcessZeroNegOne(string preProcess) { BugInfo("ProcessZerroNegOne-PreProcess = {0}", preProcess); switch (preProcess) { case "0": return true; case "-1": return false; default: throw new UnexpectedOutputException(preProcess); } } // Provides additional processing for replies that fit. private LineStatus ProcessLineStatus(string preProcess) { BugInfo("ProcessLineStatus-PreProcess = {0}", preProcess); switch (preProcess) { case "-1": return LineStatus.StatusFailed; case "0": return LineStatus.DownAvailable; case "1": return LineStatus.DownReserved; case "2": return LineStatus.OffHook; case "3": return LineStatus.DigitsDialed; case "4": return LineStatus.LineRinging; case "5": return LineStatus.RemoteEndRinging; case "6": return LineStatus.LineUp; case "7": return LineStatus.LineBusy; default: throw new UnexpectedOutputException(preProcess); } } // Trims the () off of replies. private string TrimParen(string parenString) { BugInfo("TrimParen-ParenString = {0}", parenString); int start = parenString.IndexOf("("); return parenString.Substring(start + 1, parenString.Length - start - 2); } /////////////////////// ALL METHODS BELOW ARE WRAPPERS //////////////////////// // Answers channel if not already in answer state. // Returns true if answered and false if failed. public bool Answer() { string command = "ANSWER"; string reply = ProcessAgi(command); return ProcessZeroNegOne(reply); } // Checks the status of the specified or connected channel. // Returns a LineStatus enum. public LineStatus ChannelStatus(string channelName) { string command = String.Format("CHANNEL STATUS {0}", channelName); string reply = ProcessAgi(command); return ProcessLineStatus(reply); } // Override finding status for current channel. // Returns a LineStatus enum. public LineStatus ChannelStatus() { string command = String.Format("CHANNEL STATUS"); string reply = ProcessAgi(command); return ProcessLineStatus(reply); } // Deletes an entry in the Asterisk database for a given family and key. // Returns true if successful and false if not. public bool DatabaseDel(string family, string key) { string command = String.Format("DATABASE DEL {0} {1}", family, key); string reply = ProcessAgi(command); return ProcessOneZerro(reply); } // Deletes a family or specific keytree within a family in the Asterisk database. // Returns true if successful and false if not. public bool DatabaseDelTree(string family) { string command = String.Format("DATABASE DELTREE {0}", family); string reply = ProcessAgi(command); return ProcessOneZerro(reply); } // Override deletes a family or specific keytree within a family in the Asterisk database. // Returns true if successful and false if not. public bool DatabaseDelTree(string family, string keyTree) { string command = String.Format("DATABASE DELTREE {0} {1}", family, keyTree); string reply = ProcessAgi(command); return ProcessOneZerro(reply); } // Retrieves an entry in the Asterisk database for a given family and key. // Returns true if successful and false if not. // Value comes out with the key's value. public bool DatabaseGet(string family, string key, out string value) { string command = String.Format("DATABASE GET {0} {1}", family, key); string reply = ProcessAgi(command); return ProcessParen(reply, out value); } // Adds or updates an entry in the Asterisk database for a given family, key, and value. // Returns true if successful and false if not. public bool DatabasePut(string family, string key, string value) { string command = String.Format("DATABASE PUT {0} {1} {2}", family, key, value); string reply = ProcessAgi(command); return ProcessOneZerro(reply); // TODO - CHECK PROPER RETURNS // failure: 200 result=0 // success: 200 result=1 (<value>) } // Executes a given application. // Returns true if successful and false if not. // Output comes out with what the application returns from its STDOUT. public bool Execute(string application, string options, out string output) { string command = String.Format("EXEC {0} {1}", application, options); string reply = ProcessAgi(command); switch (reply) { case "-2": output = null; return false; default: output = reply; return true; } } // Stream the given file, and receive DTMF data. // Returns a string with the digits received from the other end of the channel. public string GetData(string file, int timeout, int maxDigits) { string command = String.Format("GET DATA {0} {1} {2}", file, timeout, maxDigits); return ProcessAgi(command); // TODO - CHECK PROPER RETURNS // failure: 200 result=-1 // timeout: 200 result=<digits> (timeout) // success: 200 result=<digits> } // Gets a channel variable. // Returns true if successful and false if not. // Value comes out with the variables' value. public bool GetVariable(string variableName, out string value) { string command = String.Format("GET VARIABLE {0}", variableName); string reply = ProcessAgi(command); return ProcessParen(reply, out value); } // Hangs up the current channel. // Returns true if successful and false if not. public bool HangUp() { string command = "HANGUP"; string reply = ProcessAgi(command); return ProcessOneNegOne(reply); } // Override hangs up the current or specified channel. // Returns true if successful and false if not. public bool HangUp(string channelName) { string command = String.Format("HANGUP {0}", channelName); string reply = ProcessAgi(command); return ProcessOneNegOne(reply); } // Does nothing. // Returns nothing. public void Noop() { string command = "NOOP"; ProcessAgi(command); } // Receives text from channels supporting it. // Returns true if successful and false if not. // Value comes out with ASCII numerical value of the character if one is received. public bool ReceiveChar(int timeout, out string value) { string command = String.Format("RECEIVE CHAR {0}", timeout); string reply = ProcessAgi(command); switch (reply) { case "-1": value = null; return false; case "0": value = null; return false; default: value = reply; return true; } } // Record to a file until a given DTMF digit in the sequence is received. // Returns true if successful and false if not. public bool Record(string fileName, string format, string escapeDigits, int timeout, int offsetSamples, bool beep, int silence) { // TODO - CHECK PROPER RETURNS // failure to write: 200 result=-1 (writefile) // failure on waitfor: 200 result=-1 (waitfor) endpos=<offset> // hangup: 200 result=0 (hangup) endpos=<offset> // interrupted: 200 result=<digit> (dtmf) endpos=<offset> // timeout: 200 result=0 (timeout) endpos=<offset> // random error: 200 result=<error> (randomerror) endpos=<offset> bool result; string reply; string command = string.Format( "RECORD FILE {0} {1} {2} {3} ", fileName, format, escapeDigits, timeout); if (beep) { command += "BEEP "; } if (silence > 0) { command += "s=" + silence; } this.output.WriteLine(command); this.output.Flush(); reply = this.input.ReadLine(); switch (reply) { case "200 result=-1": result = false; break; case "200 result=0": result = true; break; default: this.errorOutput.WriteLine("RECORD FILE gave the unexpected reply of {0}", reply); this.errorOutput.Flush(); result = false; break; } return result; } // Say a given digit string, returning early if any of the given DTMF digits are received on the channel. // Returns true if successful and false if not. // KeyPressed comes out with a KeyPad if one is received. public bool SayDigits(string digits, string escapeDigits, out Keypad keyPressed) { string command = String.Format("SAY DIGITS {0} \"{1}\"", digits, escapeDigits); string reply = ProcessAgi(command); return ProcessKeyPress(reply, out keyPressed); } // Say a given number, returning early if any of the given DTMF digits are received on the channel. // Returns true if successful and false if not. // KeyPressed comes out with a KeyPad if one is received. public bool SayNumber(int number, string escapeDigits, out Keypad keyPressed) { string command = String.Format("SAY NUMBER {0} \"{1}\"", number, escapeDigits); string reply = ProcessAgi(command); return ProcessKeyPress(reply, out keyPressed); } // Returns true if successful and false if not. // KeyPressed comes out with a KeyPad if one is received. public bool SayPhonetic(string phonetic, string escapeDigits, out Keypad keyPressed) { string command = String.Format("SAY PHONETIC {0} \"{1}\"", phonetic, escapeDigits); string reply = ProcessAgi(command); return ProcessKeyPress(reply, out keyPressed); } // Returns true if successful and false if not. // KeyPressed comes out with a KeyPad if one is received. public bool SayTime(string time, string escapeDigits, out Keypad keyPressed) { string command = String.Format("SAY TIME {0} \"{1}\"", time, escapeDigits); string reply = ProcessAgi(command); return ProcessKeyPress(reply, out keyPressed); } // Sends the given image on a channel. // Returns true if successful and false if not. public bool SendImage(string image) { string command = String.Format("SEND IMAGE {0}", image); string reply = ProcessAgi(command); return ProcessZeroNegOne(reply); } // Sends the given text on a channel. // Returns true if successful and false if not. public bool SendText(string text) { string command = String.Format("SEND TEXT {0}", text); string reply = ProcessAgi(command); return ProcessZeroNegOne(reply); } // Autohangup channel in some time. // Returns nothing. public void SetAutoHangUp(int seconds) { string command = String.Format("SET AUTOHANGUP {0}", seconds); ProcessAgi(command); } // Changes the callerid of the current channel. // Returns nothing. public void SetCallerId(string number) { string command = String.Format("SET CALLERID {}", number); ProcessAgi(command); } // Sets the context for continuation upon exiting the application. // Returns nothing. public void SetContext(string context) { string command = String.Format("SET CONTEXT {0}", context); ProcessAgi(command); } // Changes the extension for continuation upon exiting the application. // Returns nothing. public void SetExtension(string extension) { string command = String.Format("SET EXTENSION {0}", extension); ProcessAgi(command); } // Enables/Disables the music on hold generator. // Returns nothing. public void SetMusic(bool on, string musicClass) { string command; if (on) { command = String.Format("SET MUSIC ON {0}", musicClass); } else { command = String.Format("SET MUSIC OFF {0}", musicClass); } ProcessAgi(command); } // Override enables/disables the music on hold generator using default channel. // Returns nothing. public void SetMusic(bool on) { string command; if (on) { command = "SET MUSIC ON"; } else { command = "SET MUSIC OFF"; } ProcessAgi(command); } // Changes the priority for continuation upon exiting the application. // Returns nothing. public void SetPriority(int priority) { string command = String.Format("SET PRIORITY {0}", priority); ProcessAgi(command); } // Sets a channel variable. // Returns nothing. public void SetVariable(string variableName, string value) { string command = String.Format("SET VARIABLE {0} {1}", variableName, value); ProcessAgi(command); } public bool StreamFile(string fileName, string escapeDigits, out Keypad keyPressed) { return StreamFile(fileName, escapeDigits, out keyPressed, 0); } // Send the given file, allowing playback to be interrupted by the given digits, if any. // Returns true if successful and false if not. // KeyPressed comes out with a KeyPad if one is received. public bool StreamFile(string fileName, string escapeDigits, out Keypad keyPressed, int sampleOffset) { string command = String.Format("STREAM FILE {0} {1} {2}", fileName, escapeDigits, sampleOffset); string reply = ProcessAgi(command); return ProcessKeyPress(reply, out keyPressed); // TODO - CHECK PROPER RETURNS // failure: 200 result=-1 endpos=<sample offset> // failure on open: 200 result=0 endpos=0 // success: 200 result=0 endpos=<offset> // digit pressed: 200 result=<digit> endpos=<offset> // <offset> is the stream position streaming stopped. If it equals <sample offset> there was probably an error. // <digit> is the ascii code for the digit pressed. } // Enable/Disable TDD transmission/reception on a channel. // Returns true if successful and false if not. public bool TddMode(bool on) { string command; if (on) { command = "TDM MODE ON"; } else { command = "TDM MODE OFF"; } string result = ProcessAgi(command); return ProcessOneZerro(result); // TODO - CHECK PROPER RETURNS // failure: 200 result=-1 // not capable: 200 result=0 // success: 200 result=1 } // Logs a message to the asterisk verbose log. // Returns nothing. public void Verbose(string message, int level) { string command = String.Format("VERBOSE {0} {1}", message, level); ProcessAgi(command); } public void Verbose(string message, params object[] args) { string command = String.Format(message, args); ProcessAgi(command); } // Waits up to 'timeout' milliseconds for channel to receive a DTMF digit. // Returns true if successful and false if not. // KeyPressed comes out with a KeyPad if one is received. public bool WaitForDigit(int timeout, out Keypad keyPressed) { string command = String.Format("WAIT FOR DIGIT {0}", timeout); string reply = ProcessAgi(command); return ProcessKeyPress(reply, out keyPressed); } } public class MonoToneException : Exception { public MonoToneException(String message) : base(message) { } public MonoToneException(String message, Exception innerException) : base(message, innerException) { } } public class FailedAgiCallException : MonoToneException { public FailedAgiCallException(String message) : base(message) { } public FailedAgiCallException(String message, Exception innerException) : base(message, innerException) { } } public class InvalidSyntaxException : MonoToneException { public InvalidSyntaxException(string message) : base(message) { } public InvalidSyntaxException(string message, Exception innerException) : base(message, innerException) { } } public class UnexpectedOutputException : MonoToneException { public UnexpectedOutputException(string message) : base(message) { } public UnexpectedOutputException(string message, Exception innerException) : base(message, innerException) { } } }