/////////////////////////////////////////////////////////////////////////////// // 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 // // Or get off your butt and find a copy of it on the Internet ;) // // And yeah, this program is not perfect. I welcome suggestions and fixes. // Email them to gabe(a)gundy.org // http://gundy.org/ /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// //////////////////////////// MONO-TONE Version .01 //////////////////////////// /////////////////////////////////////////////////////////////////////////////// using System; using System.Text.RegularExpressions; 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 { private const int REPLY_OFFSET = 11; private bool debug = false; 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; // Converst 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() { Initialize(); } // Overloaded constructor for MonoTone with options for initialization and debugging. public MonoTone(bool Init, bool BugInfo) { if(Init) Initialize(); if(BugInfo) debug = BugInfo; } // Property for the 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 RDNIS { 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() { request = Console.In.ReadLine(); channel = Console.In.ReadLine(); language = Console.In.ReadLine(); type = Console.In.ReadLine(); uniqueid = Console.In.ReadLine(); callerid = Console.In.ReadLine(); dnid = Console.In.ReadLine(); rdnis = Console.In.ReadLine(); context = Console.In.ReadLine(); extension = Console.In.ReadLine(); priority = Console.In.ReadLine(); enhanced = Console.In.ReadLine(); accountcode = Console.In.ReadLine(); Console.In.ReadLine(); } // Writes text to the Asterisk console. public void Output(string Format, params object[] Args) { Format = String.Concat("MONOTONE: ", Format); Console.Error.WriteLine(Format, Args); } // Overload public void Output(string Message) { Console.Error.WriteLine("MONOTONE: {0}", Message); } // Overloaded. writes text to the Asterisk console if Debugging in on. public void BugInfo(string Message) { if(debug) Console.Error.WriteLine("BUG INFO: {0}" , Message); } // 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) Console.Error.WriteLine(Format, Args); } // Does all of the reading and writing to STDIN and STDOUT. Trims common results. private string ProcessAGI(string Command) { BugInfo("ProcessAGI-Command = {0}", Command); Console.Out.WriteLine(Command); string reply = Console.In.ReadLine(); BugInfo("ProcessAGI-reply = {0}", reply); string result = null; Regex replyResult = new Regex("^200 result="); if (replyResult.IsMatch(reply)) { result = reply.Substring(REPLY_OFFSET, reply.Length - REPLY_OFFSET); } 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 UnexpectedOutput(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 UnexpectedOutput(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 UnexpectedOutput(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 UnexpectedOutput(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 () } // 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= (timeout) //success: 200 result= } // 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, int EscapeDigits, int TimeOut, bool OffSetSamples, bool Beep, int Silence) { //TODO - CHECK PROPER RETURNS //failure to write: 200 result=-1 (writefile) //failure on waitfor: 200 result=-1 (waitfor) endpos= //hangup: 200 result=0 (hangup) endpos= ///interrupted: 200 result= (dtmf) endpos= //timeout: 200 result=0 (timeout) endpos= //random error: 200 result= (randomerror) endpos= bool result; string reply; if (OffSetSamples) if (Beep) Console.Out.WriteLine("RECORD FILE {0} {1} {2} {3} offset samples BEEP s={4}", FileName, Format, EscapeDigits, TimeOut, Silence); else Console.Out.WriteLine("RECORD FILE {0} {1} {2} {3} offset samples s={4}", FileName, Format, EscapeDigits, TimeOut, Silence); else if (Beep) Console.Out.WriteLine("RECORD FILE {0} {1} {2} {3} BEEP s={4}", FileName, Format, EscapeDigits, TimeOut, Silence); else Console.Out.WriteLine("RECORD FILE {0} {1} {2} {3} s={4}", FileName, Format, EscapeDigits, TimeOut, Silence); reply = Console.In.ReadLine(); switch (reply) { case "200 result=-1": result = false; break; case "200 result=0": result = true; break; default: Console.Error.WriteLine("RECORD FILE gave the unexpected reply of {0}", reply); 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 Class) { string command; if (On) command = String.Format("SET MUSIC ON {0}", Class); else command = String.Format("SET MUSIC OFF {0}", Class); 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); } // 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) { string command = String.Format("STREAM FILE {0} {1} sample offset", FileName, EscapeDigits); string reply = ProcessAGI(command); return ProcessKeyPress(reply, out KeyPressed); //TODO - CHECK PROPER RETURNS //failure: 200 result=-1 endpos= //failure on open: 200 result=0 endpos=0 //success: 200 result=0 endpos= //digit pressed: 200 result= endpos= // is the stream position streaming stopped. If it equals there was probably an error. // 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); } class MonoToneException : ApplicationException { public MonoToneException(String Message) : base(Message) { } public MonoToneException(String Message, Exception InnerException) : base(Message, InnerException) { } } class FailedAGICall : MonoToneException { public FailedAGICall(String Message) : base(Message) { } public FailedAGICall(String Message, Exception InnerException) : base(Message, InnerException) { } } class InvalidSyntax : MonoToneException { public InvalidSyntax(String Message) : base(Message) { } public InvalidSyntax(String Message, Exception InnerException) : base(Message, InnerException) { } } class UnexpectedOutput : MonoToneException { public UnexpectedOutput(String Message) : base(Message) { } public UnexpectedOutput(String Message, Exception InnerException) : base(Message, InnerException) { } } } }