J.A. Roberts Tunney avatar J.A. Roberts Tunney committed 3d6174c

initial import

Comments (0)

Files changed (16)

+syntax: glob
+*.pyc
+*.pyo
+*.pyc
+*.o
+*~
+*.rej
+*.orig
+
+.figleaf*
+*.egg-info
+*.egg
+build
+.build
+dist
+pip-log.txt
+
+syntax: regexp
+(.*/)?\#[^/]*\#$

Empty file added.

Asterator/about.py

+
+import os.path
+import gobject
+import pygtk
+pygtk.require('2.0')
+import gtk
+
+class ast_about(gtk.AboutDialog):
+    def __init__(self):
+        gtk.AboutDialog.__init__(self)
+        self.set_name('asterator')
+        self.set_version('0.1')
+        self.set_copyright('Copyright (c) 2006 J.A. Roberts Tunney')
+        self.set_comments("Asterator is highly awesome and advanced\n"
+                          "GUI through which you can manage and\n"
+                          "monitor your Asterisk servers.\n"
+                          "\n"
+                          "Keep it open source pigs\n")
+        self.set_website('http://www.lobstertech.com/')
+        self.set_authors(['J.A. Roberts Tunney Tunney <jtunney@gmail.com>'])
+        license = open(os.path.join(os.path.dirname(__file__), '../COPYING')).read()
+        self.set_license(license)

Asterator/channeltree.py

