diff --git a/chat/SimpleGladeApp.py b/chat/SimpleGladeApp.py new file mode 100644 index 00000000..90c598cc --- /dev/null +++ b/chat/SimpleGladeApp.py @@ -0,0 +1,341 @@ +""" + SimpleGladeApp.py + Module that provides an object oriented abstraction to pygtk and libglade. + Copyright (C) 2004 Sandino Flores Moreno +""" + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA + +import os +import sys +import re + +import tokenize +import gtk +import gtk.glade +import weakref +import inspect + +__version__ = "1.0" +__author__ = 'Sandino "tigrux" Flores-Moreno' + +def bindtextdomain(app_name, locale_dir=None): + """ + Bind the domain represented by app_name to the locale directory locale_dir. + It has the effect of loading translations, enabling applications for different + languages. + + app_name: + a domain to look for translations, tipically the name of an application. + + locale_dir: + a directory with locales like locale_dir/lang_isocode/LC_MESSAGES/app_name.mo + If omitted or None, then the current binding for app_name is used. + """ + try: + import locale + import gettext + locale.setlocale(locale.LC_ALL, "") + gtk.glade.bindtextdomain(app_name, locale_dir) + gettext.install(app_name, locale_dir, unicode=1) + except (IOError,locale.Error), e: + print "Warning", app_name, e + __builtins__.__dict__["_"] = lambda x : x + + +class SimpleGladeApp: + + def __init__(self, path, root=None, domain=None, **kwargs): + """ + Load a glade file specified by glade_filename, using root as + root widget and domain as the domain for translations. + + If it receives extra named arguments (argname=value), then they are used + as attributes of the instance. + + path: + path to a glade filename. + If glade_filename cannot be found, then it will be searched in the + same directory of the program (sys.argv[0]) + + root: + the name of the widget that is the root of the user interface, + usually a window or dialog (a top level widget). + If None or ommited, the full user interface is loaded. + + domain: + A domain to use for loading translations. + If None or ommited, no translation is loaded. + + **kwargs: + a dictionary representing the named extra arguments. + It is useful to set attributes of new instances, for example: + glade_app = SimpleGladeApp("ui.glade", foo="some value", bar="another value") + sets two attributes (foo and bar) to glade_app. + """ + if os.path.isfile(path): + self.glade_path = path + else: + glade_dir = os.path.dirname( sys.argv[0] ) + self.glade_path = os.path.join(glade_dir, path) + for key, value in kwargs.items(): + try: + setattr(self, key, weakref.proxy(value) ) + except TypeError: + setattr(self, key, value) + self.glade = None + self.install_custom_handler(self.custom_handler) + self.glade = self.create_glade(self.glade_path, root, domain) + if root: + self.main_widget = self.get_widget(root) + else: + self.main_widget = None + self.normalize_names() + self.add_callbacks(self) + self.new() + + def __repr__(self): + class_name = self.__class__.__name__ + if self.main_widget: + root = gtk.Widget.get_name(self.main_widget) + repr = '%s(path="%s", root="%s")' % (class_name, self.glade_path, root) + else: + repr = '%s(path="%s")' % (class_name, self.glade_path) + return repr + + def new(self): + """ + Method called when the user interface is loaded and ready to be used. + At this moment, the widgets are loaded and can be refered as self.widget_name + """ + pass + + def add_callbacks(self, callbacks_proxy): + """ + It uses the methods of callbacks_proxy as callbacks. + The callbacks are specified by using: + Properties window -> Signals tab + in glade-2 (or any other gui designer like gazpacho). + + Methods of classes inheriting from SimpleGladeApp are used as + callbacks automatically. + + callbacks_proxy: + an instance with methods as code of callbacks. + It means it has methods like on_button1_clicked, on_entry1_activate, etc. + """ + self.glade.signal_autoconnect(callbacks_proxy) + + def normalize_names(self): + """ + It is internally used to normalize the name of the widgets. + It means a widget named foo:vbox-dialog in glade + is refered self.vbox_dialog in the code. + + It also sets a data "prefixes" with the list of + prefixes a widget has for each widget. + """ + for widget in self.get_widgets(): + widget_name = gtk.Widget.get_name(widget) + prefixes_name_l = widget_name.split(":") + prefixes = prefixes_name_l[ : -1] + widget_api_name = prefixes_name_l[-1] + widget_api_name = "_".join( re.findall(tokenize.Name, widget_api_name) ) + gtk.Widget.set_name(widget, widget_api_name) + if hasattr(self, widget_api_name): + raise AttributeError("instance %s already has an attribute %s" % (self,widget_api_name)) + else: + setattr(self, widget_api_name, widget) + if prefixes: + gtk.Widget.set_data(widget, "prefixes", prefixes) + + def add_prefix_actions(self, prefix_actions_proxy): + """ + By using a gui designer (glade-2, gazpacho, etc) + widgets can have a prefix in theirs names + like foo:entry1 or foo:label3 + It means entry1 and label3 has a prefix action named foo. + + Then, prefix_actions_proxy must have a method named prefix_foo which + is called everytime a widget with prefix foo is found, using the found widget + as argument. + + prefix_actions_proxy: + An instance with methods as prefix actions. + It means it has methods like prefix_foo, prefix_bar, etc. + """ + prefix_s = "prefix_" + prefix_pos = len(prefix_s) + + is_method = lambda t : callable( t[1] ) + is_prefix_action = lambda t : t[0].startswith(prefix_s) + drop_prefix = lambda (k,w): (k[prefix_pos:],w) + + members_t = inspect.getmembers(prefix_actions_proxy) + methods_t = filter(is_method, members_t) + prefix_actions_t = filter(is_prefix_action, methods_t) + prefix_actions_d = dict( map(drop_prefix, prefix_actions_t) ) + + for widget in self.get_widgets(): + prefixes = gtk.Widget.get_data(widget, "prefixes") + if prefixes: + for prefix in prefixes: + if prefix in prefix_actions_d: + prefix_action = prefix_actions_d[prefix] + prefix_action(widget) + + def custom_handler(self, + glade, function_name, widget_name, + str1, str2, int1, int2): + """ + Generic handler for creating custom widgets, internally used to + enable custom widgets (custom widgets of glade). + + The custom widgets have a creation function specified in design time. + Those creation functions are always called with str1,str2,int1,int2 as + arguments, that are values specified in design time. + + Methods of classes inheriting from SimpleGladeApp are used as + creation functions automatically. + + If a custom widget has create_foo as creation function, then the + method named create_foo is called with str1,str2,int1,int2 as arguments. + """ + try: + handler = getattr(self, function_name) + return handler(str1, str2, int1, int2) + except AttributeError: + return None + + def gtk_widget_show(self, widget, *args): + """ + Predefined callback. + The widget is showed. + Equivalent to widget.show() + """ + widget.show() + + def gtk_widget_hide(self, widget, *args): + """ + Predefined callback. + The widget is hidden. + Equivalent to widget.hide() + """ + widget.hide() + + def gtk_widget_grab_focus(self, widget, *args): + """ + Predefined callback. + The widget grabs the focus. + Equivalent to widget.grab_focus() + """ + widget.grab_focus() + + def gtk_widget_destroy(self, widget, *args): + """ + Predefined callback. + The widget is destroyed. + Equivalent to widget.destroy() + """ + widget.destroy() + + def gtk_window_activate_default(self, window, *args): + """ + Predefined callback. + The default widget of the window is activated. + Equivalent to window.activate_default() + """ + widget.activate_default() + + def gtk_true(self, *args): + """ + Predefined callback. + Equivalent to return True in a callback. + Useful for stopping propagation of signals. + """ + return True + + def gtk_false(self, *args): + """ + Predefined callback. + Equivalent to return False in a callback. + """ + return False + + def gtk_main_quit(self, *args): + """ + Predefined callback. + Equivalent to self.quit() + """ + self.quit() + + def main(self): + """ + Starts the main loop of processing events. + The default implementation calls gtk.main() + + Useful for applications that needs a non gtk main loop. + For example, applications based on gstreamer needs to override + this method with gst.main() + + Do not directly call this method in your programs. + Use the method run() instead. + """ + gtk.main() + + def quit(self): + """ + Quit processing events. + The default implementation calls gtk.main_quit() + + Useful for applications that needs a non gtk main loop. + For example, applications based on gstreamer needs to override + this method with gst.main_quit() + """ + gtk.main_quit() + + def run(self): + """ + Starts the main loop of processing events checking for Control-C. + + The default implementation checks wheter a Control-C is pressed, + then calls on_keyboard_interrupt(). + + Use this method for starting programs. + """ + try: + self.main() + except KeyboardInterrupt: + self.on_keyboard_interrupt() + + def on_keyboard_interrupt(self): + """ + This method is called by the default implementation of run() + after a program is finished by pressing Control-C. + """ + pass + + def install_custom_handler(self, custom_handler): + gtk.glade.set_custom_handler(custom_handler) + + def create_glade(self, glade_path, root, domain): + return gtk.glade.XML(self.glade_path, root, domain) + + def get_widget(self, widget_name): + return self.glade.get_widget(widget_name) + + def get_widgets(self): + return self.glade.get_widget_prefix("") diff --git a/chat/chat.glade b/chat/chat.glade new file mode 100644 index 00000000..69f58c8a --- /dev/null +++ b/chat/chat.glade @@ -0,0 +1,118 @@ + + + + + + + True + Chat + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + 440 + 250 + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_NORMAL + GDK_GRAVITY_NORTH_WEST + True + False + + + + True + 2 + 2 + False + 0 + 0 + + + + True + True + False + True + GTK_JUSTIFY_LEFT + GTK_WRAP_NONE + True + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 1 + 0 + 1 + + + + + + True + True + True + 0 + + True + + False + + + 0 + 1 + 1 + 2 + shrink|fill + + + + + + 6 + True + False + 0 + + + + 2 + 167 + True + False + False + False + True + False + False + False + + + 0 + True + True + + + + + 1 + 2 + 0 + 2 + fill + fill + + + + + + + diff --git a/chat/main.py b/chat/main.py new file mode 100755 index 00000000..5eff7429 --- /dev/null +++ b/chat/main.py @@ -0,0 +1,115 @@ +#!/usr/bin/python -t + +import os, sys, pwd +sys.path.append(os.getcwd()) +import gtk, gobject + +from SimpleGladeApp import SimpleGladeApp +import presence +import network +import avahi + +glade_dir = os.getcwd() + +class ChatApp(SimpleGladeApp): + def __init__(self, glade_file="chat.glade", root="mainWindow", domain=None, **kwargs): + + self._pdiscovery = presence.PresenceDiscovery() + self._pannounce = presence.PresenceAnnounce() + + (self._nick, self._realname) = self._get_name() + + path = os.path.join(glade_dir, glade_file) + gtk.window_set_default_icon_name("config-users") + SimpleGladeApp.__init__(self, path, root, domain, **kwargs) + + def _get_name(self): + ent = pwd.getpwuid(os.getuid()) + nick = ent[0] + if not nick or not len(nick): + nick = "n00b" + realname = ent[4] + if not realname or not len(realname): + realname = "Some Clueless User" + return (nick, realname) + + def new_service(self, action, interface, protocol, name, stype, domain, flags): + if action != 'added' or stype != presence.OLPC_CHAT_SERVICE: + return + self._pdiscovery.resolve_service(interface, protocol, name, stype, domain, self.service_resolved) + + def on_buddyList_buddy_selected(self, widget, *args): + (model, aniter) = widget.get_selection().get_selected() + name = self.treemodel.get(aniter,0) + print "Selected %s" % name + + def on_buddyList_buddy_double_clicked(self, widget, *args): + (model, aniter) = widget.get_selection().get_selected() + name = self.treemodel.get(aniter,0) + print "Double-clicked %s" % name + + def on_main_window_delete(self, widget, *args): + self.quit() + + def _recv_group_message(self, msg): + aniter = self._group_chat_buffer.get_end_iter() + self._group_chat_buffer.insert(aniter, msg['data'] + "\n") +# print "Message: %s" % msg['data'] + + def _send_group_message(self, widget, *args): + text = widget.get_text() + if len(text) > 0: + self._gc_controller.send_msg(text) + widget.set_text("") + + def _pair_to_dict(self, l): + res = {} + for el in l: + tmp = el.split('=', 1) + if len(tmp) > 1: + res[tmp[0]] = tmp[1] + else: + res[tmp[0]] = '' + return res + + def service_resolved(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags): + data = self._pair_to_dict(avahi.txt_array_to_string_array(txt)) + if len(data) > 0 and 'name' in data.keys(): + aniter = self.treemodel.insert_after(None,None) + self.treemodel.set(aniter, 0, data['name']) + + def new(self): + self._group_chat_buffer = gtk.TextBuffer() + self.chatView.set_buffer(self._group_chat_buffer) + + self.treemodel = gtk.TreeStore(gobject.TYPE_STRING) + self.buddyList.set_model(self.treemodel) + self.buddyList.connect("cursor-changed", self.on_buddyList_buddy_selected) + self.buddyList.connect("row-activated", self.on_buddyList_buddy_double_clicked) + self.mainWindow.connect("delete-event", self.on_main_window_delete) + self.entry.connect("activate", self._send_group_message) + + renderer = gtk.CellRendererText() + column = gtk.TreeViewColumn("", renderer, text=0) + column.set_resizable(True) + column.set_sizing("GTK_TREE_VIEW_COLUMN_GROW_ONLY"); + column.set_expand(True); + self.buddyList.append_column(column) + + self._pannounce.register_service(self._realname, 6666, presence.OLPC_CHAT_SERVICE, name=self._nick) + self._pdiscovery.add_service_listener(self.new_service) + self._pdiscovery.start() + + self._gc_controller = network.GroupChatController('224.0.0.221', 6666, self._recv_group_message) + self._gc_controller.start() + + def cleanup(self): + pass + +def main(): + app = ChatApp() + app.run() + app.cleanup() + +if __name__ == "__main__": + main() diff --git a/chat/network.py b/chat/network.py new file mode 100644 index 00000000..a685a38a --- /dev/null +++ b/chat/network.py @@ -0,0 +1,60 @@ +import socket +import threading +import traceback +import select +import time +import gobject + +class GroupChatController(object): + + _MAX_MSG_SIZE = 500 + + def __init__(self, address, port, data_cb): + self._address = address + self._port = port + self._data_cb = data_cb + + self._setup_sender() + self._setup_listener() + + def _setup_sender(self): + self._send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # Make the socket multicast-aware, and set TTL. + self._send_sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20) # Change TTL (=20) to suit + + def _setup_listener(self): + # Listener socket + self._listen_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + # Set some options to make it multicast-friendly + self._listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + self._listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + except: + pass + self._listen_sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 20) + self._listen_sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) + + def start(self): + # Set some more multicast options + self._listen_sock.bind(('', self._port)) + self._listen_sock.settimeout(2) + intf = socket.gethostbyname(socket.gethostname()) + self._listen_sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(intf) + socket.inet_aton('0.0.0.0')) + self._listen_sock.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(self._address) + socket.inet_aton('0.0.0.0')) + + # Watch the listener socket for data + gobject.io_add_watch(self._listen_sock, gobject.IO_IN, self._handle_incoming_data) + + def _handle_incoming_data(self, source, condition): + if not (condition & gobject.IO_IN): + return + msg = {} + msg['data'], (msg['addr'], msg['port']) = source.recvfrom(self._MAX_MSG_SIZE) + if self._data_cb: + self._data_cb(msg) + return True + + def send_msg(self, data): + self._send_sock.sendto(data, (self._address, self._port)) + diff --git a/chat/presence.py b/chat/presence.py new file mode 100644 index 00000000..55d8011c --- /dev/null +++ b/chat/presence.py @@ -0,0 +1,96 @@ +#!/usr/bin/python -t + + +import avahi, dbus, dbus.glib + +OLPC_CHAT_SERVICE = "_olpc_chat._udp" + + +class PresenceDiscovery(object): + def __init__(self): + self.bus = dbus.SystemBus() + self.server = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER) + self._service_browsers = {} + self._service_type_browsers = {} + self._service_listeners = [] + + def add_service_listener(self, listener): + self._service_listeners.append(listener) + + def start(self): + # Always browse .local + self.browse_domain(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, "local") + db = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.DomainBrowserNew(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, "", avahi.DOMAIN_BROWSER_BROWSE, dbus.UInt32(0))), avahi.DBUS_INTERFACE_DOMAIN_BROWSER) + db.connect_to_signal('ItemNew', self.new_domain) + + def _error_handler(self, err): + print "Error resolving: %s" % err + + def resolve_service(self, interface, protocol, name, stype, domain, reply_handler, error_handler=None): + if not error_handler: + error_handler = self._error_handler + self.server.ResolveService(int(interface), int(protocol), name, stype, domain, avahi.PROTO_UNSPEC, dbus.UInt32(0), reply_handler=reply_handler, error_handler=error_handler) + + def new_service(self, interface, protocol, name, stype, domain, flags): +# print "Found service '%s' (%d) of type '%s' in domain '%s' on %i.%i." % (name, flags, stype, domain, interface, protocol) + + for listener in self._service_listeners: + listener('added', interface, protocol, name, stype, domain, flags) + + def remove_service(self, interface, protocol, name, stype, domain, flags): +# print "Service '%s' of type '%s' in domain '%s' on %i.%i disappeared." % (name, stype, domain, interface, protocol) + + for listener in self._service_listeners: + listener('removed', interface, protocol, name, stype, domain, flags) + + def new_service_type(self, interface, protocol, stype, domain, flags): + # Are we already browsing this domain for this type? + if self._service_browsers.has_key((interface, protocol, stype, domain)): + return + +# print "Browsing for services of type '%s' in domain '%s' on %i.%i ..." % (stype, domain, interface, protocol) + + b = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.ServiceBrowserNew(interface, protocol, stype, domain, dbus.UInt32(0))), avahi.DBUS_INTERFACE_SERVICE_BROWSER) + b.connect_to_signal('ItemNew', self.new_service) + b.connect_to_signal('ItemRemove', self.remove_service) + + self._service_browsers[(interface, protocol, stype, domain)] = b + + def browse_domain(self, interface, protocol, domain): + # Are we already browsing this domain? + if self._service_type_browsers.has_key((interface, protocol, domain)): + return + +# print "Browsing domain '%s' on %i.%i ..." % (domain, interface, protocol) + + b = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.ServiceTypeBrowserNew(interface, protocol, domain, dbus.UInt32(0))), avahi.DBUS_INTERFACE_SERVICE_TYPE_BROWSER) + b.connect_to_signal('ItemNew', self.new_service_type) + + self._service_type_browsers[(interface, protocol, domain)] = b + + def new_domain(self,interface, protocol, domain, flags): + if domain != "local": + return + self.browse_domain(interface, protocol, domain) + + +class PresenceAnnounce(object): + def __init__(self): + self.bus = dbus.SystemBus() + self.server = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER) + self._hostname = None + + def register_service(self, rs_name, rs_port, rs_service, **kwargs): + g = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.EntryGroupNew()), avahi.DBUS_INTERFACE_ENTRY_GROUP) + + if rs_name is None: + if self._hostname is None: + self._hostname = "%s:%s" % (self.server.GetHostName(), rs_port) + rs_name = self._hostname + + info = ["%s=%s" % (k,v) for k,v in kwargs.items()] + g.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, 0, rs_name, rs_service, + "", "", # domain, host (let the system figure it out) + dbus.UInt16(rs_port), info,) + g.Commit() + return g