+
+import gobject
+import pygtk
+pygtk.require('2.0')
+import gtk
+
+(
+    NAME_COLUMN,
+    STATE_COLUMN,
+    TIME_COLUMN,
+    CALLERID_COLUMN,
+    LOCATION_COLUMN,
+    APP_COLUMN
+) = range(6)
+
+class channel_tree(gtk.TreeView):
+    def gen_model(self):
+        model = gtk.TreeStore(
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING)
+
+        self.channels = model.append(None)
+        model.set(self.channels,
+                  NAME_COLUMN, '[Orphaned]',
+                  STATE_COLUMN, '',
+                  TIME_COLUMN, '',
+                  CALLERID_COLUMN, '',
+                  LOCATION_COLUMN, '',
+                  APP_COLUMN, '')
+        
+        self.links = model.append(None)
+        model.set(self.links,
+                  NAME_COLUMN, '[Linked]',
+                  STATE_COLUMN, '',
+                  TIME_COLUMN, '',
+                  CALLERID_COLUMN, '',
+                  LOCATION_COLUMN, '',
+                  APP_COLUMN, '')
+        
+        self.parkedcalls = model.append(None)
+        model.set(self.parkedcalls,
+                  NAME_COLUMN, '[Parked]',
+                  STATE_COLUMN, '',
+                  TIME_COLUMN, '',
+                  CALLERID_COLUMN, '',
+                  LOCATION_COLUMN, '',
+                  APP_COLUMN, '')
+        
+        self.channel_cnt = 0
+        self.link_cnt = 0
+        self.parkedcall_cnt = 0
+
+        return model
+    
+    def __init__(self):
+        model = self.gen_model()
+        
+        gtk.TreeView.__init__(self, model)
+        self.set_rules_hint(True)
+        self.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
+        self.connect('realize', lambda tv: tv.expand_all())
+        self.connect('row-activated', self.on_rowactivated)
+        self.connect('button_press_event', self.on_treeview_button_press_event)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("Channel", renderer, text=NAME_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("State", renderer, text=STATE_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("Alive", renderer, text=TIME_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("CallerID", renderer, text=CALLERID_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("Location", renderer, text=LOCATION_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("App", renderer, text=APP_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+    def on_rowactivated(self, tv, path, view_col):
+        if tv.row_expanded(path):
+            tv.collapse_row(path)
+        else:
+            tv.expand_row(path, False)
+
+    def on_treeview_button_press_event(self, treeview, event):
+        if event.button > 3:
+            dlg = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT,
+                                    gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+                                    str(event.button))
+            dlg.run()
+            dlg.hide()
+        if event.button == 3:
+            self.popup.popup(None, None, None, event.button, event.time)
+            return 1
+
+    def clear(self):
+        self.set_model(self.gen_model())
+
+    def myset(self, model, chan):
+        model.set(chan.iter,
+                  NAME_COLUMN, chan.name,
+                  STATE_COLUMN, chan.state,
+                  TIME_COLUMN, chan.get_timealive(),
+                  CALLERID_COLUMN, chan.get_callerid(),
+                  LOCATION_COLUMN, chan.get_location(),
+                  APP_COLUMN, chan.get_app())
+
+    def reconcile(self, listener):
+        """A quickly written refresh method
+
+        Don't call this from within a listener callback or you'll get
+        a deadlock lolz because this locks the listener object.
+
+        ToDo: Make this better
+        
+        """
+        self.clear()
+        listener.lock()
+        for channel in listener.channels.values():
+            self.add_channel(channel)
+            if channel.parked:
+                self.prk_channel(channel)
+        for link in listener.links:
+            self.add_link(link)
+        listener.unlock()
+
+    def add_channel(self, chan):
+        model = self.get_model()
+        self.channel_cnt += 1
+        
+        iter = model.append(self.channels)
+        chan.iter = iter
+        self.myset(model, chan)
+        
+        if self.channel_cnt == 1:
+            self.expand_row(model.get_string_from_iter(self.channels), True)
+
+    def del_channel(self, chan):
+        model = self.get_model()
+        model.remove(chan.iter)
+        self.channel_cnt -= 1
+        assert self.channel_cnt >= 0
+
+    def upd_channel(self, chan):
+        model = self.get_model()
+        self.myset(model, chan)
+        
+    def prk_channel(self, chan):
+        self.del_channel(chan)
+        
+        model = self.get_model()
+        self.parkedcall_cnt += 1
+        
+        iter = model.append(self.parkedcalls)
+        chan.iter = iter
+        self.myset(model, chan)
+        
+        if self.parkedcall_cnt == 1:
+            self.expand_row(model.get_string_from_iter(self.parkedcalls), True)
+            
+    def unprk_channel(self, chan):
+        model = self.get_model()
+        model.remove(chan.iter)
+        self.add_channel(chan)
+        self.parkedcall_cnt -= 1
+        assert self.parkedcall_cnt >= 0
+
+    def add_link(self, link):
+        model = self.get_model()
+        self.link_cnt += 1
+        
+        self.del_channel(link.chan1)
+        self.del_channel(link.chan2)
+        iter = model.append(self.links)
+        link.iter = iter
+        model.set(iter,
+                  NAME_COLUMN, link.chan1.name + ' & ' + link.chan2.name,
+                  STATE_COLUMN, '',
+                  TIME_COLUMN, '',
+                  CALLERID_COLUMN, '',
+                  LOCATION_COLUMN, '',
+                  APP_COLUMN, '')
+        
+        child = model.append(iter)
+        link.chan1.iter = child
+        self.myset(model, link.chan1)
+        
+        child = model.append(iter)
+        link.chan2.iter = child
+        self.myset(model, link.chan2)
+        
+        self.expand_row(model.get_string_from_iter(iter), True)
+        if self.link_cnt == 1:
+            self.expand_row(model.get_string_from_iter(self.links), True)
+        
+    def del_link(self, link):
+        model = self.get_model()
+
+        self.add_channel(link.chan1)
+        self.add_channel(link.chan2)
+        model.remove(link.iter)
+        
+        self.link_cnt -= 1
+        assert self.link_cnt >= 0

Asterator/conftree.py

+
+import gobject
+import pygtk
+pygtk.require('2.0')
+import gtk
+
+(
+    CHANNEL_COLUMN,
+    CALLERID_COLUMN,
+    NUMBER_COLUMN,
+    TIME_COLUMN,
+    TALKING_COLUMN
+) = range(5)
+
+class conf_tree(gtk.TreeView):
+    def gen_model(self):
+        model = gtk.TreeStore(
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING)
+
+        self.confs = {}
+
+        return model
+    
+    def __init__(self):
+        model = self.gen_model()
+        
+        gtk.TreeView.__init__(self, model)
+        self.set_rules_hint(True)
+        self.connect('realize', lambda tv: tv.expand_all())
+        self.connect('row-activated', self.on_rowactivated)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("Channel", renderer, text=CHANNEL_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("CallerID", renderer, text=CALLERID_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("Member No.", renderer, text=NUMBER_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("Joined", renderer, text=TIME_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("Talking", renderer, text=TALKING_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+    def on_rowactivated(self, tv, path, view_col):
+        if tv.row_expanded(path):
+            tv.collapse_row(path)
+        else:
+            tv.expand_row(path, False)
+
+    def clear(self):
+        self.set_model(self.gen_model())
+
+    def reconcile(self, listener):
+        """A quickly written refresh method
+
+        Don't call this from within a listener callback or you'll get
+        a deadlock lolz because this locks the listener object.
+
+        ToDo: Make this better
+        
+        """
+        self.clear()
+        listener.lock()
+        for conf in listener.confs.values():
+            self.add_conf(conf)
+            for memb in conf.members.values():
+                self.add_conf_member(conf, memb)
+        listener.unlock()
+
+    def add_conf(self, conf):
+        model = self.get_model()
+        iter = model.append(None)
+        conf.iter = iter
+        model.set(conf.iter,
+                  CHANNEL_COLUMN, 'Conf: ' + conf.number,
+                  CALLERID_COLUMN, '',
+                  NUMBER_COLUMN, '',
+                  TIME_COLUMN, '',
+                  TALKING_COLUMN, '')
+
+        self.confs[conf.number] = 0
+
+    def del_conf(self, conf):
+        model = self.get_model()
+        model.remove(conf.iter)
+        del self.confs[conf.number]
+
+    def add_conf_member(self, conf, memb):
+        model = self.get_model()
+        memb.iter = model.append(conf.iter)
+        if memb.talking:
+            talking = True
+        else:
+            talking = False
+        model.set(memb.iter,
+                  CHANNEL_COLUMN, memb.chan.name,
+                  CALLERID_COLUMN, memb.chan.get_callerid(),
+                  NUMBER_COLUMN, memb.usernum,
+                  TIME_COLUMN, memb.get_timealive(),
+                  TALKING_COLUMN, talking)
+
+        self.confs[conf.number] += 1
+
+        if self.confs[conf.number] == 1:
+            self.expand_row(model.get_string_from_iter(conf.iter), True)
+
+    def del_conf_member(self, conf, memb):
+        model = self.get_model()
+        model.remove(memb.iter)
+        self.confs[conf.number] -= 1
+
+    def upd_conf_member(self, conf, memb):
+        model = self.get_model()
+        if memb.talking:
+            talking = True
+        else:
+            talking = False
+        model.set(memb.iter,
+                  CHANNEL_COLUMN, memb.chan.name,
+                  CALLERID_COLUMN, memb.chan.get_callerid(),
+                  NUMBER_COLUMN, memb.usernum,
+                  TIME_COLUMN, memb.get_timealive(),
+                  TALKING_COLUMN, talking)

Asterator/info.py

+
+import gobject
+import pygtk
+pygtk.require('2.0')
+import gtk
+import pango
+
+def mylbl(txt):
+    lbl = gtk.Label(txt)
+    lbl.set_alignment(0, 0)
+    lbl.set_selectable(True)
+    lbl.show()
+    return lbl
+
+class channel_info(gtk.Window):
+    def __init__(self, chan):
+        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
+
+        self.chan = chan
+
+        wvbox = gtk.VBox(homogeneous=False, spacing=0)
+        self.add(wvbox)
+        wvbox.show()
+        
+        self.set_title(chan.name)
+        self.set_default_size(500, 250)
+        self.set_position(gtk.WIN_POS_MOUSE)
+        
+        notebook = gtk.Notebook()
+        notebook.set_tab_pos(gtk.POS_TOP)
+        wvbox.pack_start(notebook)
+        notebook.show()
+
+        vbox = gtk.VBox(homogeneous=False, spacing=0)
+        vbox.show()
+        
+        hbox = gtk.HBox(homogeneous=False, spacing=5)
+        hbox.set_border_width(10)
+        hbox.show()
+        self.lbl_chan = gtk.Label()
+        self.lbl_chan.set_markup('<span size="xx-large">' + self.chan.name + '</span>')
+        hbox.pack_start(self.lbl_chan, False)
+        self.lbl_chan.show()
+        self.lbl_state = gtk.Label()
+        self.lbl_state.set_markup('<span size="xx-large">' + self.chan.state + '</span>')
+        hbox.pack_end(self.lbl_state, False)
+        self.lbl_state.show()
+        vbox.pack_start(hbox, False)
+
+        sep = gtk.HSeparator()
+        vbox.pack_start(sep, False)
+
+        tbl = gtk.Table(7, 4, True)
+        tbl.set_border_width(10)
+        tbl.set_row_spacings(4)
+        tbl.set_col_spacings(4)
+        vbox.pack_start(tbl, True, True)
+        tbl.show()
+
+        tbl.attach(mylbl('Unique ID:'), 0, 1, 0, 1, gtk.FILL, gtk.FILL)
+        tbl.attach(mylbl('Caller ID:'), 0, 1, 1, 2, gtk.FILL, gtk.FILL)
+        tbl.attach(mylbl('DNID Digits:'), 0, 1, 2, 3, gtk.FILL, gtk.FILL)
+        tbl.attach(mylbl('Rings:'), 0, 1, 3, 4, gtk.FILL, gtk.FILL)
+        tbl.attach(mylbl('Native Format:'), 0, 1, 4, 5, gtk.FILL, gtk.FILL)
+        tbl.attach(mylbl('Write Format:'), 0, 1, 5, 6, gtk.FILL, gtk.FILL)
+        tbl.attach(mylbl('Read Format:'), 0, 1, 6, 7, gtk.FILL, gtk.FILL)
+        tbl.attach(mylbl('Elapsed Time:'), 2, 3, 0, 1, gtk.FILL, gtk.FILL)
+        tbl.attach(mylbl('Frames In:'), 2, 3, 1, 2, gtk.FILL, gtk.FILL)
+        tbl.attach(mylbl('Frames Out:'), 2, 3, 2, 3, gtk.FILL, gtk.FILL)
+        tbl.attach(mylbl('1st File Desc:'), 2, 3, 3, 4, gtk.FILL, gtk.FILL)
+        tbl.attach(mylbl('Time to Hangup:'), 2, 3, 4, 5, gtk.FILL, gtk.FILL)
+        tbl.attach(mylbl('Direct Bridge:'), 2, 3, 5, 6, gtk.FILL, gtk.FILL)
+        tbl.attach(mylbl('Indirect Bridge:'), 2, 3, 6, 7, gtk.FILL, gtk.FILL)
+
+        self.lbl_uniqueid = mylbl('Unknown')
+        tbl.attach(self.lbl_uniqueid, 1, 2, 0, 1, gtk.FILL, gtk.FILL)
+        self.lbl_callerid = mylbl('Unknown')
+        tbl.attach(self.lbl_callerid, 1, 2, 1, 2, gtk.FILL, gtk.FILL)
+        self.lbl_dniddigits = mylbl('Unknown')
+        tbl.attach(self.lbl_dniddigits, 1, 2, 2, 3, gtk.FILL, gtk.FILL)
+        self.lbl_rings = mylbl('Unknown')
+        tbl.attach(self.lbl_rings, 1, 2, 3, 4, gtk.FILL, gtk.FILL)
+        self.lbl_nativeformat = mylbl('Unknown')
+        tbl.attach(self.lbl_nativeformat, 1, 2, 4, 5, gtk.FILL, gtk.FILL)
+        self.lbl_writeformat = mylbl('Unknown')
+        tbl.attach(self.lbl_writeformat, 1, 2, 5, 6, gtk.FILL, gtk.FILL)
+        self.lbl_readformat = mylbl('Unknown')
+        tbl.attach(self.lbl_readformat, 1, 2, 6, 7, gtk.FILL, gtk.FILL)
+        self.lbl_elapsedtime = mylbl('Unknown')
+        tbl.attach(self.lbl_elapsedtime, 3, 4, 0, 1, gtk.FILL, gtk.FILL)
+        self.lbl_framesin = mylbl('Unknown')
+        tbl.attach(self.lbl_framesin, 3, 4, 1, 2, gtk.FILL, gtk.FILL)
+        self.lbl_framesout = mylbl('Unknown')
+        tbl.attach(self.lbl_framesout, 3, 4, 2, 3, gtk.FILL, gtk.FILL)
+        self.lbl_firstfd = mylbl('Unknown')
+        tbl.attach(self.lbl_firstfd, 3, 4, 3, 4, gtk.FILL, gtk.FILL)
+        self.lbl_timetohangup = mylbl('Unknown')
+        tbl.attach(self.lbl_timetohangup, 3, 4, 4, 5, gtk.FILL, gtk.FILL)
+        self.lbl_directbridge = mylbl('Unknown')
+        tbl.attach(self.lbl_directbridge, 3, 4, 5, 6, gtk.FILL, gtk.FILL)
+        self.lbl_indirectbridge = mylbl('Unknown')
+        tbl.attach(self.lbl_indirectbridge, 3, 4, 6, 7, gtk.FILL, gtk.FILL)
+
+        notebook.append_page(vbox, gtk.Label('Info'))
+
+        bbox = gtk.HButtonBox()
+        bbox.set_border_width(5)
+        bbox.set_layout(gtk.BUTTONBOX_END)
+        bbox.set_spacing(10)
+        btn = gtk.Button(stock='gtk-refresh')
+        btn.connect('clicked', self.action_refresh)
+        bbox.pack_start(btn)
+        btn.show()
+        btn = gtk.Button(stock='gtk-close')
+        btn.connect('clicked', self.action_close)
+        bbox.pack_start(btn)
+        btn.show()
+        wvbox.pack_start(bbox, False)
+        bbox.show()
+
+        self.action_refresh()
+
+    def on_info(self, info):
+        gtk.threads_enter()
+
+        if info == None:
+            dlg = gtk.MessageDialog(self, gtk.DIALOG_DESTROY_WITH_PARENT,
+                                    gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+                                    "Error: " + frame['Message'])
+            dlg.run()
+            dlg.hide()
+            gtk.threads_leave()
+            return
+
+        self.lbl_uniqueid.set_text(info['info']['UniqueID'])
+        self.lbl_callerid.set_text(info['info']['Caller ID Name'] + ' <' + info['info']['Caller ID'] + '>')
+        self.lbl_dniddigits.set_text(info['info']['DNID Digits'])
+        self.lbl_rings.set_text(info['info']['Rings'])
+        self.lbl_nativeformat.set_text(info['info']['NativeFormat'])
+        self.lbl_writeformat.set_text(info['info']['WriteFormat'])
+        self.lbl_readformat.set_text(info['info']['ReadFormat'])
+        self.lbl_elapsedtime.set_text(info['info']['Elapsed Time'])
+        self.lbl_framesin.set_text(info['info']['Frames in'])
+        self.lbl_framesout.set_text(info['info']['Frames out'])
+        self.lbl_firstfd.set_text(info['info']['1st File Descriptor'])
+        self.lbl_timetohangup.set_text(info['info']['Time to Hangup'])
+        self.lbl_directbridge.set_text(info['info']['Direct Bridge'])
+        self.lbl_indirectbridge.set_text(info['info']['Indirect Bridge'])
+        
+        gtk.threads_leave()
+
+    def action_refresh(self, widget=None, data=None):
+        self.chan.getinfo(self.on_info)
+
+    def action_close(self, widget=None, data=None):
+        self.destroy()

Asterator/mainwin.py

+
+import time
+import thread
+import socket
+import timeoutsocket
+import gobject
+import pygtk
+pygtk.require('2.0')
+import gtk
+
+from channeltree import channel_tree
+from zaptree import zap_tree
+from conftree import conf_tree
+from about import ast_about
+from info import channel_info
+from Asterisk import listener, manager
+
+timeoutsocket.setDefaultSocketTimeout(5.0)
+
+class asterator(gtk.Window):
+    def __init__(self):
+        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
+        self.connect('destroy', self.action_quit)
+        self.set_title("asterator")
+        self.set_default_size(700, 500)
+        self.set_position(gtk.WIN_POS_MOUSE)
+        
+        vbox = gtk.VBox(homogeneous=False, spacing=0);
+        self.add(vbox)
+        vbox.show()
+        
+        self.ag = gtk.ActionGroup('asterator')
+        self.ag.add_actions([
+            ('FileMenu', None, '_File'),
+            ('HelpMenu', None, '_Help'),
+            ('DebugMenu', None, '_Debug'),
+            ('Connect', gtk.STOCK_NETWORK, '_Connect', '<control>c', None, self.action_connect),
+            ('Disconnect', gtk.STOCK_NO, '_Disconnect', '<control>d', None, self.action_disconnect),
+            ('Expand', gtk.STOCK_ZOOM_IN, '_Expand All', '<control>e', None, self.action_expand),
+            ('Collapse', gtk.STOCK_ZOOM_OUT, 'C_ollapse All', None, None, self.action_collapse),
+            ('Refresh', None, '_Refresh', '<control>r', None, self.action_refresh),
+            ('Exit', gtk.STOCK_QUIT, None, None, None, self.action_quit),
+            ('About', gtk.STOCK_ABOUT, 'A_bout', None, None, self.action_about),
+            ('Dump', gtk.STOCK_QUIT, 'Dump', None, None, self.action_quit),
+            ('Hangup', gtk.STOCK_CANCEL, 'Hangup', None, None, self.action_chan_hangup),
+            ('HangupAfter', None, 'Hangup After...', None, None, self.action_chan_hangupafter),
+            ('Redirect', gtk.STOCK_REDO, 'Redirect...', None, None, self.action_chan_redirect),
+            ])
+        self.ag.add_toggle_actions([
+            ('AutoUpdate', None, '_Auto Update', None, None, self.action_autoupdate, True),
+            ])
+        
+        self.ui = gtk.UIManager()
+        self.ui.set_add_tearoffs(True)
+        self.ui.insert_action_group(self.ag, 0)
+        self.ui.add_ui_from_string("""<ui>
+  <!-- The UI Manager Thingy is SOOOO COOL!!! -->
+  <menubar name='mainmenu'>
+    <menu name='connect' action='FileMenu'>
+      <menuitem name='connect' action='Connect'/>
+      <menuitem name='disconnect' action='Disconnect'/>
+      <separator/>
+      <menuitem name='expand' action='Expand'/>
+      <menuitem name='collapse' action='Collapse'/>
+      <separator/>
+      <menuitem name='refresh' action='Refresh'/>
+      <menuitem name='autoupdate' action='AutoUpdate'/>
+      <separator/>
+      <menuitem name='exit' action='Exit'/>
+    </menu>
+    <menu name='help' action='HelpMenu' position='bot'>
+      <menuitem name='about' action='About'/>
+    </menu>
+  </menubar>
+  <popup name='popchan'>
+    <menuitem action='Hangup'/>
+    <menuitem action='HangupAfter'/>
+    <menuitem action='Redirect'/>
+  </popup>
+</ui>
+""")
+        self.add_accel_group(self.ui.get_accel_group())
+        vbox.pack_start(self.ui.get_widget('/ui/mainmenu'), False, False, 0)
+        self.ui.get_widget('/ui/mainmenu/connect/disconnect').set_sensitive(False)
+
+        hbox = gtk.HBox(homogeneous=False, spacing=5)
+        hbox.set_border_width(10)
+        vbox.pack_start(hbox, False)
+        hbox.show()
+        sep = gtk.HSeparator()
+        vbox.pack_start(sep, False)
+        sep.show()
+
+        self.lbl_host = gtk.Label('Host/IP:')
+        self.lbl_host.show()
+        hbox.pack_start(self.lbl_host, False)
+        self.txt_host = gtk.Entry(32)
+        self.txt_host.set_width_chars(15)
+        self.txt_host.set_text('localhost')
+        self.txt_host.set_activates_default(True)
+        self.txt_host.connect('activate', self.action_connect)
+        hbox.pack_start(self.txt_host, False, False)
+        self.txt_host.show()
+
+        self.lbl_port = gtk.Label('Port:')
+        self.lbl_port.show()
+        hbox.pack_start(self.lbl_port, False)
+        self.txt_port = gtk.Entry(5)
+        self.txt_port.set_width_chars(5)
+        self.txt_port.set_text('5038')
+        self.txt_port.set_activates_default(True)
+        self.txt_port.connect('activate', self.action_connect)
+        hbox.pack_start(self.txt_port, False, False)
+        self.txt_port.show()
+        
+        sep = gtk.VSeparator()
+        hbox.pack_start(sep, False, False)
+        sep.show()
+        
+        self.lbl_user = gtk.Label('User:')
+        self.lbl_user.show()
+        hbox.pack_start(self.lbl_user, False)
+        self.txt_user = gtk.Entry(32)
+        self.txt_user.set_width_chars(10)
+        self.txt_user.set_activates_default(True)
+        self.txt_user.connect('activate', self.action_connect)
+        hbox.pack_start(self.txt_user, False, False)
+        self.txt_user.show()
+
+        self.lbl_pass = gtk.Label('Pass:')
+        self.lbl_pass.show()
+        hbox.pack_start(self.lbl_pass, False)
+        self.txt_pass = gtk.Entry(32)
+        self.txt_pass.set_visibility(False)
+        self.txt_pass.set_width_chars(10)
+        self.txt_pass.set_activates_default(True)
+        self.txt_pass.connect('activate', self.action_connect)
+        hbox.pack_start(self.txt_pass, False, False)
+        self.txt_pass.show()
+        
+        self.btn_disconnect = gtk.Button('Disconnect')
+        self.btn_disconnect.connect('clicked', self.action_disconnect)
+        self.btn_disconnect.show()
+        self.btn_disconnect.set_sensitive(False)
+        hbox.pack_end(self.btn_disconnect, False, False)
+        
+        self.btn_connect = gtk.Button('Connect')
+        self.btn_connect.connect('clicked', self.action_connect)
+        self.btn_connect.show()
+        hbox.pack_end(self.btn_connect, False, False)
+
+        notebook = gtk.Notebook()
+        notebook.set_tab_pos(gtk.POS_TOP)
+        vbox.pack_start(notebook)
+        notebook.show()
+
+        scrollbox = gtk.ScrolledWindow()
+        scrollbox.set_shadow_type(gtk.SHADOW_ETCHED_IN)
+        scrollbox.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        scrollbox.show()
+        self.chantree = channel_tree()
+        self.chantree.connect('row-activated', self.on_chan_rowactivated)
+        self.chantree.popup = self.ui.get_widget('/ui/popchan')
+        scrollbox.add(self.chantree)
+        self.chantree.show()
+
+        notebook.append_page(scrollbox, gtk.Label('Active Channels'))
+
+        scrollbox = gtk.ScrolledWindow()
+        scrollbox.set_shadow_type(gtk.SHADOW_ETCHED_IN)
+        scrollbox.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        scrollbox.show()
+        self.zaptree = zap_tree()
+        scrollbox.add(self.zaptree)
+        self.zaptree.show()
+
+        notebook.append_page(scrollbox, gtk.Label('Zaptel Channels'))
+
+        scrollbox = gtk.ScrolledWindow()
+        scrollbox.set_shadow_type(gtk.SHADOW_ETCHED_IN)
+        scrollbox.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+        scrollbox.show()
+        self.conftree = conf_tree()
+        scrollbox.add(self.conftree)
+        self.conftree.show()
+
+        notebook.append_page(scrollbox, gtk.Label('Conferences'))
+
+        self.statusbar = gtk.Statusbar()
+        vbox.pack_start(self.statusbar, False, False, 0)
+        self.statusbar.show()
+
+        self.context_id = self.statusbar.get_context_id('General Messages')
+        self.statusbar.push(self.context_id, 'Ready')
+
+        self.in_session = False
+        self.mansock = None
+        self.man = None
+        self.listen = None
+        self.thread = None
+        self.auto_update = True
+
+    def do_connect(self):
+        if self.man:
+            return
+
+        self.chantree.clear()
+        self.zaptree.clear()
+        self.conftree.clear()
+
+        self.statusbar.pop(self.context_id)
+        self.statusbar.push(self.context_id, 'Attempting to connect...')
+        
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.settimeout(5)
+        try:
+            sock.connect((self.txt_host.get_text(), int(self.txt_port.get_text())))
+        except timeoutsocket.Timeout:
+            dlg = gtk.MessageDialog(self, gtk.DIALOG_DESTROY_WITH_PARENT,
+                                    gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+                                    "Connection attempt timed out")
+            dlg.run()
+            dlg.hide()
+            self.statusbar.pop(self.context_id)
+            self.statusbar.push(self.context_id, 'Connect Timeout!')
+            return
+        except socket.error, e:
+            dlg = gtk.MessageDialog(self, gtk.DIALOG_DESTROY_WITH_PARENT,
+                                    gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+                                    "Connect Failed: " + e[1])
+            dlg.run()
+            dlg.hide()
+            self.statusbar.pop(self.context_id)
+            self.statusbar.push(self.context_id, 'Connect Failed!')
+            return
+        
+        sock.settimeout(None)
+        
+        self.statusbar.pop(self.context_id)
+        self.statusbar.push(self.context_id, 'Waiting for server...')
+
+        self.mansock = manager.mansock(sock=sock, debug=True)
+        self.mansock.handler_session = self.mansock_session
+        
+        self.in_session = False
+        gobject.timeout_add(5000, self.session_timeout)
+
+        self.man = manager.manager(sock=self.mansock, debug=True)
+
+        self.listen = listener.manager_listener(self.man)
+        self.listen.on_newchan = lambda l, c: self.invoke(self.on_newchan, l, c)
+        self.listen.on_delchan = lambda l, c: self.invoke(self.on_delchan, l, c)
+        self.listen.on_updchan = lambda l, c: self.invoke(self.on_updchan, l, c)
+        self.listen.on_newlink = lambda l, c: self.invoke(self.on_newlink, l, c)
+        self.listen.on_dellink = lambda l, c: self.invoke(self.on_dellink, l, c)
+        self.listen.on_chan_park = lambda l, c: self.invoke(self.on_chan_park, l, c)
+        self.listen.on_chan_unpark = lambda l, c: self.invoke(self.on_chan_unpark, l, c)
+        self.listen.on_chan_parktimeout = lambda l, c: self.invoke(self.on_chan_unpark, l, c)
+        self.listen.on_chan_parkgiveup = lambda l, c: self.invoke(self.on_chan_unpark, l, c)
+        self.listen.on_newzap = lambda l, c: self.invoke(self.on_newzap, l, c)
+        self.listen.on_updzap = lambda l, c: self.invoke(self.on_updzap, l, c)
+        self.listen.on_newconf = lambda l, c: self.invoke(self.on_newconf, l, c)
+        self.listen.on_delconf = lambda l, c: self.invoke(self.on_delconf, l, c)
+        self.listen.on_joinconf = lambda l, c, m: self.invoke2(self.on_joinconf, l, c, m)
+        self.listen.on_leaveconf = lambda l, c, m: self.invoke2(self.on_leaveconf, l, c, m)
+        self.listen.on_updmemberconf = lambda l, c, m: self.invoke2(self.on_updmemberconf, l, c, m)
+
+        self.thread = thread.start_new_thread(manager.run_manager, (self.man, 'ignore'))
+
+        self.ui.get_widget('/ui/mainmenu/connect/connect').set_sensitive(False)
+        self.ui.get_widget('/ui/mainmenu/connect/disconnect').set_sensitive(True)
+        self.btn_connect.set_sensitive(False)
+        self.btn_disconnect.set_sensitive(True)
+        self.lbl_host.set_sensitive(False)
+        self.txt_host.set_sensitive(False)
+        self.lbl_port.set_sensitive(False)
+        self.txt_port.set_sensitive(False)
+        self.lbl_user.set_sensitive(False)
+        self.txt_user.set_sensitive(False)
+        self.lbl_pass.set_sensitive(False)
+        self.txt_pass.set_sensitive(False)
+
+    def do_disconnect(self):
+        if not self.man:
+            return
+
+        self.man.terminate()
+        self.mansock = None
+        self.man = None
+        self.listen = None
+        self.thread = None
+
+        self.statusbar.pop(self.context_id)
+        self.statusbar.push(self.context_id, 'Disconnected')
+        self.ui.get_widget('/ui/mainmenu/connect/connect').set_sensitive(True)
+        self.ui.get_widget('/ui/mainmenu/connect/disconnect').set_sensitive(False)
+        self.btn_connect.set_sensitive(True)
+        self.btn_disconnect.set_sensitive(False)
+        self.lbl_host.set_sensitive(True)
+        self.txt_host.set_sensitive(True)
+        self.lbl_port.set_sensitive(True)
+        self.txt_port.set_sensitive(True)
+        self.lbl_user.set_sensitive(True)
+        self.txt_user.set_sensitive(True)
+        self.lbl_pass.set_sensitive(True)
+        self.txt_pass.set_sensitive(True)
+        
+    def action_connect(self, widget, data=None):
+        self.do_connect()
+
+    def action_disconnect(self, widget, data=None):
+        self.do_disconnect()
+
+    def action_expand(self, widget, data=None):
+        self.chantree.expand_all()
+
+    def action_collapse(self, widget, data=None):
+        self.chantree.collapse_all()
+
+    def action_refresh(self, widget, data=None):
+        self.chantree.reconcile(self.listen)
+        self.zaptree.reconcile(self.listen)
+        self.conftree.reconcile(self.listen)
+
+    def action_autoupdate(self, widget, data=None):
+        self.auto_update = widget.get_active()
+
+    def action_about(self, widget, data=None):
+        abt = ast_about()
+        abt.run()
+        abt.hide()
+
+    def action_quit(self, widget, data=None):
+        self.do_disconnect()
+        gtk.main_quit()
+
+    def generic_callback(self, frame):
+        gtk.threads_enter()
+        if not 'Message' in frame:
+            frame['Message'] = 'None'
+        if frame['Response'] == 'Success':
+            self.statusbar.pop(self.context_id)
+            self.statusbar.push(self.context_id, 'Success: ' + frame['Message'])
+        else:
+            dlg = gtk.MessageDialog(self, gtk.DIALOG_DESTROY_WITH_PARENT,
+                                    gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+                                    "Error: " + frame['Message'])
+            dlg.run()
+            dlg.hide()
+        gtk.threads_leave()
+
+    def on_chan_rowactivated(self, tv, path, view_col):
+        if not self.man:
+            return False
+        
+        m = self.chantree.get_model()
+        chan = None
+        self.listen.lock()
+        for c in self.listen.channels.values():
+            if m.get_path(c.iter) == path:
+                chan = c
+        self.listen.unlock()
+        if chan == None:
+            return
+        cw = channel_info(chan)
+        cw.show()
+
+    def action_chan_hangup(self, widget, data=None):
+        if not self.man:
+            return False
+        
+        sel = self.chantree.get_selection()
+        self.listen.lock()
+        found = False
+        for chan in self.listen.channels.values():
+            if sel.iter_is_selected(chan.iter):
+                chan.hangup(self.generic_callback)
+                found = True
+        self.listen.unlock()
+        if not found:
+            dlg = gtk.MessageDialog(self, gtk.DIALOG_DESTROY_WITH_PARENT,
+                                    gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+                                    "No channels selected!")
+            dlg.run()
+            dlg.hide()
+
+    def action_chan_hangupafter(self, widget, data=None):
+        pass
+
+    def action_chan_redirect(self, widget, data=None):
+        pass
+
+    def session_timeout(self):
+        if not self.man:
+            return False
+        
+        if not self.in_session:
+            gtk.threads_enter()
+            dlg = gtk.MessageDialog(self, gtk.DIALOG_DESTROY_WITH_PARENT,
+                                    gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+                                    "Server did not send valid protocol header")
+            dlg.run()
+            dlg.hide()
+            gtk.threads_leave()
+            self.do_disconnect()
+        return False
+
+    def mansock_session(self):
+        """Invoked by mansock after header is received
+
+        This callback is implemented so we know when to login.  We
+        want to wait until after we receive the protocol header from
+        the server.
+        
+        """
+        gtk.threads_enter()
+        self.statusbar.pop(self.context_id)
+        self.statusbar.push(self.context_id, 'Sending Login...')
+        gtk.threads_leave()
+        
+        self.in_session = True
+        self.man.login(self.man_login, username='user', secret='pass')
+
+    def man_login(self, frame):
+        """Invoked by manager when response to login is sent
+        """
+        if frame['Response'] == 'Success':
+            gtk.threads_enter()
+            self.statusbar.pop(self.context_id)
+            self.statusbar.push(self.context_id, 'Connected')
+            gtk.threads_leave()
+            self.listen.fetch_info()
+            
+            gobject.timeout_add(1000, lambda l, c: self.invoke(self.on_heartbeat, l, c), None, None)
+        else:
+            gtk.threads_enter()
+            dlg = gtk.MessageDialog(self, gtk.DIALOG_DESTROY_WITH_PARENT,
+                                    gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+                                    "Login Failed: " + frame['Message'])
+            dlg.run()
+            dlg.hide()
+            self.statusbar.pop(self.context_id)
+            self.statusbar.push(self.context_id, 'Login Failed')
+            gtk.threads_leave()
+            self.do_disconnect()
+
+    def invoke(self, callback, listener, chan):
+        gtk.threads_enter()
+        callback(listener, chan)
+        gtk.threads_leave()
+
+    def invoke2(self, callback, listener, c, m):
+        gtk.threads_enter()
+        callback(listener, c, m)
+        gtk.threads_leave()
+
+    def on_heartbeat(self, ignore1, ignore2):
+        """Called every second by GTK
+
+        This method is intended to update the 'Alive' column of all
+        the channels.
+
+        """
+        if not self.auto_update:
+            return
+        if not self.man:
+            return False
+
+        self.listen.lock()
+        for chan in self.listen.channels.values():
+            self.on_updchan(self.listen, chan)
+        for conf in self.listen.confs.values():
+            for memb in conf.members.values():
+                self.on_updmemberconf(self.listen, chan, memb)
+        self.listen.unlock()
+        
+        gobject.timeout_add(1000, lambda l, c: self.invoke(self.on_heartbeat, l, c), None, None)
+
+    def on_newchan(self, listener, chan):
+        if not self.auto_update:
+            return
+        self.chantree.add_channel(chan)
+
+    def on_delchan(self, listener, chan):
+        if not self.auto_update:
+            return
+        self.chantree.del_channel(chan)
+
+    def on_updchan(self, listener, chan):
+        if not self.auto_update:
+            return
+        self.chantree.upd_channel(chan)
+
+    def on_chan_park(self, listener, chan):
+        if not self.auto_update:
+            return
+        self.chantree.prk_channel(chan)
+        
+    def on_chan_unpark(self, listener, chan):
+        if not self.auto_update:
+            return
+        self.chantree.unprk_channel(chan)
+    
+    def on_newlink(self, listener, link):
+        if not self.auto_update:
+            return
+        self.chantree.add_link(link)
+    
+    def on_dellink(self, listener, link):
+        if not self.auto_update:
+            return
+        self.chantree.del_link(link)
+
+    def on_newzap(self, listener, zap):
+        if not self.auto_update:
+            return
+        self.zaptree.add_zap(zap)
+
+    def on_updzap(self, listener, zap):
+        if not self.auto_update:
+            return
+        self.zaptree.upd_zap(zap)
+
+    def on_newconf(self, listener, conf):
+        if not self.auto_update:
+            return
+        self.conftree.add_conf(conf)
+
+    def on_delconf(self, listener, conf):
+        if not self.auto_update:
+            return
+        self.conftree.del_conf(conf)
+
+    def on_joinconf(self, listener, conf, memb):
+        if not self.auto_update:
+            return
+        self.conftree.add_conf_member(conf, memb)
+
+    def on_leaveconf(self, listener, conf, memb):
+        if not self.auto_update:
+            return
+        self.conftree.del_conf_member(conf, memb)
+
+    def on_updmemberconf(self, listener, conf, memb):
+        if not self.auto_update:
+            return
+        self.conftree.upd_conf_member(conf, memb)
+
+def main():
+    gtk.threads_init()
+    win = asterator()
+    win.show()
+    gtk.main()

Asterator/zaptree.py

+
+import gobject
+import pygtk
+pygtk.require('2.0')
+import gtk
+
+(
+    CHANNEL_COLUMN,
+    STATE_COLUMN,
+    SIGNALLING_COLUMN,
+    CONTEXT_COLUMN,
+    DND_COLUMN,
+    ALARM_COLUMN
+) = range(6)
+
+class zap_tree(gtk.TreeView):
+    def gen_model(self):
+        model = gtk.TreeStore(
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING,
+            gobject.TYPE_STRING)
+        
+        return model
+    
+    def __init__(self):
+        model = self.gen_model()
+        
+        gtk.TreeView.__init__(self, model)
+        self.set_rules_hint(True)
+        self.connect('realize', lambda tv: tv.expand_all())
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("Channel", renderer, text=CHANNEL_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("State", renderer, text=STATE_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("Signalling", renderer, text=SIGNALLING_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("Context", renderer, text=CONTEXT_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("DND", renderer, text=DND_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+        
+        renderer = gtk.CellRendererText()
+        renderer.set_property("xalign", 0.0)
+        column = gtk.TreeViewColumn("Alarm", renderer, text=ALARM_COLUMN)
+        column.set_resizable(True)
+        self.append_column(column)
+
+    def clear(self):
+        self.set_model(self.gen_model())
+
+    def myset(self, model, zap):
+        if zap.dnd: dnd = 'Yes'
+        else:       dnd = 'No'
+        
+        model.set(zap.iter,
+                  CHANNEL_COLUMN, zap.channelno,
+                  STATE_COLUMN, zap.state,
+                  SIGNALLING_COLUMN, zap.signalling,
+                  CONTEXT_COLUMN, zap.context,
+                  DND_COLUMN, dnd,
+                  ALARM_COLUMN, zap.alarm)
+
+    def reconcile(self, listener):
+        """A quickly written refresh method
+
+        Don't call this from within a listener callback or you'll get
+        a deadlock lolz because this locks the listener object.
+
+        ToDo: Make this better
+        
+        """
+        self.clear()
+        listener.lock()
+        ar = listener.zaps.keys()
+        ar.sort()
+        for zapkey in ar:
+            self.add_zap(listener.zaps[zapkey])
+        listener.unlock()
+
+    def add_zap(self, zap):
+        model = self.get_model()
+        iter = model.append(None)
+        zap.iter = iter
+        self.myset(model, zap)
+
+    def del_zap(self, zap):
+        model = self.get_model()
+        model.remove(zap.iter)
+
+    def upd_zap(self, zap):
+        model = self.get_model()
+        self.myset(model, zap)
+

Empty file added.

Asterisk/listener.py

+
+import time
+import thread
+import re
+
+def format_to_str(f):
+    res = []
+    
+    if f & 1 << 0 == 1 << 0:   res.append('G723_1')
+    if f & 1 << 1 == 1 << 1:   res.append('GSM')
+    if f & 1 << 2 == 1 << 2:   res.append('ULAW')
+    if f & 1 << 3 == 1 << 3:   res.append('ALAW')
+    if f & 1 << 4 == 1 << 4:   res.append('G726')
+    if f & 1 << 5 == 1 << 5:   res.append('ADPCM')
+    if f & 1 << 6 == 1 << 6:   res.append('SLINEAR')
+    if f & 1 << 7 == 1 << 7:   res.append('LPC10')
+    if f & 1 << 8 == 1 << 8:   res.append('G729A')
+    if f & 1 << 9 == 1 << 9:   res.append('SPEEX')
+    if f & 1 << 10 == 1 << 10: res.append('ILBC')
+    if f & 1 << 15 == 1 << 15: res.append('MAX_AUDIO')
+    if f & 1 << 16 == 1 << 16: res.append('JPEG')
+    if f & 1 << 17 == 1 << 17: res.append('PNG')
+    if f & 1 << 18 == 1 << 18: res.append('H261')
+    if f & 1 << 19 == 1 << 19: res.append('H263')
+    if f & 1 << 20 == 1 << 20: res.append('H263_PLUS')
+    if f & 1 << 24 == 1 << 24: res.append('MAX_VIDEO')
+
+    if len(res) == 0:
+        return 'Unknown'
+    else:
+        return ', '.join(res)
+
+def timespan_to_english(sec):
+    sec = int(sec)
+    if sec < 60:
+        return str(sec) + " sec"
+    elif sec < 60 * 60:
+        return str(int(round(sec / 60))) + " min"
+    elif sec < 60 * 60 * 24:
+        return str(int(round(sec / 60 / 60))) + " hrs"
+    elif sec < 60 * 60 * 24 * 30:
+        return str(int(round(sec / 60 / 60 / 24))) + " days"
+    else:
+        return '~' + str(round(sec / 60 / 60 / 24 / 30)) + " months"
+
+class astchannel:
+    """Class wrapper for an active Asterisk channel
+    """
+    def __init__(self, manager, name, frame=None):
+        self.man = manager
+        self.name = name
+        self.uniqueid = '?'
+        self.callerid = '?'
+        self.calleridname = '?'
+        self.state = '?'
+        self.context = '?'
+        self.extension = '?'
+        self.priority = '?'
+        self.app = '?'
+        self.app_data = '?'
+        self.started = time.mktime(time.localtime())
+        self.hold = False
+        self.linked = False
+        self.parked = False
+        self.conf = None
+
+        if frame != None:
+            self.update(frame)
+
+    def dump(self, indent=""):
+        """Dumps information about the channel to stdout
+        """
+        print indent + "Channel: " + self.name
+        print indent + "Uniqueid: " + self.uniqueid
+        print indent + "Started: " + time.strftime("%Y-%m-%d %H:%M:%S %Z", self.started)
+        print indent + "Callerid: " + self.get_callerid()
+        print indent + "State: " + self.state
+        print indent + "Location: " + self.get_location()
+        print indent + "App: " + self.get_app()
+        print indent + ""
+
+    def get_callerid(self):
+        if self.callerid.lower() == '<unknown>':
+            if self.calleridname.lower() == '<unknown>':
+                return '? <?>'
+            else:
+                return self.calleridname + ' <?>'
+        else:
+            if self.calleridname.lower() == '<unknown>':
+                return '? <' + self.callerid + '>'
+            else:
+                return self.calleridname + ' <' + self.callerid + '>'
+
+    def get_location(self):
+        """Constructs a string representation of the:
+
+        context:extension:priority
+
+        """
+        if self.context == '?' and self.extension == '?' and self.priority == '?':
+            return 'None'
+        return self.extension + '@' + self.context + ':' + self.priority
+
+    def get_app(self):
+        if self.app == '?' and self.app_data == '?':
+            return 'None'
+        return self.app + '(' + self.app_data + ')'
+
+    def get_timealive(self):
+        return timespan_to_english(time.mktime(time.localtime()) - self.started)
+
+    def update(self, frame):
+        if 'Uniqueid' in frame:
+            self.uniqueid = frame['Uniqueid']
+        if 'State' in frame:
+            self.state = frame['State']
+        if 'CallerID' in frame:
+            self.callerid = frame['CallerID']
+        if 'CallerIDName' in frame:
+            self.calleridname = frame['CallerIDName']
+        if 'Context' in frame:
+            self.context = frame['Context']
+        if 'Extension' in frame:
+            self.extension = frame['Extension']
+        if 'Priority' in frame:
+            self.priority = frame['Priority']
+        if 'Application' in frame:
+            self.app = frame['Application']
+        if 'AppData' in frame:
+            self.app_data = frame['AppData']
+        if 'NewName' in frame:
+            self.name = frame['NewName']
+        if 'Event' in frame:
+            if frame['Event'] == 'Hold':
+                self.hold = True
+            elif frame['Event'] == 'Unhold':
+                self.hold = False
+
+    def hangup(self, callback):
+        """Hangs up the channel
+        """
+        self.man.queue_frame({'Action':  'Hangup',
+                              'Channel': self.name}, callback)
+
+    def hangup_timeout(self, callback, timeout):
+        """Hangs up the channel after a timeout
+
+        Timeout is specified in seconds
+        
+        """
+        self.man.queue_frame({'Action':  'Timeout',
+                              'Channel': self.name,
+                              'Timeout': timeout}, callback)
+
+    def redirect(self, callback, context, exten, priority=1):
+        """Transfers the channel somewhere in your dial plan
+        """
+        self.man.queue_frame({'Action':   'Redirect',
+                              'Channel':  self.name,
+                              'Context':  context,
+                              'Exten':    exten,
+                              'Priority': priority}, callback)
+        
+    def setvar(self, callback, variable, value):
+        """Sets a channel or global variable on the channel
+        """
+        self.man.queue_frame({'Action':   'Setvar',
+                              'Channel':  self.name,
+                              'Variable': variable,
+                              'Value':    value}, callback)
+
+    def getvar(self, callback, variable):
+        """Gets a channel or global variable on the channel
+        """
+        self.man.queue_frame({'Action':   'Getvar',
+                              'Channel':  self.name,
+                              'Variable': variable}, callback)
+
+    def __infocallback(self, frame):
+        res = {'info': {}, 'vars': {}, 'cdr': {}}
+        pi = re.compile("^(.+?):(.*)$")
+        pv = re.compile("^(.+?)=(.*)$")
+        pc = re.compile("^level (\d+?): (.+?)=(.*)$")
+        mode = 1
+        lines = frame['Data'].split("\n")
+        if lines[0].strip() != '-- General --':
+            return None
+        for line in lines:
+            line = line.strip()
+            if line == '':
+                continue
+            elif line == '-- General --' or line == '--   PBX   --':
+                mode = 1
+                continue
+            elif line == 'Variables:':
+                mode = 2
+                continue
+            elif line == 'CDR Variables:':
+                mode = 3
+                continue
+            if mode == 1:
+                m = pi.search(line)
+                assert m != None
+                n = m.group(1)
+                v = m.group(2).strip()
+                if n == 'NativeFormat' or n == 'WriteFormat' or n == 'ReadFormat':
+                    v = format_to_str(int(v))
+                res['info'][n] = v
+            elif mode == 2:
+                m = pv.search(line)
+                assert m != None
+                res['vars'][m.group(1)] = m.group(2)
+            elif mode == 3:
+                m = pc.search(line)
+                assert m != None
+                l = m.group(1)
+                if not l in res['cdr']:
+                    res['cdr'][l] = {}
+                res['cdr'][l][m.group(2)] = m.group(3)
+        self.__infocb(res)
+
+    def getinfo(self, callback):
+        """Returns super detailed info about channel
+
+        Uses the 'show channel' command.  Parses result and returns in
+        a dictionary to callback.
+        
+        """
+        self.__infocb = callback
+        self.man.command(self.__infocallback, 'show channel ' + self.name)
+
+class zapinfo:
+    """Class wrapper for simple information about a Zapata channel
+    """
+    def __init__(self, frame=None):
+        self.channelno = None
+        self.signalling = None
+        self.context = None
+        self.dnd = False
+        self.alarm = 'None'
+        self.state = 'Down'
+        if frame:
+            self.update(frame)
+
+    def update(self, frame):
+        if 'Channel' in frame and self.channelno == None:
+            self.channelno = frame['Channel']
+        if 'Signalling' in frame:
+            self.signalling = frame['Signalling']
+        if 'Context' in frame and self.context == None:
+            self.context = frame['Context']
+        if 'DND' in frame:
+            if frame['DND'] == 'Enabled':
+                dnd = True
+            else:
+                dnd = False
+            self.dnd = dnd
+        if 'Alarm' in frame:
+            self.alarm = frame['Alarm']
+        if 'State' in frame:
+            self.state = frame['State']
+
+class zapchannel(astchannel):
+    """Class wrapper for an active Zaptel Channel
+    """
+    def __init__(self, manager, name, zap, frame=None):
+        astchannel.__init__(self, manager, name, frame)
+        self.zap = zap
+
+    def setdnd(self, callback, dndon):
+        """Sets Do Not Disturb on Zap Channel
+
+        Set 'dndon' to True to turn on, False to turn off
+
+        Response: Success
+        Message: DND Enabled
+
+        Response: Error
+        Message: No channel specified
+
+        Response: Error
+        Message: Missing action in request
+        
+        """
+        if dndon: action = 'ZapDNDon'
+        else:     action = 'ZapDNDoff'
+        self.man.queue_frame({'Action':     action,
+                              'ZapChannel': self.channelno}, callback)
+
+    def update(self, frame):
+        astchannel.update(self, frame)
+        if self.zap and 'State' in frame:
+            self.zap.state = frame['State']
+
+class astlink:
+    """Class wrapper for an Asterisk channel bridge
+    """
+    def __init__(self, chan1, chan2):
+        self.chan1 = chan1
+        self.chan2 = chan2
+        self.started = time.mktime(time.localtime())
+
+    def dump(self, indent):
+        print indent + "Link {"
+        self.chan1.dump(indent + "    ")
+        self.chan2.dump(indent + "    ")
+        print indent + "}"
+
+class confmember:
+    """Class wrapper containing extra info about meetme users
+    """
+    def __init__(self, conf, usernum, chan):
+        self.conf = conf
+        self.usernum = usernum
+        self.chan = chan
+        self.talking = False
+        self.joined = time.mktime(time.localtime())
+
+    def get_timealive(self):
+        return timespan_to_english(time.mktime(time.localtime()) - self.joined)
+
+    def kick(self, callback):
+        self.conf.man.command(callback, 'meetme kick ' + self.conf.number + ' ' + self.usernum)
+
+    def mute(self, callback):
+        self.conf.man.command(callback, 'meetme mute ' + self.conf.number + ' ' + self.usernum)
+
+    def unmute(self, callback):
+        self.conf.man.command(callback, 'meetme unmute ' + self.conf.number + ' ' + self.usernum)
+
+class astconf:
+    """Class wrapper for an Asterisk meetme conf
+    """
+    def __init__(self, man, number):
+        self.man = man
+        self.number = number
+        self.members = {}
+
+    def dump(self, indent):
+        print indent + "Conf {"
+        for chan in self.channels:
+            chan.dump(indent + "    ")
+        print indent + "}"
+
+    def has_members(self):
+        return len(self.members) > 0
+
+    def lock(self):
+        self.man.command('meetme lock ' + self.conf.number)
+
+    def add(self, chan, usernum):
+        memb = confmember(self, usernum, chan)
+        self.members[usernum] = memb
+        return memb
+
+    def remove(self, usernum):
+        if usernum in self.members:
+            memb = self.members[usernum]
+            del self.members[usernum]
+            return memb
+
+    def set_talking(self, usernum, talking):
+        if usernum in self.members:
+            self.members[usernum].talking = talking
+            return self.members[usernum]
+
+class manager_listener:
+    def __init__(self, manager):
+        self.man = manager
+        self.__lock = thread.allocate_lock()
+        
+        self.channels = {}
+        self.links = []
+        self.confs = {}
+        self.zaps = {}
+
+        self.on_newchan = None
+        self.on_delchan = None
+        self.on_updchan = None
+        self.on_chan_park = None
+        self.on_chan_unpark = None
+        self.on_chan_parktimeout = None
+        self.on_chan_parkgiveup = None
+        self.on_newlink = None
+        self.on_dellink = None
+        self.on_newzap = None
+        self.on_updzap = None
+        self.on_newconf = None
+        self.on_delconf = None
+        self.on_joinconf = None
+        self.on_leaveconf = None
+        self.on_updmemberconf = None
+        
+        self.man.assign_handler('zapshowchannels', lambda fr: self.invoke(self.on_zapshowchannels, fr))
+        self.man.assign_handler('alarm', lambda fr: self.invoke(self.on_alarm, fr))
+        self.man.assign_handler('alarmclear', lambda fr: self.invoke(self.on_alarmclear, fr))
+        self.man.assign_handler('dndstate', lambda fr: self.invoke(self.on_dndstate, fr))
+        self.man.assign_handler('status', lambda fr: self.invoke(self.on_status, fr))
+        self.man.assign_handler('newcallerid', lambda fr: self.invoke(self.on_newcallerid, fr))
+        self.man.assign_handler('newchannel', lambda fr: self.invoke(self.on_newchannel, fr))
+        self.man.assign_handler('hangup', lambda fr: self.invoke(self.on_hangup, fr))
+        self.man.assign_handler('newstate', lambda fr: self.invoke(self.on_newstate, fr))
+        self.man.assign_handler('newexten', lambda fr: self.invoke(self.on_newexten, fr))
+        self.man.assign_handler('rename', lambda fr: self.invoke(self.on_rename, fr))
+        self.man.assign_handler('hold', lambda fr: self.invoke(self.on_hold, fr))
+        self.man.assign_handler('unhold', lambda fr: self.invoke(self.on_unhold, fr))
+        self.man.assign_handler('link', lambda fr: self.invoke(self.on_link, fr))
+        self.man.assign_handler('unlink', lambda fr: self.invoke(self.on_unlink, fr))
+        self.man.assign_handler('parkedcall', lambda fr: self.invoke(self.on_parkedcall, fr))
+        self.man.assign_handler('unparkedcall', lambda fr: self.invoke(self.on_unparkedcall, fr))
+        self.man.assign_handler('parkedcalltimeout', lambda fr: self.invoke(self.on_parkedcalltimeout, fr))
+        self.man.assign_handler('parkedcallgiveup', lambda fr: self.invoke(self.on_parkedcallgiveup, fr))
+        self.man.assign_handler('meetmejoin', lambda fr: self.invoke(self.on_meetmejoin, fr))
+        self.man.assign_handler('meetmeleave', lambda fr: self.invoke(self.on_meetmeleave, fr))
+        self.man.assign_handler('meetmetalking', lambda fr: self.invoke(self.on_meetmetalking, fr))
+        self.man.assign_handler('meetmestoptalking', lambda fr: self.invoke(self.on_meetmestoptalking, fr))
+
+    def fetch_info(self):
+        self.man.zapshowchannels()
+        self.man.status()
+        self.man.command(self.show_channels_callback, 'show channels')
+
+    def show_channels_callback(self, frame):
+        """Figures out the current application for all channels
+
+        The meetme Status action doesn't tell us any information about
+        the application that is currently being executed by a channel
+        so we need to send a 'show channels' command to the manager
+        interface and parse out the text data for that information.
+        This information is also useful to subsequent functions such
+        as probe_conferences.
+
+        """
+        p = re.compile("^(\S+)\s+\S+\s+\S+\s+(.+?)\((.*?)\)$")
+        lines = frame['Data'].split("\n")
+        for line in lines:
+            line = line.strip()
+            if line == '':
+                continue
+            m = p.search(line)
+            if m == None:
+                continue
+            chan = m.group(1)
+            app = m.group(2)
+            data = m.group(3)
+            # skip table header
+            if chan == 'Channel':
+                continue
+            # skip channels with no application
+            if app.strip() == '':
+                continue
+            self.lock()
+            if chan in self.channels:
+                self.channels[chan].app = app
+                self.channels[chan].app_data = data
+                if self.on_updchan:
+                    self.on_updchan(self, self.channels[chan])
+            self.unlock()
+        
+        self.probe_conferences()
+
+    def probe_conferences(self):
+        """Fetches information about meetme conferences
+
+        This method is a total hack because there isn't anything quite
+        like a status action for grabbing information about active
+        conferences.
+
+        The solution is to look at all the active channels and see if
+        they are currently executing a meetme application.  If yes,
+        extract the conference number from the application data and
+        issue a meetme list xxx command to asterisk to figure out what
+        the dill with the conference is.
+
+        """
+        pnum = re.compile("^(\d+)")
+        dolist = []
+        self.lock()
+        for chan in self.channels.values():
+            if chan.app.lower() != 'meetme':
+                continue
+            m = pnum.search(chan.app_data)
+            if m == None:
+                continue
+            confno = str(int(m.group(1)))
+            if confno in self.confs:
+                chan.conf = self.confs[confno]
+            else:
+                self.confs[confno] = astconf(self.man, confno)
+                chan.conf = self.confs[confno]
+                if self.on_newconf:
+                    self.on_newconf(self, chan.conf)
+                dolist.append(confno)
+        self.unlock()
+        for confno in dolist:
+            self.man.command(self.probe_conferences_callback, 'meetme list ' + confno)
+
+    def probe_conferences_callback(self, frame):
+        p = re.compile("^User #: (\d+).+?Channel: (.+?)\s*\((.+?)\)$")
+        lines = frame['Data'].split("\n")
+        for line in lines:
+            line = line.strip()
+            if line == '':
+                continue
+            m = p.search(line)
+            if m == None:
+                continue
+            # this command's result has a prepended zero for some reason
+            membno = str(int(m.group(1)))
+            chan = m.group(2)
+            if m.group(3) == 'talking':
+                talking = True
+            else:
+                talking = False
+            self.lock()
+            if not chan in self.channels:
+                self.unlock()
+                continue
+            chan = self.channels[chan]
+            if chan.conf == None:
+                self.unlock()
+                continue
+            memb = chan.conf.add(chan, membno)
+            memb.talking = talking
+            if self.on_joinconf:
+                self.on_joinconf(self, chan.conf, memb)
+            self.unlock()
+
+    def lock(self):
+        self.__lock.acquire()
+        
+    def unlock(self):
+        self.__lock.release()
+
+    def invoke(self, callback, frame):
+        self.lock()
+        callback(frame)
+        self.unlock()
+
+    def dump(self, indent=""):
+        for chan in self.channels.values():
+            chan.dump(indent)
+        for link in self.links:
+            link.dump(indent)
+        for conf in self.confs:
+            conf.dump(indent)
+
+    def updatechan(self, frame):
+        if not frame['Channel'] in self.channels:
+            chan = astchannel(self.man, frame['Channel'], frame)
+            self.channels[frame['Channel']] = chan
+            if self.on_newchan:
+                self.on_newchan(self, chan)
+        else:
+            chan = self.channels[frame['Channel']]
+            chan.update(frame)
+            if self.on_updchan:
+                self.on_updchan(self, chan)
+        return chan
+
+    def updatezap(self, frame):
+        if frame['Channel'][:3].lower() != 'zap':
+            return
+        channo = frame['Channel'][frame['Channel'].index('/')+1:]
+        p = channo.find('-')
+        if p != -1:
+            channo = channo[:p]
+        if channo in self.zaps:
+            zap = self.zaps[channo]
+            zap.update(frame)
+            if self.on_updzap:
+                self.on_updzap(self, zap)
+
+    def on_status(self, frame):
+        self.updatechan(frame)
+        self.updatezap(frame)
+        if 'Link' in frame and frame['Link'] in self.channels:
+            found = False
+            for link in self.links:
+                if (link.chan1.name == frame['Channel'] and link.chan2.name == frame['Link']) or (link.chan1.name == frame['Link'] and link.chan2.name == frame['Channel']):
+                    found = True
+                    break
+            if not found:
+                self.channels[frame['Channel']].started = time.mktime(time.localtime()) - int(frame['Seconds'])
+                self.channels[frame['Link']].started = time.mktime(time.localtime()) - int(frame['Seconds'])
+                link = astlink(self.channels[frame['Channel']], self.channels[frame['Link']])
+                self.links.append(link)
+                if self.on_newlink:
+                    self.on_newlink(self, link)
+
+    def on_zapshowchannels(self, frame):
+        zap = zapinfo(frame)
+        self.zaps[frame['Channel']] = zap
+        if self.on_newzap:
+            self.on_newzap(self, zap)
+
+    def on_alarm(self, frame):
+        frame['Channel'] = 'Zap/' + frame['Channel']
+        self.updatezap(frame)
+
+    def on_alarmclear(self, frame):
+        frame['Channel'] = 'Zap/' + frame['Channel']
+        self.updatezap(frame)
+
+    def on_dndstate(self, frame):
+        if frame['Status'] == 'enabled':
+            frame['DND'] = 'Enabled'
+        else:
+            frame['DND'] = 'Disabled'
+        self.updatezap(frame)
+    
+    def on_newcallerid(self, frame):
+        self.updatechan(frame)
+
+    def on_newchannel(self, frame):
+        self.updatechan(frame)
+        self.updatezap(frame)
+    
+    def on_hangup(self, frame):
+        if self.on_delchan:
+            self.on_delchan(self, self.channels[frame['Channel']])
+        del self.channels[frame['Channel']]
+        frame['State'] = 'Down'
+        self.updatezap(frame)
+    
+    def on_newstate(self, frame):
+        self.updatechan(frame)
+        self.updatezap(frame)
+    
+    def on_newexten(self, frame):
+        self.updatechan(frame)
+    
+    def on_rename(self, frame):
+        if frame['Oldname'] in self.channels:
+            self.channels[frame['Newname']] = self.channels[frame['Oldname']]
+            del self.channels[frame['Oldname']]
+            self.channels[frame['Newname']].name = frame['Newname']
+            if self.on_updchan:
+                self.on_updchan(self, self.channels[frame['Newname']])
+
+    def on_hold(self, frame):
+        self.updatechan(frame)
+        
+    def on_unhold(self, frame):
+        self.updatechan(frame)
+    
+    def on_link(self, frame):
+        chan1 = self.channels[frame['Channel1']]
+        chan2 = self.channels[frame['Channel2']]
+        chan1.linked = True
+        chan2.linked = True
+        link = astlink(chan1, chan2)
+        self.links.append(link)
+        if self.on_newlink:
+            self.on_newlink(self, link)
+    
+    def on_unlink(self, frame):
+        chan1 = self.channels[frame['Channel1']]
+        chan2 = self.channels[frame['Channel2']]
+        chan1.linked = False
+        chan2.linked = False
+        for link in self.links:
+            if link.chan1 == chan1 and link.chan2 == chan2:
+                self.links.remove(link)
+                if self.on_dellink:
+                    self.on_dellink(self, link)
+                break
+
+    def on_parkedcall(self, frame):
+        chan = self.updatechan(frame)
+        chan.parked = True
+        if self.on_chan_park:
+            self.on_chan_park(self, chan)
+
+    def on_unparkedcall(self, frame):
+        chan = self.updatechan(frame)
+        chan.parked = False
+        if self.on_chan_unpark:
+            self.on_chan_unpark(self, chan)
+
+    def on_parkedcalltimeout(self, frame):
+        chan = self.updatechan(frame)
+        chan.parked = False
+        if self.on_chan_parktimeout:
+            self.on_chan_parktimeout(self, chan)
+
+    def on_parkedcallgiveup(self, frame):
+        chan = self.updatechan(frame)
+        chan.parked = False
+        if self.on_chan_parkgiveup:
+            self.on_chan_parkgiveup(self, chan)
+    
+    def on_meetmejoin(self, frame):
+        if not frame['Channel'] in self.channels:
+            return
+        chan = self.channels[frame['Channel']]
+        if not frame['Meetme'] in self.confs:
+            conf = astconf(self.man, frame['Meetme'])
+            self.confs[frame['Meetme']] = conf
+            if self.on_newconf:
+                self.on_newconf(self, conf)
+        else:
+            conf = self.confs[frame['Meetme']]
+        memb = conf.add(chan, frame['Usernum'])
+        chan.conf = conf
+        if self.on_joinconf:
+            self.on_joinconf(self, conf, memb)
+
+    def on_meetmeleave(self, frame):
+        if not frame['Channel'] in self.channels:
+            return
+        chan = self.channels[frame['Channel']]
+        chan.conf = None
+        if frame['Meetme'] in self.confs:
+            conf = self.confs[frame['Meetme']]
+            memb = conf.remove(frame['Usernum'])
+            if self.on_newconf:
+                self.on_leaveconf(self, conf, memb)
+            if not conf.has_members():
+                del self.confs[frame['Meetme']]
+                if self.on_newconf:
+                    self.on_delconf(self, conf)
+
+    def on_meetmetalking(self, frame):
+        if frame['Meetme'] in self.confs:
+            conf = self.confs[frame['Meetme']]
+            memb = conf.set_talking(frame['Usernum'], True)
+            if self.on_updmemberconf:
+                self.on_updmemberconf(self, conf, memb)
+
+    def on_meetmestoptalking(self, frame):
+        if frame['Meetme'] in self.confs:
+            conf = self.confs[frame['Meetme']]
+            memb = conf.set_talking(frame['Usernum'], False)
+            if self.on_updmemberconf:
+                self.on_updmemberconf(self, conf, memb)

Asterisk/manager.py

+
+import socket
+import asyncore
+import asynchat
+import thread
+import threading
+
+(
+    AMS_HELLO,
+    AMS_SESSION,
+    AMS_HEREDOC
+) = range(3)
+
+class mansock(asynchat.async_chat):
+    def __init__(self, sock, debug=False):
+        asynchat.async_chat.__init__(self, conn=sock)
+        self.set_terminator("\r\n")
+        self.inbuf = ""
+        self.frame = {}
+        self.mode = AMS_HELLO
+        self.do_debug = debug
+        self.version = None
+
+        self.handler_frame = None
+        self.handler_session = None
+        self.handler_close = None
+
+    def debug(self, data):
+        if self.do_debug:
+            print data
+
+    def handle_close(self):
+        if self.handler_close:
+            self.handler_close()
+
+    def collect_incoming_data(self, data):
+        self.inbuf += data
+
+    def found_terminator(self):
+        data = self.inbuf
+        self.inbuf = ""
+        self.debug('<<< ' + data)
+
+        if self.mode == AMS_HELLO:
+            pos = data.index('/')
+            if data[:pos] != 'Asterisk Call Manager':
+                self.close()
+                raise ValueError, "Invalid protocol header"
+            self.version = data[pos+1:]
+            self.mode = AMS_SESSION
+            if self.handler_session:
+                self.handler_session()
+        elif self.mode == AMS_SESSION:
+            if data == '':
+                if self.handler_frame:
+                    self.handler_frame(self.frame)
+                self.frame = {}
+                return
+            pos = data.index(': ')
+            key = data[:pos]
+            val = data[pos+2:]
+            self.frame[key] = val
+            if key == 'ActionID' and 'Response' in self.frame and self.frame['Response'] == 'Follows':
+                self.mode = AMS_HEREDOC
+                self.set_terminator("--END COMMAND--\r\n")
+        elif self.mode == AMS_HEREDOC:
+            self.frame['Data'] = data
+            self.mode = AMS_SESSION
+            self.set_terminator("\r\n")
+        else:
+            raise Exception, "Invalid mode"
+    
+    def __assemble_frame(self, frame):
+        data = ''
+        for k in frame.keys():
+            line = k + ': ' + frame[k]
+            self.debug('>>> ' + line)
+            data += line + "\r\n"
+        data += "\r\n"
+        self.debug('>>> ')
+        return data
+
+    def put_frame(self, frame):
+        self.push(self.__assemble_frame(frame))
+
+class manager:
+    def __init__(self, sock, debug=False):
+        self.terminated = False
+        self.do_debug = debug
+        
+        self.sock = sock
+        self.sock.handler_frame = self.__delegate
+        self.version = None
+        self.event_handlers = {}
+        self.response_handlers = {}
+        self.generic_response_handler = None
+        self.unique = 1
+        self.lock = thread.allocate_lock()
+
+    def terminate(self):
+        self.__disconnect()
+        self.terminated = True
+
+    def debug(self, data):
+        if self.do_debug:
+            print data
+
+    def assign_handler(self, event, callback):
+        """Assigns a callback handler to a specific type of event frame
+
+        Please specify the name of the event (e.g. newchannel, link)
+        and a callback.  Your callback will be passed the event frame
+        as an argument.
+
+        Also note that event names are case _IN_sensitive, so you're
+        probably best off not twisting your brain on casing and just
+        pass the event name as lowercase.
+        
+        """
+        if event.lower() in self.event_handlers:
+            self.event_handlers[event.lower()].append(callback)
+        else:
+            self.event_handlers[event.lower()] = [callback]
+
+    def __delegate(self, frame):
+        """Delegates control of event frame to associated callback
+
+        If no handler has been assigned for the particular type of
+        event frame (e.g. NewChannel), the event frame will be
+        dropped.  This method is called when the socket is being
+        polled or if an event is mysteriously encountered during a
+        send operation.
+        
+        """
+        if 'Event' in frame:
+            if frame['Event'].lower() in self.event_handlers:
+                for callback in self.event_handlers[frame['Event'].lower()]:
+                    callback(frame)
+        elif 'Response' in frame:
+            if 'ActionID' in frame and frame['ActionID'] in self.response_handlers:
+                self.response_handlers[frame['ActionID']](frame)
+                del self.response_handlers[frame['ActionID']]
+            else:
+                if self.generic_response_handler != None:
+                    resp = self.generic_response_handler
+                    self.generic_response_handler = None
+                    resp(frame)
+                else:
+                    self.debug("!!! Unhandled repsonse sent to delegate(): " + str(frame))
+        else:
+            self.debug("!!! Invalid frame sent to delegate(): " + str(frame))
+
+    def queue_frame(self, frame, callback=None):
+        """Queues a frame for transmit
+        """
+        res = None
+        if 'Action' in frame and callback != None:
+            self.lock.acquire()
+            id = self.unique
+            self.unique += 1
+            self.lock.release()
+            frame['ActionID'] = frame['Action'].lower() + '_' + str(id)
+            self.response_handlers[frame['ActionID']] = callback
+
+        self.lock.acquire()
+        self.sock.put_frame(frame)
+        self.lock.release()
+
+    def __disconnect(self):
+        """Disconnects from the Asterisk Manager Interface
+        """
+        self.sock.close()
+        self.sock = None
+
+    def login(self, callback, username, secret):
+        """Logs in to manager interface and sets events mask
+        """
+        self.generic_response_handler = callback
+        self.queue_frame({'Action':   'Login',
+                          'Username': username,
+                          'Secret':   secret})
+
+    def command(self, callback, command):
+        """Sends a CLI command and returns output
+
+        Command output is returned in the 'data' key in the frame
+        passed to your callback
+        
+        """
+        self.queue_frame({'Action':  'Command',
+                          'Command': command}, callback)
+
+    def hangup(self, callback, channel):
+        """Hangs up a channel
+        """
+        self.queue_frame({'Action':  'Hangup',
+                          'Channel': channel}, callback)
+
+    def hangup_timeout(self, callback, channel, timeout):
+        """Hangs up a channel after a timeout
+
+        Timeout is specified in seconds
+        
+        """
+        self.queue_frame({'Action':  'Timeout',
+                          'Channel': channel,
+                          'Timeout': timeout}, callback)
+
+    def setvar(self, callback, variable, value, channel=None):
+        """Sets a channel or global variable
+
+        If you do not specify a channel, it will be a global variable.
+
+        """
+        if channel == None:
+            self.queue_frame({'Action':   'Setvar',
+                              'Variable': variable,
+                              'Value':    value}, callback)
+        else:
+            self.queue_frame({'Action':   'Setvar',
+                              'Channel':  channel,
+                              'Variable': variable,
+                              'Value':    value}, callback)
+
+    def getvar(self, callback, variable, channel=None):
+        """Gets a channel or global variable
+
+        If you do not specify a channel, it will be a global variable.
+
+        This method returns the value of the variable.
+
+        """
+        if channel == None:
+            self.queue_frame({'Action':   'Getvar',
+                              'Variable': variable}, callback)
+        else:
+            self.queue_frame({'Action':   'Getvar',
+                              'Channel':  channel,
+                              'Variable': variable}, callback)
+
+    def status(self, callback=None):
+        """Tells the manager to spill its guts
+
+        Once the status command is issued, the manager will send all
+        sorts of neat 'Status' events describing all the current
+        channels.  Be sure you have a handler installed for the
+        'status' event before calling this!
+
+        """
+        self.generic_response_handler = callback
+        self.queue_frame({'Action': 'Status'})
+
+    def zapshowchannels(self, callback=None):
+        """Tells the manager to spill its guts about Zaptel
+        """
+        self.generic_response_handler = callback
+        self.queue_frame({'Action': 'ZapShowChannels'})
+
+    def redirect(self, callback, channel, context, exten, priority=1):
+        """Transfers a channel somewhere in your dial plan
+        """
+        self.queue_frame({'Action':   'Redirect',
+                          'Channel':  channel,
+                          'Context':  context,
+                          'Exten':    exten,