diff --git a/.hgignore b/.hgignore index 1f56821c2..962a2aafb 100644 --- a/.hgignore +++ b/.hgignore @@ -2,13 +2,9 @@ syntax: glob *.orig *.gmo *.in -*.la -*.lo *.m4 *.pyc *.pyo -*.o -*.Plo *~ autom4te.cache data/defs.py @@ -23,6 +19,4 @@ Makefile syntax: regexp ^config\.* ^config\/ -^src\/\.libs -^src\/trayicon.c ^scripts\/gajim.* diff --git a/AUTHORS b/AUTHORS index a3cfefe88..3d1502324 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,5 +1,6 @@ CURRENT DEVELOPERS: +Alexander Cherniuk (ts33kr AT gmail.com) Nikos Kouremenos (kourem AT gmail.com) Yann Leboulanger (asterix AT lagaule.org) Julien Pivotto (roidelapluie AT gmail.com) diff --git a/Makefile.am b/Makefile.am index 019f7a1d9..f708a0855 100644 --- a/Makefile.am +++ b/Makefile.am @@ -50,7 +50,6 @@ MAINTAINERCLEANFILES = \ aclocal.m4 \ libtool \ po/POTFILES.in \ - src/trayicon_la-eggtrayicon.loT \ m4/intltool.m4 MAINTAINERCLEANDIRS = \ diff --git a/README.html b/README.html index 558d23a80..8ddade813 100644 --- a/README.html +++ b/README.html @@ -16,7 +16,7 @@ Welcome to Gajim and thank you for trying out our client.

Runtime Requirements

@@ -34,7 +34,6 @@ Gajim is a GTK+ app that loves GNOME. You can do 'make' so you don't require gno
  • For zeroconf (bonjour), the "enable link-local messaging" checkbox, you need dbus-glib, python-avahi
  • dnsutils (or whatever package provides the nslookup binary) for SRV support
  • gtkspell and aspell-LANG where lang is your locale eg. en, fr etc
  • -
  • GnomePythonExtras 2.10 or above (aka gnome-python-desktop) so you can avoid compiling trayicon and gtkspell
  • gnome-python-desktop (for GnomeKeyring support)
  • notification-daemon or notify-python (and D-Bus) to get cooler popups
  • D-Bus running to have gajim-remote working. Some distributions split dbus-x11, which is needed for dbus to work with Gajim. Version >= 0.80 is required.
  • @@ -53,8 +52,6 @@ the xml lib that *comes* with python and not pyxml or whatever. diff --git a/THANKS.artists b/THANKS.artists index 5cdff9134..7f8f24c99 100644 --- a/THANKS.artists +++ b/THANKS.artists @@ -1,10 +1,10 @@ Anders Ström Christophe Got Dennis Craven +Dmitry Korzhevin Guillaume Morin Gvorcek Spajreh Josef Vybíral Membris Khan Rederick Asher Jakub Szypulka - diff --git a/configure.ac b/configure.ac index 6363cb90b..af9840989 100644 --- a/configure.ac +++ b/configure.ac @@ -39,7 +39,7 @@ AM_NLS dnl **** dnl pygtk and gtk+ dnl **** -PKG_CHECK_MODULES([PYGTK], [gtk+-2.0 >= 2.12.0 pygtk-2.0 >= 2.12.0]) +PKG_CHECK_MODULES([PYGTK], [gtk+-2.0 >= 2.16.0 pygtk-2.0 >= 2.16.0]) AC_SUBST(PYGTK_CFLAGS) AC_SUBST(PYGTK_LIBS) PYGTK_DEFS=`$PKG_CONFIG --variable=defsdir pygtk-2.0` @@ -50,15 +50,6 @@ if test "x$PYTHON" = "x:"; then AC_MSG_ERROR([Python not found]) fi -dnl **** -dnl tray icon -dnl **** -AC_ARG_ENABLE(trayicon, - [ --disable-trayicon do not build trayicon module [default yes]], - enable_trayicon=$enableval, enable_trayicon=yes) -test "x$enable_trayicon" = "xyes" && have_trayicon=true || have_trayicon=false -AM_CONDITIONAL(BUILD_TRAYICON, $have_trayicon) - ACLOCAL_AMFLAGS="\${ACLOCAL_FLAGS}" AC_SUBST(ACLOCAL_AMFLAGS) @@ -91,8 +82,3 @@ AC_CONFIG_FILES([ po/Makefile.in ]) AC_OUTPUT -echo " -***************************** - Build features: - trayicon ......... ${have_trayicon} -*****************************" diff --git a/data/emoticons/tango/alien.png b/data/emoticons/tango/alien.png new file mode 100644 index 000000000..ca83124f8 Binary files /dev/null and b/data/emoticons/tango/alien.png differ diff --git a/data/emoticons/tango/angel.png b/data/emoticons/tango/angel.png new file mode 100644 index 000000000..ad98a51ca Binary files /dev/null and b/data/emoticons/tango/angel.png differ diff --git a/data/emoticons/tango/arrogant.png b/data/emoticons/tango/arrogant.png new file mode 100644 index 000000000..42b85ff14 Binary files /dev/null and b/data/emoticons/tango/arrogant.png differ diff --git a/data/emoticons/tango/beer.png b/data/emoticons/tango/beer.png new file mode 100644 index 000000000..e955770a3 Binary files /dev/null and b/data/emoticons/tango/beer.png differ diff --git a/data/emoticons/tango/bomb.png b/data/emoticons/tango/bomb.png new file mode 100644 index 000000000..9f28ff03b Binary files /dev/null and b/data/emoticons/tango/bomb.png differ diff --git a/data/emoticons/tango/clap.png b/data/emoticons/tango/clap.png new file mode 100644 index 000000000..80b0ae1b2 Binary files /dev/null and b/data/emoticons/tango/clap.png differ diff --git a/data/emoticons/tango/confused.png b/data/emoticons/tango/confused.png new file mode 100644 index 000000000..e1a76fc38 Binary files /dev/null and b/data/emoticons/tango/confused.png differ diff --git a/data/emoticons/tango/cowboy.png b/data/emoticons/tango/cowboy.png new file mode 100644 index 000000000..35e71afc8 Binary files /dev/null and b/data/emoticons/tango/cowboy.png differ diff --git a/data/emoticons/tango/crying.png b/data/emoticons/tango/crying.png new file mode 100644 index 000000000..016b82eb5 Binary files /dev/null and b/data/emoticons/tango/crying.png differ diff --git a/data/emoticons/tango/curl-lip.png b/data/emoticons/tango/curl-lip.png new file mode 100644 index 000000000..bf12d34c8 Binary files /dev/null and b/data/emoticons/tango/curl-lip.png differ diff --git a/data/emoticons/tango/cute.png b/data/emoticons/tango/cute.png new file mode 100644 index 000000000..ca6b4018a Binary files /dev/null and b/data/emoticons/tango/cute.png differ diff --git a/data/emoticons/tango/dance.png b/data/emoticons/tango/dance.png new file mode 100644 index 000000000..709701d9b Binary files /dev/null and b/data/emoticons/tango/dance.png differ diff --git a/data/emoticons/tango/devil.png b/data/emoticons/tango/devil.png new file mode 100644 index 000000000..3e043d07b Binary files /dev/null and b/data/emoticons/tango/devil.png differ diff --git a/data/emoticons/tango/disapointed.png b/data/emoticons/tango/disapointed.png new file mode 100644 index 000000000..b2aced9a3 Binary files /dev/null and b/data/emoticons/tango/disapointed.png differ diff --git a/data/emoticons/tango/doh.png b/data/emoticons/tango/doh.png new file mode 100644 index 000000000..d5cd84cc8 Binary files /dev/null and b/data/emoticons/tango/doh.png differ diff --git a/data/emoticons/tango/dont-know.png b/data/emoticons/tango/dont-know.png new file mode 100644 index 000000000..df2b647ca Binary files /dev/null and b/data/emoticons/tango/dont-know.png differ diff --git a/data/emoticons/tango/embarrassed.png b/data/emoticons/tango/embarrassed.png new file mode 100644 index 000000000..93c1c2624 Binary files /dev/null and b/data/emoticons/tango/embarrassed.png differ diff --git a/data/emoticons/tango/emoticons.py b/data/emoticons/tango/emoticons.py new file mode 100644 index 000000000..43c7ce37f --- /dev/null +++ b/data/emoticons/tango/emoticons.py @@ -0,0 +1,52 @@ +# coding=utf-8 +emoticons = { + 'angel.png': ['O:-)', 'o:-)', '0:-)'], + 'curl-lip.png': [':-)', ':)', '=)'], + 'sarcastic.png': [';D', ';-D'], + 'sad.png': [':-(', ':(', '=('], + 'wink.png': [';-)', ';)'], + 'tongue.png': [':-P', ':P', ':-p', ':p'], + 'glasses-cool': ['8-)', 'B-)', '(H)'], + 'laugh.png': [':-D', ':D'], + 'embarrassed.png': [':-[', ':['], + 'shout.png': ['=-O', ':-O', ':O'], + 'kiss.png': [':-{}', ':-*', ':*', '(K)'], + 'crying.png': [":'-(", ":'(", ';-(', ';(', ";'-("], + 'shut-mouth': [':-X'], + 'hypnotized.png': ['%)', '%-)'], + 'freaked-out.png': [':o'], + 'thinking.png': [':-|', ':|'], + 'arrogant.png': [':-/', ':/', ':-\\', ':\\', ':-S', ':S'], + 'devil.png': [':->', ']:->', '>:-)', '>:)', '(6)'], + 'musical-note.png': ['[:-}'], + 'sick.png': [':-!'], + 'giggle.png': ['*JOKINGLY*'], + 'cute.png': ['*KISSED*'], + 'sleepy.png': ['*TIRED*'], + 'terror.png': ['*STOP*'], + 'handshake.png': ['*KISSING*'], + 'rose.png': ['@}->--', '(F)'], + 'good.png': ['(Y)', '*THUMBS UP*'], + 'beer.png': ['(B)', '*DRINK*'], + 'love.png': ['<3', '(L)', '*IN LOVE*'], + 'bomb.png': ['@='], + 'question.png': ['*HELP*'], + 'cowboy.png': ['\m/'], + 'fingers-crossed.png': ['*OK*'], + 'alien.png': ['*WASSUP*'], + 'disapointed.png': ['*SORRY*'], + 'clap.png': ['*BRAVO*'], + 'rotfl.png': ['*ROFL*'], + 'dont-know.png': ['*PARDON*'], + 'confused.png': ['*NO*'], + 'silly.png': ['*CRAZY*'], + 'doh.png': ['*DONT_KNOW*'], + 'party.png': ['*DANCE*'], + 'dance.png': ['*YAHOO*'], + 'victory.png': ['*HI*'], + 'go-away.png': ['*BYE*'], + 'smirk.png': ['*YES*'], + 'pissed-off.png': ['*WALL*'], + 'mail.png': ['*WRITE*', '(E)'], + 'tremble.png': ['*SCRATCH*'], +} \ No newline at end of file diff --git a/data/emoticons/tango/fingers-crossed.png b/data/emoticons/tango/fingers-crossed.png new file mode 100644 index 000000000..6494707c5 Binary files /dev/null and b/data/emoticons/tango/fingers-crossed.png differ diff --git a/data/emoticons/tango/freaked-out.png b/data/emoticons/tango/freaked-out.png new file mode 100644 index 000000000..3bb6d58e5 Binary files /dev/null and b/data/emoticons/tango/freaked-out.png differ diff --git a/data/emoticons/tango/giggle.png b/data/emoticons/tango/giggle.png new file mode 100644 index 000000000..e583178f6 Binary files /dev/null and b/data/emoticons/tango/giggle.png differ diff --git a/data/emoticons/tango/glasses-cool.png b/data/emoticons/tango/glasses-cool.png new file mode 100644 index 000000000..6fdb5fe85 Binary files /dev/null and b/data/emoticons/tango/glasses-cool.png differ diff --git a/data/emoticons/tango/go-away.png b/data/emoticons/tango/go-away.png new file mode 100644 index 000000000..efc98af5a Binary files /dev/null and b/data/emoticons/tango/go-away.png differ diff --git a/data/emoticons/tango/good.png b/data/emoticons/tango/good.png new file mode 100644 index 000000000..127d07313 Binary files /dev/null and b/data/emoticons/tango/good.png differ diff --git a/data/emoticons/tango/handshake.png b/data/emoticons/tango/handshake.png new file mode 100644 index 000000000..5c6d6a531 Binary files /dev/null and b/data/emoticons/tango/handshake.png differ diff --git a/data/emoticons/tango/hypnotized.png b/data/emoticons/tango/hypnotized.png new file mode 100644 index 000000000..62823ce72 Binary files /dev/null and b/data/emoticons/tango/hypnotized.png differ diff --git a/data/emoticons/tango/kiss.png b/data/emoticons/tango/kiss.png new file mode 100644 index 000000000..8d0d2e8b1 Binary files /dev/null and b/data/emoticons/tango/kiss.png differ diff --git a/data/emoticons/tango/laugh.png b/data/emoticons/tango/laugh.png new file mode 100644 index 000000000..fdb4f50c0 Binary files /dev/null and b/data/emoticons/tango/laugh.png differ diff --git a/data/emoticons/tango/love.png b/data/emoticons/tango/love.png new file mode 100644 index 000000000..5171deed6 Binary files /dev/null and b/data/emoticons/tango/love.png differ diff --git a/data/emoticons/tango/mail.png b/data/emoticons/tango/mail.png new file mode 100644 index 000000000..027bdb090 Binary files /dev/null and b/data/emoticons/tango/mail.png differ diff --git a/data/emoticons/tango/musical-note.png b/data/emoticons/tango/musical-note.png new file mode 100644 index 000000000..5990ae544 Binary files /dev/null and b/data/emoticons/tango/musical-note.png differ diff --git a/data/emoticons/tango/party.png b/data/emoticons/tango/party.png new file mode 100644 index 000000000..bcc7a4ffe Binary files /dev/null and b/data/emoticons/tango/party.png differ diff --git a/data/emoticons/tango/pissed-off.png b/data/emoticons/tango/pissed-off.png new file mode 100644 index 000000000..a1c8c8c64 Binary files /dev/null and b/data/emoticons/tango/pissed-off.png differ diff --git a/data/emoticons/tango/question.png b/data/emoticons/tango/question.png new file mode 100644 index 000000000..fc657b46a Binary files /dev/null and b/data/emoticons/tango/question.png differ diff --git a/data/emoticons/tango/rose.png b/data/emoticons/tango/rose.png new file mode 100644 index 000000000..ff7180ed3 Binary files /dev/null and b/data/emoticons/tango/rose.png differ diff --git a/data/emoticons/tango/rotfl.png b/data/emoticons/tango/rotfl.png new file mode 100644 index 000000000..9f985a2e6 Binary files /dev/null and b/data/emoticons/tango/rotfl.png differ diff --git a/data/emoticons/tango/sad.png b/data/emoticons/tango/sad.png new file mode 100644 index 000000000..643fd4379 Binary files /dev/null and b/data/emoticons/tango/sad.png differ diff --git a/data/emoticons/tango/sarcastic.png b/data/emoticons/tango/sarcastic.png new file mode 100644 index 000000000..ece6dd522 Binary files /dev/null and b/data/emoticons/tango/sarcastic.png differ diff --git a/data/emoticons/tango/shout.png b/data/emoticons/tango/shout.png new file mode 100644 index 000000000..c7aa7a9d7 Binary files /dev/null and b/data/emoticons/tango/shout.png differ diff --git a/data/emoticons/tango/shut-mouth.png b/data/emoticons/tango/shut-mouth.png new file mode 100644 index 000000000..c98a29780 Binary files /dev/null and b/data/emoticons/tango/shut-mouth.png differ diff --git a/data/emoticons/tango/sick.png b/data/emoticons/tango/sick.png new file mode 100644 index 000000000..777e6fd84 Binary files /dev/null and b/data/emoticons/tango/sick.png differ diff --git a/data/emoticons/tango/silly.png b/data/emoticons/tango/silly.png new file mode 100644 index 000000000..b944b7e9f Binary files /dev/null and b/data/emoticons/tango/silly.png differ diff --git a/data/emoticons/tango/sleepy.png b/data/emoticons/tango/sleepy.png new file mode 100644 index 000000000..25449039c Binary files /dev/null and b/data/emoticons/tango/sleepy.png differ diff --git a/data/emoticons/tango/smirk.png b/data/emoticons/tango/smirk.png new file mode 100644 index 000000000..77e97ee5c Binary files /dev/null and b/data/emoticons/tango/smirk.png differ diff --git a/data/emoticons/tango/terror.png b/data/emoticons/tango/terror.png new file mode 100644 index 000000000..104d01252 Binary files /dev/null and b/data/emoticons/tango/terror.png differ diff --git a/data/emoticons/tango/thinking.png b/data/emoticons/tango/thinking.png new file mode 100644 index 000000000..9d1d752b1 Binary files /dev/null and b/data/emoticons/tango/thinking.png differ diff --git a/data/emoticons/tango/tongue.png b/data/emoticons/tango/tongue.png new file mode 100644 index 000000000..144318dd1 Binary files /dev/null and b/data/emoticons/tango/tongue.png differ diff --git a/data/emoticons/tango/tremble.png b/data/emoticons/tango/tremble.png new file mode 100644 index 000000000..3a39e8796 Binary files /dev/null and b/data/emoticons/tango/tremble.png differ diff --git a/data/emoticons/tango/victory.png b/data/emoticons/tango/victory.png new file mode 100644 index 000000000..d0b635e9b Binary files /dev/null and b/data/emoticons/tango/victory.png differ diff --git a/data/emoticons/tango/wink.png b/data/emoticons/tango/wink.png new file mode 100644 index 000000000..662baa8ce Binary files /dev/null and b/data/emoticons/tango/wink.png differ diff --git a/launch.sh b/launch.sh index bcb968553..0c73c667c 100755 --- a/launch.sh +++ b/launch.sh @@ -1,3 +1,3 @@ #!/bin/sh cd "$(dirname $0)/src" -exec python -OOt gajim.py $@ +exec python -OOt gajim.py "$@" diff --git a/scripts/gajim.in b/scripts/gajim.in index 711ddac1d..178d69b16 100644 --- a/scripts/gajim.in +++ b/scripts/gajim.in @@ -33,5 +33,4 @@ export datadir=@DATADIR@/gajim PYTHON_EXEC=@PYTHON@ cd ${datadir}/src -export PYTHONPATH="$PYTHONPATH:@LIBDIR@/gajim" exec ${PYTHON_EXEC} -OO $APP.py "$@" diff --git a/src/Makefile.am b/src/Makefile.am index 24e7e2234..9c5e23b85 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,31 +1,7 @@ -CLEANFILES = \ - trayicon.c INCLUDES = \ $(PYTHON_INCLUDES) export MACOSX_DEPLOYMENT_TARGET=10.4 -if BUILD_TRAYICON -trayiconlib_LTLIBRARIES = trayicon.la -trayiconlibdir = $(pkglibdir) -trayicon_la_LIBADD = $(PYGTK_LIBS) -trayicon_la_SOURCES = \ - eggtrayicon.c \ - trayiconmodule.c - -nodist_trayicon_la_SOURCES = \ - trayicon.c - -trayicon_la_LDFLAGS = \ - -module -avoid-version -trayicon_la_CFLAGS = $(PYGTK_CFLAGS) - -trayicon.c: - pygtk-codegen-2.0 --prefix trayicon \ - --register $(PYGTK_DEFS)/gdk-types.defs \ - --register $(PYGTK_DEFS)/gtk-types.defs \ - --override $(srcdir)/trayicon.override \ - $(srcdir)/trayicon.defs > $@ -endif gajimsrcdir = $(pkgdatadir)/src gajimsrc_PYTHON = $(srcdir)/*.py @@ -56,12 +32,7 @@ EXTRA_DIST = $(gajimsrc_PYTHON) \ $(gajimsrc2_PYTHON) \ $(gajimsrc3_PYTHON) \ $(gajimsrc4_PYTHON) \ - $(gajimsrc5_PYTHON) \ - eggtrayicon.c \ - trayiconmodule.c \ - eggtrayicon.h \ - trayicon.defs \ - trayicon.override + $(gajimsrc5_PYTHON) dist-hook: rm -f $(distdir)/ipython_view.py diff --git a/src/adhoc_commands.py b/src/adhoc_commands.py index 9b96f0fee..01d34eee6 100644 --- a/src/adhoc_commands.py +++ b/src/adhoc_commands.py @@ -35,17 +35,22 @@ import dialogs import dataforms_widget class CommandWindow: - '''Class for a window for single ad-hoc commands session. Note, that - there might be more than one for one account/jid pair in one moment. + """ + Class for a window for single ad-hoc commands session - TODO: maybe put this window into MessageWindow? consider this when - TODO: it will be possible to manage more than one window of one - TODO: account/jid pair in MessageWindowMgr. + Note, that there might be more than one for one account/jid pair in one + moment. - TODO: gtk 2.10 has a special wizard-widget, consider using it...''' + TODO: Maybe put this window into MessageWindow? consider this when it will + be possible to manage more than one window of one. + TODO: Account/jid pair in MessageWindowMgr. + TODO: GTK 2.10 has a special wizard-widget, consider using it... + """ def __init__(self, account, jid, commandnode=None): - '''Create new window.''' + """ + Create new window + """ # an account object self.account = gajim.connections[account] @@ -89,16 +94,29 @@ class CommandWindow: self.xml.signal_autoconnect(self) self.window.show_all() -# these functions are set up by appropriate stageX methods - def stage_finish(self, *anything): pass - def stage_back_button_clicked(self, *anything): assert False - def stage_forward_button_clicked(self, *anything): assert False - def stage_execute_button_clicked(self, *anything): assert False - def stage_close_button_clicked(self, *anything): assert False - def stage_adhoc_commands_window_delete_event(self, *anything): assert False - def do_nothing(self, *anything): return False + # These functions are set up by appropriate stageX methods. + def stage_finish(self, *anything): + pass -# widget callbacks + def stage_back_button_clicked(self, *anything): + assert False + + def stage_forward_button_clicked(self, *anything): + assert False + + def stage_execute_button_clicked(self, *anything): + assert False + + def stage_close_button_clicked(self, *anything): + assert False + + def stage_adhoc_commands_window_delete_event(self, *anything): + assert False + + def do_nothing(self, *anything): + return False + + # Widget callbacks... def on_back_button_clicked(self, *anything): return self.stage_back_button_clicked(*anything) @@ -123,8 +141,10 @@ class CommandWindow: # stage 1: waiting for command list def stage1(self): - '''Prepare the first stage. Request command list, - set appropriate state of widgets.''' + """ + Prepare the first stage. Request command list, set appropriate state of + widgets + """ # close old stage... self.stage_finish() @@ -164,9 +184,12 @@ class CommandWindow: # stage 2: choosing the command to execute def stage2(self): - '''Populate the command list vbox with radiobuttons - (FIXME: if there is more commands, maybe some kind of list?), - set widgets' state.''' + """ + Populate the command list vbox with radiobuttons + + FIXME: If there is more commands, maybe some kind of list, set widgets + state + """ # close old stage self.stage_finish() @@ -198,7 +221,9 @@ class CommandWindow: self.stage_adhoc_commands_window_delete_event = self.do_nothing def stage2_finish(self): - '''Remove widgets we created. Not needed when the window is destroyed.''' + """ + Remove widgets we created. Not needed when the window is destroyed + """ def remove_widget(widget): self.command_list_vbox.remove(widget) self.command_list_vbox.foreach(remove_widget) @@ -247,8 +272,10 @@ class CommandWindow: pass def stage3_close_button_clicked(self, widget): - ''' We are in the middle of executing command. Ask user if he really want to cancel - the process, then... cancel it. ''' + """ + We are in the middle of executing command. Ask user if he really want to + cancel the process, then cancel it + """ # this works also as a handler for window_delete_event, so we have to return appropriate # values if self.form_status == 'completed': @@ -362,7 +389,9 @@ class CommandWindow: # stage 4: no commands are exposed def stage4(self): - '''Display the message. Wait for user to close the window''' + """ + Display the message. Wait for user to close the window + """ # close old stage self.stage_finish() @@ -387,7 +416,9 @@ class CommandWindow: # stage 5: an error has occured def stage5(self, error=None, errorid=None, senderror=False): - '''Display the error message. Wait for user to close the window''' + """ + Display the error message. Wait for user to close the window + """ # FIXME: sending error to responder # close old stage self.stage_finish() @@ -430,8 +461,10 @@ class CommandWindow: # helpers to handle pulsing in progressbar def setup_pulsing(self, progressbar): - '''Set the progressbar to pulse. Makes a custom - function to repeatedly call progressbar.pulse() method.''' + """ + Set the progressbar to pulse. Makes a custom function to repeatedly call + progressbar.pulse() method + """ assert not self.pulse_id assert isinstance(progressbar, gtk.ProgressBar) @@ -443,14 +476,18 @@ class CommandWindow: self.pulse_id = gobject.timeout_add(80, callback) def remove_pulsing(self): - '''Stop pulsing, useful when especially when removing widget.''' + """ + Stop pulsing, useful when especially when removing widget + """ if self.pulse_id: gobject.source_remove(self.pulse_id) self.pulse_id=None # handling xml stanzas def request_command_list(self): - '''Request the command list. Change stage on delivery.''' + """ + Request the command list. Change stage on delivery + """ query = xmpp.Iq(typ='get', to=xmpp.JID(self.jid), queryNS=xmpp.NS_DISCO_ITEMS) query.setQuerynode(xmpp.NS_COMMANDS) @@ -481,7 +518,9 @@ class CommandWindow: self.account.connection.SendAndCallForResponse(query, callback) def send_command(self, action='execute'): - '''Send the command with data form. Wait for reply.''' + """ + Send the command with data form. Wait for reply + """ # create the stanza assert isinstance(self.commandnode, unicode) assert action in ('execute', 'prev', 'next', 'complete') @@ -510,7 +549,9 @@ class CommandWindow: self.account.connection.SendAndCallForResponse(stanza, callback) def send_cancel(self): - '''Send the command with action='cancel'. ''' + """ + Send the command with action='cancel' + """ assert self.commandnode if self.sessionid and self.account.connection: # we already have sessionid, so the service sent at least one reply. diff --git a/src/advanced_configuration_window.py b/src/advanced_configuration_window.py index c56ee27ad..b35461287 100644 --- a/src/advanced_configuration_window.py +++ b/src/advanced_configuration_window.py @@ -43,7 +43,9 @@ C_TYPE GTKGUI_GLADE = 'manage_accounts_window.glade' def rate_limit(rate): - ''' call func at most *rate* times per second ''' + """ + Call func at most *rate* times per second + """ def decorator(func): timeout = [None] def f(*args, **kwargs): @@ -131,8 +133,10 @@ class AdvancedConfigurationWindow(object): gajim.interface.instances['advanced_config'] = self def cb_value_column_data(self, col, cell, model, iter_): - '''check if it's boolen or holds password stuff and if yes - make the cellrenderertext not editable else it's editable''' + """ + Check if it's boolen or holds password stuff and if yes make the + cellrenderertext not editable, else - it's editable + """ optname = model[iter_][C_PREFNAME] opttype = model[iter_][C_TYPE] if opttype == self.types['boolean'] or optname == 'password': diff --git a/src/atom_window.py b/src/atom_window.py index 56449846f..4712dfc41 100644 --- a/src/atom_window.py +++ b/src/atom_window.py @@ -35,7 +35,9 @@ class AtomWindow: @classmethod def newAtomEntry(cls, entry): - ''' Queue new entry, open window if there's no one opened. ''' + """ + Queue new entry, open window if there's no one opened + """ cls.entries.append(entry) if cls.window is None: @@ -48,7 +50,9 @@ class AtomWindow: cls.window = None def __init__(self): - ''' Create new window... only if we have anything to show. ''' + """ + Create new window... only if we have anything to show + """ assert len(self.__class__.entries)>0 self.entry = None # the entry actually displayed @@ -69,7 +73,9 @@ class AtomWindow: self.feed_title_eventbox.add_events(gtk.gdk.BUTTON_PRESS_MASK) def displayNextEntry(self): - ''' Get next entry from the queue and display it in the window. ''' + """ + Get next entry from the queue and display it in the window + """ assert len(self.__class__.entries)>0 newentry = self.__class__.entries.pop(0) @@ -103,8 +109,10 @@ class AtomWindow: self.entry = newentry def updateCounter(self): - ''' We display number of events on the top of window, sometimes it needs to be - changed...''' + """ + Display number of events on the top of window, sometimes it needs to be + changed + """ count = len(self.__class__.entries) if count>0: self.new_entry_label.set_text(i18n.ngettext( diff --git a/src/chat_control.py b/src/chat_control.py index 670978f70..7850a2e3e 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -90,8 +90,9 @@ if gajim.config.get('use_speller') and HAS_GTK_SPELL: ################################################################################ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): - '''A base class containing a banner, ConversationTextview, MessageTextView - ''' + """ + A base class containing a banner, ConversationTextview, MessageTextView + """ def make_href(self, match): url_color = gajim.config.get('urlmsgcolor') @@ -99,7 +100,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): url_color, match.group()) def get_font_attrs(self): - ''' get pango font attributes for banner from theme settings ''' + """ + Get pango font attributes for banner from theme settings + """ theme = gajim.config.get('roster_theme') bannerfont = gajim.config.get_per('themes', theme, 'bannerfont') bannerfontattrs = gajim.config.get_per('themes', theme, 'bannerfontattrs') @@ -133,33 +136,45 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): type_])) def draw_banner(self): - '''Draw the fat line at the top of the window that - houses the icon, jid, ... - ''' + """ + Draw the fat line at the top of the window that houses the icon, jid, etc + + Derived types MAY implement this. + """ self.draw_banner_text() self._update_banner_state_image() - # Derived types MAY implement this def draw_banner_text(self): - pass # Derived types SHOULD implement this + """ + Derived types SHOULD implement this + """ + pass def update_ui(self): + """ + Derived types SHOULD implement this + """ self.draw_banner() - # Derived types SHOULD implement this def repaint_themed_widgets(self): + """ + Derived types MAY implement this + """ self._paint_banner() self.draw_banner() - # Derived classes MAY implement this def _update_banner_state_image(self): - pass # Derived types MAY implement this + """ + Derived types MAY implement this + """ + pass def handle_message_textview_mykey_press(self, widget, event_keyval, - event_keymod): - # Derived should implement this rather than connecting to the event - # itself. - + event_keymod): + """ + Derives types SHOULD implement this, rather than connection to the even + itself + """ event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) event.keyval = event_keyval event.state = event_keymod @@ -212,7 +227,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): helpers.launch_browser_mailer('url', url) def __init__(self, type_id, parent_win, widget_name, contact, acct, - resource = None): + resource = None): if resource is None: # We very likely got a contact with a random resource. # This is bad, we need the highest for caps etc. @@ -386,7 +401,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): dialogs.AspellDictError(lang) def on_banner_label_populate_popup(self, label, menu): - '''We override the default context menu and add our own menutiems''' + """ + Override the default context menu and add our own menutiems + """ item = gtk.SeparatorMenuItem() menu.prepend(item) @@ -400,8 +417,10 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): menu.show_all() def on_msg_textview_populate_popup(self, textview, menu): - '''we override the default context menu and we prepend an option to switch - languages''' + """ + Override the default context menu and we prepend an option to switch + languages + """ def _on_select_dictionary(widget, lang): per_type = 'contacts' if self.type_id == message_control.TYPE_GC: @@ -445,12 +464,16 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): # moved from ChatControl def _on_banner_eventbox_button_press_event(self, widget, event): - '''If right-clicked, show popup''' + """ + If right-clicked, show popup + """ if event.button == 3: # right click self.parent_win.popup_menu(event) def _on_send_button_clicked(self, widget): - '''When send button is pressed: send the current message''' + """ + When send button is pressed: send the current message + """ if gajim.connections[self.account].connected < 2: # we are not connected dialogs.ErrorDialog(_('A connection is not available'), _('Your message can not be sent until you are connected.')) @@ -465,7 +488,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.send_message(message, xhtml=xhtml) def _paint_banner(self): - '''Repaint banner with theme color''' + """ + Repaint banner with theme color + """ theme = gajim.config.get('roster_theme') bgcolor = gajim.config.get_per('themes', theme, 'bannerbgcolor') textcolor = gajim.config.get_per('themes', theme, 'bannertextcolor') @@ -512,9 +537,11 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.handlers[id_] = widget def _on_style_set_event(self, widget, style, *opts): - '''set style of widget from style class *.Frame.Eventbox + """ + Set style of widget from style class *.Frame.Eventbox opts[0] == True -> set fg color - opts[1] == True -> set bg color''' + opts[1] == True -> set bg color + """ banner_eventbox = self.xml.get_widget('banner_eventbox') self.disconnect_style_event(widget) if opts[1]: @@ -589,11 +616,11 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): return False def _on_message_textview_mykeypress_event(self, widget, event_keyval, - event_keymod): - '''When a key is pressed: - if enter is pressed without the shift key, message (if not empty) is sent - and printed in the conversation''' - + event_keymod): + """ + When a key is pressed: if enter is pressed without the shift key, message + (if not empty) is sent and printed in the conversation + """ # NOTE: handles mykeypress which is custom signal connected to this # CB in new_tab(). for this singal see message_textview.py message_textview = widget @@ -652,8 +679,11 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): event_keymod) def _on_drag_data_received(self, widget, context, x, y, selection, - target_type, timestamp): - pass # Derived classes SHOULD implement this method + target_type, timestamp): + """ + Derived types SHOULD implement this + """ + pass def _on_drag_leave(self, widget, context, time): # FIXME: DND on non editable TextView, find a better way @@ -668,10 +698,11 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.conv_textview.tv.set_editable(True) def send_message(self, message, keyID='', type_='chat', chatstate=None, - msg_id=None, composing_xep=None, resource=None, - xhtml=None, callback=None, callback_args=[], process_commands=True): - '''Send the given message to the active tab. Doesn't return None if error - ''' + msg_id=None, composing_xep=None, resource=None, xhtml=None, + callback=None, callback_args=[], process_commands=True): + """ + Send the given message to the active tab. Doesn't return None if error + """ if not message or message == '\n': return None @@ -711,10 +742,13 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.orig_msg = None def print_conversation_line(self, text, kind, name, tim, - other_tags_for_name=[], other_tags_for_time=[], other_tags_for_text=[], - count_as_new=True, subject=None, old_kind=None, xhtml=None, simple=False, - xep0184_id=None, graphics=True): - '''prints 'chat' type messages''' + other_tags_for_name=[], other_tags_for_time=[], + other_tags_for_text=[], count_as_new=True, subject=None, + old_kind=None, xhtml=None, simple=False, xep0184_id=None, + graphics=True): + """ + Print 'chat' type messages + """ jid = self.contact.jid full_jid = self.get_full_jid() textview = self.conv_textview @@ -789,8 +823,10 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.parent_win.show_title(False, self) # Disabled Urgent hint def toggle_emoticons(self): - '''hide show emoticons_button and make sure emoticons_menu is always there - when needed''' + """ + Hide show emoticons_button and make sure emoticons_menu is always there + when needed + """ emoticons_button = self.xml.get_widget('emoticons_button') if gajim.config.get('emoticons_theme'): emoticons_button.show() @@ -808,12 +844,16 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.msg_textview.grab_focus() def on_emoticons_button_clicked(self, widget): - '''popup emoticons menu''' + """ + Popup emoticons menu + """ gajim.interface.emoticon_menuitem_clicked = self.append_emoticon gajim.interface.popup_emoticons_under_button(widget, self.parent_win) def on_formattings_button_clicked(self, widget): - '''popup formattings menu''' + """ + Popup formattings menu + """ menu = gtk.Menu() menuitems = ((_('Bold'), 'bold'), @@ -875,7 +915,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): def on_actions_button_clicked(self, widget): - '''popup action menu''' + """ + Popup action menu + """ menu = self.prepare_context_menu(hide_buttonbar_items=True) menu.show_all() gtkgui_helpers.popup_emoticons_under_button(menu, widget, @@ -895,7 +937,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): buffer_.delete(start, end) def _on_history_menuitem_activate(self, widget = None, jid = None): - '''When history menuitem is pressed: call history window''' + """ + When history menuitem is pressed: call history window + """ if not jid: jid = self.contact.jid @@ -907,7 +951,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): history_window.HistoryWindow(jid, self.account) def _on_send_file(self, gc_contact=None): - '''gc_contact can be set when we are in a groupchat control''' + """ + gc_contact can be set when we are in a groupchat control + """ def _on_ok(c): gajim.interface.instances['file_transfers'].show_file_send_request( self.account, c) @@ -935,7 +981,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): _on_ok(self.contact) def on_minimize_menuitem_toggled(self, widget): - '''When a grouchat is minimized, unparent the tab, put it in roster etc''' + """ + When a grouchat is minimized, unparent the tab, put it in roster etc + """ old_value = False minimized_gc = gajim.config.get_per('accounts', self.account, 'minimized_gc').split() @@ -965,7 +1013,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): def bring_scroll_to_end(self, textview, diff_y = 0): - ''' scrolls to the end of textview if end is not visible ''' + """ + Scroll to the end of textview if end is not visible + """ if self.scroll_to_end_id: # a scroll is already planned return @@ -986,10 +1036,12 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): return False def size_request(self, msg_textview , requisition): - ''' When message_textview changes its size. If the new height - will enlarge the window, enable the scrollbar automatic policy - Also enable scrollbar automatic policy for horizontal scrollbar - if message we have in message_textview is too big''' + """ + When message_textview changes its size: if the new height will enlarge + the window, enable the scrollbar automatic policy. Also enable scrollbar + automatic policy for horizontal scrollbar if message we have in + message_textview is too big + """ if msg_textview.window is None: return @@ -1082,8 +1134,10 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.redraw_after_event_removed(jid) def redraw_after_event_removed(self, jid): - ''' We just removed a 'printed_*' event, redraw contact in roster or - gc_roster and titles in roster and msg_win ''' + """ + We just removed a 'printed_*' event, redraw contact in roster or + gc_roster and titles in roster and msg_win + """ self.parent_win.redraw_tab(self) self.parent_win.show_title() # TODO : get the contact and check notify.get_show_in_roster() @@ -1142,7 +1196,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): return color def widget_set_visible(self, widget, state): - '''Show or hide a widget. state is bool''' + """ + Show or hide a widget + """ # make the last message visible, when changing to "full view" if not state: gobject.idle_add(self.conv_textview.scroll_to_end_iter) @@ -1154,7 +1210,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): widget.show_all() def chat_buttons_set_visible(self, state): - '''Toggle chat buttons. state is bool''' + """ + Toggle chat buttons + """ MessageControl.chat_buttons_set_visible(self, state) self.widget_set_visible(self.xml.get_widget('actions_hbox'), state) @@ -1173,7 +1231,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): ################################################################################ class ChatControl(ChatControlBase): - '''A control for standard 1-1 chat''' + """ + A control for standard 1-1 chat + """ ( JINGLE_STATE_NOT_AVAILABLE, JINGLE_STATE_AVAILABLE, @@ -1279,14 +1339,12 @@ class ChatControl(ChatControlBase): self.video_state = self.JINGLE_STATE_NOT_AVAILABLE self.update_toolbar() - - self._mood_image = self.xml.get_widget('mood_image') - self._activity_image = self.xml.get_widget('activity_image') - self._tune_image = self.xml.get_widget('tune_image') - - self.update_mood() - self.update_activity() - self.update_tune() + + self._pep_images = {} + self._pep_images['mood'] = self.xml.get_widget('mood_image') + self._pep_images['activity'] = self.xml.get_widget('activity_image') + self._pep_images['tune'] = self.xml.get_widget('tune_image') + self.update_all_pep_types() # keep timeout id and window obj for possible big avatar # it is on enter-notify and leave-notify so no need to be @@ -1430,118 +1488,24 @@ class ChatControl(ChatControlBase): self._convert_to_gc_button.set_sensitive(True) else: self._convert_to_gc_button.set_sensitive(False) + + def update_all_pep_types(self): + for pep_type in self._pep_images: + self.update_pep(pep_type) - def update_mood(self): - mood = None - text = None - + def update_pep(self, pep_type): if isinstance(self.contact, GC_Contact): return - - if 'mood' in self.contact.mood: - mood = self.contact.mood['mood'].strip() - if 'text' in self.contact.mood: - text = self.contact.mood['text'].strip() - - if mood is not None: - if mood in MOODS: - self._mood_image.set_from_pixbuf(gtkgui_helpers.load_mood_icon( - mood).get_pixbuf()) - # Translate standard moods - mood = MOODS[mood] - else: - self._mood_image.set_from_pixbuf(gtkgui_helpers.load_mood_icon( - 'unknown').get_pixbuf()) - - mood = gobject.markup_escape_text(mood) - - tooltip = '%s' % mood - if text: - text = gobject.markup_escape_text(text) - tooltip += '\n' + text - self._mood_image.set_tooltip_markup(tooltip) - self._mood_image.show() - else: - self._mood_image.hide() - - def update_activity(self): - activity = None - subactivity = None - text = None - - if isinstance(self.contact, GC_Contact): + if pep_type not in self._pep_images: return - - if 'activity' in self.contact.activity: - activity = self.contact.activity['activity'].strip() - if 'subactivity' in self.contact.activity: - subactivity = self.contact.activity['subactivity'].strip() - if 'text' in self.contact.activity: - text = self.contact.activity['text'].strip() - - if activity is not None: - if activity in ACTIVITIES: - # Translate standard activities - if subactivity in ACTIVITIES[activity]: - self._activity_image.set_from_pixbuf( - gtkgui_helpers.load_activity_icon(activity, subactivity). \ - get_pixbuf()) - subactivity = ACTIVITIES[activity][subactivity] - else: - self._activity_image.set_from_pixbuf( - gtkgui_helpers.load_activity_icon(activity).get_pixbuf()) - activity = ACTIVITIES[activity]['category'] - else: - self._activity_image.set_from_pixbuf( - gtkgui_helpers.load_activity_icon('unknown').get_pixbuf()) - - # Translate standard subactivities - - tooltip = '' + gobject.markup_escape_text(activity) - if subactivity: - tooltip += ': ' + gobject.markup_escape_text(subactivity) - tooltip += '' - if text: - tooltip += '\n' + gobject.markup_escape_text(text) - self._activity_image.set_tooltip_markup(tooltip) - - self._activity_image.show() + pep = self.contact.pep + img = self._pep_images[pep_type] + if pep_type in pep: + img.set_from_pixbuf(pep[pep_type].asPixbufIcon()) + img.set_tooltip_markup(pep[pep_type].asMarkupText()) + img.show() else: - self._activity_image.hide() - - def update_tune(self): - artist = None - title = None - source = None - - if isinstance(self.contact, GC_Contact): - return - - if 'artist' in self.contact.tune: - artist = self.contact.tune['artist'].strip() - artist = gobject.markup_escape_text(artist) - if 'title' in self.contact.tune: - title = self.contact.tune['title'].strip() - title = gobject.markup_escape_text(title) - if 'source' in self.contact.tune: - source = self.contact.tune['source'].strip() - source = gobject.markup_escape_text(source) - - if artist or title: - if not artist: - artist = _('Unknown Artist') - if not title: - title = _('Unknown Title') - if not source: - source = _('Unknown Source') - - self._tune_image.set_tooltip_markup( - _('"%(title)s" by %(artist)s\n' - 'from %(source)s') % {'title': title, 'artist': artist, - 'source': source}) - self._tune_image.show() - else: - self._tune_image.hide() + img.hide() def _update_jingle(self, jingle_type): if jingle_type not in ('audio', 'video'): @@ -1631,10 +1595,10 @@ class ChatControl(ChatControlBase): self._set_jingle_state('video', state, sid=sid, reason=reason) def on_avatar_eventbox_enter_notify_event(self, widget, event): - ''' - we enter the eventbox area so we under conditions add a timeout - to show a bigger avatar after 0.5 sec - ''' + """ + Enter the eventbox area so we under conditions add a timeout to show a + bigger avatar after 0.5 sec + """ jid = self.contact.jid is_fake = False if self.type_id == message_control.TYPE_PM: @@ -1657,13 +1621,17 @@ class ChatControl(ChatControlBase): self.show_bigger_avatar, widget) def on_avatar_eventbox_leave_notify_event(self, widget, event): - '''we left the eventbox area that holds the avatar img''' + """ + Left the eventbox area that holds the avatar img + """ # did we add a timeout? if yes remove it if self.show_bigger_avatar_timeout_id is not None: gobject.source_remove(self.show_bigger_avatar_timeout_id) def on_avatar_eventbox_button_press_event(self, widget, event): - '''If right-clicked, show popup''' + """ + If right-clicked, show popup + """ if event.button == 3: # right click menu = gtk.Menu() menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS) @@ -1681,7 +1649,9 @@ class ChatControl(ChatControlBase): return True def _on_window_motion_notify(self, widget, event): - '''it gets called no matter if it is the active window or not''' + """ + It gets called no matter if it is the active window or not + """ if self.parent_win.get_active_jid() == self.contact.jid: # if window is the active one, change vars assisting chatstate self.mouse_over_in_last_5_secs = True @@ -1734,9 +1704,10 @@ class ChatControl(ChatControlBase): banner_status_img.set_from_pixbuf(scaled_pix) def draw_banner_text(self): - '''Draw the text in the fat line at the top of the window that - houses the name, jid. - ''' + """ + Draw the text in the fat line at the top of the window that houses the + name, jid + """ contact = self.contact jid = contact.jid @@ -1919,8 +1890,11 @@ class ChatControl(ChatControlBase): self._show_lock_image(self.gpg_is_active, 'GPG', self.gpg_is_active, loggable, True) - def _show_lock_image(self, visible, enc_type = '', enc_enabled = False, chat_logged = False, authenticated = False): - '''Set lock icon visibility and create tooltip''' + def _show_lock_image(self, visible, enc_type = '', enc_enabled = False, + chat_logged = False, authenticated = False): + """ + Set lock icon visibility and create tooltip + """ #encryption %s active status_string = enc_enabled and _('is') or _('is NOT') #chat session %s be logged @@ -1955,7 +1929,9 @@ class ChatControl(ChatControlBase): def send_message(self, message, keyID='', chatstate=None, xhtml=None, process_commands=True): - '''Send a message to contact''' + """ + Send a message to contact + """ if message in ('', None, '\n'): return None @@ -2019,10 +1995,11 @@ class ChatControl(ChatControlBase): process_commands=process_commands) def check_for_possible_paused_chatstate(self, arg): - ''' did we move mouse of that window or write something in message - textview in the last 5 seconds? - if yes we go active for mouse, composing for kbd - if no we go paused if we were previously composing ''' + """ + Did we move mouse of that window or write something in message textview + in the last 5 seconds? If yes - we go active for mouse, composing for + kbd. If not - we go paused if we were previously composing + """ contact = self.contact jid = contact.jid current_state = contact.our_chatstate @@ -2046,10 +2023,10 @@ class ChatControl(ChatControlBase): return True # loop forever def check_for_possible_inactive_chatstate(self, arg): - ''' did we move mouse over that window or wrote something in message - textview in the last 30 seconds? - if yes we go active - if no we go inactive ''' + """ + Did we move mouse over that window or wrote something in message textview + in the last 30 seconds? if yes - we go active. If no - we go inactive + """ contact = self.contact current_state = contact.our_chatstate @@ -2079,7 +2056,9 @@ class ChatControl(ChatControlBase): ChatControlBase.print_conversation_line(self, msg, 'status', '', None) def print_esession_details(self): - '''print esession settings to textview''' + """ + Print esession settings to textview + """ e2e_is_active = bool(self.session) and self.session.enable_encryption if e2e_is_active: msg = _('This session is encrypted') @@ -2101,16 +2080,19 @@ class ChatControl(ChatControlBase): self.session.is_loggable(), self.session and self.session.verified_identity) def print_conversation(self, text, frm='', tim=None, encrypted=False, - subject=None, xhtml=None, simple=False, xep0184_id=None): - '''Print a line in the conversation: - if frm is set to status: it's a status message - if frm is set to error: it's an error message - The difference between status and error is mainly that with error, msg - count as a new message (in systray and in control). - if frm is set to info: it's a information message - if frm is set to print_queue: it is incomming from queue - if frm is set to another value: it's an outgoing message - if frm is not set: it's an incomming message''' + subject=None, xhtml=None, simple=False, xep0184_id=None): + """ + Print a line in the conversation + + If frm is set to status: it's a status message. + if frm is set to error: it's an error message. The difference between + status and error is mainly that with error, msg count as a new message + (in systray and in control). + If frm is set to info: it's a information message. + If frm is set to print_queue: it is incomming from queue. + If frm is set to another value: it's an outgoing message. + If frm is not set: it's an incomming message. + """ contact = self.contact if frm == 'status': @@ -2248,12 +2230,12 @@ class ChatControl(ChatControlBase): return tab_img def prepare_context_menu(self, hide_buttonbar_items=False): - '''sets compact view menuitem active state - sets active and sensitivity state for toggle_gpg_menuitem - sets sensitivity for history_menuitem (False for tranasports) - and file_transfer_menuitem - and hide()/show() for add_to_roster_menuitem - ''' + """ + Set compact view menuitem active state sets active and sensitivity state + for toggle_gpg_menuitem sets sensitivity for history_menuitem (False for + tranasports) and file_transfer_menuitem and hide()/show() for + add_to_roster_menuitem + """ menu = gui_menu_builder.get_contact_menu(self.contact, self.account, use_multiple_contacts=False, show_start_chat=False, show_encryption=True, control=self, @@ -2261,9 +2243,11 @@ class ChatControl(ChatControlBase): return menu def send_chatstate(self, state, contact = None): - ''' sends OUR chatstate as STANDLONE chat state message (eg. no body) + """ + Send OUR chatstate as STANDLONE chat state message (eg. no body) to contact only if new chatstate is different from the previous one - if jid is not specified, send to active tab''' + if jid is not specified, send to active tab + """ # JEP 85 does not allow resending the same chatstate # this function checks for that and just returns so it's safe to call it # with same state. @@ -2409,7 +2393,9 @@ class ChatControl(ChatControlBase): on_yes(self) def handle_incoming_chatstate(self): - ''' handle incoming chatstate that jid SENT TO us ''' + """ + Handle incoming chatstate that jid SENT TO us + """ self.draw_banner_text() # update chatstate in tab for this chat self.parent_win.redraw_tab(self, self.contact.chatstate) @@ -2593,8 +2579,9 @@ class ChatControl(ChatControlBase): self.conv_textview.print_empty_line() def read_queue(self): - '''read queue and print messages containted in it''' - + """ + Read queue and print messages containted in it + """ jid = self.contact.jid jid_with_resource = jid if self.resource: @@ -2649,8 +2636,10 @@ class ChatControl(ChatControlBase): control.remove_contact(nick) def show_bigger_avatar(self, small_avatar): - '''resizes the avatar, if needed, so it has at max half the screen size - and shows it''' + """ + Resize the avatar, if needed, so it has at max half the screen size and + shows it + """ if not small_avatar.window: # Tab has been closed since we hovered the avatar return @@ -2717,14 +2706,18 @@ class ChatControl(ChatControlBase): window.show_all() def _on_window_avatar_leave_notify_event(self, widget, event): - '''we just left the popup window that holds avatar''' + """ + Just left the popup window that holds avatar + """ self.bigger_avatar_window.destroy() self.bigger_avatar_window = None # Re-show the small avatar self.show_avatar() def _on_window_motion_notify_event(self, widget, event): - '''we just moved the mouse so show the cursor''' + """ + Just moved the mouse so show the cursor + """ cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR) self.bigger_avatar_window.window.set_cursor(cursor) @@ -2741,7 +2734,9 @@ class ChatControl(ChatControlBase): self._toggle_gpg() def _on_convert_to_gc_menuitem_activate(self, widget): - '''user want to invite some friends to chat''' + """ + User wants to invite some friends to chat + """ dialogs.TransformChatToMUC(self.account, [self.contact.jid]) def _on_toggle_e2e_menuitem_activate(self, widget): @@ -2788,7 +2783,9 @@ class ChatControl(ChatControlBase): self.draw_banner() def update_status_display(self, name, uf_show, status): - '''print the contact's status and update the status/GPG image''' + """ + Print the contact's status and update the status/GPG image + """ self.update_ui() self.parent_win.redraw_tab(self) diff --git a/src/command_system/__init__.py b/src/command_system/__init__.py index c0a48f863..ff61c186e 100644 --- a/src/command_system/__init__.py +++ b/src/command_system/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/dispatching.py b/src/command_system/dispatching.py index 2bfd76b96..7f365a915 100644 --- a/src/command_system/dispatching.py +++ b/src/command_system/dispatching.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/errors.py b/src/command_system/errors.py index 992e83ccf..877a22acb 100644 --- a/src/command_system/errors.py +++ b/src/command_system/errors.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/framework.py b/src/command_system/framework.py index db9eb2e78..30f5cd53c 100644 --- a/src/command_system/framework.py +++ b/src/command_system/framework.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/implementation/__init__.py b/src/command_system/implementation/__init__.py index 66d097f42..c77c23e3f 100644 --- a/src/command_system/implementation/__init__.py +++ b/src/command_system/implementation/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/implementation/custom.py b/src/command_system/implementation/custom.py index 4f54670da..e4aa32dbf 100644 --- a/src/command_system/implementation/custom.py +++ b/src/command_system/implementation/custom.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/implementation/hosts.py b/src/command_system/implementation/hosts.py index b38bb1a35..a90dab464 100644 --- a/src/command_system/implementation/hosts.py +++ b/src/command_system/implementation/hosts.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/implementation/middleware.py b/src/command_system/implementation/middleware.py index 9ef4bea29..2f262f8ed 100644 --- a/src/command_system/implementation/middleware.py +++ b/src/command_system/implementation/middleware.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/implementation/standard.py b/src/command_system/implementation/standard.py index 4cb19c466..d0e585fec 100644 --- a/src/command_system/implementation/standard.py +++ b/src/command_system/implementation/standard.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,10 +17,14 @@ Provides an actual implementation for the standard commands. """ +from time import localtime, strftime +from datetime import date + import dialogs from common import gajim from common import helpers from common.exceptions import GajimGeneralException +from common.logger import Constants from ..errors import CommandError from ..framework import CommandContainer, command, documentation @@ -28,6 +32,10 @@ from ..mapping import generate_usage from hosts import ChatCommands, PrivateChatCommands, GroupChatCommands +# This holds constants fron the logger, which we'll be using in some of our +# commands. +lc = Constants() + class StandardCommonCommands(CommandContainer): """ This command container contains standard commands which are common to all - @@ -84,6 +92,42 @@ class StandardCommonCommands(CommandContainer): def me(self, action): self.send("/me %s" % action) + @command('lastlog', overlap=True) + @documentation(_("Show logged messages which mention given text")) + def grep(self, text, limit=None): + results = gajim.logger.get_search_results_for_query(self.contact.jid, + text, self.account) + + if not results: + raise CommandError(_("%s: Nothing found") % text) + + if limit: + try: + results = results[len(results) - int(limit):] + except ValueError: + raise CommandError(_("Limit must be an integer")) + + for row in results: + contact, time, kind, show, message, subject = row + + if not contact: + if kind == lc.KIND_CHAT_MSG_SENT: + contact = gajim.nicks[self.account] + else: + contact = self.contact.name + + time_obj = localtime(time) + date_obj = date.fromtimestamp(time) + date_ = strftime('%Y-%m-%d', time_obj) + time_ = strftime('%H:%M:%S', time_obj) + + if date_obj == date.today(): + formatted = "[%s] %s: %s" % (time_, contact, message) + else: + formatted = "[%s, %s] %s: %s" % (date_, time_, contact, message) + + self.echo(formatted) + class StandardChatCommands(CommandContainer): """ This command container contains standard command which are unique to a chat. @@ -98,6 +142,22 @@ class StandardChatCommands(CommandContainer): raise CommandError(_('Command is not supported for zeroconf accounts')) gajim.connections[self.account].sendPing(self.contact) + @command('dtmf') + @documentation(_("Sends DTMF events through an open audio session")) + def dtmf(self, events): + if not self.audio_sid: + raise CommandError(_("There is no open audio session with this contact")) + # Valid values for DTMF tones are *, # or a number + events = [event for event in events + if event in ('*', '#') or event.isdigit()] + if events: + session = gajim.connections[self.account].get_jingle_session( + self.contact.get_full_jid(), self.audio_sid) + content = session.get_content('audio') + content.batch_dtmf(events) + else: + raise CommandError(_("No valid DTMF event specified")) + @command('audio') @documentation(_("Toggle audio session")) def audio(self): diff --git a/src/command_system/mapping.py b/src/command_system/mapping.py index 707866a20..ecf8f0783 100644 --- a/src/command_system/mapping.py +++ b/src/command_system/mapping.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/common/caps.py b/src/common/caps.py index b4a8d2a05..f34dfcda4 100644 --- a/src/common/caps.py +++ b/src/common/caps.py @@ -37,8 +37,11 @@ import base64 import hashlib from common.xmpp import NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, NS_CHATSTATES +from common.xmpp import NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO +from common.xmpp import NS_JINGLE_RTP_VIDEO # Features where we cannot safely assume that the other side supports them -FEATURE_BLACKLIST = [NS_CHATSTATES, NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION] +FEATURE_BLACKLIST = [NS_CHATSTATES, NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, + NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO] # Query entry status codes NEW = 0 diff --git a/src/common/connection.py b/src/common/connection.py index 96d096131..3abb512de 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -98,17 +98,493 @@ ssl_error = { 32: _("Key usage does not include certificate signing"), 50: _("Application verification failure") } -class Connection(ConnectionHandlers): - '''Connection class''' + +class CommonConnection: + ''' + Common connection class, can be derivated for normal connection or zeroconf + connection + ''' def __init__(self, name): - ConnectionHandlers.__init__(self) self.name = name # self.connected: # 0=>offline, # 1=>connection in progress, - # 2=>authorised + # 2=>online + # 3=>free for chat + # ... self.connected = 0 self.connection = None # xmpppy ClientCommon instance + self.on_purpose = False + self.is_zeroconf = False + self.password = '' + self.server_resource = self._compute_resource() + self.gpg = None + self.USE_GPG = False + if gajim.HAVE_GPG: + self.USE_GPG = True + self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent')) + self.status = '' + self.old_show = '' + self.priority = gajim.get_priority(name, 'offline') + self.time_to_reconnect = None + self.bookmarks = [] + + self.blocked_list = [] + self.blocked_contacts = [] + self.blocked_groups = [] + self.blocked_all = False + + self.pep_supported = False + self.pep = {} + # Do we continue connection when we get roster (send presence,get vcard..) + self.continue_connect_info = None + + # To know the groupchat jid associated with a sranza ID. Useful to + # request vcard or os info... to a real JID but act as if it comes from + # the fake jid + self.groupchat_jids = {} # {ID : groupchat_jid} + + self.privacy_rules_supported = False + self.vcard_supported = False + self.private_storage_supported = False + + self.muc_jid = {} # jid of muc server for each transport type + + self.get_config_values_or_default() + + def _compute_resource(self): + resource = gajim.config.get_per('accounts', self.name, 'resource') + # All valid resource substitution strings should be added to this hash. + if resource: + resource = Template(resource).safe_substitute({ + 'hostname': socket.gethostname() + }) + return resource + + def dispatch(self, event, data): + '''always passes account name as first param''' + gajim.interface.dispatch(event, self.name, data) + + def _reconnect(self): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def quit(self, kill_core): + if kill_core and gajim.account_is_connected(self.name): + self.disconnect(on_purpose=True) + + def test_gpg_passphrase(self, password): + '''Returns 'ok', 'bad_pass' or 'expired' ''' + if not self.gpg: + return False + self.gpg.passphrase = password + keyID = gajim.config.get_per('accounts', self.name, 'keyid') + signed = self.gpg.sign('test', keyID) + self.gpg.password = None + if signed == 'KEYEXPIRED': + return 'expired' + elif signed == 'BAD_PASSPHRASE': + return 'bad_pass' + return 'ok' + + def get_signed_msg(self, msg, callback = None): + '''returns the signed message if possible + or an empty string if gpg is not used + or None if waiting for passphrase. + callback is the function to call when user give the passphrase''' + signed = '' + keyID = gajim.config.get_per('accounts', self.name, 'keyid') + if keyID and self.USE_GPG: + use_gpg_agent = gajim.config.get('use_gpg_agent') + if self.gpg.passphrase is None and not use_gpg_agent: + # We didn't set a passphrase + return None + if self.gpg.passphrase is not None or use_gpg_agent: + signed = self.gpg.sign(msg, keyID) + if signed == 'BAD_PASSPHRASE': + self.USE_GPG = False + signed = '' + self.dispatch('BAD_PASSPHRASE', ()) + return signed + + def _on_disconnected(self): + ''' called when a disconnect request has completed successfully''' + self.disconnect(on_purpose=True) + self.dispatch('STATUS', 'offline') + + def get_status(self): + return gajim.SHOW_LIST[self.connected] + + def check_jid(self, jid): + '''this function must be implemented by derivated classes. + It has to return the valid jid, or raise a helpers.InvalidFormat exception + ''' + raise NotImplementedError + + def _prepare_message(self, jid, msg, keyID, type_='chat', subject='', + chatstate=None, msg_id=None, composing_xep=None, resource=None, + user_nick=None, xhtml=None, session=None, forward_from=None, form_node=None, + original_message=None, delayed=None, callback=None): + if not self.connection or self.connected < 2: + return 1 + try: + jid = self.check_jid(jid) + except helpers.InvalidFormat: + self.dispatch('ERROR', (_('Invalid Jabber ID'), + _('It is not possible to send a message to %s, this JID is not ' + 'valid.') % jid)) + return + + if msg and not xhtml and gajim.config.get( + 'rst_formatting_outgoing_messages'): + from common.rst_xhtml_generator import create_xhtml + xhtml = create_xhtml(msg) + if not msg and chatstate is None and form_node is None: + return + fjid = jid + if resource: + fjid += '/' + resource + msgtxt = msg + msgenc = '' + + if session: + fjid = session.get_to() + + if keyID and self.USE_GPG: + xhtml = None + if keyID == 'UNKNOWN': + error = _('Neither the remote presence is signed, nor a key was ' + 'assigned.') + elif keyID.endswith('MISMATCH'): + error = _('The contact\'s key (%s) does not match the key assigned ' + 'in Gajim.' % keyID[:8]) + else: + def encrypt_thread(msg, keyID, always_trust=False): + # encrypt message. This function returns (msgenc, error) + return self.gpg.encrypt(msg, [keyID], always_trust) + def _on_encrypted(output): + msgenc, error = output + if error == 'NOT_TRUSTED': + def _on_always_trust(answer): + if answer: + gajim.thread_interface(encrypt_thread, [msg, keyID, + True], _on_encrypted, []) + else: + self._message_encrypted_cb(output, type_, msg, msgtxt, + original_message, fjid, resource, jid, xhtml, + subject, chatstate, composing_xep, forward_from, + delayed, session, form_node, user_nick, keyID, + callback) + self.dispatch('GPG_ALWAYS_TRUST', _on_always_trust) + else: + self._message_encrypted_cb(output, type_, msg, msgtxt, + original_message, fjid, resource, jid, xhtml, subject, + chatstate, composing_xep, forward_from, delayed, session, + form_node, user_nick, keyID, callback) + gajim.thread_interface(encrypt_thread, [msg, keyID, False], + _on_encrypted, []) + return + + self._message_encrypted_cb(('', error), type_, msg, msgtxt, + original_message, fjid, resource, jid, xhtml, subject, chatstate, + composing_xep, forward_from, delayed, session, form_node, user_nick, + keyID, callback) + + self._on_continue_message(type_, msg, msgtxt, original_message, fjid, + resource, jid, xhtml, subject, msgenc, keyID, chatstate, composing_xep, + forward_from, delayed, session, form_node, user_nick, callback) + + def _message_encrypted_cb(self, output, type_, msg, msgtxt, original_message, + fjid, resource, jid, xhtml, subject, chatstate, composing_xep, forward_from, + delayed, session, form_node, user_nick, keyID, callback): + msgenc, error = output + + if msgenc and not error: + msgtxt = '[This message is *encrypted* (See :XEP:`27`]' + lang = os.getenv('LANG') + if lang is not None and lang != 'en': # we're not english + # one in locale and one en + msgtxt = _('[This message is *encrypted* (See :XEP:`27`]') + \ + ' (' + msgtxt + ')' + self._on_continue_message(type_, msg, msgtxt, original_message, fjid, + resource, jid, xhtml, subject, msgenc, keyID, chatstate, + composing_xep, forward_from, delayed, session, form_node, user_nick, + callback) + return + # Encryption failed, do not send message + tim = localtime() + self.dispatch('MSGNOTSENT', (jid, error, msgtxt, tim, session)) + + def _on_continue_message(self, type_, msg, msgtxt, original_message, fjid, + resource, jid, xhtml, subject, msgenc, keyID, chatstate, composing_xep, + forward_from, delayed, session, form_node, user_nick, callback): + if type_ == 'chat': + msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ=type_, + xhtml=xhtml) + else: + if subject: + msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal', + subject=subject, xhtml=xhtml) + else: + msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal', + xhtml=xhtml) + if msgenc: + msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc) + + if form_node: + msg_iq.addChild(node=form_node) + + # XEP-0172: user_nickname + if user_nick: + msg_iq.setTag('nick', namespace = common.xmpp.NS_NICK).setData( + user_nick) + + # TODO: We might want to write a function so we don't need to + # reproduce that ugly if somewhere else. + if resource: + contact = gajim.contacts.get_contact(self.name, jid, resource) + else: + contact = gajim.contacts.get_contact_with_highest_priority(self.name, + jid) + + # chatstates - if peer supports xep85 or xep22, send chatstates + # please note that the only valid tag inside a message containing a + # tag is the active event + if chatstate is not None and contact: + if ((composing_xep == 'XEP-0085' or not composing_xep) \ + and composing_xep != 'asked_once') or \ + contact.supports(common.xmpp.NS_CHATSTATES): + # XEP-0085 + msg_iq.setTag(chatstate, namespace=common.xmpp.NS_CHATSTATES) + if composing_xep in ('XEP-0022', 'asked_once') or \ + not composing_xep: + # XEP-0022 + chatstate_node = msg_iq.setTag('x', namespace=common.xmpp.NS_EVENT) + if chatstate is 'composing' or msgtxt: + chatstate_node.addChild(name='composing') + + if forward_from: + addresses = msg_iq.addChild('addresses', + namespace=common.xmpp.NS_ADDRESS) + addresses.addChild('address', attrs = {'type': 'ofrom', + 'jid': forward_from}) + + # XEP-0203 + if delayed: + our_jid = gajim.get_jid_from_account(self.name) + '/' + \ + self.server_resource + timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(delayed)) + msg_iq.addChild('delay', namespace=common.xmpp.NS_DELAY2, + attrs={'from': our_jid, 'stamp': timestamp}) + + # XEP-0184 + if msgtxt and gajim.config.get_per('accounts', self.name, + 'request_receipt') and contact and contact.supports( + common.xmpp.NS_RECEIPTS): + msg_iq.setTag('request', namespace=common.xmpp.NS_RECEIPTS) + + if session: + # XEP-0201 + session.last_send = time.time() + msg_iq.setThread(session.thread_id) + + # XEP-0200 + if session.enable_encryption: + msg_iq = session.encrypt_stanza(msg_iq) + + if callback: + callback(jid, msg, keyID, forward_from, session, original_message, + subject, type_, msg_iq) + + def log_message(self, jid, msg, forward_from, session, original_message, + subject, type_): + if not forward_from and session and session.is_loggable(): + ji = gajim.get_jid_without_resource(jid) + if gajim.config.should_log(self.name, ji): + log_msg = msg + if original_message is not None: + log_msg = original_message + if subject: + log_msg = _('Subject: %(subject)s\n%(message)s') % \ + {'subject': subject, 'message': log_msg} + if log_msg: + if type_ == 'chat': + kind = 'chat_msg_sent' + else: + kind = 'single_msg_sent' + try: + gajim.logger.write(kind, jid, log_msg) + except exceptions.PysqliteOperationalError, e: + self.dispatch('ERROR', (_('Disk Write Error'), str(e))) + except exceptions.DatabaseMalformed: + pritext = _('Database Error') + sectext = _('The database file (%s) cannot be read. Try to ' + 'repair it (see http://trac.gajim.org/wiki/DatabaseBackup)' + ' or remove it (all history will be lost).') % \ + common.logger.LOG_DB_PATH + + def ack_subscribed(self, jid): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def ack_unsubscribed(self, jid): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def request_subscription(self, jid, msg='', name='', groups=[], + auto_auth=False): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def send_authorization(self, jid): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def refuse_authorization(self, jid): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def unsubscribe(self, jid, remove_auth = True): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def unsubscribe_agent(self, agent): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def update_contact(self, jid, name, groups): + if self.connection: + self.connection.getRoster().setItem(jid=jid, name=name, groups=groups) + + def update_contacts(self, contacts): + '''update multiple roster items''' + if self.connection: + self.connection.getRoster().setItemMulti(contacts) + + def new_account(self, name, config, sync=False): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def _on_new_account(self, con=None, con_type=None): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def account_changed(self, new_name): + self.name = new_name + + def request_last_status_time(self, jid, resource): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def request_os_info(self, jid, resource): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def get_settings(self): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def get_bookmarks(self): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def store_bookmarks(self): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def get_metacontacts(self): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def send_agent_status(self, agent, ptype): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def gpg_passphrase(self, passphrase): + if self.gpg: + use_gpg_agent = gajim.config.get('use_gpg_agent') + if use_gpg_agent: + self.gpg.passphrase = None + else: + self.gpg.passphrase = passphrase + + def ask_gpg_keys(self): + if self.gpg: + keys = self.gpg.get_keys() + return keys + return None + + def ask_gpg_secrete_keys(self): + if self.gpg: + keys = self.gpg.get_secret_keys() + return keys + return None + + def load_roster_from_db(self): + # Do nothing by default + return + + def _event_dispatcher(self, realm, event, data): + if realm == '': + if event == common.xmpp.transports_nb.DATA_RECEIVED: + self.dispatch('STANZA_ARRIVED', unicode(data, errors='ignore')) + elif event == common.xmpp.transports_nb.DATA_SENT: + self.dispatch('STANZA_SENT', unicode(data)) + + def change_status(self, show, msg, auto=False): + if not show in ['offline', 'online', 'chat', 'away', 'xa', 'dnd']: + return -1 + if not msg: + msg = '' + sign_msg = False + if not auto and not show == 'offline': + sign_msg = True + if show != 'invisible': + # We save it only when privacy list is accepted + self.status = msg + if show != 'offline' and self.connected < 1: + # set old_show to requested 'show' in case we need to + # recconect before we auth to server + self.old_show = show + self.on_purpose = False + self.server_resource = self._compute_resource() + if gajim.HAVE_GPG: + self.USE_GPG = True + self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent')) + self.connect_and_init(show, msg, sign_msg) + + elif show == 'offline': + self.connected = 0 + if self.connection: + p = common.xmpp.Presence(typ = 'unavailable') + p = self.add_sha(p, False) + if msg: + p.setStatus(msg) + + self.connection.RegisterDisconnectHandler(self._on_disconnected) + self.connection.send(p, now=True) + self.connection.start_disconnect() + else: + self._on_disconnected() + + elif show != 'offline' and self.connected > 0: + # dont'try to connect, when we are in state 'connecting' + if self.connected == 1: + return + if show == 'invisible': + self._change_to_invisible(msg) + return + was_invisible = self.connected == gajim.SHOW_LIST.index('invisible') + self.connected = gajim.SHOW_LIST.index(show) + if was_invisible: + self._change_from_invisible() + self._update_status(show, msg) + +class Connection(CommonConnection, ConnectionHandlers): + '''Connection class''' + def __init__(self, name): + CommonConnection.__init__(self, name) + ConnectionHandlers.__init__(self) # this property is used to prevent double connections self.last_connection = None # last ClientCommon instance # If we succeed to connect, remember it so next time we try (after a @@ -117,36 +593,39 @@ class Connection(ConnectionHandlers): self.lang = None if locale.getdefaultlocale()[0]: self.lang = locale.getdefaultlocale()[0].split('_')[0] - self.is_zeroconf = False - self.gpg = None - self.USE_GPG = False - if gajim.HAVE_GPG: - self.USE_GPG = True - self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent')) - self.status = '' - self.priority = gajim.get_priority(name, 'offline') - self.old_show = '' # increase/decrease default timeout for server responses self.try_connecting_for_foo_secs = 45 # holds the actual hostname to which we are connected self.connected_hostname = None - self.time_to_reconnect = None self.last_time_to_reconnect = None self.new_account_info = None self.new_account_form = None - self.bookmarks = [] self.annotations = {} - self.on_purpose = False self.last_io = gajim.idlequeue.current_time() self.last_sent = [] self.last_history_time = {} self.password = passwords.get_password(name) - self.server_resource = gajim.config.get_per('accounts', name, 'resource') - # All valid resource substitution strings should be added to this hash. - if self.server_resource: - self.server_resource = Template(self.server_resource).safe_substitute({ - 'hostname': socket.gethostname() - }) + + # Used to ask privacy only once at connection + self.music_track_info = 0 + self.pubsub_supported = False + self.pubsub_publish_options_supported = False + # Do we auto accept insecure connection + self.connection_auto_accepted = False + self.pasword_callback = None + + self.on_connect_success = None + self.on_connect_failure = None + self.retrycount = 0 + self.jids_for_auto_auth = [] # list of jid to auto-authorize + self.available_transports = {} # list of available transports on this + # server {'icq': ['icq.server.com', 'icq2.server.com'], } + self.private_storage_supported = True + self.streamError = '' + self.secret_hmac = str(random.random())[2:] + # END __init__ + + def get_config_values_or_default(self): if gajim.config.get_per('accounts', self.name, 'keep_alives_enabled'): self.keepalives = gajim.config.get_per('accounts', self.name, 'keep_alive_every_foo_secs') @@ -157,47 +636,9 @@ class Connection(ConnectionHandlers): 'ping_alive_every_foo_secs') else: self.pingalives = 0 - self.privacy_rules_supported = False - # Used to ask privacy only once at connection - self.privacy_rules_requested = False - self.blocked_list = [] - self.blocked_contacts = [] - self.blocked_groups = [] - self.blocked_all = False - self.music_track_info = 0 - self.pubsub_supported = False - self.pubsub_publish_options_supported = False - self.pep_supported = False - self.mood = {} - self.tune = {} - self.activity = {} - # Do we continue connection when we get roster (send presence,get vcard..) - self.continue_connect_info = None - # Do we auto accept insecure connection - self.connection_auto_accepted = False - # To know the groupchat jid associated with a sranza ID. Useful to - # request vcard or os info... to a real JID but act as if it comes from - # the fake jid - self.groupchat_jids = {} # {ID : groupchat_jid} - self.pasword_callback = None - - self.on_connect_success = None - self.on_connect_failure = None - self.retrycount = 0 - self.jids_for_auto_auth = [] # list of jid to auto-authorize - self.muc_jid = {} # jid of muc server for each transport type - self.available_transports = {} # list of available transports on this - # server {'icq': ['icq.server.com', 'icq2.server.com'], } - self.vcard_supported = False - self.private_storage_supported = True - self.streamError = '' - self.secret_hmac = str(random.random())[2:] - # END __init__ - - def dispatch(self, event, data): - '''always passes account name as first param''' - gajim.interface.dispatch(event, self.name, data) + def check_jid(self, jid): + return helpers.parse_jid(jid) def _reconnect(self): # Do not try to reco while we are already trying @@ -224,6 +665,8 @@ class Connection(ConnectionHandlers): if self.connection: # make sure previous connection is completely closed gajim.proxy65_manager.disconnect(self.connection) + self.terminate_sessions() + self.remove_all_transfers() self.connection.disconnect() self.last_connection = None self.connection = None @@ -279,6 +722,7 @@ class Connection(ConnectionHandlers): _('Reconnect manually.'))) def _event_dispatcher(self, realm, event, data): + CommonConnection._event_dispatcher(self, realm, event, data) if realm == common.xmpp.NS_REGISTER: if event == common.xmpp.features_nb.REGISTER_DATA_RECEIVED: # data is (agent, DataFrom, is_form, error_msg) @@ -384,11 +828,6 @@ class Connection(ConnectionHandlers): elif event == common.xmpp.features_nb.PRIVACY_LISTS_ACTIVE_DEFAULT: # data is (dict) self.dispatch('PRIVACY_LISTS_ACTIVE_DEFAULT', (data)) - elif realm == '': - if event == common.xmpp.transports_nb.DATA_RECEIVED: - self.dispatch('STANZA_ARRIVED', unicode(data, errors = 'ignore')) - elif event == common.xmpp.transports_nb.DATA_SENT: - self.dispatch('STANZA_SENT', unicode(data)) def _select_next_host(self, hosts): '''Selects the next host according to RFC2782 p.3 based on it's @@ -802,10 +1241,6 @@ class Connection(ConnectionHandlers): self.on_connect_auth = None # END connect - def quit(self, kill_core): - if kill_core and gajim.account_is_connected(self.name): - self.disconnect(on_purpose=True) - def add_lang(self, stanza): if self.lang: stanza.setAttr('xml:lang', self.lang) @@ -985,45 +1420,11 @@ class Connection(ConnectionHandlers): #Inform GUI we just signed in self.dispatch('SIGNED_IN', ()) - def test_gpg_passphrase(self, password): - '''Returns 'ok', 'bad_pass' or 'expired' ''' - if not self.gpg: - return False - self.gpg.passphrase = password - keyID = gajim.config.get_per('accounts', self.name, 'keyid') - signed = self.gpg.sign('test', keyID) - self.gpg.password = None - if signed == 'KEYEXPIRED': - return 'expired' - elif signed == 'BAD_PASSPHRASE': - return 'bad_pass' - return 'ok' - def get_signed_presence(self, msg, callback = None): if gajim.config.get_per('accounts', self.name, 'gpg_sign_presence'): return self.get_signed_msg(msg, callback) return '' - def get_signed_msg(self, msg, callback = None): - '''returns the signed message if possible - or an empty string if gpg is not used - or None if waiting for passphrase. - callback is the function to call when user give the passphrase''' - signed = '' - keyID = gajim.config.get_per('accounts', self.name, 'keyid') - if keyID and self.USE_GPG: - use_gpg_agent = gajim.config.get('use_gpg_agent') - if self.gpg.passphrase is None and not use_gpg_agent: - # We didn't set a passphrase - return None - if self.gpg.passphrase is not None or use_gpg_agent: - signed = self.gpg.sign(msg, keyID) - if signed == 'BAD_PASSPHRASE': - self.USE_GPG = False - signed = '' - self.dispatch('BAD_PASSPHRASE', ()) - return signed - def connect_and_auth(self): self.on_connect_success = self._connect_success self.on_connect_failure = self._connect_failure @@ -1080,92 +1481,30 @@ class Connection(ConnectionHandlers): p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed) self.connection.send(p) - def change_status(self, show, msg, auto = False): - if not show in gajim.SHOW_LIST: - return -1 - sshow = helpers.get_xmpp_show(show) - if not msg: - msg = '' - sign_msg = False - if not auto and not show == 'offline': - sign_msg = True - if show != 'invisible': - # We save it only when privacy list is accepted - self.status = msg - if show != 'offline' and self.connected < 1: - # set old_show to requested 'show' in case we need to - # recconect before we auth to server - self.old_show = show - self.on_purpose = False - self.server_resource = gajim.config.get_per('accounts', self.name, - 'resource') - # All valid resource substitution strings should be added to this hash. - if self.server_resource: - self.server_resource = Template(self.server_resource).\ - safe_substitute({ - 'hostname': socket.gethostname() - }) - if gajim.HAVE_GPG: - self.USE_GPG = True - self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent')) - self.connect_and_init(show, msg, sign_msg) + def _change_to_invisible(self, msg): + signed = self.get_signed_presence(msg) + self.send_invisible_presence(msg, signed) - elif show == 'offline': - self.connected = 0 - if self.connection: - self.terminate_sessions() - - self.on_purpose = True - p = common.xmpp.Presence(typ = 'unavailable') - p = self.add_sha(p, False) - if msg: - p.setStatus(msg) - self.remove_all_transfers() - self.time_to_reconnect = None - - self.connection.RegisterDisconnectHandler(self._on_disconnected) - self.connection.send(p, now=True) - self.connection.start_disconnect() - #self.connection.start_disconnect(p, self._on_disconnected) - else: - self.time_to_reconnect = None - self._on_disconnected() - - elif show != 'offline' and self.connected > 0: - # dont'try to connect, when we are in state 'connecting' - if self.connected == 1: - return - if show == 'invisible': - signed = self.get_signed_presence(msg) - self.send_invisible_presence(msg, signed) - return - was_invisible = self.connected == gajim.SHOW_LIST.index('invisible') - self.connected = gajim.SHOW_LIST.index(show) - if was_invisible and self.privacy_rules_supported: - iq = self.build_privacy_rule('visible', 'allow') - self.connection.send(iq) - self.activate_privacy_rule('visible') - priority = unicode(gajim.get_priority(self.name, sshow)) - p = common.xmpp.Presence(typ = None, priority = priority, show = sshow) - p = self.add_sha(p) - if msg: - p.setStatus(msg) - signed = self.get_signed_presence(msg) - if signed: - p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed) - if self.connection: - self.connection.send(p) - self.priority = priority - self.dispatch('STATUS', show) - - def _on_disconnected(self): - ''' called when a disconnect request has completed successfully''' - self.disconnect(on_purpose=True) - self.dispatch('STATUS', 'offline') - - def get_status(self): - return gajim.SHOW_LIST[self.connected] + def _change_from_invisible(self): + if self.privacy_rules_supported: + iq = self.build_privacy_rule('visible', 'allow') + self.connection.send(iq) + self.activate_privacy_rule('visible') + def _update_status(self, show, msg): + xmpp_show = helpers.get_xmpp_show(show) + priority = unicode(gajim.get_priority(self.name, xmpp_show)) + p = common.xmpp.Presence(typ=None, priority=priority, show=xmpp_show) + p = self.add_sha(p) + if msg: + p.setStatus(msg) + signed = self.get_signed_presence(msg) + if signed: + p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed) + if self.connection: + self.connection.send(p) + self.priority = priority + self.dispatch('STATUS', show) def send_motd(self, jid, subject = '', msg = '', xhtml = None): if not self.connection: @@ -1179,202 +1518,23 @@ class Connection(ConnectionHandlers): chatstate=None, msg_id=None, composing_xep=None, resource=None, user_nick=None, xhtml=None, session=None, forward_from=None, form_node=None, original_message=None, delayed=None, callback=None, callback_args=[]): - if not self.connection or self.connected < 2: - return 1 - try: + + def cb(jid, msg, keyID, forward_from, session, original_message, subject, + type_, msg_iq): + msg_id = self.connection.send(msg_iq) jid = helpers.parse_jid(jid) - except helpers.InvalidFormat: - self.dispatch('ERROR', (_('Invalid Jabber ID'), - _('It is not possible to send a message to %s, this JID is not ' - 'valid.') % jid)) - return + self.dispatch('MSGSENT', (jid, msg, keyID)) + if callback: + callback(msg_id, *callback_args) - if msg and not xhtml and gajim.config.get( - 'rst_formatting_outgoing_messages'): - from common.rst_xhtml_generator import create_xhtml - xhtml = create_xhtml(msg) - if not msg and chatstate is None and form_node is None: - return - fjid = jid - if resource: - fjid += '/' + resource - msgtxt = msg - msgenc = '' + self.log_message(jid, msg, forward_from, session, original_message, + subject, type_) - if session: - fjid = session.get_to() - - if keyID and self.USE_GPG: - xhtml = None - if keyID == 'UNKNOWN': - error = _('Neither the remote presence is signed, nor a key was assigned.') - elif keyID.endswith('MISMATCH'): - error = _('The contact\'s key (%s) does not match the key assigned in Gajim.' % keyID[:8]) - else: - def encrypt_thread(msg, keyID, always_trust=False): - # encrypt message. This function returns (msgenc, error) - return self.gpg.encrypt(msg, [keyID], always_trust) - def _on_encrypted(output): - msgenc, error = output - if error == 'NOT_TRUSTED': - def _on_always_trust(answer): - if answer: - gajim.thread_interface(encrypt_thread, [msg, keyID, - True], _on_encrypted, []) - else: - self._on_message_encrypted(output, type_, msg, msgtxt, - original_message, fjid, resource, jid, xhtml, - subject, chatstate, composing_xep, forward_from, - delayed, session, form_node, user_nick, keyID, - callback, callback_args) - self.dispatch('GPG_ALWAYS_TRUST', _on_always_trust) - else: - self._on_message_encrypted(output, type_, msg, msgtxt, - original_message, fjid, resource, jid, xhtml, subject, - chatstate, composing_xep, forward_from, delayed, session, - form_node, user_nick, keyID, callback, callback_args) - gajim.thread_interface(encrypt_thread, [msg, keyID, False], - _on_encrypted, []) - return - - self._on_message_encrypted(('', error), type_, msg, msgtxt, - original_message, fjid, resource, jid, xhtml, subject, chatstate, - composing_xep, forward_from, delayed, session, form_node, user_nick, - keyID, callback, callback_args) - - self._on_continue_message(type_, msg, msgtxt, original_message, fjid, - resource, jid, xhtml, subject, msgenc, keyID, chatstate, composing_xep, - forward_from, delayed, session, form_node, user_nick, callback, - callback_args) - - def _on_message_encrypted(self, output, type_, msg, msgtxt, original_message, - fjid, resource, jid, xhtml, subject, chatstate, composing_xep, forward_from, - delayed, session, form_node, user_nick, keyID, callback, callback_args): - msgenc, error = output - - if msgenc and not error: - msgtxt = '[This message is *encrypted* (See :XEP:`27`]' - lang = os.getenv('LANG') - if lang is not None and lang != 'en': # we're not english - # one in locale and one en - msgtxt = _('[This message is *encrypted* (See :XEP:`27`]') + \ - ' (' + msgtxt + ')' - self._on_continue_message(type_, msg, msgtxt, original_message, fjid, - resource, jid, xhtml, subject, msgenc, keyID, chatstate, - composing_xep, forward_from, delayed, session, form_node, user_nick, - callback, callback_args) - return - # Encryption failed, do not send message - tim = localtime() - self.dispatch('MSGNOTSENT', (jid, error, msgtxt, tim, session)) - - def _on_continue_message(self, type_, msg, msgtxt, original_message, fjid, - resource, jid, xhtml, subject, msgenc, keyID, chatstate, composing_xep, - forward_from, delayed, session, form_node, user_nick, callback, - callback_args): - if type_ == 'chat': - msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ=type_, - xhtml=xhtml) - else: - if subject: - msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal', - subject=subject, xhtml=xhtml) - else: - msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal', - xhtml=xhtml) - if msgenc: - msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc) - - if form_node: - msg_iq.addChild(node=form_node) - - # XEP-0172: user_nickname - if user_nick: - msg_iq.setTag('nick', namespace = common.xmpp.NS_NICK).setData( - user_nick) - - # TODO: We might want to write a function so we don't need to - # reproduce that ugly if somewhere else. - if resource: - contact = gajim.contacts.get_contact(self.name, jid, resource) - else: - contact = gajim.contacts.get_contact_with_highest_priority(self.name, - jid) - - # chatstates - if peer supports xep85 or xep22, send chatstates - # please note that the only valid tag inside a message containing a - # tag is the active event - if chatstate is not None and contact: - if ((composing_xep == 'XEP-0085' or not composing_xep) \ - and composing_xep != 'asked_once') or \ - contact.supports(common.xmpp.NS_CHATSTATES): - # XEP-0085 - msg_iq.setTag(chatstate, namespace=common.xmpp.NS_CHATSTATES) - if composing_xep in ('XEP-0022', 'asked_once') or \ - not composing_xep: - # XEP-0022 - chatstate_node = msg_iq.setTag('x', namespace=common.xmpp.NS_EVENT) - if chatstate is 'composing' or msgtxt: - chatstate_node.addChild(name='composing') - - if forward_from: - addresses = msg_iq.addChild('addresses', - namespace=common.xmpp.NS_ADDRESS) - addresses.addChild('address', attrs = {'type': 'ofrom', - 'jid': forward_from}) - - # XEP-0203 - if delayed: - our_jid = gajim.get_jid_from_account(self.name) + '/' + \ - self.server_resource - timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(delayed)) - msg_iq.addChild('delay', namespace=common.xmpp.NS_DELAY2, - attrs={'from': our_jid, 'stamp': timestamp}) - - # XEP-0184 - if msgtxt and gajim.config.get_per('accounts', self.name, - 'request_receipt') and contact and contact.supports( - common.xmpp.NS_RECEIPTS): - msg_iq.setTag('request', namespace=common.xmpp.NS_RECEIPTS) - - if session: - # XEP-0201 - session.last_send = time.time() - msg_iq.setThread(session.thread_id) - - # XEP-0200 - if session.enable_encryption: - msg_iq = session.encrypt_stanza(msg_iq) - - msg_id = self.connection.send(msg_iq) - if not forward_from and session and session.is_loggable(): - ji = gajim.get_jid_without_resource(jid) - if gajim.config.should_log(self.name, ji): - log_msg = msg - if original_message is not None: - log_msg = original_message - if subject: - log_msg = _('Subject: %(subject)s\n%(message)s') % \ - {'subject': subject, 'message': msg} - if log_msg: - if type_ == 'chat': - kind = 'chat_msg_sent' - else: - kind = 'single_msg_sent' - try: - gajim.logger.write(kind, jid, log_msg) - except exceptions.PysqliteOperationalError, e: - self.dispatch('ERROR', (_('Disk Write Error'), str(e))) - except exceptions.DatabaseMalformed: - pritext = _('Database Error') - sectext = _('The database file (%s) cannot be read. Try to ' - 'repair it (see http://trac.gajim.org/wiki/DatabaseBackup)' - ' or remove it (all history will be lost).') % \ - common.logger.LOG_DB_PATH - self.dispatch('MSGSENT', (jid, msg, keyID)) - - if callback: - callback(msg_id, *callback_args) + self._prepare_message(jid, msg, keyID, type_=type_, subject=subject, + chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep, + resource=resource, user_nick=user_nick, xhtml=xhtml, session=session, + forward_from=forward_from, form_node=form_node, + original_message=original_message, delayed=delayed, callback=cb) def send_contacts(self, contacts, jid): '''Send contacts with RosterX (Xep-0144)''' @@ -1479,17 +1639,6 @@ class Connection(ConnectionHandlers): self.connection.send(iq) self.connection.getRoster().delItem(agent) - def update_contact(self, jid, name, groups): - '''update roster item on jabber server''' - if self.connection: - self.connection.getRoster().setItem(jid = jid, name = name, - groups = groups) - - def update_contacts(self, contacts): - '''update multiple roster items on jabber server''' - if self.connection: - self.connection.getRoster().setItemMulti(contacts) - def send_new_account_infos(self, form, is_form): if is_form: # Get username and password and put them in new_account_info @@ -1507,7 +1656,7 @@ class Connection(ConnectionHandlers): self.new_account_form = form self.new_account(self.name, self.new_account_info) - def new_account(self, name, config, sync = False): + def new_account(self, name, config, sync=False): # If a connection already exist we cannot create a new account if self.connection: return @@ -1518,7 +1667,7 @@ class Connection(ConnectionHandlers): self.on_connect_failure = self._on_new_account self.connect(config) - def _on_new_account(self, con = None, con_type = None): + def _on_new_account(self, con=None, con_type=None): if not con_type: if len(self._connection_types) or len(self._hosts): # There are still other way to try to connect @@ -1530,9 +1679,6 @@ class Connection(ConnectionHandlers): self.connection = con common.xmpp.features_nb.getRegInfo(con, self._hostname) - def account_changed(self, new_name): - self.name = new_name - def request_last_status_time(self, jid, resource, groupchat_jid=None): '''groupchat_jid is used when we want to send a request to a real jid and act as if the answer comes from the groupchat_jid''' @@ -1923,26 +2069,6 @@ class Connection(ConnectionHandlers): query.addChild(node = form) self.connection.send(iq) - def gpg_passphrase(self, passphrase): - if self.gpg: - use_gpg_agent = gajim.config.get('use_gpg_agent') - if use_gpg_agent: - self.gpg.passphrase = None - else: - self.gpg.passphrase = passphrase - - def ask_gpg_keys(self): - if self.gpg: - keys = self.gpg.get_keys() - return keys - return None - - def ask_gpg_secrete_keys(self): - if self.gpg: - keys = self.gpg.get_secret_keys() - return keys - return None - def change_password(self, password): if not self.connection: return diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 856653eb2..d4a89eca0 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -46,11 +46,10 @@ import common.xmpp from common import helpers from common import gajim -from common import atom -from common import pep from common import exceptions from common.commands import ConnectionCommands from common.pubsub import ConnectionPubSub +from common.pep import ConnectionPEP from common.caps import ConnectionCaps from common.message_archiving import ConnectionArchive from common.message_archiving import ARCHIVING_COLLECTIONS_ARRIVED @@ -323,15 +322,20 @@ class ConnectionBytestream: field.setValue(common.xmpp.NS_BYTESTREAM) self.connection.send(iq) + def _ft_get_our_jid(self): + our_jid = gajim.get_jid_from_account(self.name) + resource = self.server_resource + return our_jid + '/' + resource + + def _ft_get_receiver_jid(self, file_props): + return file_props['receiver'].jid + '/' + file_props['receiver'].resource + def send_file_request(self, file_props): ''' send iq for new FT request ''' if not self.connection or self.connected < 2: return - our_jid = gajim.get_jid_from_account(self.name) - resource = self.server_resource - frm = our_jid + '/' + resource - file_props['sender'] = frm - fjid = file_props['receiver'].jid + '/' + file_props['receiver'].resource + file_props['sender'] = self._ft_get_our_jid() + fjid = self._ft_get_receiver_jid(file_props) iq = common.xmpp.Protocol(name = 'iq', to = fjid, typ = 'set') iq.setID(file_props['sid']) @@ -424,6 +428,9 @@ class ConnectionBytestream: self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, '')) raise common.xmpp.NodeProcessed + def _ft_get_from(self, iq_obj): + return helpers.get_full_jid_from_iq(iq_obj) + def _bytestreamSetCB(self, con, iq_obj): log.debug('_bytestreamSetCB') target = unicode(iq_obj.getAttr('to')) @@ -440,7 +447,7 @@ class ConnectionBytestream: 'target': target, 'id': id_, 'sid': sid, - 'initiator': helpers.get_full_jid_from_iq(iq_obj) + 'initiator': self._ft_get_from(iq_obj) } for attr in item.getAttrs(): host_dict[attr] = item.getAttr(attr) @@ -478,7 +485,7 @@ class ConnectionBytestream: return if not real_id.startswith('au_'): return - frm = helpers.get_full_jid_from_iq(iq_obj) + frm = self._ft_get_from(iq_obj) id_ = real_id[3:] if id_ in self.files_props: file_props = self.files_props[id_] @@ -488,9 +495,12 @@ class ConnectionBytestream: gajim.socks5queue.activate_proxy(host['idx']) raise common.xmpp.NodeProcessed + def _ft_get_streamhost_jid_attr(self, streamhost): + return helpers.parse_jid(streamhost.getAttr('jid')) + def _bytestreamResultCB(self, con, iq_obj): log.debug('_bytestreamResultCB') - frm = helpers.get_full_jid_from_iq(iq_obj) + frm = self._ft_get_from(iq_obj) real_id = unicode(iq_obj.getAttr('id')) query = iq_obj.getTag('query') gajim.proxy65_manager.resolve_result(frm, query) @@ -518,7 +528,7 @@ class ConnectionBytestream: gajim.socks5queue.activate_proxy(host['idx']) break raise common.xmpp.NodeProcessed - jid = helpers.parse_jid(streamhost.getAttr('jid')) + jid = self._ft_get_streamhost_jid_attr(streamhost) if 'streamhost-used' in file_props and \ file_props['streamhost-used'] is True: raise common.xmpp.NodeProcessed @@ -575,7 +585,7 @@ class ConnectionBytestream: if 'request-id' in file_props: # we have already sent streamhosts info return - file_props['receiver'] = helpers.get_full_jid_from_iq(iq_obj) + file_props['receiver'] = self._ft_get_from(iq_obj) si = iq_obj.getTag('si') file_tag = si.getTag('file') range_tag = None @@ -601,9 +611,9 @@ class ConnectionBytestream: def _siSetCB(self, con, iq_obj): log.debug('_siSetCB') - jid = helpers.get_jid_from_iq(iq_obj) + jid = self._ft_get_from(iq_obj) file_props = {'type': 'r'} - file_props['sender'] = helpers.get_full_jid_from_iq(iq_obj) + file_props['sender'] = jid file_props['request-id'] = unicode(iq_obj.getAttr('id')) si = iq_obj.getTag('si') profile = si.getAttr('profile') @@ -641,7 +651,7 @@ class ConnectionBytestream: file_props['mime-type'] = mime_type our_jid = gajim.get_jid_from_account(self.name) resource = self.server_resource - file_props['receiver'] = our_jid + '/' + resource + file_props['receiver'] = self._ft_get_our_jid() file_props['sid'] = unicode(si.getAttr('id')) file_props['transfered_size'] = [] gajim.socks5queue.add_file_props(self.name, file_props) @@ -662,7 +672,7 @@ class ConnectionBytestream: if file_props is None: # file properties for jid is none return - jid = helpers.get_jid_from_iq(iq_obj) + jid = self._ft_get_from(iq_obj) file_props['error'] = -3 self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, '')) raise common.xmpp.NodeProcessed @@ -1533,13 +1543,15 @@ sent a message to.''' return sess -class ConnectionHandlers(ConnectionArchive, ConnectionVcard, ConnectionBytestream, ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionCaps, ConnectionHandlersBase, ConnectionJingle): +class ConnectionHandlers(ConnectionArchive, ConnectionVcard, ConnectionBytestream, ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionPEP, ConnectionCaps, ConnectionHandlersBase, ConnectionJingle): def __init__(self): ConnectionArchive.__init__(self) ConnectionVcard.__init__(self) ConnectionBytestream.__init__(self) ConnectionCommands.__init__(self) ConnectionPubSub.__init__(self) + ConnectionPEP.__init__(self, account=self.name, dispatcher=self, + pubsub_connection=self) ConnectionJingle.__init__(self) ConnectionHandlersBase.__init__(self) self.gmail_url = None @@ -1817,7 +1829,7 @@ class ConnectionHandlers(ConnectionArchive, ConnectionVcard, ConnectionBytestrea # wrong answer return tzo = qp.getTag('tzo').getData() - if tzo == 'Z': + if tzo.lower() == 'z': tzo = '0:0' tzoh, tzom = tzo.split(':') utc_time = qp.getTag('utc').getData() @@ -1942,9 +1954,23 @@ class ConnectionHandlers(ConnectionArchive, ConnectionVcard, ConnectionBytestrea log.warn('Invalid JID: %s, ignoring it' % item.getAttr('jid')) continue name = item.getAttr('name') - groups=[] + contact = gajim.contacts.get_contact(self.name, jid) + groups = [] + same_groups = True for group in item.getTags('group'): groups.append(group.getData()) + # check that all suggested groups are in the groups we have for this + # contact + if not contact or group not in contact.groups: + same_groups = False + if contact: + # check that all groups we have for this contact are in the + # suggested groups + for group in contact.groups: + if group not in groups: + same_groups = False + if contact.sub in ('both', 'to') and same_groups: + continue exchange_items_list[jid] = [] exchange_items_list[jid].append(name) exchange_items_list[jid].append(groups) @@ -1955,15 +1981,7 @@ class ConnectionHandlers(ConnectionArchive, ConnectionVcard, ConnectionBytestrea def _messageCB(self, con, msg): '''Called when we receive a message''' log.debug('MessageCB') - mtype = msg.getType() - # check if the message is pubsub#event - if msg.getTag('event') is not None: - if mtype == 'groupchat': - return - if msg.getTag('error') is None: - self._pubsubEventCB(con, msg) - return # check if the message is a roster item exchange (XEP-0144) if msg.getTag('x', namespace=common.xmpp.NS_ROSTERX): @@ -2229,42 +2247,6 @@ class ConnectionHandlers(ConnectionArchive, ConnectionVcard, ConnectionBytestrea self.dispatch('GC_INVITATION',(frm, jid_from, reason, password, is_continued)) - def _pubsubEventCB(self, con, msg): - ''' Called when we receive with pubsub event. ''' - # TODO: Logging? (actually services where logging would be useful, should - # TODO: allow to access archives remotely...) - jid = helpers.get_full_jid_from_iq(msg) - event = msg.getTag('event') - - # XEP-0107: User Mood - items = event.getTag('items', {'node': common.xmpp.NS_MOOD}) - if items: pep.user_mood(items, self.name, jid) - # XEP-0118: User Tune - items = event.getTag('items', {'node': common.xmpp.NS_TUNE}) - if items: pep.user_tune(items, self.name, jid) - # XEP-0080: User Geolocation - items = event.getTag('items', {'node': common.xmpp.NS_GEOLOC}) - if items: pep.user_geoloc(items, self.name, jid) - # XEP-0108: User Activity - items = event.getTag('items', {'node': common.xmpp.NS_ACTIVITY}) - if items: pep.user_activity(items, self.name, jid) - # XEP-0172: User Nickname - items = event.getTag('items', {'node': common.xmpp.NS_NICK}) - if items: pep.user_nickname(items, self.name, jid) - - items = event.getTag('items') - if items is None: return - - for item in items.getTags('item'): - entry = item.getTag('entry') - if entry is not None: - # for each entry in feed (there shouldn't be more than one, - # but to be sure... - self.dispatch('ATOM_ENTRY', (atom.OldEntry(node=entry),)) - continue - # unknown type... probably user has another client who understands that event - raise common.xmpp.NodeProcessed - def _presenceCB(self, con, prs): '''Called when we receive a presence''' ptype = prs.getType() @@ -2832,6 +2814,11 @@ class ConnectionHandlers(ConnectionArchive, ConnectionVcard, ConnectionBytestrea con.RegisterHandler('message', self._messageCB) con.RegisterHandler('presence', self._presenceCB) con.RegisterHandler('presence', self._capsPresenceCB) + # We use makefirst so that this handler is called before _messageCB, and + # can prevent calling it when it's not needed. + # We also don't check for namespace, else it cannot stop _messageCB to be + # called + con.RegisterHandler('message', self._pubsubEventCB, makefirst=True) con.RegisterHandler('iq', self._vCardCB, 'result', common.xmpp.NS_VCARD) con.RegisterHandler('iq', self._rosterSetCB, 'set', diff --git a/src/common/contacts.py b/src/common/contacts.py index 86175567f..5a9df3499 100644 --- a/src/common/contacts.py +++ b/src/common/contacts.py @@ -94,7 +94,7 @@ class Contact(CommonContact): def __init__(self, jid, account, name='', groups=[], show='', status='', sub='', ask='', resource='', priority=0, keyID='', client_caps=None, our_chatstate=None, chatstate=None, last_status_time=None, msg_id = None, - composing_xep=None, mood={}, tune={}, activity={}): + composing_xep=None): CommonContact.__init__(self, jid, account, resource, show, status, name, our_chatstate, composing_xep, chatstate, client_caps=client_caps) @@ -110,9 +110,7 @@ class Contact(CommonContact): self.msg_id = msg_id self.last_status_time = last_status_time - self.mood = mood.copy() - self.tune = tune.copy() - self.activity = activity.copy() + self.pep = {} def get_full_jid(self): if self.resource: @@ -229,23 +227,25 @@ class Contacts: def create_contact(self, jid, account, name='', groups=[], show='', status='', sub='', ask='', resource='', priority=0, keyID='', client_caps=None, our_chatstate=None, chatstate=None, last_status_time=None, - composing_xep=None, mood={}, tune={}, activity={}): + composing_xep=None): account = self._accounts.get(account, account) # Use Account object if available return Contact(jid=jid, account=account, name=name, groups=groups, show=show, status=status, sub=sub, ask=ask, resource=resource, priority=priority, keyID=keyID, client_caps=client_caps, our_chatstate=our_chatstate, chatstate=chatstate, last_status_time=last_status_time, - composing_xep=composing_xep, mood=mood, tune=tune, activity=activity) + composing_xep=composing_xep) - def create_self_contact(self, jid, account, resource, show, status, priority, keyID=''): + def create_self_contact(self, jid, account, resource, show, status, priority, + name='', keyID=''): conn = common.gajim.connections[account] - nick = common.gajim.nicks[account] + nick = name or common.gajim.nicks[account] account = self._accounts.get(account, account) # Use Account object if available - return self.create_contact(jid=jid, account=account, + self_contact = self.create_contact(jid=jid, account=account, name=nick, groups=['self_contact'], show=show, status=status, sub='both', ask='none', priority=priority, keyID=keyID, - resource=resource, mood=conn.mood, tune=conn.tune, - activity=conn.activity) + resource=resource) + self_contact.pep = conn.pep + return self_contact def create_not_in_roster_contact(self, jid, account, resource='', name='', keyID=''): account = self._accounts.get(account, account) # Use Account object if available diff --git a/src/common/helpers.py b/src/common/helpers.py index 70bdfc9fe..341ebb15f 100644 --- a/src/common/helpers.py +++ b/src/common/helpers.py @@ -569,8 +569,6 @@ def datetime_tuple(timestamp): # import gajim only when needed (after decode_string is defined) see #4764 import gajim -import pep - def convert_bytes(string): suffix = '' @@ -777,53 +775,6 @@ def get_global_status(): status = gajim.connections[account].status return status -def get_pep_dict(account): - pep_dict = {} - con = gajim.connections[account] - # activity - if 'activity' in con.activity and con.activity['activity'] in pep.ACTIVITIES: - activity = con.activity['activity'] - if 'subactivity' in con.activity and con.activity['subactivity'] in \ - pep.ACTIVITIES[activity]: - subactivity = con.activity['subactivity'] - else: - subactivity = 'other' - else: - activity = '' - subactivity = '' - if 'text' in con.activity: - text = con.activity['text'] - else: - text = '' - pep_dict['activity'] = activity - pep_dict['subactivity'] = subactivity - pep_dict['activity_text'] = text - - # mood - if 'mood' in con.mood and con.mood['mood'] in pep.MOODS: - mood = con.mood['mood'] - else: - mood = '' - if 'text' in con.mood: - text = con.mood['text'] - else: - text = '' - pep_dict['mood'] = mood - pep_dict['mood_text'] = text - return pep_dict - -def get_global_pep(): - maxi = 0 - pep_dict = {'activity': '', 'mood': ''} - for account in gajim.connections: - if not gajim.config.get_per('accounts', account, - 'sync_with_global_status'): - continue - connected = gajim.connections[account].connected - if connected > maxi: - maxi = connected - pep_dict = get_pep_dict(account) - return pep_dict def statuses_unified(): '''testing if all statuses are the same.''' diff --git a/src/common/jingle.py b/src/common/jingle.py index f05886533..8a369ff7e 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -13,17 +13,6 @@ ''' Handles the jingle signalling protocol. ''' #TODO: -# * things in XEP 0166, including: -# - 'senders' attribute of 'content' element -# - security preconditions -# * actions: -# - content-modify -# - description-info, session-info -# - security-info -# - transport-accept, transport-reject -# * sid/content related: -# - tiebreaking -# - if there already is a session, use it # * things in XEP 0176, including: # - http://xmpp.org/extensions/xep-0176.html#protocol-restarts # - http://xmpp.org/extensions/xep-0176.html#fallback @@ -36,1034 +25,15 @@ # - codecs # - STUN -# * DONE: figure out why it doesn't work with pidgin: -# That's a bug in pidgin: http://xmpp.org/extensions/xep-0176.html#protocol-checks +# * figure out why it doesn't work with pidgin: +# That's maybe a bug in pidgin: +# http://xmpp.org/extensions/xep-0176.html#protocol-checks -# * timeout - -# * split this file in several modules -# For example, a file dedicated for XEP0166, one for XEP0176, -# and one for XEP0167 - -# * handle different kinds of sink and src elements - -import gajim import xmpp import helpers -import farsight, gst - -def get_first_gst_element(elements): - ''' Returns, if it exists, the first available element of the list. ''' - for name in elements: - factory = gst.element_factory_find(name) - if factory: - return factory.create() - -#FIXME: Move it to JingleSession.States? -class JingleStates(object): - ''' States in which jingle session may exist. ''' - ended = 0 - pending = 1 - active = 2 - -#FIXME: Move it to JingleTransport.Type? -class TransportType(object): - ''' Possible types of a JingleTransport ''' - datagram = 1 - streaming = 2 - -class OutOfOrder(Exception): - ''' Exception that should be raised when an action is received when in the wrong state. ''' - -class TieBreak(Exception): - ''' Exception that should be raised in case of a tie, when we overrule the other action. ''' - -class JingleSession(object): - ''' This represents one jingle session. ''' - def __init__(self, con, weinitiate, jid, sid=None): - ''' con -- connection object, - weinitiate -- boolean, are we the initiator? - jid - jid of the other entity''' - self.contents = {} # negotiated contents - self.connection = con # connection to use - # our full jid - self.ourjid = gajim.get_jid_from_account(self.connection.name) + '/' + \ - con.server_resource - self.peerjid = jid # jid we connect to - # jid we use as the initiator - self.initiator = weinitiate and self.ourjid or self.peerjid - # jid we use as the responder - self.responder = weinitiate and self.peerjid or self.ourjid - # are we an initiator? - self.weinitiate = weinitiate - # what state is session in? (one from JingleStates) - self.state = JingleStates.ended - if not sid: - sid = con.connection.getAnID() - self.sid = sid # sessionid - - self.accepted = True # is this session accepted by user - - # callbacks to call on proper contents - # use .prepend() to add new callbacks, especially when you're going - # to send error instead of ack - self.callbacks = { - 'content-accept': [self.__contentAcceptCB, self.__broadcastCB, - self.__defaultCB], - 'content-add': [self.__contentAddCB, self.__broadcastCB, - self.__defaultCB], #TODO - 'content-modify': [self.__defaultCB], #TODO - 'content-reject': [self.__defaultCB, self.__contentRemoveCB], #TODO - 'content-remove': [self.__defaultCB, self.__contentRemoveCB], - 'description-info': [self.__broadcastCB, self.__defaultCB], #TODO - 'security-info': [self.__defaultCB], #TODO - 'session-accept': [self.__sessionAcceptCB, self.__contentAcceptCB, - self.__broadcastCB, self.__defaultCB], - 'session-info': [self.__sessionInfoCB, self.__broadcastCB, self.__defaultCB], - 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, - self.__defaultCB], - 'session-terminate': [self.__sessionTerminateCB, self.__broadcastAllCB, - self.__defaultCB], - 'transport-info': [self.__broadcastCB, self.__defaultCB], - 'transport-replace': [self.__broadcastCB, self.__transportReplaceCB], #TODO - 'transport-accept': [self.__defaultCB], #TODO - 'transport-reject': [self.__defaultCB], #TODO - 'iq-result': [], - 'iq-error': [self.__errorCB], - } - - ''' Interaction with user ''' - def approve_session(self): - ''' Called when user accepts session in UI (when we aren't the initiator). - ''' - self.accept_session() - - def decline_session(self): - ''' Called when user declines session in UI (when we aren't the initiator) - ''' - reason = xmpp.Node('reason') - reason.addChild('decline') - self._session_terminate(reason) - - def approve_content(self, media): - content = self.get_content(media) - if content: - content.accepted = True - self.on_session_state_changed(content) - - def reject_content(self, media): - content = self.get_content(media) - if content: - if self.state == JingleStates.active: - self.__content_reject(content) - content.destroy() - self.on_session_state_changed() - - def end_session(self): - ''' Called when user stops or cancel session in UI. ''' - reason = xmpp.Node('reason') - if self.state == JingleStates.active: - reason.addChild('success') - else: - reason.addChild('cancel') - self._session_terminate(reason) - - ''' Middle-level functions to manage contents. Handle local content - cache and send change notifications. ''' - def get_content(self, media=None): - if media == 'audio': - cls = JingleVoIP - elif media == 'video': - cls = JingleVideo - #elif media == None: - # cls = JingleContent - else: - return None - - for content in self.contents.values(): - if isinstance(content, cls): - return content - - def add_content(self, name, content, creator='we'): - ''' Add new content to session. If the session is active, - this will send proper stanza to update session. - Creator must be one of ('we', 'peer', 'initiator', 'responder')''' - assert creator in ('we', 'peer', 'initiator', 'responder') - - if (creator == 'we' and self.weinitiate) or (creator == 'peer' and \ - not self.weinitiate): - creator = 'initiator' - elif (creator == 'peer' and self.weinitiate) or (creator == 'we' and \ - not self.weinitiate): - creator = 'responder' - content.creator = creator - content.name = name - self.contents[(creator, name)] = content - - if (creator == 'initiator') == self.weinitiate: - # The content is from us, accept it - content.accepted = True - - def remove_content(self, creator, name): - ''' We do not need this now ''' - #TODO: - if (creator, name) in self.contents: - content = self.contents[(creator, name)] - if len(self.contents) > 1: - self.__content_remove(content) - self.contents[(creator, name)].destroy() - if len(self.contents) == 0: - self.end_session() - - def modify_content(self, creator, name, *someother): - ''' We do not need this now ''' - pass - - def on_session_state_changed(self, content=None): - if self.state == JingleStates.ended: - # Session not yet started, only one action possible: session-initiate - if self.is_ready() and self.weinitiate: - self.__session_initiate() - elif self.state == JingleStates.pending: - # We can either send a session-accept or a content-add - if self.is_ready() and not self.weinitiate: - self.__session_accept() - elif content and (content.creator == 'initiator') == self.weinitiate: - self.__content_add(content) - elif content and self.weinitiate: - self.__content_accept(content) - elif self.state == JingleStates.active: - # We can either send a content-add or a content-accept - if not content: - return - if (content.creator == 'initiator') == self.weinitiate: - # We initiated this content. It's a pending content-add. - self.__content_add(content) - else: - # The other side created this content, we accept it. - self.__content_accept(content) - - def is_ready(self): - ''' Returns True when all codecs and candidates are ready - (for all contents). ''' - return (all((content.is_ready() for content in self.contents.itervalues())) - and self.accepted) - - ''' Middle-level function to do stanza exchange. ''' - def accept_session(self): - ''' Mark the session as accepted. ''' - self.accepted = True - self.on_session_state_changed() - - def start_session(self): - ''' Mark the session as ready to be started. ''' - self.accepted = True - self.on_session_state_changed() - - def send_session_info(self): - pass - - def send_content_accept(self, content): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-accept') - jingle.addChild(node=content) - self.connection.connection.send(stanza) - - def send_transport_info(self, content): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('transport-info') - jingle.addChild(node=content) - self.connection.connection.send(stanza) - - ''' Session callbacks. ''' - def stanzaCB(self, stanza): - ''' A callback for ConnectionJingle. It gets stanza, then - tries to send it to all internally registered callbacks. - First one to raise xmpp.NodeProcessed breaks function.''' - jingle = stanza.getTag('jingle') - error = stanza.getTag('error') - if error: - # it's an iq-error stanza - action = 'iq-error' - elif jingle: - # it's a jingle action - action = jingle.getAttr('action') - if action not in self.callbacks: - self.__send_error(stanza, 'bad_request') - return - #FIXME: If we aren't initiated and it's not a session-initiate... - if action != 'session-initiate' and self.state == JingleStates.ended: - self.__send_error(stanza, 'item-not-found', 'unknown-session') - return - else: - # it's an iq-result (ack) stanza - action = 'iq-result' - - callables = self.callbacks[action] - - try: - for callable in callables: - callable(stanza=stanza, jingle=jingle, error=error, action=action) - except xmpp.NodeProcessed: - pass - except TieBreak: - self.__send_error(stanza, 'conflict', 'tiebreak') - except OutOfOrder: - self.__send_error(stanza, 'unexpected-request', 'out-of-order')#FIXME - - def __defaultCB(self, stanza, jingle, error, action): - ''' Default callback for action stanzas -- simple ack - and stop processing. ''' - response = stanza.buildReply('result') - self.connection.connection.send(response) - - def __errorCB(self, stanza, jingle, error, action): - #FIXME - text = error.getTagData('text') - jingle_error = None - xmpp_error = None - for child in error.getChildren(): - if child.getNamespace() == xmpp.NS_JINGLE_ERRORS: - jingle_error = child.getName() - elif child.getNamespace() == xmpp.NS_STANZAS: - xmpp_error = child.getName() - self.__dispatch_error(xmpp_error, jingle_error, text) - #FIXME: Not sure when we would want to do that... - if xmpp_error == 'item-not-found': - self.connection.delete_jingle_session(self.peerjid, self.sid) - - def __transportReplaceCB(self, stanza, jingle, error, action): - for content in jingle.iterTags('content'): - creator = content['creator'] - name = content['name'] - if (creator, name) in self.contents: - transport_ns = content.getTag('transport').getNamespace() - if transport_ns == xmpp.JINGLE_ICE_UDP: - #FIXME: We don't manage anything else than ICE-UDP now... - #What was the previous transport?!? - #Anyway, content's transport is not modifiable yet - pass - else: - stanza, jingle = self.__make_jingle('transport-reject') - content = jingle.setTag('content', attrs={'creator': creator, - 'name': name}) - content.setTag('transport', namespace=transport_ns) - self.connection.connection.send(stanza) - raise xmpp.NodeProcessed - else: - #FIXME: This ressource is unknown to us, what should we do? - #For now, reject the transport - stanza, jingle = self.__make_jingle('transport-reject') - c = jingle.setTag('content', attrs={'creator': creator, - 'name': name}) - c.setTag('transport', namespace=transport_ns) - self.connection.connection.send(stanza) - raise xmpp.NodeProcessed - - def __sessionInfoCB(self, stanza, jingle, error, action): - #TODO: ringing, active, (un)hold, (un)mute - payload = jingle.getPayload() - if len(payload) > 0: - self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info') - raise xmpp.NodeProcessed - - def __contentRemoveCB(self, stanza, jingle, error, action): - for content in jingle.iterTags('content'): - creator = content['creator'] - name = content['name'] - if (creator, name) in self.contents: - content = self.contents[(creator, name)] - #TODO: this will fail if content is not an RTP content - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, content.media, 'removed')) - content.destroy() - if len(self.contents) == 0: - reason = xmpp.Node('reason') - reason.setTag('success') - self._session_terminate(reason) - - def __sessionAcceptCB(self, stanza, jingle, error, action): - if self.state != JingleStates.pending: #FIXME - raise OutOfOrder - self.state = JingleStates.active - - def __contentAcceptCB(self, stanza, jingle, error, action): - ''' Called when we get content-accept stanza or equivalent one - (like session-accept).''' - # check which contents are accepted - for content in jingle.iterTags('content'): - creator = content['creator'] - name = content['name']#TODO... - - def __contentAddCB(self, stanza, jingle, error, action): - if self.state == JingleStates.ended: - raise OutOfOrder - - parse_result = self.__parse_contents(jingle) - contents = parse_result[2] - rejected_contents = parse_result[3] - - for name, creator in rejected_contents: - #TODO: - content = JingleContent() - self.add_content(name, content, creator) - self.__content_reject(content) - self.contents[(content.creator, content.name)].destroy() - - self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, - contents)) - - def __sessionInitiateCB(self, stanza, jingle, error, action): - ''' We got a jingle session request from other entity, - therefore we are the receiver... Unpack the data, - inform the user. ''' - - if self.state != JingleStates.ended: - raise OutOfOrder - - self.initiator = jingle['initiator'] - self.responder = self.ourjid - self.peerjid = self.initiator - self.accepted = False # user did not accept this session yet - - # TODO: If the initiator is unknown to the receiver (e.g., via presence - # subscription) and the receiver has a policy of not communicating via - # Jingle with unknown entities, it SHOULD return a - # error. - - # Lets check what kind of jingle session does the peer want - contents_ok, transports_ok, contents, pouet = self.__parse_contents(jingle) - - # If there's no content we understand... - if not contents_ok: - # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate - reason = xmpp.Node('reason') - reason.setTag('unsupported-applications') - self.__defaultCB(stanza, jingle, error, action) - self._session_terminate(reason) - raise xmpp.NodeProcessed - - if not transports_ok: - # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate - reason = xmpp.Node('reason') - reason.setTag('unsupported-transports') - self.__defaultCB(stanza, jingle, error, action) - self._session_terminate(reason) - raise xmpp.NodeProcessed - - self.state = JingleStates.pending - - # Send event about starting a session - self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, - contents)) - - def __broadcastCB(self, stanza, jingle, error, action): - ''' Broadcast the stanza contents to proper content handlers. ''' - for content in jingle.iterTags('content'): - name = content['name'] - creator = content['creator'] - cn = self.contents[(creator, name)] - cn.stanzaCB(stanza, content, error, action) - - def __sessionTerminateCB(self, stanza, jingle, error, action): - self.connection.delete_jingle_session(self.peerjid, self.sid) - reason, text = self.__reason_from_stanza(jingle) - if reason not in ('success', 'cancel', 'decline'): - self.__dispatch_error(reason, reason, text) - if text: - text = '%s (%s)' % (reason, text) - else: - text = reason#TODO - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, None, text)) - - def __broadcastAllCB(self, stanza, jingle, error, action): - ''' Broadcast the stanza to all content handlers. ''' - for content in self.contents.itervalues(): - content.stanzaCB(stanza, None, error, action) - - ''' Internal methods. ''' - def __parse_contents(self, jingle): - #TODO: Needs some reworking - contents = [] - contents_rejected = [] - contents_ok = False - transports_ok = False - - for element in jingle.iterTags('content'): - desc = element.getTag('description') - desc_ns = desc.getNamespace() - tran_ns = element.getTag('transport').getNamespace() - if desc_ns == xmpp.NS_JINGLE_RTP and desc['media'] in ('audio', 'video'): - contents_ok = True - #TODO: Everything here should be moved somewhere else - if tran_ns == xmpp.NS_JINGLE_ICE_UDP: - if desc['media'] == 'audio': - self.add_content(element['name'], JingleVoIP(self), 'peer') - else: - self.add_content(element['name'], JingleVideo(self), 'peer') - contents.append((desc['media'],)) - transports_ok = True - else: - contents_rejected.append((element['name'], 'peer')) - else: - contents_rejected.append((element['name'], 'peer')) - - return (contents_ok, transports_ok, contents, contents_rejected) - - def __dispatch_error(self, error, jingle_error=None, text=None): - if jingle_error: - error = jingle_error - if text: - text = '%s (%s)' % (error, text) - else: - text = error - self.connection.dispatch('JINGLE_ERROR', (self.peerjid, self.sid, text)) - - def __reason_from_stanza(self, stanza): - reason = 'success' - reasons = ['success', 'busy', 'cancel', 'connectivity-error', - 'decline', 'expired', 'failed-application', 'failed-transport', - 'general-error', 'gone', 'incompatible-parameters', 'media-error', - 'security-error', 'timeout', 'unsupported-applications', - 'unsupported-transports'] - tag = stanza.getTag('reason') - if tag: - text = tag.getTagData('text') - for r in reasons: - if tag.getTag(r): - reason = r - break - return (reason, text) - - ''' Methods that make/send proper pieces of XML. They check if the session - is in appropriate state. ''' - def __make_jingle(self, action): - stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid)) - attrs = {'action': action, - 'sid': self.sid} - if action == 'session-initiate': - attrs['initiator'] = self.initiator - elif action == 'session-accept': - attrs['responder'] = self.responder - jingle = stanza.addChild('jingle', attrs=attrs, namespace=xmpp.NS_JINGLE) - return stanza, jingle - - def __send_error(self, stanza, error, jingle_error=None, text=None): - err = xmpp.Error(stanza, error) - err.setNamespace(xmpp.NS_STANZAS) - if jingle_error: - err.setTag(jingle_error, namespace=xmpp.NS_JINGLE_ERRORS) - if text: - err.setTagData('text', text) - self.connection.connection.send(err) - self.__dispatch_error(error, jingle_error, text) - - def __append_content(self, jingle, content): - ''' Append element to element, - with (full=True) or without (full=False) - children. ''' - jingle.addChild('content', - attrs={'name': content.name, 'creator': content.creator}) - - def __append_contents(self, jingle): - ''' Append all elements to .''' - # TODO: integrate with __appendContent? - # TODO: parameters 'name', 'content'? - for content in self.contents.values(): - self.__append_content(jingle, content) - - def __session_initiate(self): - assert self.state == JingleStates.ended - stanza, jingle = self.__make_jingle('session-initiate') - self.__append_contents(jingle) - self.__broadcastCB(stanza, jingle, None, 'session-initiate-sent') - self.connection.connection.send(stanza) - self.state = JingleStates.pending - - def __session_accept(self): - assert self.state == JingleStates.pending - stanza, jingle = self.__make_jingle('session-accept') - self.__append_contents(jingle) - self.__broadcastCB(stanza, jingle, None, 'session-accept-sent') - self.connection.connection.send(stanza) - self.state = JingleStates.active - - def __session_info(self, payload=None): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('session-info') - if payload: - jingle.addChild(node=payload) - self.connection.connection.send(stanza) - - def _session_terminate(self, reason=None): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('session-terminate') - if reason is not None: - jingle.addChild(node=reason) - self.__broadcastAllCB(stanza, jingle, None, 'session-terminate-sent') - self.connection.connection.send(stanza) - reason, text = self.__reason_from_stanza(jingle) - if reason not in ('success', 'cancel', 'decline'): - self.__dispatch_error(reason, reason, text) - if text: - text = '%s (%s)' % (reason, text) - else: - text = reason - self.connection.delete_jingle_session(self.peerjid, self.sid) - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, None, text)) - - def __content_add(self, content): - #TODO: test - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-add') - self.__append_content(jingle, content) - self.__broadcastCB(stanza, jingle, None, 'content-add-sent') - self.connection.connection.send(stanza) - - def __content_accept(self, content): - #TODO: test - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-accept') - self.__append_content(jingle, content) - self.__broadcastCB(stanza, jingle, None, 'content-accept-sent') - self.connection.connection.send(stanza) - - def __content_reject(self, content): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-reject') - self.__append_content(jingle, content) - self.connection.connection.send(stanza) - #TODO: this will fail if content is not an RTP content - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, content.media, 'rejected')) - - def __content_modify(self): - assert self.state != JingleStates.ended - - def __content_remove(self, content): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-remove') - self.__append_content(jingle, content) - self.connection.connection.send(stanza) - #TODO: this will fail if content is not an RTP content - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, content.media, 'removed')) - - def content_negociated(self, media): - self.connection.dispatch('JINGLE_CONNECTED', (self.peerjid, self.sid, - media)) - -#TODO: -#class JingleTransport(object): -# ''' An abstraction of a transport in Jingle sessions. ''' -# def __init__(self): -# pass - - -class JingleContent(object): - ''' An abstraction of content in Jingle sessions. ''' - def __init__(self, session, node=None): - self.session = session - # will be filled by JingleSession.add_content() - # don't uncomment these lines, we will catch more buggy code then - # (a JingleContent not added to session shouldn't send anything) - #self.creator = None - #self.name = None - self.accepted = False - self.sent = False - self.candidates = [] # Local transport candidates - self.remote_candidates = [] # Remote transport candidates - - self.senders = 'both' #FIXME - self.allow_sending = True # Used for stream direction, attribute 'senders' - - self.callbacks = { - # these are called when *we* get stanzas - 'content-accept': [self.__transportInfoCB], - 'content-add': [self.__transportInfoCB], - 'content-modify': [], - 'content-reject': [], - 'content-remove': [], - 'description-info': [], - 'security-info': [], - 'session-accept': [self.__transportInfoCB], - 'session-info': [], - 'session-initiate': [self.__transportInfoCB], - 'session-terminate': [], - 'transport-info': [self.__transportInfoCB], - 'transport-replace': [], - 'transport-accept': [], - 'transport-reject': [], - 'iq-result': [], - 'iq-error': [], - # these are called when *we* sent these stanzas - 'content-accept-sent': [self.__fillJingleStanza], - 'content-add-sent': [self.__fillJingleStanza], - 'session-initiate-sent': [self.__fillJingleStanza], - 'session-accept-sent': [self.__fillJingleStanza], - 'session-terminate-sent': [], - } - - def is_ready(self): - #print '[%s] %s, %s' % (self.media, self.candidates_ready, - # self.p2psession.get_property('codecs-ready')) - return (self.accepted and self.candidates_ready and not self.sent - and self.p2psession.get_property('codecs-ready')) - - def stanzaCB(self, stanza, content, error, action): - ''' Called when something related to our content was sent by peer. ''' - if action in self.callbacks: - for callback in self.callbacks[action]: - callback(stanza, content, error, action) - - def __transportInfoCB(self, stanza, content, error, action): - ''' Got a new transport candidate. ''' - candidates = [] - transport = content.getTag('transport') - for candidate in transport.iterTags('candidate'): - cand = farsight.Candidate() - cand.component_id = int(candidate['component']) - cand.ip = str(candidate['ip']) - cand.port = int(candidate['port']) - cand.foundation = str(candidate['foundation']) - #cand.type = farsight.CANDIDATE_TYPE_LOCAL - cand.priority = int(candidate['priority']) - - if candidate['protocol'] == 'udp': - cand.proto = farsight.NETWORK_PROTOCOL_UDP - else: - # we actually don't handle properly different tcp options in jingle - cand.proto = farsight.NETWORK_PROTOCOL_TCP - - cand.username = str(transport['ufrag']) - cand.password = str(transport['pwd']) - - #FIXME: huh? - types = {'host': farsight.CANDIDATE_TYPE_HOST, - 'srflx': farsight.CANDIDATE_TYPE_SRFLX, - 'prflx': farsight.CANDIDATE_TYPE_PRFLX, - 'relay': farsight.CANDIDATE_TYPE_RELAY, - 'multicast': farsight.CANDIDATE_TYPE_MULTICAST} - if 'type' in candidate and candidate['type'] in types: - cand.type = types[candidate['type']] - else: - print 'Unknown type %s', candidate['type'] - candidates.append(cand) - #FIXME: connectivity should not be etablished yet - # Instead, it should be etablished after session-accept! - if len(candidates) > 0: - if self.sent: - self.p2pstream.set_remote_candidates(candidates) - else: - self.remote_candidates.extend(candidates) - #self.p2pstream.set_remote_candidates(candidates) - #print self.media, self.creator, self.name, candidates - - def __content(self, payload=[]): - ''' Build a XML content-wrapper for our data. ''' - return xmpp.Node('content', - attrs={'name': self.name, 'creator': self.creator}, - payload=payload) - - def __candidate(self, candidate): - types = {farsight.CANDIDATE_TYPE_HOST: 'host', - farsight.CANDIDATE_TYPE_SRFLX: 'srflx', - farsight.CANDIDATE_TYPE_PRFLX: 'prflx', - farsight.CANDIDATE_TYPE_RELAY: 'relay', - farsight.CANDIDATE_TYPE_MULTICAST: 'multicast'} - attrs = { - 'component': candidate.component_id, - 'foundation': '1', # hack - 'generation': '0', - 'ip': candidate.ip, - 'network': '0', - 'port': candidate.port, - 'priority': int(candidate.priority), # hack - } - if candidate.type in types: - attrs['type'] = types[candidate.type] - if candidate.proto == farsight.NETWORK_PROTOCOL_UDP: - attrs['protocol'] = 'udp' - else: - # we actually don't handle properly different tcp options in jingle - attrs['protocol'] = 'tcp' - return xmpp.Node('candidate', attrs=attrs) - - def iter_candidates(self): - for candidate in self.candidates: - yield self.__candidate(candidate) - - def send_candidate(self, candidate): - content = self.__content() - transport = content.addChild(xmpp.NS_JINGLE_ICE_UDP + ' transport') - - if candidate.username and candidate.password: - transport['ufrag'] = candidate.username - transport['pwd'] = candidate.password - - transport.addChild(node=self.__candidate(candidate)) - self.session.send_transport_info(content) - - def __fillJingleStanza(self, stanza, content, error, action): - ''' Add our things to session-initiate stanza. ''' - self._fillContent(content) - - self.sent = True - - if self.candidates and self.candidates[0].username and \ - self.candidates[0].password: - attrs = {'ufrag': self.candidates[0].username, - 'pwd': self.candidates[0].password} - else: - attrs = {} - content.addChild(xmpp.NS_JINGLE_ICE_UDP + ' transport', attrs=attrs, - payload=self.iter_candidates()) - - def destroy(self): - self.callbacks = None - del self.session.contents[(self.creator, self.name)] - - -class JingleRTPContent(JingleContent): - def __init__(self, session, media, node=None): - JingleContent.__init__(self, session, node) - self.media = media - self.farsight_media = {'audio': farsight.MEDIA_TYPE_AUDIO, - 'video': farsight.MEDIA_TYPE_VIDEO}[media] - self.got_codecs = False - - self.candidates_ready = False # True when local candidates are prepared - - self.callbacks['session-initiate'] += [self.__getRemoteCodecsCB] - self.callbacks['content-add'] += [self.__getRemoteCodecsCB] - self.callbacks['content-accept'] += [self.__getRemoteCodecsCB, - self.__contentAcceptCB] - self.callbacks['session-accept'] += [self.__getRemoteCodecsCB, - self.__contentAcceptCB] - self.callbacks['session-accept-sent'] += [self.__contentAcceptCB] - self.callbacks['content-accept-sent'] += [self.__contentAcceptCB] - self.callbacks['session-terminate'] += [self.__stop] - self.callbacks['session-terminate-sent'] += [self.__stop] - - def setup_stream(self): - # pipeline and bus - self.pipeline = gst.Pipeline() - bus = self.pipeline.get_bus() - bus.add_signal_watch() - bus.connect('message', self._on_gst_message) - - # conference - self.conference = gst.element_factory_make('fsrtpconference') - self.conference.set_property("sdes-cname", self.session.ourjid) - self.pipeline.add(self.conference) - self.funnel = None - - self.p2psession = self.conference.new_session(self.farsight_media) - - participant = self.conference.new_participant(self.session.peerjid) - #FIXME: Consider a workaround, here... - # pidgin and telepathy-gabble don't follow the XEP, and it won't work - # due to bad controlling-mode - params = {'controlling-mode': self.session.weinitiate,# 'debug': False} - 'stun-ip': '69.0.208.27', 'debug': False} - - self.p2pstream = self.p2psession.new_stream(participant, - farsight.DIRECTION_RECV, 'nice', params) - - def _fillContent(self, content): - content.addChild(xmpp.NS_JINGLE_RTP + ' description', - attrs={'media': self.media}, payload=self.iter_codecs()) - - def _setup_funnel(self): - self.funnel = gst.element_factory_make('fsfunnel') - self.pipeline.add(self.funnel) - self.funnel.set_state(gst.STATE_PLAYING) - self.sink.set_state(gst.STATE_PLAYING) - self.funnel.link(self.sink) - - def _on_src_pad_added(self, stream, pad, codec): - if not self.funnel: - self._setup_funnel() - pad.link(self.funnel.get_pad('sink%d')) - - def _on_gst_message(self, bus, message): - if message.type == gst.MESSAGE_ELEMENT: - name = message.structure.get_name() - if name == 'farsight-new-active-candidate-pair': - pass - elif name == 'farsight-recv-codecs-changed': - pass - elif name == 'farsight-codecs-changed': - if self.is_ready(): - self.session.on_session_state_changed(self) - #TODO: description-info - elif name == 'farsight-local-candidates-prepared': - self.candidates_ready = True - if self.is_ready(): - self.session.on_session_state_changed(self) - elif name == 'farsight-new-local-candidate': - candidate = message.structure['candidate'] - self.candidates.append(candidate) - if self.candidates_ready: - #FIXME: Is this case even possible? - self.send_candidate(candidate) - elif name == 'farsight-component-state-changed': - state = message.structure['state'] - print message.structure['component'], state - if state == farsight.STREAM_STATE_FAILED: - reason = xmpp.Node('reason') - reason.setTag('failed-transport') - self.session._session_terminate(reason) - elif name == 'farsight-error': - print 'Farsight error #%d!' % message.structure['error-no'] - print 'Message: %s' % message.structure['error-msg'] - print 'Debug: %s' % message.structure['debug-msg'] - else: - print name - - def __contentAcceptCB(self, stanza, content, error, action): - if self.accepted: - if len(self.remote_candidates) > 0: - self.p2pstream.set_remote_candidates(self.remote_candidates) - self.remote_candidates = [] - #TODO: farsight.DIRECTION_BOTH only if senders='both' - self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) - self.session.content_negociated(self.media) - - def __getRemoteCodecsCB(self, stanza, content, error, action): - ''' Get peer codecs from what we get from peer. ''' - if self.got_codecs: - return - - codecs = [] - for codec in content.getTag('description').iterTags('payload-type'): - c = farsight.Codec(int(codec['id']), codec['name'], - self.farsight_media, int(codec['clockrate'])) - if 'channels' in codec: - c.channels = int(codec['channels']) - else: - c.channels = 1 - c.optional_params = [(str(p['name']), str(p['value'])) for p in \ - codec.iterTags('parameter')] - codecs.append(c) - - if len(codecs) > 0: - #FIXME: Handle this case: - # glib.GError: There was no intersection between the remote codecs and - # the local ones - self.p2pstream.set_remote_codecs(codecs) - self.got_codecs = True - - def iter_codecs(self): - codecs = self.p2psession.get_property('codecs') - for codec in codecs: - attrs = {'name': codec.encoding_name, - 'id': codec.id, - 'channels': codec.channels} - if codec.clock_rate: - attrs['clockrate'] = codec.clock_rate - if codec.optional_params: - payload = (xmpp.Node('parameter', {'name': name, 'value': value}) - for name, value in codec.optional_params) - else: payload = () - yield xmpp.Node('payload-type', attrs, payload) - - def __stop(self, *things): - self.pipeline.set_state(gst.STATE_NULL) - - def __del__(self): - self.__stop() - - def destroy(self): - JingleContent.destroy(self) - self.p2pstream.disconnect_by_func(self._on_src_pad_added) - self.pipeline.get_bus().disconnect_by_func(self._on_gst_message) - - -class JingleVoIP(JingleRTPContent): - ''' Jingle VoIP sessions consist of audio content transported - over an ICE UDP protocol. ''' - def __init__(self, session, node=None): - JingleRTPContent.__init__(self, session, 'audio', node) - self.setup_stream() - - - ''' Things to control the gstreamer's pipeline ''' - def setup_stream(self): - JingleRTPContent.setup_stream(self) - - # Configure SPEEX - # Workaround for psi (not needed since rev - # 147aedcea39b43402fe64c533d1866a25449888a): - # place 16kHz before 8kHz, as buggy psi versions will take in - # account only the first codec - - codecs = [farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', - farsight.MEDIA_TYPE_AUDIO, 16000), - farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', - farsight.MEDIA_TYPE_AUDIO, 8000)] - self.p2psession.set_codec_preferences(codecs) - - # the local parts - # TODO: use gconfaudiosink? - # sink = get_first_gst_element(['alsasink', 'osssink', 'autoaudiosink']) - self.sink = gst.element_factory_make('alsasink') - self.sink.set_property('sync', False) - #sink.set_property('latency-time', 20000) - #sink.set_property('buffer-time', 80000) - - # TODO: use gconfaudiosrc? - src_mic = gst.element_factory_make('alsasrc') - src_mic.set_property('blocksize', 320) - - self.mic_volume = gst.element_factory_make('volume') - self.mic_volume.set_property('volume', 1) - - # link gst elements - self.pipeline.add(self.sink, src_mic, self.mic_volume) - src_mic.link(self.mic_volume) - - self.mic_volume.get_pad('src').link(self.p2psession.get_property( - 'sink-pad')) - self.p2pstream.connect('src-pad-added', self._on_src_pad_added) - - # The following is needed for farsight to process ICE requests: - self.pipeline.set_state(gst.STATE_PLAYING) - - -class JingleVideo(JingleRTPContent): - def __init__(self, session, node=None): - JingleRTPContent.__init__(self, session, 'video', node) - self.setup_stream() - - ''' Things to control the gstreamer's pipeline ''' - def setup_stream(self): - #TODO: Everything is not working properly: - # sometimes, one window won't show up, - # sometimes it'll freeze... - JingleRTPContent.setup_stream(self) - # the local parts - src_vid = gst.element_factory_make('videotestsrc') - src_vid.set_property('is-live', True) - videoscale = gst.element_factory_make('videoscale') - caps = gst.element_factory_make('capsfilter') - caps.set_property('caps', gst.caps_from_string('video/x-raw-yuv, width=320, height=240')) - colorspace = gst.element_factory_make('ffmpegcolorspace') - - self.pipeline.add(src_vid, videoscale, caps, colorspace) - gst.element_link_many(src_vid, videoscale, caps, colorspace) - - self.sink = gst.element_factory_make('xvimagesink') - self.pipeline.add(self.sink) - - colorspace.get_pad('src').link(self.p2psession.get_property('sink-pad')) - self.p2pstream.connect('src-pad-added', self._on_src_pad_added) - - # The following is needed for farsight to process ICE requests: - self.pipeline.set_state(gst.STATE_PLAYING) +from jingle_session import JingleSession +from jingle_rtp import JingleAudio, JingleVideo class ConnectionJingle(object): @@ -1128,11 +98,11 @@ class ConnectionJingle(object): return self.get_jingle_session(jid, media='audio').sid jingle = self.get_jingle_session(jid, media='video') if jingle: - jingle.add_content('voice', JingleVoIP(jingle)) + jingle.add_content('voice', JingleAudio(jingle)) else: jingle = JingleSession(self, weinitiate=True, jid=jid) self.add_jingle(jingle) - jingle.add_content('voice', JingleVoIP(jingle)) + jingle.add_content('voice', JingleAudio(jingle)) jingle.start_session() return jingle.sid diff --git a/src/common/jingle_content.py b/src/common/jingle_content.py new file mode 100644 index 000000000..2feb5d8de --- /dev/null +++ b/src/common/jingle_content.py @@ -0,0 +1,109 @@ +## +## Copyright (C) 2006 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +''' Handles Jingle contents (XEP 0166). ''' + +contents = {} + +def get_jingle_content(node): + namespace = node.getNamespace() + if namespace in contents: + return contents[namespace](node) + else: + return None + + +class JingleContent(object): + ''' An abstraction of content in Jingle sessions. ''' + def __init__(self, session, transport): + self.session = session + self.transport = transport + # will be filled by JingleSession.add_content() + # don't uncomment these lines, we will catch more buggy code then + # (a JingleContent not added to session shouldn't send anything) + #self.creator = None + #self.name = None + self.accepted = False + self.sent = False + + self.media = None + + self.senders = 'both' #FIXME + self.allow_sending = True # Used for stream direction, attribute 'senders' + + self.callbacks = { + # these are called when *we* get stanzas + 'content-accept': [self.__transportInfoCB], + 'content-add': [self.__transportInfoCB], + 'content-modify': [], + 'content-reject': [], + 'content-remove': [], + 'description-info': [], + 'security-info': [], + 'session-accept': [self.__transportInfoCB], + 'session-info': [], + 'session-initiate': [self.__transportInfoCB], + 'session-terminate': [], + 'transport-info': [self.__transportInfoCB], + 'transport-replace': [], + 'transport-accept': [], + 'transport-reject': [], + 'iq-result': [], + 'iq-error': [], + # these are called when *we* sent these stanzas + 'content-accept-sent': [self.__fillJingleStanza], + 'content-add-sent': [self.__fillJingleStanza], + 'session-initiate-sent': [self.__fillJingleStanza], + 'session-accept-sent': [self.__fillJingleStanza], + 'session-terminate-sent': [], + } + + def is_ready(self): + return (self.accepted and not self.sent) + + def add_remote_candidates(self, candidates): + ''' Add a list of candidates to the list of remote candidates. ''' + pass + + def stanzaCB(self, stanza, content, error, action): + ''' Called when something related to our content was sent by peer. ''' + if action in self.callbacks: + for callback in self.callbacks[action]: + callback(stanza, content, error, action) + + def __transportInfoCB(self, stanza, content, error, action): + ''' Got a new transport candidate. ''' + candidates = self.transport.parse_transport_stanza( + content.getTag('transport')) + if candidates: + self.add_remote_candidates(candidates) + + def __content(self, payload=[]): + ''' Build a XML content-wrapper for our data. ''' + return xmpp.Node('content', + attrs={'name': self.name, 'creator': self.creator}, + payload=payload) + + def send_candidate(self, candidate): + content = self.__content() + content.addChild(self.transport.make_transport([candidate])) + self.session.send_transport_info(content) + + def __fillJingleStanza(self, stanza, content, error, action): + ''' Add our things to session-initiate stanza. ''' + self._fillContent(content) + self.sent = True + content.addChild(node=self.transport.make_transport()) + + def destroy(self): + self.callbacks = None + del self.session.contents[(self.creator, self.name)] diff --git a/src/common/jingle_rtp.py b/src/common/jingle_rtp.py new file mode 100644 index 000000000..d67b0f465 --- /dev/null +++ b/src/common/jingle_rtp.py @@ -0,0 +1,321 @@ +## +## Copyright (C) 2006 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +''' Handles Jingle RTP sessions (XEP 0167). ''' + + +import gobject + +import xmpp +import farsight, gst + +from jingle_transport import JingleTransportICEUDP +from jingle_content import contents, JingleContent + +# TODO: Will that be even used? +def get_first_gst_element(elements): + ''' Returns, if it exists, the first available element of the list. ''' + for name in elements: + factory = gst.element_factory_find(name) + if factory: + return factory.create() + + +class JingleRTPContent(JingleContent): + def __init__(self, session, media, transport=None): + if transport is None: + transport = JingleTransportICEUDP() + JingleContent.__init__(self, session, transport) + self.media = media + self._dtmf_running = False + self.farsight_media = {'audio': farsight.MEDIA_TYPE_AUDIO, + 'video': farsight.MEDIA_TYPE_VIDEO}[media] + self.got_codecs = False + + self.candidates_ready = False # True when local candidates are prepared + + self.callbacks['session-initiate'] += [self.__getRemoteCodecsCB] + self.callbacks['content-add'] += [self.__getRemoteCodecsCB] + self.callbacks['content-accept'] += [self.__getRemoteCodecsCB, + self.__contentAcceptCB] + self.callbacks['session-accept'] += [self.__getRemoteCodecsCB, + self.__contentAcceptCB] + self.callbacks['session-accept-sent'] += [self.__contentAcceptCB] + self.callbacks['content-accept-sent'] += [self.__contentAcceptCB] + self.callbacks['session-terminate'] += [self.__stop] + self.callbacks['session-terminate-sent'] += [self.__stop] + + def setup_stream(self): + # pipeline and bus + self.pipeline = gst.Pipeline() + bus = self.pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', self._on_gst_message) + + # conference + self.conference = gst.element_factory_make('fsrtpconference') + self.conference.set_property("sdes-cname", self.session.ourjid) + self.pipeline.add(self.conference) + self.funnel = None + + self.p2psession = self.conference.new_session(self.farsight_media) + + participant = self.conference.new_participant(self.session.peerjid) + #FIXME: Consider a workaround, here... + # pidgin and telepathy-gabble don't follow the XEP, and it won't work + # due to bad controlling-mode + params = {'controlling-mode': self.session.weinitiate,# 'debug': False} + 'stun-ip': '69.0.208.27', 'debug': False} + + self.p2pstream = self.p2psession.new_stream(participant, + farsight.DIRECTION_RECV, 'nice', params) + + def is_ready(self): + return (JingleContent.is_ready(self) and self.candidates_ready + and self.p2psession.get_property('codecs-ready')) + + def add_remote_candidates(self, candidates): + JingleContent.add_remote_candidates(self, candidates) + #FIXME: connectivity should not be etablished yet + # Instead, it should be etablished after session-accept! + if self.sent: + self.p2pstream.set_remote_candidates(candidates) + + def batch_dtmf(self, events): + if self._dtmf_running: + raise Exception #TODO: Proper exception + self._dtmf_running = True + self._start_dtmf(events.pop(0)) + gobject.timeout_add(500, self._next_dtmf, events) + + def _next_dtmf(self, events): + self._stop_dtmf() + if events: + self._start_dtmf(events.pop(0)) + gobject.timeout_add(500, self._next_dtmf, events) + else: + self._dtmf_running = False + + def _start_dtmf(self, event): + if event in ('*', '#'): + event = {'*': farsight.DTMF_EVENT_STAR, + '#': farsight.DTMF_EVENT_POUND}[event] + else: + event = int(event) + self.p2psession.start_telephony_event(event, 2, + farsight.DTMF_METHOD_RTP_RFC4733) + + def _stop_dtmf(self): + self.p2psession.stop_telephony_event(farsight.DTMF_METHOD_RTP_RFC4733) + + def _fillContent(self, content): + content.addChild(xmpp.NS_JINGLE_RTP + ' description', + attrs={'media': self.media}, payload=self.iter_codecs()) + + def _setup_funnel(self): + self.funnel = gst.element_factory_make('fsfunnel') + self.pipeline.add(self.funnel) + self.funnel.set_state(gst.STATE_PLAYING) + self.sink.set_state(gst.STATE_PLAYING) + self.funnel.link(self.sink) + + def _on_src_pad_added(self, stream, pad, codec): + if not self.funnel: + self._setup_funnel() + pad.link(self.funnel.get_pad('sink%d')) + + def _on_gst_message(self, bus, message): + if message.type == gst.MESSAGE_ELEMENT: + name = message.structure.get_name() + if name == 'farsight-new-active-candidate-pair': + pass + elif name == 'farsight-recv-codecs-changed': + pass + elif name == 'farsight-codecs-changed': + if self.is_ready(): + self.session.on_session_state_changed(self) + #TODO: description-info + elif name == 'farsight-local-candidates-prepared': + self.candidates_ready = True + if self.is_ready(): + self.session.on_session_state_changed(self) + elif name == 'farsight-new-local-candidate': + candidate = message.structure['candidate'] + self.transport.candidates.append(candidate) + if self.candidates_ready: + #FIXME: Is this case even possible? + self.send_candidate(candidate) + elif name == 'farsight-component-state-changed': + state = message.structure['state'] + print message.structure['component'], state + if state == farsight.STREAM_STATE_FAILED: + reason = xmpp.Node('reason') + reason.setTag('failed-transport') + self.session._session_terminate(reason) + elif name == 'farsight-error': + print 'Farsight error #%d!' % message.structure['error-no'] + print 'Message: %s' % message.structure['error-msg'] + print 'Debug: %s' % message.structure['debug-msg'] + else: + print name + + def __contentAcceptCB(self, stanza, content, error, action): + if self.accepted: + if self.transport.remote_candidates: + self.p2pstream.set_remote_candidates(self.transport.remote_candidates) + self.transport.remote_candidates = [] + #TODO: farsight.DIRECTION_BOTH only if senders='both' + self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) + self.session.content_negociated(self.media) + + def __getRemoteCodecsCB(self, stanza, content, error, action): + ''' Get peer codecs from what we get from peer. ''' + if self.got_codecs: + return + + codecs = [] + for codec in content.getTag('description').iterTags('payload-type'): + c = farsight.Codec(int(codec['id']), codec['name'], + self.farsight_media, int(codec['clockrate'])) + if 'channels' in codec: + c.channels = int(codec['channels']) + else: + c.channels = 1 + c.optional_params = [(str(p['name']), str(p['value'])) for p in \ + codec.iterTags('parameter')] + codecs.append(c) + + if len(codecs) > 0: + #FIXME: Handle this case: + # glib.GError: There was no intersection between the remote codecs and + # the local ones + self.p2pstream.set_remote_codecs(codecs) + self.got_codecs = True + + def iter_codecs(self): + codecs = self.p2psession.get_property('codecs') + for codec in codecs: + attrs = {'name': codec.encoding_name, + 'id': codec.id, + 'channels': codec.channels} + if codec.clock_rate: + attrs['clockrate'] = codec.clock_rate + if codec.optional_params: + payload = (xmpp.Node('parameter', {'name': name, 'value': value}) + for name, value in codec.optional_params) + else: payload = () + yield xmpp.Node('payload-type', attrs, payload) + + def __stop(self, *things): + self.pipeline.set_state(gst.STATE_NULL) + + def __del__(self): + self.__stop() + + def destroy(self): + JingleContent.destroy(self) + self.p2pstream.disconnect_by_func(self._on_src_pad_added) + self.pipeline.get_bus().disconnect_by_func(self._on_gst_message) + + +class JingleAudio(JingleRTPContent): + ''' Jingle VoIP sessions consist of audio content transported + over an ICE UDP protocol. ''' + def __init__(self, session, transport=None): + JingleRTPContent.__init__(self, session, 'audio', transport) + self.setup_stream() + + + ''' Things to control the gstreamer's pipeline ''' + def setup_stream(self): + JingleRTPContent.setup_stream(self) + + # Configure SPEEX + # Workaround for psi (not needed since rev + # 147aedcea39b43402fe64c533d1866a25449888a): + # place 16kHz before 8kHz, as buggy psi versions will take in + # account only the first codec + + codecs = [farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', + farsight.MEDIA_TYPE_AUDIO, 16000), + farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', + farsight.MEDIA_TYPE_AUDIO, 8000)] + self.p2psession.set_codec_preferences(codecs) + + # the local parts + # TODO: use gconfaudiosink? + # sink = get_first_gst_element(['alsasink', 'osssink', 'autoaudiosink']) + self.sink = gst.element_factory_make('alsasink') + self.sink.set_property('sync', False) + #sink.set_property('latency-time', 20000) + #sink.set_property('buffer-time', 80000) + + # TODO: use gconfaudiosrc? + src_mic = gst.element_factory_make('alsasrc') + src_mic.set_property('blocksize', 320) + + self.mic_volume = gst.element_factory_make('volume') + self.mic_volume.set_property('volume', 1) + + # link gst elements + self.pipeline.add(self.sink, src_mic, self.mic_volume) + src_mic.link(self.mic_volume) + + self.mic_volume.get_pad('src').link(self.p2psession.get_property( + 'sink-pad')) + self.p2pstream.connect('src-pad-added', self._on_src_pad_added) + + # The following is needed for farsight to process ICE requests: + self.pipeline.set_state(gst.STATE_PLAYING) + + +class JingleVideo(JingleRTPContent): + def __init__(self, session, transport=None): + JingleRTPContent.__init__(self, session, 'video', transport) + self.setup_stream() + + ''' Things to control the gstreamer's pipeline ''' + def setup_stream(self): + #TODO: Everything is not working properly: + # sometimes, one window won't show up, + # sometimes it'll freeze... + JingleRTPContent.setup_stream(self) + # the local parts + src_vid = gst.element_factory_make('videotestsrc') + src_vid.set_property('is-live', True) + videoscale = gst.element_factory_make('videoscale') + caps = gst.element_factory_make('capsfilter') + caps.set_property('caps', gst.caps_from_string('video/x-raw-yuv, width=320, height=240')) + colorspace = gst.element_factory_make('ffmpegcolorspace') + + self.pipeline.add(src_vid, videoscale, caps, colorspace) + gst.element_link_many(src_vid, videoscale, caps, colorspace) + + self.sink = gst.element_factory_make('xvimagesink') + self.pipeline.add(self.sink) + + colorspace.get_pad('src').link(self.p2psession.get_property('sink-pad')) + self.p2pstream.connect('src-pad-added', self._on_src_pad_added) + + # The following is needed for farsight to process ICE requests: + self.pipeline.set_state(gst.STATE_PLAYING) + + +def get_content(desc): + if desc['media'] == 'audio': + return JingleAudio + elif desc['media'] == 'video': + return JingleVideo + else: + return None + +contents[xmpp.NS_JINGLE_RTP] = get_content diff --git a/src/common/jingle_session.py b/src/common/jingle_session.py new file mode 100644 index 000000000..9118affdf --- /dev/null +++ b/src/common/jingle_session.py @@ -0,0 +1,613 @@ +## +## Copyright (C) 2006 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +''' Handles Jingle sessions (XEP 0166). ''' + +#TODO: +# * Have JingleContent here +# * 'senders' attribute of 'content' element +# * security preconditions +# * actions: +# - content-modify +# - description-info, session-info +# - security-info +# - transport-accept, transport-reject +# - Tie-breaking +# * timeout + +import gajim #Get rid of that? +import xmpp +from jingle_transport import get_jingle_transport +from jingle_content import get_jingle_content + +#FIXME: Move it to JingleSession.States? +class JingleStates(object): + ''' States in which jingle session may exist. ''' + ended = 0 + pending = 1 + active = 2 + +class OutOfOrder(Exception): + ''' Exception that should be raised when an action is received when in the wrong state. ''' + +class TieBreak(Exception): + ''' Exception that should be raised in case of a tie, when we overrule the other action. ''' + +class JingleSession(object): + ''' This represents one jingle session. ''' + def __init__(self, con, weinitiate, jid, sid=None): + ''' con -- connection object, + weinitiate -- boolean, are we the initiator? + jid - jid of the other entity''' + self.contents = {} # negotiated contents + self.connection = con # connection to use + # our full jid + #FIXME: Get rid of gajim here? + self.ourjid = gajim.get_jid_from_account(self.connection.name) + '/' + \ + con.server_resource + self.peerjid = jid # jid we connect to + # jid we use as the initiator + self.initiator = weinitiate and self.ourjid or self.peerjid + # jid we use as the responder + self.responder = weinitiate and self.peerjid or self.ourjid + # are we an initiator? + self.weinitiate = weinitiate + # what state is session in? (one from JingleStates) + self.state = JingleStates.ended + if not sid: + sid = con.connection.getAnID() + self.sid = sid # sessionid + + self.accepted = True # is this session accepted by user + + # callbacks to call on proper contents + # use .prepend() to add new callbacks, especially when you're going + # to send error instead of ack + self.callbacks = { + 'content-accept': [self.__contentAcceptCB, self.__broadcastCB, + self.__defaultCB], + 'content-add': [self.__contentAddCB, self.__broadcastCB, + self.__defaultCB], #TODO + 'content-modify': [self.__defaultCB], #TODO + 'content-reject': [self.__defaultCB, self.__contentRemoveCB], #TODO + 'content-remove': [self.__defaultCB, self.__contentRemoveCB], + 'description-info': [self.__broadcastCB, self.__defaultCB], #TODO + 'security-info': [self.__defaultCB], #TODO + 'session-accept': [self.__sessionAcceptCB, self.__contentAcceptCB, + self.__broadcastCB, self.__defaultCB], + 'session-info': [self.__sessionInfoCB, self.__broadcastCB, self.__defaultCB], + 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, + self.__defaultCB], + 'session-terminate': [self.__sessionTerminateCB, self.__broadcastAllCB, + self.__defaultCB], + 'transport-info': [self.__broadcastCB, self.__defaultCB], + 'transport-replace': [self.__broadcastCB, self.__transportReplaceCB], #TODO + 'transport-accept': [self.__defaultCB], #TODO + 'transport-reject': [self.__defaultCB], #TODO + 'iq-result': [], + 'iq-error': [self.__errorCB], + } + + ''' Interaction with user ''' + def approve_session(self): + ''' Called when user accepts session in UI (when we aren't the initiator). + ''' + self.accept_session() + + def decline_session(self): + ''' Called when user declines session in UI (when we aren't the initiator) + ''' + reason = xmpp.Node('reason') + reason.addChild('decline') + self._session_terminate(reason) + + def approve_content(self, media): + content = self.get_content(media) + if content: + content.accepted = True + self.on_session_state_changed(content) + + def reject_content(self, media): + content = self.get_content(media) + if content: + if self.state == JingleStates.active: + self.__content_reject(content) + content.destroy() + self.on_session_state_changed() + + def end_session(self): + ''' Called when user stops or cancel session in UI. ''' + reason = xmpp.Node('reason') + if self.state == JingleStates.active: + reason.addChild('success') + else: + reason.addChild('cancel') + self._session_terminate(reason) + + ''' Middle-level functions to manage contents. Handle local content + cache and send change notifications. ''' + def get_content(self, media=None): + if media is None: + return None + + for content in self.contents.values(): + if content.media == media: + return content + + def add_content(self, name, content, creator='we'): + ''' Add new content to session. If the session is active, + this will send proper stanza to update session. + Creator must be one of ('we', 'peer', 'initiator', 'responder')''' + assert creator in ('we', 'peer', 'initiator', 'responder') + + if (creator == 'we' and self.weinitiate) or (creator == 'peer' and \ + not self.weinitiate): + creator = 'initiator' + elif (creator == 'peer' and self.weinitiate) or (creator == 'we' and \ + not self.weinitiate): + creator = 'responder' + content.creator = creator + content.name = name + self.contents[(creator, name)] = content + + if (creator == 'initiator') == self.weinitiate: + # The content is from us, accept it + content.accepted = True + + def remove_content(self, creator, name): + ''' We do not need this now ''' + #TODO: + if (creator, name) in self.contents: + content = self.contents[(creator, name)] + if len(self.contents) > 1: + self.__content_remove(content) + self.contents[(creator, name)].destroy() + if len(self.contents) == 0: + self.end_session() + + def modify_content(self, creator, name, *someother): + ''' We do not need this now ''' + pass + + def on_session_state_changed(self, content=None): + if self.state == JingleStates.ended: + # Session not yet started, only one action possible: session-initiate + if self.is_ready() and self.weinitiate: + self.__session_initiate() + elif self.state == JingleStates.pending: + # We can either send a session-accept or a content-add + if self.is_ready() and not self.weinitiate: + self.__session_accept() + elif content and (content.creator == 'initiator') == self.weinitiate: + self.__content_add(content) + elif content and self.weinitiate: + self.__content_accept(content) + elif self.state == JingleStates.active: + # We can either send a content-add or a content-accept + if not content: + return + if (content.creator == 'initiator') == self.weinitiate: + # We initiated this content. It's a pending content-add. + self.__content_add(content) + else: + # The other side created this content, we accept it. + self.__content_accept(content) + + def is_ready(self): + ''' Returns True when all codecs and candidates are ready + (for all contents). ''' + return (all((content.is_ready() for content in self.contents.itervalues())) + and self.accepted) + + ''' Middle-level function to do stanza exchange. ''' + def accept_session(self): + ''' Mark the session as accepted. ''' + self.accepted = True + self.on_session_state_changed() + + def start_session(self): + ''' Mark the session as ready to be started. ''' + self.accepted = True + self.on_session_state_changed() + + def send_session_info(self): + pass + + def send_content_accept(self, content): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-accept') + jingle.addChild(node=content) + self.connection.connection.send(stanza) + + def send_transport_info(self, content): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('transport-info') + jingle.addChild(node=content) + self.connection.connection.send(stanza) + + ''' Session callbacks. ''' + def stanzaCB(self, stanza): + ''' A callback for ConnectionJingle. It gets stanza, then + tries to send it to all internally registered callbacks. + First one to raise xmpp.NodeProcessed breaks function.''' + jingle = stanza.getTag('jingle') + error = stanza.getTag('error') + if error: + # it's an iq-error stanza + action = 'iq-error' + elif jingle: + # it's a jingle action + action = jingle.getAttr('action') + if action not in self.callbacks: + self.__send_error(stanza, 'bad_request') + return + #FIXME: If we aren't initiated and it's not a session-initiate... + if action != 'session-initiate' and self.state == JingleStates.ended: + self.__send_error(stanza, 'item-not-found', 'unknown-session') + return + else: + # it's an iq-result (ack) stanza + action = 'iq-result' + + callables = self.callbacks[action] + + try: + for callable in callables: + callable(stanza=stanza, jingle=jingle, error=error, action=action) + except xmpp.NodeProcessed: + pass + except TieBreak: + self.__send_error(stanza, 'conflict', 'tiebreak') + except OutOfOrder: + self.__send_error(stanza, 'unexpected-request', 'out-of-order')#FIXME + + def __defaultCB(self, stanza, jingle, error, action): + ''' Default callback for action stanzas -- simple ack + and stop processing. ''' + response = stanza.buildReply('result') + self.connection.connection.send(response) + + def __errorCB(self, stanza, jingle, error, action): + #FIXME + text = error.getTagData('text') + jingle_error = None + xmpp_error = None + for child in error.getChildren(): + if child.getNamespace() == xmpp.NS_JINGLE_ERRORS: + jingle_error = child.getName() + elif child.getNamespace() == xmpp.NS_STANZAS: + xmpp_error = child.getName() + self.__dispatch_error(xmpp_error, jingle_error, text) + #FIXME: Not sure when we would want to do that... + if xmpp_error == 'item-not-found': + self.connection.delete_jingle_session(self.peerjid, self.sid) + + def __transportReplaceCB(self, stanza, jingle, error, action): + for content in jingle.iterTags('content'): + creator = content['creator'] + name = content['name'] + if (creator, name) in self.contents: + transport_ns = content.getTag('transport').getNamespace() + if transport_ns == xmpp.JINGLE_ICE_UDP: + #FIXME: We don't manage anything else than ICE-UDP now... + #What was the previous transport?!? + #Anyway, content's transport is not modifiable yet + pass + else: + stanza, jingle = self.__make_jingle('transport-reject') + content = jingle.setTag('content', attrs={'creator': creator, + 'name': name}) + content.setTag('transport', namespace=transport_ns) + self.connection.connection.send(stanza) + raise xmpp.NodeProcessed + else: + #FIXME: This ressource is unknown to us, what should we do? + #For now, reject the transport + stanza, jingle = self.__make_jingle('transport-reject') + c = jingle.setTag('content', attrs={'creator': creator, + 'name': name}) + c.setTag('transport', namespace=transport_ns) + self.connection.connection.send(stanza) + raise xmpp.NodeProcessed + + def __sessionInfoCB(self, stanza, jingle, error, action): + #TODO: ringing, active, (un)hold, (un)mute + payload = jingle.getPayload() + if len(payload) > 0: + self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info') + raise xmpp.NodeProcessed + + def __contentRemoveCB(self, stanza, jingle, error, action): + for content in jingle.iterTags('content'): + creator = content['creator'] + name = content['name'] + if (creator, name) in self.contents: + content = self.contents[(creator, name)] + #TODO: this will fail if content is not an RTP content + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, content.media, 'removed')) + content.destroy() + if len(self.contents) == 0: + reason = xmpp.Node('reason') + reason.setTag('success') + self._session_terminate(reason) + + def __sessionAcceptCB(self, stanza, jingle, error, action): + if self.state != JingleStates.pending: #FIXME + raise OutOfOrder + self.state = JingleStates.active + + def __contentAcceptCB(self, stanza, jingle, error, action): + ''' Called when we get content-accept stanza or equivalent one + (like session-accept).''' + # check which contents are accepted + for content in jingle.iterTags('content'): + creator = content['creator'] + name = content['name']#TODO... + + def __contentAddCB(self, stanza, jingle, error, action): + if self.state == JingleStates.ended: + raise OutOfOrder + + parse_result = self.__parse_contents(jingle) + contents = parse_result[2] + rejected_contents = parse_result[3] + + for name, creator in rejected_contents: + #TODO: + content = JingleContent() + self.add_content(name, content, creator) + self.__content_reject(content) + self.contents[(content.creator, content.name)].destroy() + + self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, + contents)) + + def __sessionInitiateCB(self, stanza, jingle, error, action): + ''' We got a jingle session request from other entity, + therefore we are the receiver... Unpack the data, + inform the user. ''' + + if self.state != JingleStates.ended: + raise OutOfOrder + + self.initiator = jingle['initiator'] + self.responder = self.ourjid + self.peerjid = self.initiator + self.accepted = False # user did not accept this session yet + + # TODO: If the initiator is unknown to the receiver (e.g., via presence + # subscription) and the receiver has a policy of not communicating via + # Jingle with unknown entities, it SHOULD return a + # error. + + # Lets check what kind of jingle session does the peer want + contents_ok, transports_ok, contents, pouet = self.__parse_contents(jingle) + + # If there's no content we understand... + if not contents_ok: + # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate + reason = xmpp.Node('reason') + reason.setTag('unsupported-applications') + self.__defaultCB(stanza, jingle, error, action) + self._session_terminate(reason) + raise xmpp.NodeProcessed + + if not transports_ok: + # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate + reason = xmpp.Node('reason') + reason.setTag('unsupported-transports') + self.__defaultCB(stanza, jingle, error, action) + self._session_terminate(reason) + raise xmpp.NodeProcessed + + self.state = JingleStates.pending + + # Send event about starting a session + self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, + contents)) + + def __broadcastCB(self, stanza, jingle, error, action): + ''' Broadcast the stanza contents to proper content handlers. ''' + for content in jingle.iterTags('content'): + name = content['name'] + creator = content['creator'] + cn = self.contents[(creator, name)] + cn.stanzaCB(stanza, content, error, action) + + def __sessionTerminateCB(self, stanza, jingle, error, action): + self.connection.delete_jingle_session(self.peerjid, self.sid) + reason, text = self.__reason_from_stanza(jingle) + if reason not in ('success', 'cancel', 'decline'): + self.__dispatch_error(reason, reason, text) + if text: + text = '%s (%s)' % (reason, text) + else: + text = reason#TODO + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, None, text)) + + def __broadcastAllCB(self, stanza, jingle, error, action): + ''' Broadcast the stanza to all content handlers. ''' + for content in self.contents.itervalues(): + content.stanzaCB(stanza, None, error, action) + + ''' Internal methods. ''' + def __parse_contents(self, jingle): + #TODO: Needs some reworking + contents = [] + contents_rejected = [] + contents_ok = False + transports_ok = False + + for element in jingle.iterTags('content'): + transport = get_jingle_transport(element.getTag('transport')) + content_type = get_jingle_content(element.getTag('description')) + if content_type: + contents_ok = True + if transport: + content = content_type(self, transport) + self.add_content(element['name'], + content, 'peer') + contents.append((content.media,)) + transports_ok = True + else: + contents_rejected.append((element['name'], 'peer')) + else: + contents_rejected.append((element['name'], 'peer')) + + return (contents_ok, transports_ok, contents, contents_rejected) + + def __dispatch_error(self, error, jingle_error=None, text=None): + if jingle_error: + error = jingle_error + if text: + text = '%s (%s)' % (error, text) + else: + text = error + self.connection.dispatch('JINGLE_ERROR', (self.peerjid, self.sid, text)) + + def __reason_from_stanza(self, stanza): + reason = 'success' + reasons = ['success', 'busy', 'cancel', 'connectivity-error', + 'decline', 'expired', 'failed-application', 'failed-transport', + 'general-error', 'gone', 'incompatible-parameters', 'media-error', + 'security-error', 'timeout', 'unsupported-applications', + 'unsupported-transports'] + tag = stanza.getTag('reason') + if tag: + text = tag.getTagData('text') + for r in reasons: + if tag.getTag(r): + reason = r + break + return (reason, text) + + ''' Methods that make/send proper pieces of XML. They check if the session + is in appropriate state. ''' + def __make_jingle(self, action): + stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid)) + attrs = {'action': action, + 'sid': self.sid} + if action == 'session-initiate': + attrs['initiator'] = self.initiator + elif action == 'session-accept': + attrs['responder'] = self.responder + jingle = stanza.addChild('jingle', attrs=attrs, namespace=xmpp.NS_JINGLE) + return stanza, jingle + + def __send_error(self, stanza, error, jingle_error=None, text=None): + err = xmpp.Error(stanza, error) + err.setNamespace(xmpp.NS_STANZAS) + if jingle_error: + err.setTag(jingle_error, namespace=xmpp.NS_JINGLE_ERRORS) + if text: + err.setTagData('text', text) + self.connection.connection.send(err) + self.__dispatch_error(error, jingle_error, text) + + def __append_content(self, jingle, content): + ''' Append element to element, + with (full=True) or without (full=False) + children. ''' + jingle.addChild('content', + attrs={'name': content.name, 'creator': content.creator}) + + def __append_contents(self, jingle): + ''' Append all elements to .''' + # TODO: integrate with __appendContent? + # TODO: parameters 'name', 'content'? + for content in self.contents.values(): + self.__append_content(jingle, content) + + def __session_initiate(self): + assert self.state == JingleStates.ended + stanza, jingle = self.__make_jingle('session-initiate') + self.__append_contents(jingle) + self.__broadcastCB(stanza, jingle, None, 'session-initiate-sent') + self.connection.connection.send(stanza) + self.state = JingleStates.pending + + def __session_accept(self): + assert self.state == JingleStates.pending + stanza, jingle = self.__make_jingle('session-accept') + self.__append_contents(jingle) + self.__broadcastCB(stanza, jingle, None, 'session-accept-sent') + self.connection.connection.send(stanza) + self.state = JingleStates.active + + def __session_info(self, payload=None): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('session-info') + if payload: + jingle.addChild(node=payload) + self.connection.connection.send(stanza) + + def _session_terminate(self, reason=None): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('session-terminate') + if reason is not None: + jingle.addChild(node=reason) + self.__broadcastAllCB(stanza, jingle, None, 'session-terminate-sent') + self.connection.connection.send(stanza) + reason, text = self.__reason_from_stanza(jingle) + if reason not in ('success', 'cancel', 'decline'): + self.__dispatch_error(reason, reason, text) + if text: + text = '%s (%s)' % (reason, text) + else: + text = reason + self.connection.delete_jingle_session(self.peerjid, self.sid) + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, None, text)) + + def __content_add(self, content): + #TODO: test + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-add') + self.__append_content(jingle, content) + self.__broadcastCB(stanza, jingle, None, 'content-add-sent') + self.connection.connection.send(stanza) + + def __content_accept(self, content): + #TODO: test + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-accept') + self.__append_content(jingle, content) + self.__broadcastCB(stanza, jingle, None, 'content-accept-sent') + self.connection.connection.send(stanza) + + def __content_reject(self, content): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-reject') + self.__append_content(jingle, content) + self.connection.connection.send(stanza) + #TODO: this will fail if content is not an RTP content + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, content.media, 'rejected')) + + def __content_modify(self): + assert self.state != JingleStates.ended + + def __content_remove(self, content): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-remove') + self.__append_content(jingle, content) + self.connection.connection.send(stanza) + #TODO: this will fail if content is not an RTP content + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, content.media, 'removed')) + + def content_negociated(self, media): + self.connection.dispatch('JINGLE_CONNECTED', (self.peerjid, self.sid, + media)) + diff --git a/src/common/jingle_transport.py b/src/common/jingle_transport.py new file mode 100644 index 000000000..487c66d70 --- /dev/null +++ b/src/common/jingle_transport.py @@ -0,0 +1,135 @@ +## +## Copyright (C) 2006 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +''' Handles Jingle Transports (currently only ICE-UDP). ''' + +import xmpp + +transports = {} + +def get_jingle_transport(node): + namespace = node.getNamespace() + if namespace in transports: + return transports[namespace]() + else: + return None + + +class TransportType(object): + ''' Possible types of a JingleTransport ''' + datagram = 1 + streaming = 2 + + +class JingleTransport(object): + ''' An abstraction of a transport in Jingle sessions. ''' + def __init__(self, type_): + self.type = type_ + self.candidates = [] + self.remote_candidates = [] + + def _iter_candidates(self): + for candidate in self.candidates: + yield self.make_candidate(candidate) + + def make_candidate(self, candidate): + ''' Build a candidate stanza for the given candidate. ''' + pass + + def make_transport(self, candidates=None): + ''' Build a transport stanza with the given candidates (or + self.candidates if candidates is None). ''' + if not candidates: + candidates = self._iter_candidates() + transport = xmpp.Node('transport', payload=candidates) + return transport + + def parse_transport_stanza(self, transport): + ''' Returns the list of transport candidates from a transport stanza. ''' + return [] + + +import farsight + +class JingleTransportICEUDP(JingleTransport): + def __init__(self): + JingleTransport.__init__(self, TransportType.datagram) + + def make_candidate(self, candidate): + types = {farsight.CANDIDATE_TYPE_HOST: 'host', + farsight.CANDIDATE_TYPE_SRFLX: 'srflx', + farsight.CANDIDATE_TYPE_PRFLX: 'prflx', + farsight.CANDIDATE_TYPE_RELAY: 'relay', + farsight.CANDIDATE_TYPE_MULTICAST: 'multicast'} + attrs = { + 'component': candidate.component_id, + 'foundation': '1', # hack + 'generation': '0', + 'ip': candidate.ip, + 'network': '0', + 'port': candidate.port, + 'priority': int(candidate.priority), # hack + } + if candidate.type in types: + attrs['type'] = types[candidate.type] + if candidate.proto == farsight.NETWORK_PROTOCOL_UDP: + attrs['protocol'] = 'udp' + else: + # we actually don't handle properly different tcp options in jingle + attrs['protocol'] = 'tcp' + return xmpp.Node('candidate', attrs=attrs) + + def make_transport(self, candidates=None): + transport = JingleTransport.make_transport(self, candidates) + transport.setNamespace(xmpp.NS_JINGLE_ICE_UDP) + if self.candidates and self.candidates[0].username and \ + self.candidates[0].password: + transport.setAttr('ufrag', self.candidates[0].username) + transport.setAttr('pwd', self.candidates[0].password) + return transport + + def parse_transport_stanza(self, transport): + candidates = [] + for candidate in transport.iterTags('candidate'): + cand = farsight.Candidate() + cand.component_id = int(candidate['component']) + cand.ip = str(candidate['ip']) + cand.port = int(candidate['port']) + cand.foundation = str(candidate['foundation']) + #cand.type = farsight.CANDIDATE_TYPE_LOCAL + cand.priority = int(candidate['priority']) + + if candidate['protocol'] == 'udp': + cand.proto = farsight.NETWORK_PROTOCOL_UDP + else: + # we actually don't handle properly different tcp options in jingle + cand.proto = farsight.NETWORK_PROTOCOL_TCP + + cand.username = str(transport['ufrag']) + cand.password = str(transport['pwd']) + + #FIXME: huh? + types = {'host': farsight.CANDIDATE_TYPE_HOST, + 'srflx': farsight.CANDIDATE_TYPE_SRFLX, + 'prflx': farsight.CANDIDATE_TYPE_PRFLX, + 'relay': farsight.CANDIDATE_TYPE_RELAY, + 'multicast': farsight.CANDIDATE_TYPE_MULTICAST} + if 'type' in candidate and candidate['type'] in types: + cand.type = types[candidate['type']] + else: + print 'Unknown type %s', candidate['type'] + candidates.append(cand) + self.remote_candidates.extend(candidates) + return candidates + +transports[xmpp.NS_JINGLE_ICE_UDP] = JingleTransportICEUDP + diff --git a/src/common/passwords.py b/src/common/passwords.py index 98a7f61d9..9000085c4 100644 --- a/src/common/passwords.py +++ b/src/common/passwords.py @@ -122,6 +122,8 @@ class GnomePasswordStorage(PasswordStorage): user = gajim.config.get_per('accounts', account_name, 'name') display_name = _('XMPP account %s@%s') % (user, server) attributes1 = dict(server=str(server), user=str(user), protocol='xmpp') + if password is None: + password = str() try: auth_token = gnomekeyring.item_create_sync( self.keyring, gnomekeyring.ITEM_NETWORK_PASSWORD, diff --git a/src/common/pep.py b/src/common/pep.py index 54012514f..5ba476c0c 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -23,9 +23,6 @@ ## along with Gajim. If not, see . ## -import common.gajim -from common import xmpp - MOODS = { 'afraid': _('Afraid'), 'amazed': _('Amazed'), @@ -192,390 +189,377 @@ ACTIVITIES = { 'studying': _('Studying'), 'writing': _('Writing')}} -def user_mood(items, name, jid): - has_child = False - retract = False - mood = None - text = None - for item in items.getTags('item'): - child = item.getTag('mood') - if child is not None: - has_child = True - for ch in child.getChildren(): - if ch.getName() != 'text': - mood = ch.getName() - else: - text = ch.getData() - if items.getTag('retract') is not None: - retract = True +TUNE_DATA = ['artist', 'title', 'source', 'track', 'length'] - if jid == common.gajim.get_jid_from_account(name): - acc = common.gajim.connections[name] - if has_child: - if 'mood' in acc.mood: - del acc.mood['mood'] - if 'text' in acc.mood: - del acc.mood['text'] - if mood is not None: - acc.mood['mood'] = mood - if text is not None: - acc.mood['text'] = text - elif retract: - if 'mood' in acc.mood: - del acc.mood['mood'] - if 'text' in acc.mood: - del acc.mood['text'] +import gobject +import gtk - (user, resource) = common.gajim.get_room_and_nick_from_fjid(jid) - for contact in common.gajim.contacts.get_contacts(name, user): - if has_child: - if 'mood' in contact.mood: - del contact.mood['mood'] - if 'text' in contact.mood: - del contact.mood['text'] - if mood is not None: - contact.mood['mood'] = mood - if text is not None: - contact.mood['text'] = text - elif retract: - if 'mood' in contact.mood: - del contact.mood['mood'] - if 'text' in contact.mood: - del contact.mood['text'] +import logging +log = logging.getLogger('gajim.c.pep') - if jid == common.gajim.get_jid_from_account(name): - common.gajim.interface.roster.draw_account(name) - common.gajim.interface.roster.draw_mood(user, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) - if ctrl: - ctrl.update_mood() +from common import helpers +from common import atom +from common import xmpp +from common import gajim -def user_tune(items, name, jid): - has_child = False - retract = False - artist = None - title = None - source = None - track = None - length = None +import gtkgui_helpers - for item in items.getTags('item'): - child = item.getTag('tune') - if child is not None: - has_child = True - for ch in child.getChildren(): - if ch.getName() == 'artist': - artist = ch.getData() - elif ch.getName() == 'title': - title = ch.getData() - elif ch.getName() == 'source': - source = ch.getData() - elif ch.getName() == 'track': - track = ch.getData() - elif ch.getName() == 'length': - length = ch.getData() - if items.getTag('retract') is not None: - retract = True - if jid == common.gajim.get_jid_from_account(name): - acc = common.gajim.connections[name] - if has_child: - if 'artist' in acc.tune: - del acc.tune['artist'] - if 'title' in acc.tune: - del acc.tune['title'] - if 'source' in acc.tune: - del acc.tune['source'] - if 'track' in acc.tune: - del acc.tune['track'] - if 'length' in acc.tune: - del acc.tune['length'] - if artist is not None: - acc.tune['artist'] = artist - if title is not None: - acc.tune['title'] = title - if source is not None: - acc.tune['source'] = source - if track is not None: - acc.tune['track'] = track - if length is not None: - acc.tune['length'] = length - elif retract: - if 'artist' in acc.tune: - del acc.tune['artist'] - if 'title' in acc.tune: - del acc.tune['title'] - if 'source' in acc.tune: - del acc.tune['source'] - if 'track' in acc.tune: - del acc.tune['track'] - if 'length' in acc.tune: - del acc.tune['length'] +class AbstractPEP(object): + + type = '' + namespace = '' + + @classmethod + def get_tag_as_PEP(cls, jid, account, event_tag): + items = event_tag.getTag('items', {'node': cls.namespace}) + if items: + log.debug("Received PEP 'user %s' from %s" % (cls.type, jid)) + return cls(jid, account, items) + else: + return None + + def __init__(self, jid, account, items): + self._pep_specific_data, self._retracted = self._extract_info(items) + + self._update_contacts(jid, account) + if jid == gajim.get_jid_from_account(account): + self._update_account(account) + + def _extract_info(self, items): + '''To be implemented by subclasses''' + raise NotImplementedError + + def _update_contacts(self, jid, account): + for contact in gajim.contacts.get_contacts(account, jid): + if self._retracted: + if self.type in contact.pep: + del contact.pep[self.type] + else: + contact.pep[self.type] = self + + def _update_account(self, account): + acc = gajim.connections[account] + if self._retracted: + if self.type in acc.pep: + del acc.pep[self.type] + else: + acc.pep[self.type] = self + + def asPixbufIcon(self): + '''SHOULD be implemented by subclasses''' + return None + + def asMarkupText(self): + '''SHOULD be implemented by subclasses''' + return '' + + +class UserMoodPEP(AbstractPEP): + '''XEP-0107: User Mood''' + + type = 'mood' + namespace = xmpp.NS_MOOD + + def _extract_info(self, items): + mood_dict = {} + + for item in items.getTags('item'): + mood_tag = item.getTag('mood') + if mood_tag: + for child in mood_tag.getChildren(): + name = child.getName().strip() + if name == 'text': + mood_dict['text'] = child.getData() + else: + mood_dict['mood'] = name + + retracted = items.getTag('retract') or not 'mood' in mood_dict + return (mood_dict, retracted) + + def asPixbufIcon(self): + assert not self._retracted + received_mood = self._pep_specific_data['mood'] + mood = received_mood if received_mood in MOODS else 'unknown' + pixbuf = gtkgui_helpers.load_mood_icon(mood).get_pixbuf() + return pixbuf + + def asMarkupText(self): + assert not self._retracted + untranslated_mood = self._pep_specific_data['mood'] + mood = self._translate_mood(untranslated_mood) + markuptext = '%s' % gobject.markup_escape_text(mood) + if 'text' in self._pep_specific_data: + text = self._pep_specific_data['text'] + markuptext += ' (%s)' % gobject.markup_escape_text(text) + return markuptext - user = common.gajim.get_room_and_nick_from_fjid(jid)[0] - for contact in common.gajim.contacts.get_contacts(name, user): - if has_child: - if 'artist' in contact.tune: - del contact.tune['artist'] - if 'title' in contact.tune: - del contact.tune['title'] - if 'source' in contact.tune: - del contact.tune['source'] - if 'track' in contact.tune: - del contact.tune['track'] - if 'length' in contact.tune: - del contact.tune['length'] - if artist is not None: - contact.tune['artist'] = artist - if title is not None: - contact.tune['title'] = title - if source is not None: - contact.tune['source'] = source - if track is not None: - contact.tune['track'] = track - if length is not None: - contact.tune['length'] = length - elif retract: - if 'artist' in contact.tune: - del contact.tune['artist'] - if 'title' in contact.tune: - del contact.tune['title'] - if 'source' in contact.tune: - del contact.tune['source'] - if 'track' in contact.tune: - del contact.tune['track'] - if 'length' in contact.tune: - del contact.tune['length'] + def _translate_mood(self, mood): + if mood in MOODS: + return MOODS[mood] + else: + return mood - if jid == common.gajim.get_jid_from_account(name): - common.gajim.interface.roster.draw_account(name) - common.gajim.interface.roster.draw_tune(user, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) - if ctrl: - ctrl.update_tune() -def user_geoloc(items, name, jid): - pass +class UserTunePEP(AbstractPEP): + '''XEP-0118: User Tune''' + + type = 'tune' + namespace = xmpp.NS_TUNE + + def _extract_info(self, items): + tune_dict = {} + + for item in items.getTags('item'): + tune_tag = item.getTag('tune') + if tune_tag: + for child in tune_tag.getChildren(): + name = child.getName().strip() + data = child.getData().strip() + if child.getName() in TUNE_DATA: + tune_dict[name] = data + + retracted = items.getTag('retract') or not ('artist' in tune_dict or + 'title' in tune_dict) + return (tune_dict, retracted) + + def asPixbufIcon(self): + import os + path = os.path.join(gajim.DATA_DIR, 'emoticons', 'static', 'music.png') + return gtk.gdk.pixbuf_new_from_file(path) + + def asMarkupText(self): + assert not self._retracted + tune = self._pep_specific_data -def user_activity(items, name, jid): - has_child = False - retract = False - activity = None - subactivity = None - text = None + artist = tune.get('artist', _('Unknown Artist')) + artist = gobject.markup_escape_text(artist) + + title = tune.get('title', _('Unknown Title')) + title = gobject.markup_escape_text(title) - for item in items.getTags('item'): - child = item.getTag('activity') - if child is not None: - has_child = True - for ch in child.getChildren(): - if ch.getName() != 'text': - activity = ch.getName() - for chi in ch.getChildren(): - subactivity = chi.getName() - else: - text = ch.getData() - if items.getTag('retract') is not None: - retract = True + source = tune.get('source', _('Unknown Source')) + source = gobject.markup_escape_text(source) - if jid == common.gajim.get_jid_from_account(name): - acc = common.gajim.connections[name] - if has_child: - if 'activity' in acc.activity: - del acc.activity['activity'] - if 'subactivity' in acc.activity: - del acc.activity['subactivity'] - if 'text' in acc.activity: - del acc.activity['text'] - if activity is not None: - acc.activity['activity'] = activity - if subactivity is not None and subactivity != 'other': - acc.activity['subactivity'] = subactivity - if text is not None: - acc.activity['text'] = text - elif retract: - if 'activity' in acc.activity: - del acc.activity['activity'] - if 'subactivity' in acc.activity: - del acc.activity['subactivity'] - if 'text' in acc.activity: - del acc.activity['text'] + tune_string = _('"%(title)s" by %(artist)s\n' + 'from %(source)s') % {'title': title, + 'artist': artist, 'source': source} + return tune_string + - user = common.gajim.get_room_and_nick_from_fjid(jid)[0] - for contact in common.gajim.contacts.get_contacts(name, user): - if has_child: - if 'activity' in contact.activity: - del contact.activity['activity'] - if 'subactivity' in contact.activity: - del contact.activity['subactivity'] - if 'text' in contact.activity: - del contact.activity['text'] - if activity is not None: - contact.activity['activity'] = activity - if subactivity is not None and subactivity != 'other': - contact.activity['subactivity'] = subactivity - if text is not None: - contact.activity['text'] = text - elif retract: - if 'activity' in contact.activity: - del contact.activity['activity'] - if 'subactivity' in contact.activity: - del contact.activity['subactivity'] - if 'text' in contact.activity: - del contact.activity['text'] +class UserActivityPEP(AbstractPEP): + '''XEP-0108: User Activity''' + + type = 'activity' + namespace = xmpp.NS_ACTIVITY + + def _extract_info(self, items): + activity_dict = {} + + for item in items.getTags('item'): + activity_tag = item.getTag('activity') + if activity_tag: + for child in activity_tag.getChildren(): + name = child.getName().strip() + data = child.getData().strip() + if name == 'text': + activity_dict['text'] = data + else: + activity_dict['activity'] = name + for subactivity in child.getChildren(): + subactivity_name = subactivity.getName().strip() + activity_dict['subactivity'] = subactivity_name + + retracted = items.getTag('retract') or not 'activity' in activity_dict + return (activity_dict, retracted) + + def asPixbufIcon(self): + assert not self._retracted + pep = self._pep_specific_data + activity = pep['activity'] + + has_known_activity = activity in ACTIVITIES + has_known_subactivity = (has_known_activity and ('subactivity' in pep) + and (pep['subactivity'] in ACTIVITIES[activity])) + + if has_known_activity: + if has_known_subactivity: + subactivity = pep['subactivity'] + return gtkgui_helpers.load_activity_icon(activity, subactivity).get_pixbuf() + else: + return gtkgui_helpers.load_activity_icon(activity).get_pixbuf() + else: + return gtkgui_helpers.load_activity_icon('unknown').get_pixbuf() + + def asMarkupText(self): + assert not self._retracted + pep = self._pep_specific_data + activity = pep['activity'] + subactivity = pep['subactivity'] if 'subactivity' in pep else None + text = pep['text'] if 'text' in pep else None - if jid == common.gajim.get_jid_from_account(name): - common.gajim.interface.roster.draw_account(name) - common.gajim.interface.roster.draw_activity(user, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) - if ctrl: - ctrl.update_activity() + if activity in ACTIVITIES: + # Translate standard activities + if subactivity in ACTIVITIES[activity]: + subactivity = ACTIVITIES[activity][subactivity] + activity = ACTIVITIES[activity]['category'] + + markuptext = '' + gobject.markup_escape_text(activity) + if subactivity: + markuptext += ': ' + gobject.markup_escape_text(subactivity) + markuptext += '' + if text: + markuptext += ' (%s)' % gobject.markup_escape_text(text) + return markuptext + + +class UserNicknamePEP(AbstractPEP): + '''XEP-0172: User Nickname''' + + type = 'nickname' + namespace = xmpp.NS_NICK + + def _extract_info(self, items): + nick = '' + for item in items.getTags('item'): + child = item.getTag('nick') + if child: + nick = child.getData() + break + + retracted = items.getTag('retract') or not nick + return (nick, retracted) + + def _update_contacts(self, jid, account): + nick = '' if self._retracted else self._pep_specific_data + for contact in gajim.contacts.get_contacts(account, jid): + contact.contact_name = nick + + def _update_account(self, account): + if self._retracted: + gajim.nicks[account] = gajim.config.get_per('accounts', account, 'name') + else: + gajim.nicks[account] = self._pep_specific_data -def user_nickname(items, name, jid): - has_child = False - retract = False - nick = None + +SUPPORTED_PERSONAL_USER_EVENTS = [UserMoodPEP, UserTunePEP, UserActivityPEP, + UserNicknamePEP] - for item in items.getTags('item'): - child = item.getTag('nick') - if child is not None: - has_child = True - nick = child.getData() - break +class ConnectionPEP(object): - if items.getTag('retract') is not None: - retract = True + def __init__(self, account, dispatcher, pubsub_connection): + self._account = account + self._dispatcher = dispatcher + self._pubsub_connection = pubsub_connection + + def _pubsubEventCB(self, xmpp_dispatcher, msg): + ''' Called when we receive with pubsub event. ''' + if not msg.getTag('event'): + return + if msg.getTag('error'): + log.debug('PubsubEventCB received error stanza. Ignoring') + raise xmpp.NodeProcessed + + jid = helpers.get_full_jid_from_iq(msg) + event_tag = msg.getTag('event') - if jid == common.gajim.get_jid_from_account(name): - if has_child: - common.gajim.nicks[name] = nick - if retract: - common.gajim.nicks[name] = common.gajim.config.get_per('accounts', - name, 'name') + for pep_class in SUPPORTED_PERSONAL_USER_EVENTS: + pep = pep_class.get_tag_as_PEP(jid, self._account, event_tag) + if pep: + self._dispatcher.dispatch('PEP_RECEIVED', (jid, pep.type)) + + items = event_tag.getTag('items') + if items: + for item in items.getTags('item'): + entry = item.getTag('entry') + if entry: + # for each entry in feed (there shouldn't be more than one, + # but to be sure... + self._dispatcher.dispatch('ATOM_ENTRY', + (atom.OldEntry(node=entry),)) + + raise xmpp.NodeProcessed + + def send_activity(self, activity, subactivity=None, message=None): + if not self.pep_supported: + return + item = xmpp.Node('activity', {'xmlns': xmpp.NS_ACTIVITY}) + if activity: + i = item.addChild(activity) + if subactivity: + i.addChild(subactivity) + if message: + i = item.addChild('text') + i.addData(message) + self._pubsub_connection.send_pb_publish('', xmpp.NS_ACTIVITY, item, '0') + + def retract_activity(self): + if not self.pep_supported: + return + # not all server support retract, so send empty pep first + self.send_activity(None) + self._pubsub_connection.send_pb_retract('', xmpp.NS_ACTIVITY, '0') - user = common.gajim.get_room_and_nick_from_fjid(jid)[0] - if has_child: - if nick is not None: - for contact in common.gajim.contacts.get_contacts(name, user): - contact.contact_name = nick - common.gajim.interface.roster.draw_contact(user, name) + def send_mood(self, mood, message=None): + if not self.pep_supported: + return + item = xmpp.Node('mood', {'xmlns': xmpp.NS_MOOD}) + if mood: + item.addChild(mood) + if message: + i = item.addChild('text') + i.addData(message) + self._pubsub_connection.send_pb_publish('', xmpp.NS_MOOD, item, '0') + + def retract_mood(self): + if not self.pep_supported: + return + self.send_mood(None) + self._pubsub_connection.send_pb_retract('', xmpp.NS_MOOD, '0') + + def send_tune(self, artist='', title='', source='', track=0, length=0, + items=None): + if not self.pep_supported: + return + item = xmpp.Node('tune', {'xmlns': xmpp.NS_TUNE}) + if artist: + i = item.addChild('artist') + i.addData(artist) + if title: + i = item.addChild('title') + i.addData(title) + if source: + i = item.addChild('source') + i.addData(source) + if track: + i = item.addChild('track') + i.addData(track) + if length: + i = item.addChild('length') + i.addData(length) + if items: + item.addChild(payload=items) + self._pubsub_connection.send_pb_publish('', xmpp.NS_TUNE, item, '0') - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) - if ctrl: - ctrl.update_ui() - win = ctrl.parent_win - win.redraw_tab(ctrl) - win.show_title() - elif retract: - contact.contact_name = '' + def retract_tune(self): + if not self.pep_supported: + return + # not all server support retract, so send empty pep first + self.send_tune(None) + self._pubsub_connection.send_pb_retract('', xmpp.NS_TUNE, '0') -def user_send_mood(account, mood, message=''): - if not common.gajim.connections[account].pep_supported: - return - item = xmpp.Node('mood', {'xmlns': xmpp.NS_MOOD}) - if mood != '': - item.addChild(mood) - if message != '': - i = item.addChild('text') - i.addData(message) + def send_nickname(self, nick): + if not self.pep_supported: + return + item = xmpp.Node('nick', {'xmlns': xmpp.NS_NICK}) + item.addData(nick) + self._pubsub_connection.send_pb_publish('', xmpp.NS_NICK, item, '0') - common.gajim.connections[account].send_pb_publish('', xmpp.NS_MOOD, item, - '0') - -def user_send_activity(account, activity, subactivity='', message=''): - if not common.gajim.connections[account].pep_supported: - return - item = xmpp.Node('activity', {'xmlns': xmpp.NS_ACTIVITY}) - if activity != '': - i = item.addChild(activity) - if subactivity != '': - i.addChild(subactivity) - if message != '': - i = item.addChild('text') - i.addData(message) - - common.gajim.connections[account].send_pb_publish('', xmpp.NS_ACTIVITY, item, - '0') - -def user_send_tune(account, artist='', title='', source='', track=0, length=0, -items=None): - if not (common.gajim.config.get_per('accounts', account, 'publish_tune') and\ - common.gajim.connections[account].pep_supported): - return - item = xmpp.Node('tune', {'xmlns': xmpp.NS_TUNE}) - if artist != '': - i = item.addChild('artist') - i.addData(artist) - if title != '': - i = item.addChild('title') - i.addData(title) - if source != '': - i = item.addChild('source') - i.addData(source) - if track != 0: - i = item.addChild('track') - i.addData(track) - if length != 0: - i = item.addChild('length') - i.addData(length) - if items is not None: - item.addChild(payload=items) - - common.gajim.connections[account].send_pb_publish('', xmpp.NS_TUNE, item, - '0') - -def user_send_nickname(account, nick): - if not common.gajim.connections[account].pep_supported: - return - item = xmpp.Node('nick', {'xmlns': xmpp.NS_NICK}) - item.addData(nick) - - common.gajim.connections[account].send_pb_publish('', xmpp.NS_NICK, item, - '0') - -def user_retract_mood(account): - common.gajim.connections[account].send_pb_retract('', xmpp.NS_MOOD, '0') - -def user_retract_activity(account): - common.gajim.connections[account].send_pb_retract('', xmpp.NS_ACTIVITY, '0') - -def user_retract_tune(account): - common.gajim.connections[account].send_pb_retract('', xmpp.NS_TUNE, '0') - -def user_retract_nickname(account): - common.gajim.connections[account].send_pb_retract('', xmpp.NS_NICK, '0') - -def delete_pep(jid, name): - user = common.gajim.get_room_and_nick_from_fjid(jid)[0] - - if jid == common.gajim.get_jid_from_account(name): - acc = common.gajim.connections[name] - del acc.activity - acc.activity = {} - user_send_tune(name) - del acc.tune - acc.tune = {} - del acc.mood - acc.mood = {} - - for contact in common.gajim.contacts.get_contacts(name, user): - del contact.activity - contact.activity = {} - del contact.tune - contact.tune = {} - del contact.mood - contact.mood = {} - - if jid == common.gajim.get_jid_from_account(name): - common.gajim.interface.roster.draw_account(name) - - common.gajim.interface.roster.draw_activity(user, name) - common.gajim.interface.roster.draw_tune(user, name) - common.gajim.interface.roster.draw_mood(user, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) - if ctrl: - ctrl.update_activity() - ctrl.update_tune() - ctrl.update_mood() + def retract_nickname(self): + if not self.pep_supported: + return + # not all server support retract, so send empty pep first + self.send_nickname(None) + self._pubsub_connection.send_pb_retract('', xmpp.NS_NICK, '0') # vim: se ts=3: diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index 489885f76..7d82ae4e3 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -112,7 +112,7 @@ class NonBlockingClient: log.debug('calling on_proxy_failure cb') self.on_proxy_failure(reason=message) else: - log.debug('ccalling on_connect_failure cb') + log.debug('calling on_connect_failure cb') self.on_connect_failure() else: # we are connected to XMPP server diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index 8f4746f68..477d3e6c4 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -247,25 +247,23 @@ class XMPPDispatcher(PlugIn): ''' Register user callback as stanzas handler of declared type. - Callback must take (if chained, see later) arguments: + Callback arguments: dispatcher instance (for replying), incoming return of previous handlers. The callback must raise xmpp.NodeProcessed just before return if it wants - other callbacks to be called with the same stanza as argument _and_, more - importantly library from returning stanza to sender with error set. + to prevent other callbacks to be called with the same stanza as argument + _and_, more importantly library from returning stanza to sender with error set. :param name: name of stanza. F.e. "iq". :param handler: user callback. :param typ: value of stanza's "type" attribute. If not specified any value will match :param ns: namespace of child that stanza must contain. - :param chained: chain together output of several handlers. - :param makefirst: insert handler in the beginning of handlers list instea + :param makefirst: insert handler in the beginning of handlers list instead of adding it to the end. Note that more common handlers i.e. w/o "typ" and " will be called first nevertheless. :param system: call handler even if NodeProcessed Exception were raised already. ''' - # FIXME: What does chain mean and where is it handled? if not xmlns: xmlns=self._owner.defaultNamespace log.debug('Registering handler %s for "%s" type->%s ns->%s(%s)' % diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index 39930d675..3cf75fea3 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -93,6 +93,7 @@ NS_PRIVACY ='jabber:iq:privacy' NS_PRIVATE ='jabber:iq:private' NS_PROFILE ='http://jabber.org/protocol/profile' # XEP-0154 NS_PUBSUB ='http://jabber.org/protocol/pubsub' # XEP-0060 +NS_PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event' NS_PUBSUB_PUBLISH_OPTIONS = NS_PUBSUB + '#publish-options' # XEP-0060 NS_PUBSUB_OWNER ='http://jabber.org/protocol/pubsub#owner' # JEP-0060 NS_REGISTER ='jabber:iq:register' diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 704cad170..e6d63bcb3 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -72,7 +72,7 @@ def get_proxy_data_from_dict(proxy): # with proxy!=bosh or with bosh over HTTP proxy we're connecting to proxy # machine tcp_host, tcp_port = proxy['host'], proxy['port'] - if proxy['useauth']: + if proxy.get('useauth', False): proxy_user, proxy_pass = proxy['user'], proxy['pass'] return tcp_host, tcp_port, proxy_user, proxy_pass @@ -92,6 +92,7 @@ RECV_BUFSIZE = 32768 # 2x maximum size of ssl packet, should be plenty DATA_RECEIVED = 'DATA RECEIVED' DATA_SENT = 'DATA SENT' +DATA_ERROR = 'DATA ERROR' DISCONNECTED = 'DISCONNECTED' DISCONNECTING = 'DISCONNECTING' @@ -650,7 +651,7 @@ class NonBlockingHTTP(NonBlockingTCP): self.recvbuff = '%s%s' % (self.recvbuff or '', data) statusline, headers, httpbody, buffer_rest = self.parse_http_message( self.recvbuff) - + if not (statusline and headers and httpbody): log.debug('Received incomplete HTTP response') return @@ -662,12 +663,12 @@ class NonBlockingHTTP(NonBlockingTCP): self.expected_length = int(headers['Content-Length']) if 'Connection' in headers and headers['Connection'].strip()=='close': self.close_current_connection = True - + if self.expected_length > len(httpbody): # If we haven't received the whole HTTP mess yet, let's end the thread. # It will be finnished from one of following recvs on plugged socket. - log.info('not enough bytes in HTTP response - %d expected, %d got' % - (self.expected_length, len(self.recvbuff))) + log.info('not enough bytes in HTTP response - %d expected, got %d' % + (self.expected_length, len(httpbody))) else: # First part of buffer has been extraced and is going to be handled, # remove it from buffer @@ -719,6 +720,7 @@ class NonBlockingHTTP(NonBlockingTCP): http_rest - what is left in the message after a full HTTP header + body ''' message = message.replace('\r','') + message = message.lstrip('\n') splitted = message.split('\n\n') if len(splitted) < 2: # no complete http message. Keep filling the buffer until we find one @@ -726,9 +728,6 @@ class NonBlockingHTTP(NonBlockingTCP): return ('', '', '', buffer_rest) else: (header, httpbody) = splitted[:2] - if httpbody.endswith('\n'): - httpbody = httpbody[:-1] - buffer_rest = "\n\n".join(splitted[2:]) header = header.split('\n') statusline = header[0].split(' ', 2) header = header[1:] @@ -736,6 +735,12 @@ class NonBlockingHTTP(NonBlockingTCP): for dummy in header: row = dummy.split(' ', 1) headers[row[0][:-1]] = row[1] + body_size = headers['Content-Length'] + rest_splitted = splitted[2:] + while (len(httpbody) < body_size) and rest_splitted: + # Complete httpbody until it has the announced size + httpbody = '\n\n'.join([httpbody, rest_splitted.pop(0)]) + buffer_rest = "\n\n".join(rest_splitted) return (statusline, headers, httpbody, buffer_rest) diff --git a/src/common/zeroconf/client_zeroconf.py b/src/common/zeroconf/client_zeroconf.py index 730eb3ec3..ab863709d 100644 --- a/src/common/zeroconf/client_zeroconf.py +++ b/src/common/zeroconf/client_zeroconf.py @@ -23,7 +23,7 @@ from common.xmpp.idlequeue import IdleObject from common.xmpp import dispatcher_nb, simplexml from common.xmpp.plugin import * from common.xmpp.simplexml import ustr -from common.xmpp.transports_nb import DATA_RECEIVED, DATA_SENT +from common.xmpp.transports_nb import DATA_RECEIVED, DATA_SENT, DATA_ERROR from common.zeroconf import zeroconf from common.xmpp.protocol import * @@ -288,6 +288,7 @@ class P2PClient(IdleObject): pass def _register_handlers(self): + self._caller.peerhost = self.Connection._sock.getsockname() self.RegisterHandler('message', lambda conn, data:self._caller._messageCB( self.Server, conn, data)) self.RegisterHandler('iq', self._caller._siSetCB, 'set', @@ -395,6 +396,7 @@ class P2PConnection(IdleObject, PlugIn): False, else send it instantly. If supplied data is unicode string, encode it to utf-8. ''' + print 'ici' if self.state <= 0: return @@ -416,8 +418,11 @@ class P2PConnection(IdleObject, PlugIn): ids = self.client.conn_holder.ids_of_awaiting_messages if self.fd in ids and len(ids[self.fd]) > 0: for (id_, thread_id) in ids[self.fd]: - self._owner.Dispatcher.Event('', DATA_ERROR, (self.client.to, - thread_id)) + if hasattr(self._owner, 'Dispatcher'): + self._owner.Dispatcher.Event('', DATA_ERROR, (self.client.to, + thread_id)) + else: + self._owner.on_not_ok('conenction timeout') ids[self.fd] = [] self.pollend() @@ -578,6 +583,8 @@ class ClientZeroconf: self.hash_to_port = {} self.listener = None self.ids_of_awaiting_messages = {} + self.disconnect_handlers = [] + self.disconnecting = False def connect(self, show, msg): self.port = self.start_listener(self.caller.port) @@ -632,6 +639,9 @@ class ClientZeroconf: self.last_msg = msg def disconnect(self): + # to avoid recursive calls + if self.disconnecting: + return if self.listener: self.listener.disconnect() self.listener = None @@ -642,6 +652,14 @@ class ClientZeroconf: self.roster.zeroconf = None self.roster._data = None self.roster = None + self.disconnecting = True + for i in reversed(self.disconnect_handlers): + log.debug('Calling disconnect handler %s' % i) + i() + self.disconnecting = False + + def start_disconnect(self): + self.disconnect() def kill_all_connections(self): for connection in self.connections.values(): @@ -720,6 +738,14 @@ class ClientZeroconf: P2PClient(None, item['address'], item['port'], self, [(stanza, is_message)], to, on_ok=on_ok, on_not_ok=on_not_ok) + def RegisterDisconnectHandler(self, handler): + ''' Register handler that will be called on disconnect.''' + self.disconnect_handlers.append(handler) + + def UnregisterDisconnectHandler(self, handler): + ''' Unregister handler that is called on disconnect.''' + self.disconnect_handlers.remove(handler) + def SendAndWaitForResponse(self, stanza, timeout=None, func=None, args=None): ''' Send stanza and wait for recipient's response to it. Will call transports diff --git a/src/common/zeroconf/connection_handlers_zeroconf.py b/src/common/zeroconf/connection_handlers_zeroconf.py index 09818be47..8c036f3f8 100644 --- a/src/common/zeroconf/connection_handlers_zeroconf.py +++ b/src/common/zeroconf/connection_handlers_zeroconf.py @@ -35,6 +35,8 @@ from common import helpers from common import gajim from common.zeroconf import zeroconf from common.commands import ConnectionCommands +from common.message_archiving import ConnectionArchive +from common.pep import ConnectionPEP import logging log = logging.getLogger('gajim.c.z.connection_handlers_zeroconf') @@ -57,10 +59,10 @@ from session import ChatControlSession class ConnectionVcard(connection_handlers.ConnectionVcard): def add_sha(self, p, send_caps = True): - pass + return p def add_caps(self, p): - pass + return p def request_vcard(self, jid = None, is_fake_jid = False): pass @@ -69,318 +71,27 @@ class ConnectionVcard(connection_handlers.ConnectionVcard): pass class ConnectionBytestream(connection_handlers.ConnectionBytestream): - def send_socks5_info(self, file_props, fast = True, receiver = None, - sender = None): - ''' send iq for the present streamhosts and proxies ''' - if not isinstance(self.peerhost, tuple): - return - port = gajim.config.get('file_transfers_port') - ft_add_hosts_to_send = gajim.config.get('ft_add_hosts_to_send') - if receiver is None: - receiver = file_props['receiver'] - if sender is None: - sender = file_props['sender'] - sha_str = helpers.get_auth_sha(file_props['sid'], sender, - receiver) - file_props['sha_str'] = sha_str - ft_add_hosts = [] - if ft_add_hosts_to_send: - ft_add_hosts_to_send = [e.strip() for e in ft_add_hosts_to_send.split(',')] - for ft_host in ft_add_hosts_to_send: - try: - ft_host = socket.gethostbyname(ft_host) - ft_add_hosts.append(ft_host) - except socket.gaierror: - self.dispatch('ERROR', (_('Wrong host'), _('The host %s you configured as the ft_add_hosts_to_send advanced option is not valid, so ignored.') % ft_host)) - listener = gajim.socks5queue.start_listener(port, - sha_str, self._result_socks5_sid, file_props['sid']) - if listener is None: - file_props['error'] = -5 - self.dispatch('FILE_REQUEST_ERROR', (unicode(receiver), file_props, - '')) - self._connect_error(unicode(receiver), file_props['sid'], - file_props['sid'], code = 406) - return + def _ft_get_from(self, iq_obj): + return unicode(iq_obj.getFrom()) - iq = common.xmpp.Protocol(name = 'iq', to = unicode(receiver), - typ = 'set') - file_props['request-id'] = 'id_' + file_props['sid'] - iq.setID(file_props['request-id']) - query = iq.setTag('query') - query.setNamespace(common.xmpp.NS_BYTESTREAM) - query.setAttr('mode', 'tcp') - query.setAttr('sid', file_props['sid']) - for ft_host in ft_add_hosts: - # The streamhost, if set - ostreamhost = common.xmpp.Node(tag = 'streamhost') - query.addChild(node = ostreamhost) - ostreamhost.setAttr('port', unicode(port)) - ostreamhost.setAttr('host', ft_host) - ostreamhost.setAttr('jid', sender) - for thehost in self.peerhost: - thehost = self.peerhost[0] - streamhost = common.xmpp.Node(tag = 'streamhost') # My IP - query.addChild(node = streamhost) - streamhost.setAttr('port', unicode(port)) - streamhost.setAttr('host', thehost) - streamhost.setAttr('jid', sender) - self.connection.send(iq) + def _ft_get_our_jid(self): + return gajim.get_jid_from_account(self.name) - def send_file_request(self, file_props): - ''' send iq for new FT request ''' - if not self.connection or self.connected < 2: - return - our_jid = gajim.get_jid_from_account(self.name) - frm = our_jid - file_props['sender'] = frm - fjid = file_props['receiver'].jid - iq = common.xmpp.Protocol(name = 'iq', to = fjid, - typ = 'set') - iq.setID(file_props['sid']) - self.files_props[file_props['sid']] = file_props - si = iq.setTag('si') - si.setNamespace(common.xmpp.NS_SI) - si.setAttr('profile', common.xmpp.NS_FILE) - si.setAttr('id', file_props['sid']) - file_tag = si.setTag('file') - file_tag.setNamespace(common.xmpp.NS_FILE) - file_tag.setAttr('name', file_props['name']) - file_tag.setAttr('size', file_props['size']) - desc = file_tag.setTag('desc') - if 'desc' in file_props: - desc.setData(file_props['desc']) - file_tag.setTag('range') - feature = si.setTag('feature') - feature.setNamespace(common.xmpp.NS_FEATURE) - _feature = common.xmpp.DataForm(typ='form') - feature.addChild(node=_feature) - field = _feature.setField('stream-method') - field.setAttr('type', 'list-single') - field.addOption(common.xmpp.NS_BYTESTREAM) - self.connection.send(iq) + def _ft_get_receiver_jid(self, file_props): + return file_props['receiver'].jid - def _bytestreamSetCB(self, con, iq_obj): - log.debug('_bytestreamSetCB') - target = unicode(iq_obj.getAttr('to')) - id_ = unicode(iq_obj.getAttr('id')) - query = iq_obj.getTag('query') - sid = unicode(query.getAttr('sid')) - file_props = gajim.socks5queue.get_file_props( - self.name, sid) - streamhosts=[] - for item in query.getChildren(): - if item.getName() == 'streamhost': - host_dict={ - 'state': 0, - 'target': target, - 'id': id_, - 'sid': sid, - 'initiator': unicode(iq_obj.getFrom()) - } - for attr in item.getAttrs(): - host_dict[attr] = item.getAttr(attr) - streamhosts.append(host_dict) - if file_props is None: - if sid in self.files_props: - file_props = self.files_props[sid] - file_props['fast'] = streamhosts - if file_props['type'] == 's': - if 'streamhosts' in file_props: - file_props['streamhosts'].extend(streamhosts) - else: - file_props['streamhosts'] = streamhosts - if not gajim.socks5queue.get_file_props(self.name, sid): - gajim.socks5queue.add_file_props(self.name, file_props) - gajim.socks5queue.connect_to_hosts(self.name, sid, - self.send_success_connect_reply, None) - raise common.xmpp.NodeProcessed + def _ft_get_streamhost_jid_attr(self, streamhost): + return streamhost.getAttr('jid') - file_props['streamhosts'] = streamhosts - if file_props['type'] == 'r': - gajim.socks5queue.connect_to_hosts(self.name, sid, - self.send_success_connect_reply, self._connect_error) - raise common.xmpp.NodeProcessed - - def _ResultCB(self, con, iq_obj): - log.debug('_ResultCB') - # if we want to respect jep-0065 we have to check for proxy - # activation result in any result iq - real_id = unicode(iq_obj.getAttr('id')) - if not real_id.startswith('au_'): - return - frm = unicode(iq_obj.getFrom()) - id_ = real_id[3:] - if id_ in self.files_props: - file_props = self.files_props[id_] - if file_props['streamhost-used']: - for host in file_props['proxyhosts']: - if host['initiator'] == frm and 'idx' in host: - gajim.socks5queue.activate_proxy(host['idx']) - raise common.xmpp.NodeProcessed - - def _bytestreamResultCB(self, con, iq_obj): - log.debug('_bytestreamResultCB') - frm = unicode(iq_obj.getFrom()) - real_id = unicode(iq_obj.getAttr('id')) - query = iq_obj.getTag('query') - gajim.proxy65_manager.resolve_result(frm, query) - - try: - streamhost = query.getTag('streamhost-used') - except Exception: # this bytestream result is not what we need - pass - id_ = real_id[3:] - if id_ in self.files_props: - file_props = self.files_props[id_] - else: - raise common.xmpp.NodeProcessed - if streamhost is None: - # proxy approves the activate query - if real_id.startswith('au_'): - id_ = real_id[3:] - if 'streamhost-used' not in file_props or \ - file_props['streamhost-used'] is False: - raise common.xmpp.NodeProcessed - if 'proxyhosts' not in file_props: - raise common.xmpp.NodeProcessed - for host in file_props['proxyhosts']: - if host['initiator'] == frm and \ - unicode(query.getAttr('sid')) == file_props['sid']: - gajim.socks5queue.activate_proxy(host['idx']) - break - raise common.xmpp.NodeProcessed - jid = streamhost.getAttr('jid') - if 'streamhost-used' in file_props and \ - file_props['streamhost-used'] is True: - raise common.xmpp.NodeProcessed - - if real_id.startswith('au_'): - gajim.socks5queue.send_file(file_props, self.name) - raise common.xmpp.NodeProcessed - - proxy = None - if 'proxyhosts' in file_props: - for proxyhost in file_props['proxyhosts']: - if proxyhost['jid'] == jid: - proxy = proxyhost - - if proxy is not None: - file_props['streamhost-used'] = True - if 'streamhosts' not in file_props: - file_props['streamhosts'] = [] - file_props['streamhosts'].append(proxy) - file_props['is_a_proxy'] = True - receiver = socks5.Socks5Receiver(gajim.idlequeue, proxy, file_props['sid'], file_props) - gajim.socks5queue.add_receiver(self.name, receiver) - proxy['idx'] = receiver.queue_idx - gajim.socks5queue.on_success = self._proxy_auth_ok - raise common.xmpp.NodeProcessed - - else: - gajim.socks5queue.send_file(file_props, self.name) - if 'fast' in file_props: - fasts = file_props['fast'] - if len(fasts) > 0: - self._connect_error(frm, fasts[0]['id'], file_props['sid'], - code = 406) - - raise common.xmpp.NodeProcessed - - def _siResultCB(self, con, iq_obj): - log.debug('_siResultCB') - self.peerhost = con._owner.Connection._sock.getsockname() - id_ = iq_obj.getAttr('id') - if id_ not in self.files_props: - # no such jid - return - file_props = self.files_props[id_] - if file_props is None: - # file properties for jid is none - return - if 'request-id' in file_props: - # we have already sent streamhosts info - return - file_props['receiver'] = unicode(iq_obj.getFrom()) - si = iq_obj.getTag('si') - file_tag = si.getTag('file') - range_tag = None - if file_tag: - range_tag = file_tag.getTag('range') - if range_tag: - offset = range_tag.getAttr('offset') - if offset: - file_props['offset'] = int(offset) - length = range_tag.getAttr('length') - if length: - file_props['length'] = int(length) - feature = si.setTag('feature') - if feature.getNamespace() != common.xmpp.NS_FEATURE: - return - form_tag = feature.getTag('x') - form = common.xmpp.DataForm(node=form_tag) - field = form.getField('stream-method') - if field.getValue() != common.xmpp.NS_BYTESTREAM: - return - self.send_socks5_info(file_props, fast = True) - raise common.xmpp.NodeProcessed - - def _siSetCB(self, con, iq_obj): - log.debug('_siSetCB') - jid = unicode(iq_obj.getFrom()) - si = iq_obj.getTag('si') - profile = si.getAttr('profile') - mime_type = si.getAttr('mime-type') - if profile != common.xmpp.NS_FILE: - return - file_tag = si.getTag('file') - file_props = {'type': 'r'} - for attribute in file_tag.getAttrs(): - if attribute in ('name', 'size', 'hash', 'date'): - val = file_tag.getAttr(attribute) - if val is None: - continue - file_props[attribute] = val - file_desc_tag = file_tag.getTag('desc') - if file_desc_tag is not None: - file_props['desc'] = file_desc_tag.getData() - - if mime_type is not None: - file_props['mime-type'] = mime_type - our_jid = gajim.get_jid_from_account(self.name) - file_props['receiver'] = our_jid - file_props['sender'] = unicode(iq_obj.getFrom()) - file_props['request-id'] = unicode(iq_obj.getAttr('id')) - file_props['sid'] = unicode(si.getAttr('id')) - file_props['transfered_size'] = [] - gajim.socks5queue.add_file_props(self.name, file_props) - self.dispatch('FILE_REQUEST', (jid, file_props)) - raise common.xmpp.NodeProcessed - - def _siErrorCB(self, con, iq_obj): - log.debug('_siErrorCB') - si = iq_obj.getTag('si') - profile = si.getAttr('profile') - if profile != common.xmpp.NS_FILE: - return - id_ = iq_obj.getAttr('id') - if id_ not in self.files_props: - # no such jid - return - file_props = self.files_props[id_] - if file_props is None: - # file properties for jid is none - return - jid = unicode(iq_obj.getFrom()) - file_props['error'] = -3 - self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, '')) - raise common.xmpp.NodeProcessed class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream, -ConnectionCommands, connection_handlers.ConnectionHandlersBase): +ConnectionCommands, ConnectionPEP, ConnectionArchive, +connection_handlers.ConnectionHandlersBase): def __init__(self): ConnectionVcard.__init__(self) ConnectionBytestream.__init__(self) ConnectionCommands.__init__(self) + ConnectionArchive.__init__(self) connection_handlers.ConnectionHandlersBase.__init__(self) try: diff --git a/src/common/zeroconf/connection_zeroconf.py b/src/common/zeroconf/connection_zeroconf.py index 1a073ad38..1b43117c2 100644 --- a/src/common/zeroconf/connection_zeroconf.py +++ b/src/common/zeroconf/connection_zeroconf.py @@ -43,66 +43,27 @@ if os.name != 'nt': import getpass import gobject +from common.connection import CommonConnection from common import gajim from common import GnuPG from common.zeroconf import client_zeroconf from common.zeroconf import zeroconf from connection_handlers_zeroconf import * -class ConnectionZeroconf(ConnectionHandlersZeroconf): +class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf): '''Connection class''' def __init__(self, name): ConnectionHandlersZeroconf.__init__(self) # system username self.username = None - self.name = name self.server_resource = '' # zeroconf has no resource, fake an empty one - self.connected = 0 # offline - self.connection = None - self.gpg = None - self.USE_GPG = False - if gajim.HAVE_GPG: - self.USE_GPG = True - self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent')) self.is_zeroconf = True - self.privacy_rules_supported = False - self.blocked_list = [] - self.blocked_contacts = [] - self.blocked_groups = [] - self.blocked_all = False - self.status = '' - self.old_show = '' - self.priority = 0 - self.call_resolve_timeout = False - - self.time_to_reconnect = None - #self.new_account_info = None - self.bookmarks = [] - - #we don't need a password, but must be non-empty + # we don't need a password, but must be non-empty self.password = 'zeroconf' - self.autoconnect = False - self.sync_with_global_status = True - self.no_log_for = False - self.pep_supported = False - self.mood = {} - self.tune = {} - self.activity = {} - # Do we continue connection when we get roster (send presence,get vcard...) - self.continue_connect_info = None - if gajim.HAVE_GPG: - self.USE_GPG = True - self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent')) - - self.get_config_values_or_default() - - self.muc_jid = {} # jid of muc server for each transport type - self.vcard_supported = False - self.private_storage_supported = False - self.archive_pref_supported = False + CommonConnection.__init__(self, name) def get_config_values_or_default(self): ''' get name, host, port from config, or @@ -111,79 +72,63 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): if not gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name'): gajim.log.debug('Creating zeroconf account') gajim.config.add_per('accounts', gajim.ZEROCONF_ACC_NAME) - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'autoconnect', True) - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'no_log_for', '') - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'password', 'zeroconf') - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'sync_with_global_status', True) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'autoconnect', True) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'no_log_for', + '') + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'password', + 'zeroconf') + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'sync_with_global_status', True) - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port', 5298) - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'is_zeroconf', True) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'custom_port', 5298) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'is_zeroconf', True) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'use_ft_proxies', False) #XXX make sure host is US-ASCII self.host = unicode(socket.gethostname()) - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'hostname', self.host) - self.port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port') - self.autoconnect = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'autoconnect') - self.sync_with_global_status = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'sync_with_global_status') - self.no_log_for = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'no_log_for') - self.first = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_first_name') - self.last = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_last_name') - self.jabber_id = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_jabber_id') - self.email = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_email') + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'hostname', + self.host) + self.port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'custom_port') + self.autoconnect = gajim.config.get_per('accounts', + gajim.ZEROCONF_ACC_NAME, 'autoconnect') + self.sync_with_global_status = gajim.config.get_per('accounts', + gajim.ZEROCONF_ACC_NAME, 'sync_with_global_status') + self.first = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_first_name') + self.last = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_last_name') + self.jabber_id = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_jabber_id') + self.email = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_email') if not self.username: self.username = unicode(getpass.getuser()) - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name', self.username) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name', + self.username) else: - self.username = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name') + self.username = gajim.config.get_per('accounts', + gajim.ZEROCONF_ACC_NAME, 'name') # END __init__ - def dispatch(self, event, data): - gajim.interface.dispatch(event, self.name, data) + def check_jid(self, jid): + return jid def _reconnect(self): # Do not try to reco while we are already trying self.time_to_reconnect = None gajim.log.debug('reconnect') -# signed = self.get_signed_msg(self.status) self.disconnect() self.change_status(self.old_show, self.status) - def quit(self, kill_core): - if kill_core and self.connected > 1: - self.disconnect() - def disable_account(self): self.disconnect() - def test_gpg_passphrase(self, password): - self.gpg.passphrase = password - keyID = gajim.config.get_per('accounts', self.name, 'keyid') - signed = self.gpg.sign('test', keyID) - self.gpg.password = None - return signed != 'BAD_PASSPHRASE' - - def get_signed_msg(self, msg): - signed = '' - keyID = gajim.config.get_per('accounts', self.name, 'keyid') - if keyID and self.USE_GPG: - use_gpg_agent = gajim.config.get('use_gpg_agent') - if self.connected < 2 and self.gpg.passphrase is None and \ - not use_gpg_agent: - # We didn't set a passphrase - self.dispatch('ERROR', (_('OpenPGP passphrase was not given'), - #%s is the account name here - _('You will be connected to %s without OpenPGP.') % self.name)) - self.USE_GPG = False - elif self.gpg.passphrase is not None or use_gpg_agent: - signed = self.gpg.sign(msg, keyID) - if signed == 'BAD_PASSPHRASE': - self.USE_GPG = False - signed = '' - if self.connected < 2: - self.dispatch('BAD_PASSPHRASE', ()) - return signed - def _on_resolve_timeout(self): if self.connected: self.connection.resolve_all() @@ -200,8 +145,10 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): # callbacks called from zeroconf def _on_new_service(self, jid): self.roster.setItem(jid) - self.dispatch('ROSTER_INFO', (jid, self.roster.getName(jid), 'both', 'no', self.roster.getGroups(jid))) - self.dispatch('NOTIFY', (jid, self.roster.getStatus(jid), self.roster.getMessage(jid), 'local', 0, None, 0, None)) + self.dispatch('ROSTER_INFO', (jid, self.roster.getName(jid), 'both', 'no', + self.roster.getGroups(jid))) + self.dispatch('NOTIFY', (jid, self.roster.getStatus(jid), + self.roster.getMessage(jid), 'local', 0, None, 0, None)) def _on_remove_service(self, jid): self.roster.delItem(jid) @@ -209,15 +156,6 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): # keyID, timestamp, contact_nickname)) self.dispatch('NOTIFY', (jid, 'offline', '', 'local', 0, None, 0, None)) - def _on_disconnected(self): - self.disconnect() - self.dispatch('STATUS', 'offline') - self.dispatch('CONNECTION_LOST', - (_('Connection with account "%s" has been lost') % self.name, - _('To continue sending and receiving messages, you will need to reconnect.'))) - self.status = 'offline' - self.disconnect() - def _disconnectedReconnCB(self): '''Called when we are disconnected. Comes from network manager for example we don't try to reconnect, network manager will tell us when we can''' @@ -237,9 +175,10 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): self.dispatch('ZC_NAME_CONFLICT', alt_name) def _on_error(self, message): - self.dispatch('ERROR', (_('Avahi error'), _("%s\nLink-local messaging might not work properly.") % message)) + self.dispatch('ERROR', (_('Avahi error'), + _('%s\nLink-local messaging might not work properly.') % message)) - def connect(self, show = 'online', msg = ''): + def connect(self, show='online', msg=''): self.get_config_values_or_default() if not self.connection: self.connection = client_zeroconf.ClientZeroconf(self) @@ -270,10 +209,12 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): self.roster = self.connection.getRoster() self.dispatch('ROSTER', self.roster) - #display contacts already detected and resolved + # display contacts already detected and resolved for jid in self.roster.keys(): - self.dispatch('ROSTER_INFO', (jid, self.roster.getName(jid), 'both', 'no', self.roster.getGroups(jid))) - self.dispatch('NOTIFY', (jid, self.roster.getStatus(jid), self.roster.getMessage(jid), 'local', 0, None, 0, None)) + self.dispatch('ROSTER_INFO', (jid, self.roster.getName(jid), 'both', + 'no', self.roster.getGroups(jid))) + self.dispatch('NOTIFY', (jid, self.roster.getStatus(jid), + self.roster.getMessage(jid), 'local', 0, None, 0, None)) self.connected = STATUS_LIST.index(show) @@ -282,7 +223,7 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): gobject.timeout_add_seconds(5, self._on_resolve_timeout) return True - def disconnect(self, on_purpose = False): + def disconnect(self, on_purpose=False): self.connected = 0 self.time_to_reconnect = None if self.connection: @@ -294,15 +235,20 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): def reannounce(self): if self.connected: txt = {} - txt['1st'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_first_name') - txt['last'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_last_name') - txt['jid'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_jabber_id') - txt['email'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_email') + txt['1st'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_first_name') + txt['last'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_last_name') + txt['jid'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_jabber_id') + txt['email'] = gajim.config.get_per('accounts', + gajim.ZEROCONF_ACC_NAME, 'zeroconf_email') self.connection.reannounce(txt) def update_details(self): if self.connection: - port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port') + port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'custom_port') if port != self.port: self.port = port last_msg = self.connection.last_msg @@ -314,41 +260,18 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): else: self.reannounce() - def change_status(self, show, msg, sync = False, auto = False): - if not show in STATUS_LIST: - return -1 - self.status = show - - check = True #to check for errors from zeroconf - # 'connect' - if show != 'offline' and not self.connected: - if not self.connect(show, msg): - return - if show != 'invisible': - check = self.connection.announce() - else: - self.connected = STATUS_LIST.index(show) - self.dispatch('SIGNED_IN', ()) - - # 'disconnect' - elif show == 'offline' and self.connected: - self.disconnect() - self.time_to_reconnect = None - - # update status - elif show != 'offline' and self.connected: - was_invisible = self.connected == STATUS_LIST.index('invisible') + def connect_and_init(self, show, msg, sign_msg): + # to check for errors from zeroconf + check = True + if not self.connect(show, msg): + return + if show != 'invisible': + check = self.connection.announce() + else: self.connected = STATUS_LIST.index(show) - if show == 'invisible': - check = check and self.connection.remove_announce() - elif was_invisible: - if not self.connected: - check = check and self.connect(show, msg) - check = check and self.connection.announce() - if self.connection and not show == 'invisible': - check = check and self.connection.set_show_msg(show, msg) + self.dispatch('SIGNED_IN', ()) - #stay offline when zeroconf does something wrong + # stay offline when zeroconf does something wrong if check: self.dispatch('STATUS', show) else: @@ -359,136 +282,63 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): (_('Could not change status of account "%s"') % self.name, _('Please check if avahi-daemon is running.'))) - def get_status(self): - return STATUS_LIST[self.connected] + def _change_to_invisible(self, msg): + if self.connection.remove_announce(): + self.dispatch('STATUS', 'invisible') + else: + # show notification that avahi or system bus is down + self.dispatch('STATUS', 'offline') + self.status = 'offline' + self.dispatch('CONNECTION_LOST', + (_('Could not change status of account "%s"') % self.name, + _('Please check if avahi-daemon is running.'))) + + def _change_from_invisible(self): + self.connection.announce() + + def _update_status(self, show, msg): + if self.connection.set_show_msg(show, msg): + self.dispatch('STATUS', show) + else: + # show notification that avahi or system bus is down + self.dispatch('STATUS', 'offline') + self.status = 'offline' + self.dispatch('CONNECTION_LOST', + (_('Could not change status of account "%s"') % self.name, + _('Please check if avahi-daemon is running.'))) def send_message(self, jid, msg, keyID, type_='chat', subject='', chatstate=None, msg_id=None, composing_xep=None, resource=None, user_nick=None, xhtml=None, session=None, forward_from=None, form_node=None, original_message=None, delayed=None, callback=None, callback_args=[]): - fjid = jid - - if msg and not xhtml and gajim.config.get( - 'rst_formatting_outgoing_messages'): - from common.rst_xhtml_generator import create_xhtml - xhtml = create_xhtml(msg) - if not self.connection: - return - if not msg and chatstate is None: - return - - if self.status in ('invisible', 'offline'): - self.dispatch('MSGERROR', [unicode(jid), -1, - _('You are not connected or not visible to others. Your message ' - 'could not be sent.'), None, None, session]) - return - - msgtxt = msg - msgenc = '' - if keyID and self.USE_GPG: - if keyID == 'UNKNOWN': - error = _('Neither the remote presence is signed, nor a key was assigned.') - elif keyID.endswith('MISMATCH'): - error = _('The contact\'s key (%s) does not match the key assigned in Gajim.' % keyID[:8]) - else: - # encrypt - msgenc, error = self.gpg.encrypt(msg, [keyID]) - if msgenc and not error: - msgtxt = '[This message is encrypted]' - lang = os.getenv('LANG') - if lang is not None or lang != 'en': # we're not english - msgtxt = _('[This message is encrypted]') +\ - ' ([This message is encrypted])' # one in locale and one en - else: - # Encryption failed, do not send message - tim = time.localtime() - self.dispatch('MSGNOTSENT', (jid, error, msgtxt, tim, session)) - return - - if type_ == 'chat': - msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ=type_, - xhtml=xhtml) - - else: - if subject: - msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal', - subject=subject, xhtml=xhtml) - else: - msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal', - xhtml=xhtml) - - if msgenc: - msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc) - - # chatstates - if peer supports jep85 or jep22, send chatstates - # please note that the only valid tag inside a message containing a - # tag is the active event - if chatstate is not None: - if composing_xep == 'XEP-0085' or not composing_xep: - # JEP-0085 - msg_iq.setTag(chatstate, namespace = common.xmpp.NS_CHATSTATES) - if composing_xep == 'XEP-0022' or not composing_xep: - # JEP-0022 - chatstate_node = msg_iq.setTag('x', namespace = common.xmpp.NS_EVENT) - if not msgtxt: # when no , add - if not msg_id: # avoid putting 'None' in tag - msg_id = '' - chatstate_node.setTagData('id', msg_id) - # when msgtxt, requests JEP-0022 composing notification - if chatstate is 'composing' or msgtxt: - chatstate_node.addChild(name = 'composing') - - if forward_from: - addresses = msg_iq.addChild('addresses', - namespace=common.xmpp.NS_ADDRESS) - addresses.addChild('address', attrs = {'type': 'ofrom', - 'jid': forward_from}) - - # XEP-0203 - if delayed: - our_jid = gajim.get_jid_from_account(self.name) + '/' + \ - self.server_resource - timestamp = time.strftime('%Y-%m-%dT%TZ', time.gmtime(delayed)) - msg_iq.addChild('delay', namespace=common.xmpp.NS_DELAY2, - attrs={'from': our_jid, 'stamp': timestamp}) - - if session: - session.last_send = time.time() - msg_iq.setThread(session.thread_id) - - if session.enable_encryption: - msg_iq = session.encrypt_stanza(msg_iq) - - def on_send_ok(id): - no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for') - ji = gajim.get_jid_without_resource(jid) - if session.is_loggable() and self.name not in no_log_for and\ - ji not in no_log_for: - log_msg = msg - if subject: - log_msg = _('Subject: %(subject)s\n%(message)s') % \ - {'subject': subject, 'message': msg} - if log_msg: - if type_ == 'chat': - kind = 'chat_msg_sent' - else: - kind = 'single_msg_sent' - gajim.logger.write(kind, jid, log_msg) + def on_send_ok(msg_id): self.dispatch('MSGSENT', (jid, msg, keyID)) - if callback: - callback(id, *callback_args) + callback(msg_id, *callback_args) + + self.log_message(jid, msg, forward_from, session, original_message, + subject, type_) def on_send_not_ok(reason): reason += ' ' + _('Your message could not be sent.') self.dispatch('MSGERROR', [jid, -1, reason, None, None, session]) - ret = self.connection.send(msg_iq, msg is not None, on_ok=on_send_ok, - on_not_ok=on_send_not_ok) - if ret == -1: - # Contact Offline - self.dispatch('MSGERROR', [jid, -1, _('Contact is offline. Your message could not be sent.'), None, None, session]) + def cb(jid, msg, keyID, forward_from, session, original_message, subject, + type_, msg_iq): + ret = self.connection.send(msg_iq, msg is not None, on_ok=on_send_ok, + on_not_ok=on_send_not_ok) + + if ret == -1: + # Contact Offline + self.dispatch('MSGERROR', [jid, -1, _('Contact is offline. Your ' + 'message could not be sent.'), None, None, session]) + + self._prepare_message(jid, msg, keyID, type_=type_, subject=subject, + chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep, + resource=resource, user_nick=user_nick, xhtml=xhtml, session=session, + forward_from=forward_from, form_node=form_node, + original_message=original_message, delayed=delayed, callback=cb) def send_stanza(self, stanza): # send a stanza untouched @@ -498,95 +348,10 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): stanza = common.xmpp.Protocol(node=stanza) self.connection.send(stanza) - def ack_subscribed(self, jid): - gajim.log.debug('This should not happen (ack_subscribed)') - - def ack_unsubscribed(self, jid): - gajim.log.debug('This should not happen (ack_unsubscribed)') - - def request_subscription(self, jid, msg = '', name = '', groups = [], - auto_auth = False): - gajim.log.debug('This should not happen (request_subscription)') - - def send_authorization(self, jid): - gajim.log.debug('This should not happen (send_authorization)') - - def refuse_authorization(self, jid): - gajim.log.debug('This should not happen (refuse_authorization)') - - def unsubscribe(self, jid, remove_auth = True): - gajim.log.debug('This should not happen (unsubscribe)') - - def unsubscribe_agent(self, agent): - gajim.log.debug('This should not happen (unsubscribe_agent)') - - def update_contact(self, jid, name, groups): - if self.connection: - self.connection.getRoster().setItem(jid = jid, name = name, - groups = groups) - - def update_contacts(self, contacts): - '''update multiple roster items''' - if self.connection: - self.connection.getRoster().setItemMulti(contacts) - - def new_account(self, name, config, sync = False): - gajim.log.debug('This should not happen (new_account)') - - def _on_new_account(self, con = None, con_type = None): - gajim.log.debug('This should not happen (_on_new_account)') - - def account_changed(self, new_name): - self.name = new_name - - def request_last_status_time(self, jid, resource): - gajim.log.debug('This should not happen (request_last_status_time)') - - def request_os_info(self, jid, resource): - gajim.log.debug('This should not happen (request_os_info)') - - def get_settings(self): - gajim.log.debug('This should not happen (get_settings)') - - def get_bookmarks(self): - gajim.log.debug('This should not happen (get_bookmarks)') - - def store_bookmarks(self): - gajim.log.debug('This should not happen (store_bookmarks)') - - def get_metacontacts(self): - gajim.log.debug('This should not happen (get_metacontacts)') - - def send_agent_status(self, agent, ptype): - gajim.log.debug('This should not happen (send_agent_status)') - - def gpg_passphrase(self, passphrase): - if self.gpg: - use_gpg_agent = gajim.config.get('use_gpg_agent') - if use_gpg_agent: - self.gpg.passphrase = None - else: - self.gpg.passphrase = passphrase - - def ask_gpg_keys(self): - if self.gpg: - keys = self.gpg.get_keys() - return keys - return None - - def ask_gpg_secrete_keys(self): - if self.gpg: - keys = self.gpg.get_secret_keys() - return keys - return None - def _event_dispatcher(self, realm, event, data): + CommonConnection._event_dispatcher(self, realm, event, data) if realm == '': - if event == common.xmpp.transports_nb.DATA_RECEIVED: - self.dispatch('STANZA_ARRIVED', unicode(data, errors = 'ignore')) - elif event == common.xmpp.transports_nb.DATA_SENT: - self.dispatch('STANZA_SENT', unicode(data)) - elif event == common.xmpp.transports.DATA_ERROR: + if event == common.xmpp.transports_nb.DATA_ERROR: thread_id = data[1] frm = unicode(data[0]) session = self.get_or_create_session(frm, thread_id) @@ -594,9 +359,6 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): _('Connection to host could not be established: Timeout while ' 'sending data.'), None, None, session]) - def load_roster_from_db(self): - return - # END ConnectionZeroconf # vim: se ts=3: diff --git a/src/config.py b/src/config.py index a8e3b0902..adf5078c5 100644 --- a/src/config.py +++ b/src/config.py @@ -63,17 +63,23 @@ from common.exceptions import GajimGeneralException #---------- PreferencesWindow class -------------# class PreferencesWindow: - '''Class for Preferences window''' + """ + Class for Preferences window + """ def on_preferences_window_destroy(self, widget): - '''close window''' + """ + Close window + """ del gajim.interface.instances['preferences'] def on_close_button_clicked(self, widget): self.window.destroy() def __init__(self): - '''Initialize Preferences window''' + """ + Initialize Preferences window + """ self.xml = gtkgui_helpers.get_glade('preferences_window.glade') self.window = self.xml.get_widget('preferences_window') self.window.set_transient_for(gajim.interface.roster.window) @@ -297,8 +303,6 @@ class PreferencesWindow: systray_combobox.set_active(1) else: systray_combobox.set_active(2) - if not gajim.interface.systray_capabilities: - systray_combobox.set_sensitive(False) # sounds if gajim.config.get('sounds_on'): @@ -497,8 +501,10 @@ class PreferencesWindow: self.window.hide() def get_per_account_option(self, opt): - '''Return the value of the option opt if it's the same in all accounts - else returns "mixed"''' + """ + Return the value of the option opt if it's the same in all accounts else + returns "mixed" + """ if len(gajim.connections) == 0: # a non existant key return default value return gajim.config.get_per('accounts', '__default__', opt) @@ -587,7 +593,9 @@ class PreferencesWindow: self.toggle_emoticons() def toggle_emoticons(self): - '''Update emoticons state in Opened Chat Windows''' + """ + Update emoticons state in Opened Chat Windows + """ for win in gajim.interface.msg_win_mgr.windows(): win.toggle_emoticons() @@ -781,7 +789,9 @@ class PreferencesWindow: self.sounds_preferences.window.present() def update_text_tags(self): - '''Update color tags in Opened Chat Windows''' + """ + Update color tags in opened chat windows + """ for win in gajim.interface.msg_win_mgr.windows(): win.update_tags() @@ -802,7 +812,9 @@ class PreferencesWindow: gajim.interface.save_config() def update_text_font(self): - '''Update text font in Opened Chat Windows''' + """ + Update text font in opened chat windows + """ for win in gajim.interface.msg_win_mgr.windows(): win.update_font() @@ -881,7 +893,9 @@ class PreferencesWindow: gajim.interface.save_config() def _set_color(self, state, widget_name, option): - ''' set color value in prefs and update the UI ''' + """ + Set color value in prefs and update the UI + """ if state: color = self.xml.get_widget(widget_name).get_color() color_string = gtkgui_helpers.make_color_string(color) @@ -1348,7 +1362,10 @@ class ManageProxiesWindow: #---------- AccountsWindow class -------------# class AccountsWindow: - '''Class for accounts window: list of accounts''' + """ + Class for accounts window: list of accounts + """ + def on_accounts_window_destroy(self, widget): del gajim.interface.instances['accounts'] @@ -1416,7 +1433,9 @@ class AccountsWindow: iter_ = model.iter_next(iter_) def init_accounts(self): - '''initialize listStore with existing accounts''' + """ + Initialize listStore with existing accounts + """ self.remove_button.set_sensitive(False) self.rename_button.set_sensitive(False) self.current_account = None @@ -1442,7 +1461,9 @@ class AccountsWindow: elif self.need_relogin and self.current_account and \ gajim.connections[self.current_account].connected > 0: def login(account, show_before, status_before): - ''' login with previous status''' + """ + Login with previous status + """ # first make sure connection is really closed, # 0.5 may not be enough gajim.connections[account].disconnect(True) @@ -1475,7 +1496,9 @@ class AccountsWindow: self.resend_presence = False def on_accounts_treeview_cursor_changed(self, widget): - '''Activate modify buttons when a row is selected, update accounts info''' + """ + Activate modify buttons when a row is selected, update accounts info + """ sel = self.accounts_treeview.get_selection() (model, iter_) = sel.get_selected() if iter_: @@ -1741,7 +1764,9 @@ class AccountsWindow: gajim.config.get_per('accounts', account, 'use_ft_proxies')) def on_add_button_clicked(self, widget): - '''When add button is clicked: open an account information window''' + """ + When add button is clicked: open an account information window + """ if 'account_creation_wizard' in gajim.interface.instances: gajim.interface.instances['account_creation_wizard'].window.present() else: @@ -1749,8 +1774,10 @@ class AccountsWindow: AccountCreationWizardWindow() def on_remove_button_clicked(self, widget): - '''When delete button is clicked: - Remove an account from the listStore and from the config file''' + """ + When delete button is clicked: Remove an account from the listStore and + from the config file + """ if not self.current_account: return account = self.current_account @@ -2411,8 +2438,11 @@ class AccountsWindow: 'zeroconf_email', email) class FakeDataForm(gtk.Table, object): - '''Class for forms that are in XML format value1 - infos in a table {entry1: value1, }''' + """ + Class for forms that are in XML format value1 infos in a + table {entry1: value1} + """ + def __init__(self, infos): gtk.Table.__init__(self) self.infos = infos @@ -2420,7 +2450,9 @@ class FakeDataForm(gtk.Table, object): self._draw_table() def _draw_table(self): - '''Draw the table''' + """ + Draw the table + """ nbrow = 0 if 'instructions' in self.infos: nbrow = 1 @@ -2454,9 +2486,11 @@ class FakeDataForm(gtk.Table, object): return self.infos class ServiceRegistrationWindow: - '''Class for Service registration window: - Window that appears when we want to subscribe to a service - if is_form we use dataforms_widget else we use service_registarion_window''' + """ + Class for Service registration window. Window that appears when we want to + subscribe to a service if is_form we use dataforms_widget else we use + service_registarion_window + """ def __init__(self, service, infos, account, is_form): self.service = service self.account = account @@ -2503,7 +2537,7 @@ class ServiceRegistrationWindow: self.window.destroy() class GroupchatConfigWindow: - '''GroupchatConfigWindow class''' + def __init__(self, account, room_jid, form = None): self.account = account self.room_jid = room_jid @@ -2656,7 +2690,9 @@ class GroupchatConfigWindow: self.remove_button[affiliation].set_sensitive(True) def affiliation_list_received(self, users_dict): - '''Fill the affiliation treeview''' + """ + Fill the affiliation treeview + """ for jid in users_dict: affiliation = users_dict[jid]['affiliation'] if affiliation not in self.affiliation_labels.keys(): @@ -2705,8 +2741,10 @@ class GroupchatConfigWindow: #---------- RemoveAccountWindow class -------------# class RemoveAccountWindow: - '''ask for removing from gajim only or from gajim and server too - and do removing of the account given''' + """ + Ask for removing from gajim only or from gajim and server too and do + removing of the account given + """ def on_remove_account_window_destroy(self, widget): if self.account in gajim.interface.instances: @@ -2906,7 +2944,9 @@ class ManageBookmarksWindow: del gajim.interface.instances['manage_bookmarks'] def on_add_bookmark_button_clicked(self, widget): - '''Add a new bookmark.''' + """ + Add a new bookmark + """ # Get the account that is currently used # (the parent of the currently selected item) (model, iter_) = self.selection.get_selected() @@ -2931,9 +2971,9 @@ class ManageBookmarksWindow: self.view.set_cursor(model.get_path(iter_)) def on_remove_bookmark_button_clicked(self, widget): - ''' - Remove selected bookmark. - ''' + """ + Remove selected bookmark + """ (model, iter_) = self.selection.get_selected() if not iter_: # Nothing selected return @@ -2946,9 +2986,9 @@ class ManageBookmarksWindow: self.clear_fields() def check_valid_bookmark(self): - ''' - Check if all neccessary fields are entered correctly. - ''' + """ + Check if all neccessary fields are entered correctly + """ (model, iter_) = self.selection.get_selected() if not model.iter_parent(iter_): @@ -2965,10 +3005,10 @@ class ManageBookmarksWindow: return True def on_ok_button_clicked(self, widget): - ''' - Parse the treestore data into our new bookmarks array, - then send the new bookmarks to the server. - ''' + """ + Parse the treestore data into our new bookmarks array, then send the new + bookmarks to the server. + """ (model, iter_) = self.selection.get_selected() if iter_ and model.iter_parent(iter_): #bookmark selected, check it @@ -2999,9 +3039,9 @@ class ManageBookmarksWindow: self.window.destroy() def bookmark_selected(self, selection): - ''' + """ Fill in the bookmark's data into the fields. - ''' + """ (model, iter_) = selection.get_selected() if not iter_: @@ -3148,6 +3188,7 @@ class AccountCreationWizardWindow: self.xml = gtkgui_helpers.get_glade( 'account_creation_wizard_window.glade') self.window = self.xml.get_widget('account_creation_wizard_window') + self.window.set_transient_for(gajim.interface.roster.window) completion = gtk.EntryCompletion() # Connect events from comboboxentry.child @@ -3445,8 +3486,10 @@ class AccountCreationWizardWindow: return True # loop forever def new_acc_connected(self, form, is_form, ssl_msg, ssl_err, ssl_cert, - ssl_fingerprint): - '''connection to server succeded, present the form to the user.''' + ssl_fingerprint): + """ + Connection to server succeded, present the form to the user + """ if self.update_progressbar_timeout_id is not None: gobject.source_remove(self.update_progressbar_timeout_id) self.back_button.show() @@ -3480,7 +3523,9 @@ class AccountCreationWizardWindow: self.notebook.set_current_page(4) # show form page def new_acc_not_connected(self, reason): - '''Account creation failed: connection to server failed''' + """ + Account creation failed: connection to server failed + """ if self.account not in gajim.connections: return if self.update_progressbar_timeout_id is not None: @@ -3500,7 +3545,9 @@ class AccountCreationWizardWindow: self.notebook.set_current_page(6) # show finish page def acc_is_ok(self, config): - '''Account creation succeeded''' + """ + Account creation succeeded + """ self.create_vars(config) self.show_finish_page() @@ -3508,7 +3555,9 @@ class AccountCreationWizardWindow: gobject.source_remove(self.update_progressbar_timeout_id) def acc_is_not_ok(self, reason): - '''Account creation failed''' + """ + Account creation failed + """ self.back_button.show() self.cancel_button.show() self.go_online_checkbutton.hide() diff --git a/src/conversation_textview.py b/src/conversation_textview.py index 837e2a0ce..7744ef685 100644 --- a/src/conversation_textview.py +++ b/src/conversation_textview.py @@ -67,13 +67,15 @@ def has_focus(widget): class TextViewImage(gtk.Image): - def __init__(self, anchor): + def __init__(self, anchor, text): super(TextViewImage, self).__init__() self.anchor = anchor self._selected = False self._disconnect_funcs = [] self.connect('parent-set', self.on_parent_set) self.connect('expose-event', self.on_expose) + self.set_tooltip_text(text) + self.anchor.set_data('plaintext', text) def _get_selected(self): parent = self.get_parent() @@ -156,8 +158,10 @@ class TextViewImage(gtk.Image): class ConversationTextview(gobject.GObject): - '''Class for the conversation textview (where user reads already said - messages) for chat/groupchat windows''' + """ + Class for the conversation textview (where user reads already said messages) + for chat/groupchat windows + """ __gsignals__ = dict( quote = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, # return value @@ -175,8 +179,10 @@ class ConversationTextview(gobject.GObject): SCROLL_DELAY = 33 # milliseconds def __init__(self, account, used_in_history_window = False): - '''if used_in_history_window is True, then we do not show - Clear menuitem in context menu''' + """ + If used_in_history_window is True, then we do not show Clear menuitem in + context menu + """ gobject.GObject.__init__(self) self.used_in_history_window = used_in_history_window @@ -640,8 +646,9 @@ class ConversationTextview(gobject.GObject): return False def on_textview_motion_notify_event(self, widget, event): - '''change the cursor to a hand when we are over a mail or an - url''' + """ + Change the cursor to a hand when we are over a mail or an url + """ pointer_x, pointer_y = self.tv.window.get_pointer()[0:2] x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer_x, pointer_y) @@ -686,7 +693,9 @@ class ConversationTextview(gobject.GObject): self.change_cursor = True def clear(self, tv = None): - '''clear text in the textview''' + """ + Clear text in the textview + """ buffer_ = self.tv.get_buffer() start, end = buffer_.get_bounds() buffer_.delete(start, end) @@ -696,15 +705,18 @@ class ConversationTextview(gobject.GObject): self.focus_out_end_mark = None def visit_url_from_menuitem(self, widget, link): - '''basically it filters out the widget instance''' + """ + Basically it filters out the widget instance + """ helpers.launch_browser_mailer('url', link) def on_textview_populate_popup(self, textview, menu): - '''we override the default context menu and we prepend Clear - (only if used_in_history_window is False) - and if we have sth selected we show a submenu with actions on - the phrase (see on_conversation_textview_button_press_event)''' - + """ + Override the default context menu and we prepend Clear (only if + used_in_history_window is False) and if we have sth selected we show a + submenu with actions on the phrase (see + on_conversation_textview_button_press_event) + """ separator_menuitem_was_added = False if not self.used_in_history_window: item = gtk.SeparatorMenuItem() @@ -969,13 +981,13 @@ class ConversationTextview(gobject.GObject): def detect_and_print_special_text(self, otext, other_tags, graphics=True): - '''detects special text (emots & links & formatting) - prints normal text before any special text it founts, - then print special text (that happens many times until - last special text is printed) and then returns the index + """ + Detect special text (emots & links & formatting), print normal text + before any special text it founds, then print special text (that happens + many times until last special text is printed) and then return the index after *last* special text, so we can print it in - print_conversation_line()''' - + print_conversation_line() + """ buffer_ = self.tv.get_buffer() insert_tags_func = buffer_.insert_with_tags_by_name @@ -1021,8 +1033,10 @@ class ConversationTextview(gobject.GObject): return buffer_.get_end_iter() def print_special_text(self, special_text, other_tags, graphics=True): - '''is called by detect_and_print_special_text and prints - special text (emots, links, formatting)''' + """ + Is called by detect_and_print_special_text and prints special text + (emots, links, formatting) + """ tags = [] use_other_tags = True text_is_valid_uri = False @@ -1043,7 +1057,7 @@ class ConversationTextview(gobject.GObject): emot_ascii = possible_emot_ascii_caps end_iter = buffer_.get_end_iter() anchor = buffer_.create_child_anchor(end_iter) - img = TextViewImage(anchor) + img = TextViewImage(anchor, special_text) animations = gajim.interface.emoticons_animations if not emot_ascii in animations: animations[emot_ascii] = gtk.gdk.PixbufAnimation( @@ -1161,9 +1175,12 @@ class ConversationTextview(gobject.GObject): buffer_.insert_with_tags_by_name(end_iter, '\n', 'eol') def print_conversation_line(self, text, jid, kind, name, tim, - other_tags_for_name=[], other_tags_for_time=[], other_tags_for_text=[], - subject=None, old_kind=None, xhtml=None, simple=False, graphics=True): - '''prints 'chat' type messages''' + other_tags_for_name=[], other_tags_for_time=[], + other_tags_for_text=[], subject=None, old_kind=None, xhtml=None, + simple=False, graphics=True): + """ + Print 'chat' type messages + """ buffer_ = self.tv.get_buffer() buffer_.begin_user_action() if self.marks_queue.full(): @@ -1261,8 +1278,10 @@ class ConversationTextview(gobject.GObject): buffer_.end_user_action() def get_time_to_show(self, tim): - '''Get the time, with the day before if needed and return it. - It DOESN'T format a fuzzy time''' + """ + Get the time, with the day before if needed and return it. It DOESN'T + format a fuzzy time + """ format = '' # get difference in days since epoch (86400 = 24*3600) # number of days since epoch for current time (in GMT) - @@ -1315,8 +1334,10 @@ class ConversationTextview(gobject.GObject): self.print_empty_line() def print_real_text(self, text, text_tags=[], name=None, xhtml=None, - graphics=True): - '''this adds normal and special text. call this to add text''' + graphics=True): + """ + Add normal and special text. call this to add text + """ if xhtml: try: if name and (text.startswith('/me ') or text.startswith('/me\n')): diff --git a/src/dataforms_widget.py b/src/dataforms_widget.py index 18195dccd..4a9681dab 100644 --- a/src/dataforms_widget.py +++ b/src/dataforms_widget.py @@ -38,7 +38,10 @@ import itertools class DataFormWidget(gtk.Alignment, object): # "public" interface - ''' Data Form widget. Use like any other widget. ''' + """ + Data Form widget. Use like any other widget + """ + def __init__(self, dataformnode=None): ''' Create a widget. ''' gtk.Alignment.__init__(self, xscale=1.0, yscale=1.0) @@ -65,7 +68,9 @@ class DataFormWidget(gtk.Alignment, object): selection.set_mode(gtk.SELECTION_MULTIPLE) def set_data_form(self, dataform): - ''' Set the data form (xmpp.DataForm) displayed in widget. ''' + """ + Set the data form (xmpp.DataForm) displayed in widget + """ assert isinstance(dataform, dataforms.DataForm) self.del_data_form() @@ -84,7 +89,9 @@ class DataFormWidget(gtk.Alignment, object): gtkgui_helpers.label_set_autowrap(self.instructions_label) def get_data_form(self): - ''' Data form displayed in the widget or None if no form. ''' + """ + Data form displayed in the widget or None if no form + """ return self._data_form def del_data_form(self): @@ -95,8 +102,10 @@ class DataFormWidget(gtk.Alignment, object): 'Data form presented in a widget') def get_title(self): - ''' Get the title of data form, as a unicode object. If no - title or no form, returns u''. Useful for setting window title. ''' + """ + Get the title of data form, as a unicode object. If no title or no form, + returns u''. Useful for setting window title + """ if self._data_form is not None: if self._data_form.title is not None: return self._data_form.title @@ -117,9 +126,11 @@ class DataFormWidget(gtk.Alignment, object): pass def clean_data_form(self): - '''Remove data about existing form. This metod is empty, because - it is rewritten by build_*_data_form, according to type of form - which is actually displayed.''' + """ + Remove data about existing form. This metod is empty, because it is + rewritten by build_*_data_form, according to type of form which is + actually displayed + """ pass def build_single_data_form(self): @@ -138,14 +149,18 @@ class DataFormWidget(gtk.Alignment, object): self.clean_data_form = self.clean_single_data_form def clean_single_data_form(self): - '''(Called as clean_data_form, read the docs of clean_data_form()). - Remove form from widget.''' + """ + Called as clean_data_form, read the docs of clean_data_form(). Remove + form from widget + """ self.singleform.destroy() self.clean_data_form = self.empty_method # we won't call it twice del self.singleform def build_multiple_data_form(self): - '''Invoked when new multiple form is to be created.''' + """ + Invoked when new multiple form is to be created + """ assert isinstance(self._data_form, dataforms.MultipleDataForm) self.clean_data_form() @@ -196,13 +211,17 @@ class DataFormWidget(gtk.Alignment, object): self.refresh_multiple_buttons() def clean_multiple_data_form(self): - '''(Called as clean_data_form, read the docs of clean_data_form()). - Remove form from widget.''' + """ + Called as clean_data_form, read the docs of clean_data_form(). Remove + form from widget + """ self.clean_data_form = self.empty_method # we won't call it twice del self.multiplemodel def refresh_multiple_buttons(self): - ''' Checks for treeview state and makes control buttons sensitive.''' + """ + Checks for treeview state and makes control buttons sensitive + """ selection = self.records_treeview.get_selection() model = self.records_treeview.get_model() count = selection.count_selected_rows() @@ -273,9 +292,12 @@ class DataFormWidget(gtk.Alignment, object): self.refresh_multiple_buttons() class SingleForm(gtk.Table, object): - ''' Widget that represent DATAFORM_SINGLE mode form. Because this is used - not only to display single forms, but to form input windows of multiple-type - forms, it is in another class.''' + """ + Widget that represent DATAFORM_SINGLE mode form. Because this is used not + only to display single forms, but to form input windows of multiple-type + forms, it is in another class + """ + def __init__(self, dataform): assert isinstance(dataform, dataforms.SimpleDataForm) @@ -284,9 +306,11 @@ class SingleForm(gtk.Table, object): self.set_row_spacings(6) def decorate_with_tooltip(widget, field): - ''' Adds a tooltip containing field's description to a widget. - Creates EventBox if widget doesn't have its own gdk window. - Returns decorated widget. ''' + """ + Adds a tooltip containing field's description to a widget. Creates + EventBox if widget doesn't have its own gdk window. Returns decorated + widget + """ if field.description != '': if widget.flags() & gtk.NO_WINDOW: evbox = gtk.EventBox() diff --git a/src/dialogs.py b/src/dialogs.py index d52c41770..1060b2dd7 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -61,9 +61,14 @@ from common import dataforms from common.exceptions import GajimGeneralException class EditGroupsDialog: - '''Class for the edit group dialog window''' + """ + Class for the edit group dialog window + """ + def __init__(self, list_): - '''list_ is a list of (contact, account) tuples''' + """ + list_ is a list of (contact, account) tuples + """ self.xml = gtkgui_helpers.get_glade('edit_groups_dialog.glade') self.dialog = self.xml.get_widget('edit_groups_dialog') self.dialog.set_transient_for(gajim.interface.roster.window) @@ -96,7 +101,9 @@ class EditGroupsDialog: self.dialog.destroy() def remove_group(self, group): - '''remove group group from all contacts and all their brothers''' + """ + Remove group group from all contacts and all their brothers + """ for (contact, account) in self.list_: gajim.interface.roster.remove_contact_from_groups(contact.jid, account, [group]) @@ -104,7 +111,9 @@ class EditGroupsDialog: gajim.interface.roster.draw_group(_('General'), account) def add_group(self, group): - '''add group group to all contacts and all their brothers''' + """ + Add group group to all contacts and all their brothers + """ for (contact, account) in self.list_: gajim.interface.roster.add_contact_to_groups(contact.jid, account, [group]) @@ -199,7 +208,9 @@ class EditGroupsDialog: column.set_attributes(renderer, active=1, inconsistent=2) class PassphraseDialog: - '''Class for Passphrase dialog''' + """ + Class for Passphrase dialog + """ def __init__(self, titletext, labeltext, checkbuttontext=None, ok_handler=None, cancel_handler=None): self.xml = gtkgui_helpers.get_glade('passphrase_dialog.glade') @@ -258,7 +269,10 @@ class PassphraseDialog: self.cancel_handler() class ChooseGPGKeyDialog: - '''Class for GPG key dialog''' + """ + Class for GPG key dialog + """ + def __init__(self, title_text, prompt_text, secret_keys, on_response, selected=None): '''secret_keys : {keyID: userName, ...}''' @@ -428,9 +442,9 @@ class ChangeActivityDialog: self.subactivity = data[1] def on_ok_button_clicked(self, widget): - ''' + """ Return activity and messsage (None if no activity selected) - ''' + """ if self.checkbutton.get_active(): self.on_response(self.activity, self.subactivity, self.entry.get_text().decode('utf-8')) @@ -524,10 +538,10 @@ class ChangeMoodDialog: self.window.destroy() class TimeoutDialog: - ''' + """ Class designed to be derivated to create timeout'd dialogs (dialogs that closes automatically after a timeout) - ''' + """ def __init__(self, timeout, on_timeout): self.countdown_left = timeout self.countdown_enabled = True @@ -540,7 +554,9 @@ class TimeoutDialog: gobject.timeout_add_seconds(1, self.countdown) def on_timeout(): - '''To be implemented in derivated classes''' + """ + To be implemented in derivated classes + """ pass def countdown(self): @@ -638,7 +654,9 @@ class ChangeStatusMessageDialog(TimeoutDialog): self.dialog.show_all() def draw_activity(self): - '''Set activity button''' + """ + Set activity button + """ img = self.xml.get_widget('activity_image') label = self.xml.get_widget('activity_button_label') if 'activity' in self.pep_dict and self.pep_dict['activity'] in \ @@ -661,7 +679,9 @@ class ChangeStatusMessageDialog(TimeoutDialog): label.set_text('') def draw_mood(self): - '''Set mood button''' + """ + Set mood button + """ img = self.xml.get_widget('mood_image') label = self.xml.get_widget('mood_button_label') if self.pep_dict['mood'] in pep.MOODS: @@ -803,13 +823,17 @@ class ChangeStatusMessageDialog(TimeoutDialog): self.pep_dict['mood_text']) class AddNewContactWindow: - '''Class for AddNewContactWindow''' + """ + Class for AddNewContactWindow + """ + uid_labels = {'jabber': _('Jabber ID:'), 'aim': _('AIM Address:'), 'gadu-gadu': _('GG Number:'), 'icq': _('ICQ Number:'), 'msn': _('MSN Address:'), 'yahoo': _('Yahoo! Address:')} + def __init__(self, account=None, jid=None, user_nick=None, group=None): self.account = account if account is None: @@ -983,11 +1007,15 @@ _('Please fill in the data of the contact you want to add in account %s') %accou self.window.destroy() def on_cancel_button_clicked(self, widget): - '''When Cancel button is clicked''' + """ + When Cancel button is clicked + """ self.window.destroy() def on_add_button_clicked(self, widget): - '''When Subscribe button is clicked''' + """ + When Subscribe button is clicked + """ jid = self.uid_entry.get_text().decode('utf-8').strip() if not jid: return @@ -1111,7 +1139,10 @@ _('Please fill in the data of the contact you want to add in account %s') %accou self.add_button.set_sensitive(False) class AboutDialog: - '''Class for about dialog''' + """ + Class for about dialog + """ + def __init__(self): dlg = gtk.AboutDialog() dlg.set_transient_for(gajim.interface.roster.window) @@ -1184,7 +1215,9 @@ class AboutDialog: return str_[0:-1] # remove latest . def get_path(self, filename): - '''where can we find this Credits file ?''' + """ + Where can we find this Credits file? + """ if os.path.isfile(os.path.join(gajim.defs.docdir, filename)): return os.path.join(gajim.defs.docdir, filename) elif os.path.isfile('../' + filename): @@ -1273,14 +1306,18 @@ class HigDialog(gtk.MessageDialog): self.destroy() def popup(self): - '''show dialog''' + """ + Show dialog + """ vb = self.get_children()[0].get_children()[0] # Give focus to top vbox vb.set_flags(gtk.CAN_FOCUS) vb.grab_focus() self.show_all() class FileChooserDialog(gtk.FileChooserDialog): - '''Non-blocking FileChooser Dialog around gtk.FileChooserDialog''' + """ + Non-blocking FileChooser Dialog around gtk.FileChooserDialog + """ def __init__(self, title_text, action, buttons, default_response, select_multiple = False, current_folder = None, on_response_ok = None, on_response_cancel = None): @@ -1332,7 +1369,10 @@ class AspellDictError: gajim.config.set('use_speller', False) class ConfirmationDialog(HigDialog): - '''HIG compliant confirmation dialog.''' + """ + HIG compliant confirmation dialog + """ + def __init__(self, pritext, sectext='', on_response_ok=None, on_response_cancel=None): self.user_response_ok = on_response_ok @@ -1359,7 +1399,10 @@ class ConfirmationDialog(HigDialog): self.destroy() class NonModalConfirmationDialog(HigDialog): - '''HIG compliant non modal confirmation dialog.''' + """ + HIG compliant non modal confirmation dialog + """ + def __init__(self, pritext, sectext='', on_response_ok=None, on_response_cancel=None): self.user_response_ok = on_response_ok @@ -1386,8 +1429,11 @@ class NonModalConfirmationDialog(HigDialog): self.destroy() class WarningDialog(HigDialog): + """ + HIG compliant warning dialog + """ + def __init__(self, pritext, sectext=''): - '''HIG compliant warning dialog.''' HigDialog.__init__( self, None, gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, pritext, sectext) self.set_modal(False) @@ -1396,8 +1442,11 @@ class WarningDialog(HigDialog): self.popup() class InformationDialog(HigDialog): + """ + HIG compliant info dialog + """ + def __init__(self, pritext, sectext=''): - '''HIG compliant info dialog.''' HigDialog.__init__(self, None, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, pritext, sectext) self.set_modal(False) @@ -1405,16 +1454,22 @@ class InformationDialog(HigDialog): self.popup() class ErrorDialog(HigDialog): + """ + HIG compliant error dialog + """ + def __init__(self, pritext, sectext=''): - '''HIG compliant error dialog.''' HigDialog.__init__( self, None, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, pritext, sectext) self.popup() class YesNoDialog(HigDialog): + """ + HIG compliant YesNo dialog + """ + def __init__(self, pritext, sectext='', checktext='', on_response_yes=None, - on_response_no=None): - '''HIG compliant YesNo dialog.''' + on_response_no=None): self.user_response_yes = on_response_yes self.user_response_no = on_response_no HigDialog.__init__( self, None, @@ -1448,15 +1503,20 @@ class YesNoDialog(HigDialog): self.destroy() def is_checked(self): - ''' Get active state of the checkbutton ''' + """ + Get active state of the checkbutton + """ if not self.checkbutton: return False return self.checkbutton.get_active() class ConfirmationDialogCheck(ConfirmationDialog): - '''HIG compliant confirmation dialog with checkbutton.''' - def __init__(self, pritext, sectext='', checktext='', - on_response_ok=None, on_response_cancel=None, is_modal=True): + """ + HIG compliant confirmation dialog with checkbutton + """ + + def __init__(self, pritext, sectext='', checktext='', on_response_ok=None, + on_response_cancel=None, is_modal=True): self.user_response_ok = on_response_ok self.user_response_cancel = on_response_cancel @@ -1495,11 +1555,16 @@ class ConfirmationDialogCheck(ConfirmationDialog): self.destroy() def is_checked(self): - ''' Get active state of the checkbutton ''' + """ + Get active state of the checkbutton + """ return self.checkbutton.get_active() class ConfirmationDialogDubbleCheck(ConfirmationDialog): - '''HIG compliant confirmation dialog with 2 checkbuttons.''' + """ + HIG compliant confirmation dialog with 2 checkbuttons + """ + def __init__(self, pritext, sectext='', checktext1='', checktext2='', on_response_ok=None, on_response_cancel=None, is_modal=True): self.user_response_ok = on_response_ok @@ -1560,7 +1625,10 @@ class ConfirmationDialogDubbleCheck(ConfirmationDialog): return [is_checked_1, is_checked_2] class FTOverwriteConfirmationDialog(ConfirmationDialog): - '''HIG compliant confirmation dialog to overwrite or resume a file transfert''' + """ + HIG compliant confirmation dialog to overwrite or resume a file transfert + """ + def __init__(self, pritext, sectext='', propose_resume=True, on_response=None): HigDialog.__init__(self, None, gtk.MESSAGE_QUESTION, gtk.BUTTONS_CANCEL, @@ -1597,7 +1665,10 @@ class FTOverwriteConfirmationDialog(ConfirmationDialog): self.destroy() class CommonInputDialog: - '''Common Class for Input dialogs''' + """ + Common Class for Input dialogs + """ + def __init__(self, title, label_str, is_modal, ok_handler, cancel_handler): self.dialog = self.xml.get_widget('input_dialog') label = self.xml.get_widget('label') @@ -1633,9 +1704,12 @@ class CommonInputDialog: self.dialog.destroy() class InputDialog(CommonInputDialog): - '''Class for Input dialog''' + """ + Class for Input dialog + """ + def __init__(self, title, label_str, input_str=None, is_modal=True, - ok_handler=None, cancel_handler=None): + ok_handler=None, cancel_handler=None): self.xml = gtkgui_helpers.get_glade('input_dialog.glade') CommonInputDialog.__init__(self, title, label_str, is_modal, ok_handler, cancel_handler) @@ -1651,9 +1725,12 @@ class InputDialog(CommonInputDialog): return self.input_entry.get_text().decode('utf-8') class InputDialogCheck(InputDialog): - '''Class for Input dialog''' + """ + Class for Input dialog + """ + def __init__(self, title, label_str, checktext='', input_str=None, - is_modal=True, ok_handler=None, cancel_handler=None): + is_modal=True, ok_handler=None, cancel_handler=None): self.xml = gtkgui_helpers.get_glade('input_dialog.glade') InputDialog.__init__(self, title, label_str, input_str=input_str, is_modal=is_modal, ok_handler=ok_handler, @@ -1683,7 +1760,9 @@ class InputDialogCheck(InputDialog): return self.input_entry.get_text().decode('utf-8') def is_checked(self): - ''' Get active state of the checkbutton ''' + """ + Get active state of the checkbutton + """ try: return self.checkbutton.get_active() except Exception: @@ -1691,7 +1770,10 @@ class InputDialogCheck(InputDialog): return False class ChangeNickDialog(InputDialogCheck): - '''Class for changing room nickname in case of conflict''' + """ + Class for changing room nickname in case of conflict + """ + def __init__(self, account, room_jid, title, prompt, check_text=None): InputDialogCheck.__init__(self, title, '', checktext=check_text, input_str='', is_modal=True, ok_handler=None, cancel_handler=None) @@ -1773,7 +1855,10 @@ class ChangeNickDialog(InputDialogCheck): self.room_queue.append((account, room_jid, prompt)) class InputTextDialog(CommonInputDialog): - '''Class for multilines Input dialog (more place than InputDialog)''' + """ + Class for multilines Input dialog (more place than InputDialog) + """ + def __init__(self, title, label_str, input_str=None, is_modal=True, ok_handler=None, cancel_handler=None): self.xml = gtkgui_helpers.get_glade('input_text_dialog.glade') @@ -1790,7 +1875,10 @@ class InputTextDialog(CommonInputDialog): return self.input_buffer.get_text(start_iter, end_iter).decode('utf-8') class DubbleInputDialog: - '''Class for Dubble Input dialog''' + """ + Class for Dubble Input dialog + """ + def __init__(self, title, label_str1, label_str2, input_str1=None, input_str2=None, is_modal=True, ok_handler=None, cancel_handler=None): self.xml = gtkgui_helpers.get_glade('dubbleinput_dialog.glade') @@ -1876,7 +1964,9 @@ class SubscriptionRequestWindow: self.window.destroy() def on_authorize_button_clicked(self, widget): - '''accept the request''' + """ + Accept the request + """ gajim.connections[self.account].send_authorization(self.jid) self.window.destroy() contact = gajim.contacts.get_contact(self.account, self.jid) @@ -1884,7 +1974,9 @@ class SubscriptionRequestWindow: AddNewContactWindow(self.account, self.jid, self.user_nick) def on_contact_info_activate(self, widget): - '''ask vcard''' + """ + Ask vcard + """ if self.jid in gajim.interface.instances[self.account]['infos']: gajim.interface.instances[self.account]['infos'][self.jid].window.present() else: @@ -1896,11 +1988,15 @@ class SubscriptionRequestWindow: get_widget('information_notebook').remove_page(0) def on_start_chat_activate(self, widget): - '''open chat''' + """ + Open chat + """ gajim.interface.new_chat_from_jid(self.account, self.jid) def on_deny_button_clicked(self, widget): - '''refuse the request''' + """ + Refuse the request + """ gajim.connections[self.account].refuse_authorization(self.jid) contact = gajim.contacts.get_contact(self.account, self.jid) if contact and _('Not in Roster') in contact.get_shown_groups(): @@ -1908,7 +2004,9 @@ class SubscriptionRequestWindow: self.window.destroy() def on_actions_button_clicked(self, widget): - '''popup action menu''' + """ + Popup action menu + """ menu = self.prepare_popup_menu() menu.show_all() gtkgui_helpers.popup_emoticons_under_button(menu, widget, self.window.window) @@ -1916,11 +2014,12 @@ class SubscriptionRequestWindow: class JoinGroupchatWindow: def __init__(self, account=None, room_jid='', nick='', password='', - automatic=False): - '''automatic is a dict like {'invities': []} - If automatic is not empty, this means room must be automaticaly configured - and when done, invities must be automatically invited''' - + automatic=False): + """ + Automatic is a dict like {'invities': []}. If automatic is not empty, + this means room must be automaticaly configured and when done, invities + must be automatically invited + """ if account: if room_jid != '' and room_jid in gajim.gc_connected[account] and\ gajim.gc_connected[account][room_jid]: @@ -2007,7 +2106,9 @@ class JoinGroupchatWindow: self.window.show_all() def on_join_groupchat_window_destroy(self, widget): - '''close window''' + """ + Close window + """ if self.account and 'join_gc' in gajim.interface.instances[self.account]: # remove us from open windows del gajim.interface.instances[self.account]['join_gc'] @@ -2039,7 +2140,9 @@ class JoinGroupchatWindow: self._room_jid_entry.set_text(room_jid) def on_cancel_button_clicked(self, widget): - '''When Cancel button is clicked''' + """ + When Cancel button is clicked + """ self.window.destroy() def on_bookmark_checkbutton_toggled(self, widget): @@ -2050,7 +2153,9 @@ class JoinGroupchatWindow: auto_join_checkbutton.set_sensitive(False) def on_join_button_clicked(self, widget): - '''When Join button is clicked''' + """ + When Join button is clicked + """ if not self.account: ErrorDialog(_('Invalid Account'), _('You have to choose an account from which you want to join the ' @@ -2138,7 +2243,9 @@ class SynchroniseSelectAccountDialog: self.window.destroy() def init_accounts(self): - '''initialize listStore with existing accounts''' + """ + Initialize listStore with existing accounts + """ model = self.accounts_treeview.get_model() model.clear() for remote_account in gajim.connections: @@ -2204,7 +2311,9 @@ class SynchroniseSelectContactsDialog: self.window.destroy() def init_contacts(self): - '''initialize listStore with existing accounts''' + """ + Initialize listStore with existing accounts + """ model = self.contacts_treeview.get_model() model.clear() @@ -2267,7 +2376,9 @@ class NewChatDialog(InputDialog): self.dialog.show_all() def new_chat_response(self, jid): - ''' called when ok button is clicked ''' + """ + Called when ok button is clicked + """ if gajim.connections[self.account].connected <= 1: #if offline or connecting ErrorDialog(_('Connection not available'), @@ -2433,10 +2544,10 @@ class PopupNotificationWindow: self.adjust_height_and_move_popup_notification_windows() class SingleMessageWindow: - '''SingleMessageWindow can send or show a received - singled message depending on action argument which can be 'send' - or 'receive'. - ''' + """ + SingleMessageWindow can send or show a received singled message depending on + action argument which can be 'send' or 'receive' + """ # Keep a reference on windows so garbage collector don't restroy them instances = [] def __init__(self, account, to='', action='', from_whom='', subject='', @@ -2843,10 +2954,16 @@ class XMLConsoleWindow: # it's expanded!! self.input_textview.grab_focus() +#Action that can be done with an incoming list of contacts +TRANSLATED_ACTION = {'add': _('add'), 'modify': _('modify'), + 'remove': _('remove')} class RosterItemExchangeWindow: - ''' Windows used when someone send you a exchange contact suggestion ''' + """ + Windows used when someone send you a exchange contact suggestion + """ + def __init__(self, account, action, exchange_list, jid_from, - message_body=None): + message_body=None): self.account = account self.action = action self.exchange_list = exchange_list @@ -2866,12 +2983,13 @@ class RosterItemExchangeWindow: # Set labels # self.action can be 'add', 'modify' or 'remove' - self.type_label.set_label(\ - _('%s would like you to %s some contacts in your ' - 'roster.') % (jid_from, _(self.action))) + self.type_label.set_label( + _('%(jid)s would like you to %(action)s some contacts ' + 'in your roster.') % {'jid': jid_from, + 'action': TRANSLATED_ACTION[self.action]}) if message_body: - buffer = self.body_textview.get_buffer() - buffer.set_text(self.message_body) + buffer_ = self.body_textview.get_buffer() + buffer_.set_text(self.message_body) else: self.body_scrolledwindow.hide() # Treeview @@ -2924,8 +3042,8 @@ class RosterItemExchangeWindow: groups = groups + group + ', ' if not is_in_roster: show_dialog = True - iter = model.append() - model.set(iter, 0, True, 1, jid, 2, name, 3, groups) + iter_ = model.append() + model.set(iter_, 0, True, 1, jid, 2, name, 3, groups) # Change label for accept_button to action name instead of 'OK'. self.accept_button_label.set_label(_('Add')) @@ -2955,8 +3073,8 @@ class RosterItemExchangeWindow: groups = groups + group + ', ' if not is_right and is_in_roster: show_dialog = True - iter = model.append() - model.set(iter, 0, True, 1, jid, 2, name, 3, groups) + iter_ = model.append() + model.set(iter_, 0, True, 1, jid, 2, name, 3, groups) # Change label for accept_button to action name instead of 'OK'. self.accept_button_label.set_label(_('Modify')) @@ -2979,8 +3097,8 @@ class RosterItemExchangeWindow: groups = groups + group + ', ' if is_in_roster: show_dialog = True - iter = model.append() - model.set(iter, 0, True, 1, jid, 2, name, 3, groups) + iter_ = model.append() + model.set(iter_, 0, True, 1, jid, 2, name, 3, groups) # Change label for accept_button to action name instead of 'OK'. self.accept_button_label.set_label(_('Delete')) @@ -2991,49 +3109,49 @@ class RosterItemExchangeWindow: def toggled_callback(self, cell, path): model = self.items_list_treeview.get_model() - iter = model.get_iter(path) - model[iter][0] = not cell.get_active() + iter_ = model.get_iter(path) + model[iter_][0] = not cell.get_active() def on_accept_button_clicked(self, widget): model = self.items_list_treeview.get_model() - iter = model.get_iter_root() + iter_ = model.get_iter_root() if self.action == 'add': a = 0 - while iter: - if model[iter][0]: + while iter_: + if model[iter_][0]: a+=1 # it is selected - #remote_jid = model[iter][1].decode('utf-8') + #remote_jid = model[iter_][1].decode('utf-8') message = _('%s suggested me to add you in my roster.' % self.jid_from) # keep same groups and same nickname - groups = model[iter][3].split(', ') + groups = model[iter_][3].split(', ') if groups == ['']: groups = [] - jid = model[iter][1].decode('utf-8') + jid = model[iter_][1].decode('utf-8') if gajim.jid_is_transport(self.jid_from): gajim.connections[self.account].automatically_added.append( jid) gajim.interface.roster.req_sub(self, jid, message, - self.account, groups=groups, nickname=model[iter][2], + self.account, groups=groups, nickname=model[iter_][2], auto_auth=True) - iter = model.iter_next(iter) - InformationDialog('Added %s contacts' % str(a)) + iter_ = model.iter_next(iter_) + InformationDialog(_('Added %s contacts') % str(a)) elif self.action == 'modify': a = 0 - while iter: - if model[iter][0]: + while iter_: + if model[iter_][0]: a+=1 # it is selected - jid = model[iter][1].decode('utf-8') + jid = model[iter_][1].decode('utf-8') # keep same groups and same nickname - groups = model[iter][3].split(', ') + groups = model[iter_][3].split(', ') if groups == ['']: groups = [] for u in gajim.contacts.get_contact(self.account, jid): - u.name = model[iter][2] + u.name = model[iter_][2] gajim.connections[self.account].update_contact(jid, - model[iter][2], groups) + model[iter_][2], groups) self.draw_contact(jid, account) # Update opened chat ctrl = gajim.interface.msg_win_mgr.get_control(jid, self.account) @@ -3043,19 +3161,19 @@ class RosterItemExchangeWindow: self.account) win.redraw_tab(ctrl) win.show_title() - iter = model.iter_next(iter) + iter_ = model.iter_next(iter_) elif self.action == 'delete': a = 0 - while iter: - if model[iter][0]: + while iter_: + if model[iter_][0]: a+=1 # it is selected - jid = model[iter][1].decode('utf-8') + jid = model[iter_][1].decode('utf-8') gajim.connections[self.account].unsubscribe(jid) gajim.interface.roster.remove_contact(jid, self.account) gajim.contacts.remove_jid(self.account, jid) - iter = model.iter_next(iter) - InformationDialog('Removed %s contacts' % str(a)) + iter_ = model.iter_next(iter_) + InformationDialog(_('Removed %s contacts') % str(a)) self.window.destroy() def on_cancel_button_clicked(self, widget): @@ -3416,8 +3534,10 @@ class ArchivingPreferencesWindow: class PrivacyListWindow: - '''Window that is used for creating NEW or EDITING already there privacy - lists''' + """ + Window that is used for creating NEW or EDITING already there privacy lists + """ + def __init__(self, account, privacy_list_name, action): '''action is 'EDIT' or 'NEW' depending on if we create a new priv list or edit an already existing one''' @@ -3429,6 +3549,8 @@ class PrivacyListWindow: self.global_rules = {} self.list_of_groups = {} + self.max_order = 0 + # Default Edit Values self.edit_rule_type = 'jid' self.allow_deny = 'allow' @@ -3524,6 +3646,8 @@ class PrivacyListWindow: else: text_item = _('Order: %(order)s, action: %(action)s') % \ {'order': rule['order'], 'action': rule['action']} + if int(rule['order']) > self.max_order: + self.max_order = int(rule['order']) self.global_rules[text_item] = rule self.list_of_rules_combobox.append_text(text_item) if len(rules) == 0: @@ -3671,7 +3795,7 @@ class PrivacyListWindow: self.edit_view_status_checkbutton.set_active(False) self.edit_send_status_checkbutton.set_active(False) self.edit_all_checkbutton.set_active(False) - self.edit_order_spinbutton.set_value(1) + self.edit_order_spinbutton.set_value(self.max_order + 1) self.edit_type_group_combobox.set_active(0) self.edit_type_subscription_combobox.set_active(0) self.add_edit_rule_label.set_label( @@ -3714,6 +3838,8 @@ class PrivacyListWindow: def on_save_rule_button_clicked(self, widget): tags=[] current_tags = self.get_current_tags() + if int(current_tags['order']) > self.max_order: + self.max_order = int(current_tags['order']) if self.active_rule == '': tags.append(current_tags) @@ -3749,9 +3875,10 @@ class PrivacyListWindow: self.window.destroy() class PrivacyListsWindow: - '''Window that is the main window for Privacy Lists; - we can list there the privacy lists and ask to create a new one - or edit an already there one''' + """ + Window that is the main window for Privacy Lists; we can list there the + privacy lists and ask to create a new one or edit an already there one + """ def __init__(self, account): self.account = account self.privacy_lists_save = [] @@ -3908,9 +4035,10 @@ class InvitationReceivedDialog: class ProgressDialog: def __init__(self, title_text, during_text, messages_queue): - '''during text is what to show during the procedure, - messages_queue has the message to show - in the textview''' + """ + During text is what to show during the procedure, messages_queue has the + message to show in the textview + """ self.xml = gtkgui_helpers.get_glade('progress_dialog.glade') self.dialog = self.xml.get_widget('progress_dialog') self.label = self.xml.get_widget('label') @@ -3937,10 +4065,14 @@ class ProgressDialog: class SoundChooserDialog(FileChooserDialog): def __init__(self, path_to_snd_file='', on_response_ok=None, - on_response_cancel=None): - '''optionally accepts path_to_snd_file so it has that as selected''' + on_response_cancel=None): + """ + Optionally accepts path_to_snd_file so it has that as selected + """ def on_ok(widget, callback): - '''check if file exists and call callback''' + """ + Check if file exists and call callback + """ path_to_snd_file = self.get_filename() path_to_snd_file = gtkgui_helpers.decode_filechooser_file_paths( (path_to_snd_file,))[0] @@ -3976,8 +4108,10 @@ class SoundChooserDialog(FileChooserDialog): class ImageChooserDialog(FileChooserDialog): def __init__(self, path_to_file='', on_response_ok=None, - on_response_cancel=None): - '''optionally accepts path_to_snd_file so it has that as selected''' + on_response_cancel=None): + """ + Optionally accepts path_to_snd_file so it has that as selected + """ def on_ok(widget, callback): '''check if file exists and call callback''' path_to_file = self.get_filename() @@ -4068,8 +4202,10 @@ class AvatarChooserDialog(ImageChooserDialog): class AddSpecialNotificationDialog: def __init__(self, jid): - '''jid is the jid for which we want to add special notification - (sound and notification popups)''' + """ + jid is the jid for which we want to add special notification (sound and + notification popups) + """ self.xml = gtkgui_helpers.get_glade('add_special_notification_window.glade') self.window = self.xml.get_widget('add_special_notification_window') self.condition_combobox = self.xml.get_widget('condition_combobox') @@ -4189,7 +4325,9 @@ class AdvancedNotificationsWindow: self.window.show_all() def initiate_rule_state(self): - '''Set values for all widgets''' + """ + Set values for all widgets + """ if self.active_num < 0: return # event @@ -4580,8 +4718,10 @@ class TransformChatToMUC: # Keep a reference on windows so garbage collector don't restroy them instances = [] def __init__(self, account, jids, preselected=None): - '''This window is used to trasform a one-to-one chat to a MUC. - We do 2 things: first select the server and then make a guests list.''' + """ + This window is used to trasform a one-to-one chat to a MUC. We do 2 + things: first select the server and then make a guests list + """ self.instances.append(self) self.account = account @@ -4732,7 +4872,9 @@ class DataFormWindow(Dialog): self.destroy() class ESessionInfoWindow: - '''Class for displaying information about a XEP-0116 encrypted session''' + """ + Class for displaying information about a XEP-0116 encrypted session + """ def __init__(self, session): self.session = session @@ -4811,7 +4953,9 @@ class ESessionInfoWindow: YesNoDialog(pritext, sectext, on_response_yes=on_yes, on_response_no=on_no) class GPGInfoWindow: - '''Class for displaying information about a XEP-0116 encrypted session''' + """ + Class for displaying information about a XEP-0116 encrypted session + """ def __init__(self, control): xml = gtkgui_helpers.get_glade('esession_info_window.glade') security_image = xml.get_widget('security_image') diff --git a/src/disco.py b/src/disco.py index 0d3621f13..c38c7ae7f 100644 --- a/src/disco.py +++ b/src/disco.py @@ -124,16 +124,21 @@ _cat_to_descr = { class CacheDictionary: - '''A dictionary that keeps items around for only a specific time. - Lifetime is in minutes. Getrefresh specifies whether to refresh when - an item is merely accessed instead of set aswell.''' + """ + A dictionary that keeps items around for only a specific time. Lifetime is + in minutes. Getrefresh specifies whether to refresh when an item is merely + accessed instead of set aswell + """ + def __init__(self, lifetime, getrefresh = True): self.lifetime = lifetime * 1000 * 60 self.getrefresh = getrefresh self.cache = {} class CacheItem: - '''An object to store cache items and their timeouts.''' + """ + An object to store cache items and their timeouts + """ def __init__(self, value): self.value = value self.source = None @@ -149,13 +154,17 @@ class CacheDictionary: del self.cache[key] def _expire_timeout(self, key): - '''The timeout has expired, remove the object.''' + """ + The timeout has expired, remove the object + """ if key in self.cache: del self.cache[key] return False def _refresh_timeout(self, key): - '''The object was accessed, refresh the timeout.''' + """ + The object was accessed, refresh the timeout + """ item = self.cache[key] if item.source: gobject.source_remove(item.source) @@ -187,20 +196,25 @@ class CacheDictionary: _icon_cache = CacheDictionary(15) def get_agent_address(jid, node = None): - '''Returns an agent's address for displaying in the GUI.''' + """ + Get an agent's address for displaying in the GUI + """ if node: return '%s@%s' % (node, str(jid)) else: return str(jid) class Closure(object): - '''A weak reference to a callback with arguments as an object. + """ + A weak reference to a callback with arguments as an object Weak references to methods immediatly die, even if the object is still alive. Besides a handy way to store a callback, this provides a workaround that keeps a reference to the object instead. - Userargs and removeargs must be tuples.''' + Userargs and removeargs must be tuples. + """ + def __init__(self, cb, userargs = (), remove = None, removeargs = ()): self.userargs = userargs self.remove = remove @@ -229,8 +243,11 @@ class Closure(object): class ServicesCache: - '''Class that caches our query results. Each connection will have it's own - ServiceCache instance.''' + """ + Class that caches our query results. Each connection will have it's own + ServiceCache instance + """ + def __init__(self, account): self.account = account self._items = CacheDictionary(0, getrefresh = False) @@ -256,7 +273,9 @@ class ServicesCache: del self._cbs[cbkey] def get_icon(self, identities = []): - '''Return the icon for an agent.''' + """ + Return the icon for an agent + """ # Grab the first identity with an icon for identity in identities: try: @@ -284,7 +303,9 @@ class ServicesCache: return pix def get_browser(self, identities=[], features=[]): - '''Return the browser class for an agent.''' + """ + Return the browser class for an agent + """ # First pass, we try to find a ToplevelAgentBrowser for identity in identities: try: @@ -316,7 +337,9 @@ class ServicesCache: return None def get_info(self, jid, node, cb, force = False, nofetch = False, args = ()): - '''Get info for an agent.''' + """ + Get info for an agent + """ addr = get_agent_address(jid, node) # Check the cache if addr in self._info: @@ -338,7 +361,9 @@ class ServicesCache: gajim.connections[self.account].discoverInfo(jid, node) def get_items(self, jid, node, cb, force = False, nofetch = False, args = ()): - '''Get a list of items in an agent.''' + """ + Get a list of items in an agent + """ addr = get_agent_address(jid, node) # Check the cache if addr in self._items: @@ -360,7 +385,9 @@ class ServicesCache: gajim.connections[self.account].discoverItems(jid, node) def agent_info(self, jid, node, identities, features, data): - '''Callback for when we receive an agent's info.''' + """ + Callback for when we receive an agent's info + """ addr = get_agent_address(jid, node) # Store in cache @@ -376,7 +403,9 @@ class ServicesCache: del self._cbs[cbkey] def agent_items(self, jid, node, items): - '''Callback for when we receive an agent's items.''' + """ + Callback for when we receive an agent's items + """ addr = get_agent_address(jid, node) # Store in cache @@ -392,8 +421,10 @@ class ServicesCache: del self._cbs[cbkey] def agent_info_error(self, jid): - '''Callback for when a query fails. (even after the browse and agents - namespaces)''' + """ + Callback for when a query fails. Even after the browse and agents + namespaces + """ addr = get_agent_address(jid) # Call callbacks @@ -406,8 +437,10 @@ class ServicesCache: del self._cbs[cbkey] def agent_items_error(self, jid): - '''Callback for when a query fails. (even after the browse and agents - namespaces)''' + """ + Callback for when a query fails. Even after the browse and agents + namespaces + """ addr = get_agent_address(jid) # Call callbacks @@ -421,7 +454,10 @@ class ServicesCache: # object is needed so that @property works class ServiceDiscoveryWindow(object): - '''Class that represents the Services Discovery window.''' + """ + Class that represents the Services Discovery window + """ + def __init__(self, account, jid = '', node = '', address_entry = False, parent = None): self.account = account @@ -510,8 +546,10 @@ _('Without a connection, you can not browse available services')) self.browser.account = value def _initial_state(self): - '''Set some initial state on the window. Separated in a method because - it's handy to use within browser's cleanup method.''' + """ + Set some initial state on the window. Separated in a method because it's + handy to use within browser's cleanup method + """ self.progressbar.hide() title_text = _('Service Discovery using account %s') % self.account self.window.set_title(title_text) @@ -550,7 +588,9 @@ _('Without a connection, you can not browse available services')) self.banner.set_markup(markup) def paint_banner(self): - '''Repaint the banner with theme color''' + """ + Repaint the banner with theme color + """ theme = gajim.config.get('roster_theme') bgcolor = gajim.config.get_per('themes', theme, 'bannerbgcolor') textcolor = gajim.config.get_per('themes', theme, 'bannertextcolor') @@ -584,10 +624,11 @@ _('Without a connection, you can not browse available services')) self._on_style_set_event, set_fg, set_bg) def _on_style_set_event(self, widget, style, *opts): - ''' set style of widget from style class *.Frame.Eventbox + """ + Set style of widget from style class *.Frame.Eventbox opts[0] == True -> set fg color - opts[1] == True -> set bg color ''' - + opts[1] == True -> set bg color + """ self.disconnect_style_event() if opts[1]: bg_color = widget.style.bg[gtk.STATE_SELECTED] @@ -599,9 +640,11 @@ _('Without a connection, you can not browse available services')) self.connect_style_event(opts[0], opts[1]) def destroy(self, chain = False): - '''Close the browser. This can optionally close its children and - propagate to the parent. This should happen on actions like register, - or join to kill off the entire browser chain.''' + """ + Close the browser. This can optionally close its children and propagate + to the parent. This should happen on actions like register, or join to + kill off the entire browser chain + """ if self.dying: return self.dying = True @@ -632,7 +675,9 @@ _('Without a connection, you can not browse available services')) self.cache.cleanup() def travel(self, jid, node): - '''Travel to an agent within the current services window.''' + """ + Travel to an agent within the current services window + """ if self.browser: self.browser.cleanup() self.browser = None @@ -649,7 +694,9 @@ _('Without a connection, you can not browse available services')) self.cache.get_info(jid, node, self._travel) def _travel(self, jid, node, identities, features, data): - '''Continuation of travel.''' + """ + Continuation of travel + """ if self.dying or jid != self.jid or node != self.node: return if not identities: @@ -671,7 +718,9 @@ _('This type of service does not contain any items to browse.')) self.browser.browse() def open(self, jid, node): - '''Open an agent. By default, this happens in a new window.''' + """ + Open an agent. By default, this happens in a new window + """ try: win = gajim.interface.instances[self.account]['disco']\ [get_agent_address(jid, node)] @@ -737,10 +786,13 @@ _('This type of service does not contain any items to browse.')) class AgentBrowser: - '''Class that deals with browsing agents and appearance of the browser - window. This class and subclasses should basically be treated as "part" - of the ServiceDiscoveryWindow class, but had to be separated because this part - is dynamic.''' + """ + Class that deals with browsing agents and appearance of the browser window. + This class and subclasses should basically be treated as "part" of the + ServiceDiscoveryWindow class, but had to be separated because this part is + dynamic + """ + def __init__(self, account, jid, node): self.account = account self.jid = jid @@ -751,20 +803,26 @@ class AgentBrowser: self.active = False def _get_agent_address(self): - '''Returns the agent's address for displaying in the GUI.''' + """ + Get the agent's address for displaying in the GUI + """ return get_agent_address(self.jid, self.node) def _set_initial_title(self): - '''Set the initial window title based on agent address.''' + """ + Set the initial window title based on agent address + """ self.window.window.set_title(_('Browsing %(address)s using account ' '%(account)s') % {'address': self._get_agent_address(), 'account': self.account}) self.window._set_window_banner_text(self._get_agent_address()) def _create_treemodel(self): - '''Create the treemodel for the services treeview. When subclassing, - note that the first two columns should ALWAYS be of type string and - contain the JID and node of the item respectively.''' + """ + Create the treemodel for the services treeview. When subclassing, note + that the first two columns should ALWAYS be of type string and contain + the JID and node of the item respectively + """ # JID, node, name, address self.model = gtk.ListStore(str, str, str, str) self.model.set_sort_column_id(3, gtk.SORT_ASCENDING) @@ -792,8 +850,10 @@ class AgentBrowser: self.window.services_treeview.set_headers_visible(False) def _add_actions(self): - '''Add the action buttons to the buttonbox for actions the browser can - perform.''' + """ + Add the action buttons to the buttonbox for actions the browser can + perform + """ self.browse_button = gtk.Button() image = gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_BUTTON) label = gtk.Label(_('_Browse')) @@ -807,13 +867,17 @@ class AgentBrowser: self.browse_button.show_all() def _clean_actions(self): - '''Remove the action buttons specific to this browser.''' + """ + Remove the action buttons specific to this browser + """ if self.browse_button: self.browse_button.destroy() self.browse_button = None def _set_title(self, jid, node, identities, features, data): - '''Set the window title based on agent info.''' + """ + Set the window title based on agent info + """ # Set the banner and window title if 'name' in identities[0]: name = identities[0]['name'] @@ -830,8 +894,10 @@ class AgentBrowser: pass def prepare_window(self, window): - '''Prepare the service discovery window. Called when a browser is hooked - up with a ServiceDiscoveryWindow instance.''' + """ + Prepare the service discovery window. Called when a browser is hooked up + with a ServiceDiscoveryWindow instance + """ self.window = window self.cache = window.cache @@ -852,7 +918,9 @@ class AgentBrowser: self.cache.get_info(self.jid, self.node, self._set_title) def cleanup(self): - '''Cleanup when the window intends to switch browsers.''' + """ + Cleanup when the window intends to switch browsers + """ self.active = False self._clean_actions() @@ -862,12 +930,16 @@ class AgentBrowser: self.window._initial_state() def update_theme(self): - '''Called when the default theme is changed.''' + """ + Called when the default theme is changed + """ pass def on_browse_button_clicked(self, widget = None): - '''When we want to browse an agent: - Open a new services window with a browser for the agent type.''' + """ + When we want to browse an agent: open a new services window with a + browser for the agent type + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -877,8 +949,9 @@ class AgentBrowser: self.window.open(jid, node) def update_actions(self): - '''When we select a row: - activate action buttons based on the agent's info.''' + """ + When we select a row: activate action buttons based on the agent's info + """ if self.browse_button: self.browse_button.set_sensitive(False) model, iter_ = self.window.services_treeview.get_selection().get_selected() @@ -890,7 +963,9 @@ class AgentBrowser: self.cache.get_info(jid, node, self._update_actions, nofetch = True) def _update_actions(self, jid, node, identities, features, data): - '''Continuation of update_actions.''' + """ + Continuation of update_actions + """ if not identities or not self.browse_button: return klass = self.cache.get_browser(identities, features) @@ -898,8 +973,10 @@ class AgentBrowser: self.browse_button.set_sensitive(True) def default_action(self): - '''When we double-click a row: - perform the default action on the selected item.''' + """ + When we double-click a row: perform the default action on the selected + item + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -909,7 +986,9 @@ class AgentBrowser: self.cache.get_info(jid, node, self._default_action, nofetch = True) def _default_action(self, jid, node, identities, features, data): - '''Continuation of default_action.''' + """ + Continuation of default_action + """ if self.cache.get_browser(identities, features): # Browse if we can self.on_browse_button_clicked() @@ -917,7 +996,9 @@ class AgentBrowser: return False def browse(self, force = False): - '''Fill the treeview with agents, fetching the info if necessary.''' + """ + Fill the treeview with agents, fetching the info if necessary + """ self.model.clear() self._total_items = self._progress = 0 self.window.progressbar.show() @@ -926,15 +1007,19 @@ class AgentBrowser: force = force, args = (force,)) def _pulse_timeout_cb(self, *args): - '''Simple callback to keep the progressbar pulsing.''' + """ + Simple callback to keep the progressbar pulsing + """ if not self.active: return False self.window.progressbar.pulse() return True def _find_item(self, jid, node): - '''Check if an item is already in the treeview. Return an iter to it - if so, None otherwise.''' + """ + Check if an item is already in the treeview. Return an iter to it if so, + None otherwise + """ iter_ = self.model.get_iter_root() while iter_: cjid = self.model.get_value(iter_, 0).decode('utf-8') @@ -947,7 +1032,9 @@ class AgentBrowser: return None def _agent_items(self, jid, node, items, force): - '''Callback for when we receive a list of agent items.''' + """ + Callback for when we receive a list of agent items + """ self.model.clear() self._total_items = 0 gobject.source_remove(self._pulse_timeout) @@ -973,7 +1060,9 @@ _('This service does not contain any items to browse.')) self.window.services_treeview.set_model(self.model) def _agent_info(self, jid, node, identities, features, data): - '''Callback for when we receive info about an agent's item.''' + """ + Callback for when we receive info about an agent's item + """ iter_ = self._find_item(jid, node) if not iter_: # Not in the treeview, stop @@ -987,21 +1076,27 @@ _('This service does not contain any items to browse.')) self.update_actions() def _add_item(self, jid, node, parent_node, item, force): - '''Called when an item should be added to the model. The result of a - disco#items query.''' + """ + Called when an item should be added to the model. The result of a + disco#items query + """ self.model.append((jid, node, item.get('name', ''), get_agent_address(jid, node))) self.cache.get_info(jid, node, self._agent_info, force = force) def _update_item(self, iter_, jid, node, item): - '''Called when an item should be updated in the model. The result of a - disco#items query. (seldom)''' + """ + Called when an item should be updated in the model. The result of a + disco#items query + """ if 'name' in item: self.model[iter_][2] = item['name'] def _update_info(self, iter_, jid, node, identities, features, data): - '''Called when an item should be updated in the model with further info. - The result of a disco#info query.''' + """ + Called when an item should be updated in the model with further info. + The result of a disco#info query + """ name = identities[0].get('name', '') if name: self.model[iter_][2] = name @@ -1012,8 +1107,11 @@ _('This service does not contain any items to browse.')) class ToplevelAgentBrowser(AgentBrowser): - '''This browser is used at the top level of a jabber server to browse - services such as transports, conference servers, etc.''' + """ + This browser is used at the top level of a jabber server to browse services + such as transports, conference servers, etc + """ + def __init__(self, *args): AgentBrowser.__init__(self, *args) self._progressbar_sourceid = None @@ -1029,7 +1127,9 @@ class ToplevelAgentBrowser(AgentBrowser): self._scroll_signal = None def _pixbuf_renderer_data_func(self, col, cell, model, iter_): - '''Callback for setting the pixbuf renderer's properties.''' + """ + Callback for setting the pixbuf renderer's properties + """ jid = model.get_value(iter_, 0) if jid: pix = model.get_value(iter_, 2) @@ -1039,7 +1139,9 @@ class ToplevelAgentBrowser(AgentBrowser): cell.set_property('visible', False) def _text_renderer_data_func(self, col, cell, model, iter_): - '''Callback for setting the text renderer's properties.''' + """ + Callback for setting the text renderer's properties + """ jid = model.get_value(iter_, 0) markup = model.get_value(iter_, 3) state = model.get_value(iter_, 4) @@ -1060,7 +1162,9 @@ class ToplevelAgentBrowser(AgentBrowser): cell.set_property('foreground_set', False) def _treemodel_sort_func(self, model, iter1, iter2): - '''Sort function for our treemodel.''' + """ + Sort function for our treemode + """ # Compare state statecmp = cmp(model.get_value(iter1, 4), model.get_value(iter2, 4)) if statecmp == 0: @@ -1120,8 +1224,9 @@ class ToplevelAgentBrowser(AgentBrowser): self._show_tooltip, state) def on_treeview_event_hide_tooltip(self, widget, event): - ''' This happens on scroll_event, key_press_event - and button_press_event ''' + """ + This happens on scroll_event, key_press_event and button_press_event + """ self.tooltip.hide_tooltip() def _create_treemodel(self): @@ -1236,8 +1341,9 @@ class ToplevelAgentBrowser(AgentBrowser): AgentBrowser._clean_actions(self) def on_search_button_clicked(self, widget = None): - '''When we want to search something: - open search window''' + """ + When we want to search something: open search window + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -1261,8 +1367,9 @@ class ToplevelAgentBrowser(AgentBrowser): self.window.services_treeview.queue_draw() def on_execute_button_clicked(self, widget=None): - '''When we want to execute a command: - open adhoc command window''' + """ + When we want to execute a command: open adhoc command window + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -1271,9 +1378,10 @@ class ToplevelAgentBrowser(AgentBrowser): adhoc_commands.CommandWindow(self.account, service, commandnode=node) def on_register_button_clicked(self, widget = None): - '''When we want to register an agent: - request information about registering with the agent and close the - window.''' + """ + When we want to register an agent: request information about registering + with the agent and close the window + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -1283,8 +1391,10 @@ class ToplevelAgentBrowser(AgentBrowser): self.window.destroy(chain = True) def on_join_button_clicked(self, widget): - '''When we want to join an IRC room or create a new MUC room: - Opens the join_groupchat_window.''' + """ + When we want to join an IRC room or create a new MUC room: Opens the + join_groupchat_window + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -1375,7 +1485,9 @@ class ToplevelAgentBrowser(AgentBrowser): AgentBrowser.browse(self, force = force) def _expand_all(self): - '''Expand all items in the treeview''' + """ + Expand all items in the treeview + """ # GTK apparently screws up here occasionally. :/ #def expand_all(*args): # self.window.services_treeview.expand_all() @@ -1386,7 +1498,9 @@ class ToplevelAgentBrowser(AgentBrowser): self.window.services_treeview.expand_all() def _update_progressbar(self): - '''Update the progressbar.''' + """ + Update the progressbar + """ # Refresh this every update if self._progressbar_sourceid: gobject.source_remove(self._progressbar_sourceid) @@ -1408,13 +1522,17 @@ class ToplevelAgentBrowser(AgentBrowser): self.window.progressbar.set_fraction(fraction) def _hide_progressbar_cb(self, *args): - '''Simple callback to hide the progressbar a second after we finish.''' + """ + Simple callback to hide the progressbar a second after we finish + """ if self.active: self.window.progressbar.hide() return False def _friendly_category(self, category, type_=None): - '''Get the friendly category name and priority.''' + """ + Get the friendly category name and priority + """ cat = None if type_: # Try type-specific override @@ -1430,12 +1548,16 @@ class ToplevelAgentBrowser(AgentBrowser): return cat, prio def _create_category(self, cat, type_=None): - '''Creates a category row.''' + """ + Creates a category row + """ cat, prio = self._friendly_category(cat, type_) return self.model.append(None, ('', '', None, cat, prio)) def _find_category(self, cat, type_=None): - '''Looks up a category row and returns the iterator to it, or None.''' + """ + Looks up a category row and returns the iterator to it, or None + """ cat = self._friendly_category(cat, type_)[0] iter_ = self.model.get_iter_root() while iter_: @@ -1670,8 +1792,10 @@ class MucBrowser(AgentBrowser): _('You can manage your bookmarks via Actions menu in your roster.')) def on_join_button_clicked(self, *args): - '''When we want to join a conference: - Ask specific informations about the selected agent and close the window''' + """ + When we want to join a conference: ask specific informations about the + selected agent and close the window + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -1697,18 +1821,24 @@ class MucBrowser(AgentBrowser): self.on_join_button_clicked() def _start_info_query(self): - '''Idle callback to start checking for visible rows.''' + """ + Idle callback to start checking for visible rows + """ self._fetch_source = None self._query_visible() return False def on_scroll(self, *args): - '''Scrollwindow callback to trigger new queries on scolling.''' + """ + Scrollwindow callback to trigger new queries on scolling + """ # This apparently happens when inactive sometimes self._query_visible() def _query_visible(self): - '''Query the next visible row for info.''' + """ + Query the next visible row for info + """ if self._fetch_source: # We're already fetching return @@ -1751,9 +1881,10 @@ class MucBrowser(AgentBrowser): self._fetch_source = None def _channel_altinfo(self, jid, node, items, name = None): - '''Callback for the alternate disco#items query. We try to atleast get - the amount of users in the room if the service does not support MUC - dataforms.''' + """ + Callback for the alternate disco#items query. We try to atleast get the + amount of users in the room if the service does not support MUC dataforms + """ if items == 0: # The server returned an error self._broken += 1 @@ -1816,15 +1947,20 @@ class MucBrowser(AgentBrowser): self.cache.get_items(jid, node, self._channel_altinfo) def PubSubBrowser(account, jid, node): - ''' Returns an AgentBrowser subclass that will display service discovery - for particular pubsub service. Different pubsub services may need to - present different data during browsing. ''' + """ + Return an AgentBrowser subclass that will display service discovery for + particular pubsub service. Different pubsub services may need to present + different data during browsing + """ # for now, only discussion groups are supported... # TODO: check if it has appropriate features to be such kind of service return DiscussionGroupsBrowser(account, jid, node) class DiscussionGroupsBrowser(AgentBrowser): - ''' For browsing pubsub-based discussion groups service. ''' + """ + For browsing pubsub-based discussion groups service + """ + def __init__(self, account, jid, node): AgentBrowser.__init__(self, account, jid, node) @@ -1840,7 +1976,9 @@ class DiscussionGroupsBrowser(AgentBrowser): gajim.connections[account].send_pb_subscription_query(jid, self._subscriptionsCB) def _create_treemodel(self): - ''' Create treemodel for the window. ''' + """ + Create treemodel for the window + """ # JID, node, name (with description) - pango markup, dont have info?, subscribed? self.model = gtk.TreeStore(str, str, str, bool, bool) # sort by name @@ -1891,8 +2029,10 @@ class DiscussionGroupsBrowser(AgentBrowser): return self.in_list def _add_item(self, jid, node, parent_node, item, force): - ''' Called when we got basic information about new node from query. - Show the item. ''' + """ + Called when we got basic information about new node from query. Show the + item + """ name = item.get('name', '') if self.subscriptions is not None: @@ -1962,8 +2102,10 @@ class DiscussionGroupsBrowser(AgentBrowser): self.unsubscribe_button = None def update_actions(self): - '''Called when user selected a row. Make subscribe/unsubscribe buttons - sensitive appropriatelly.''' + """ + Called when user selected a row. Make subscribe/unsubscribe buttons + sensitive appropriatelly + """ # we have nothing to do if we don't have buttons... if self.subscribe_button is None: return @@ -1980,7 +2122,9 @@ class DiscussionGroupsBrowser(AgentBrowser): self.unsubscribe_button.set_sensitive(subscribed) def on_post_button_clicked(self, widget): - '''Called when 'post' button is pressed. Open window to create post''' + """ + Called when 'post' button is pressed. Open window to create post + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if iter_ is None: return @@ -1989,7 +2133,9 @@ class DiscussionGroupsBrowser(AgentBrowser): groups.GroupsPostWindow(self.account, self.jid, groupnode) def on_subscribe_button_clicked(self, widget): - '''Called when 'subscribe' button is pressed. Send subscribtion request.''' + """ + Called when 'subscribe' button is pressed. Send subscribtion request + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if iter_ is None: return @@ -1998,7 +2144,9 @@ class DiscussionGroupsBrowser(AgentBrowser): gajim.connections[self.account].send_pb_subscribe(self.jid, groupnode, self._subscribeCB, groupnode) def on_unsubscribe_button_clicked(self, widget): - '''Called when 'unsubscribe' button is pressed. Send unsubscription request.''' + """ + Called when 'unsubscribe' button is pressed. Send unsubscription request + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if iter_ is None: return @@ -2007,8 +2155,10 @@ class DiscussionGroupsBrowser(AgentBrowser): gajim.connections[self.account].send_pb_unsubscribe(self.jid, groupnode, self._unsubscribeCB, groupnode) def _subscriptionsCB(self, conn, request): - ''' We got the subscribed groups list stanza. Now, if we already - have items on the list, we should actualize them. ''' + """ + We got the subscribed groups list stanza. Now, if we already have items + on the list, we should actualize them + """ try: subscriptions = request.getTag('pubsub').getTag('subscriptions') except Exception: @@ -2036,7 +2186,9 @@ class DiscussionGroupsBrowser(AgentBrowser): raise xmpp.NodeProcessed def _subscribeCB(self, conn, request, groupnode): - '''We have just subscribed to a node. Update UI''' + """ + We have just subscribed to a node. Update UI + """ self.subscriptions.add(groupnode) model = self.window.services_treeview.get_model() @@ -2050,7 +2202,9 @@ class DiscussionGroupsBrowser(AgentBrowser): raise xmpp.NodeProcessed def _unsubscribeCB(self, conn, request, groupnode): - '''We have just unsubscribed from a node. Update UI''' + """ + We have just unsubscribed from a node. Update UI + """ self.subscriptions.remove(groupnode) model = self.window.services_treeview.get_model() diff --git a/src/eggtrayicon.c b/src/eggtrayicon.c deleted file mode 100644 index 56b3a0fb9..000000000 --- a/src/eggtrayicon.c +++ /dev/null @@ -1,584 +0,0 @@ -/* eggtrayicon.c - * Copyright (C) 2002 Anders Carlsson - * - * 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 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. - */ - -#include -#include -#include - -#include "eggtrayicon.h" - -#include -#if defined (GDK_WINDOWING_X11) -#include -#include -#elif defined (GDK_WINDOWING_WIN32) -#include -#endif - -#ifndef EGG_COMPILATION -#ifndef _ -#define _(x) dgettext (GETTEXT_PACKAGE, x) -#define N_(x) x -#endif -#else -#define _(x) x -#define N_(x) x -#endif - -#define SYSTEM_TRAY_REQUEST_DOCK 0 -#define SYSTEM_TRAY_BEGIN_MESSAGE 1 -#define SYSTEM_TRAY_CANCEL_MESSAGE 2 - -#define SYSTEM_TRAY_ORIENTATION_HORZ 0 -#define SYSTEM_TRAY_ORIENTATION_VERT 1 - -enum { - PROP_0, - PROP_ORIENTATION -}; - -static GtkPlugClass *parent_class = NULL; - -static void egg_tray_icon_init (EggTrayIcon *icon); -static void egg_tray_icon_class_init (EggTrayIconClass *klass); - -static void egg_tray_icon_get_property (GObject *object, - guint prop_id, - GValue *value, - GParamSpec *pspec); - -static void egg_tray_icon_add (GtkContainer *container, GtkWidget *widget); - -static void egg_tray_icon_realize (GtkWidget *widget); -static void egg_tray_icon_unrealize (GtkWidget *widget); - -#ifdef GDK_WINDOWING_X11 -static void egg_tray_icon_update_manager_window (EggTrayIcon *icon, - gboolean dock_if_realized); -static void egg_tray_icon_manager_window_destroyed (EggTrayIcon *icon); -#endif - -GType -egg_tray_icon_get_type (void) -{ - static GType our_type = 0; - - if (our_type == 0) - { - static const GTypeInfo our_info = - { - sizeof (EggTrayIconClass), - (GBaseInitFunc) NULL, - (GBaseFinalizeFunc) NULL, - (GClassInitFunc) egg_tray_icon_class_init, - NULL, /* class_finalize */ - NULL, /* class_data */ - sizeof (EggTrayIcon), - 0, /* n_preallocs */ - (GInstanceInitFunc) egg_tray_icon_init - }; - - our_type = g_type_register_static (GTK_TYPE_PLUG, "EggTrayIcon", &our_info, 0); - } - - return our_type; -} - -static void -egg_tray_icon_init (EggTrayIcon *icon) -{ - icon->stamp = 1; - icon->orientation = GTK_ORIENTATION_HORIZONTAL; - - gtk_widget_add_events (GTK_WIDGET (icon), GDK_PROPERTY_CHANGE_MASK); -} - -static void -egg_tray_icon_class_init (EggTrayIconClass *klass) -{ - GObjectClass *gobject_class = (GObjectClass *)klass; - GtkWidgetClass *widget_class = (GtkWidgetClass *)klass; - GtkContainerClass *container_class = (GtkContainerClass *)klass; - - parent_class = g_type_class_peek_parent (klass); - - gobject_class->get_property = egg_tray_icon_get_property; - - widget_class->realize = egg_tray_icon_realize; - widget_class->unrealize = egg_tray_icon_unrealize; - - container_class->add = egg_tray_icon_add; - - g_object_class_install_property (gobject_class, - PROP_ORIENTATION, - g_param_spec_enum ("orientation", - _("Orientation"), - _("The orientation of the tray."), - GTK_TYPE_ORIENTATION, - GTK_ORIENTATION_HORIZONTAL, - G_PARAM_READABLE)); - -#if defined (GDK_WINDOWING_X11) - /* Nothing */ -#elif defined (GDK_WINDOWING_WIN32) - g_warning ("Port eggtrayicon to Win32"); -#else - g_warning ("Port eggtrayicon to this GTK+ backend"); -#endif -} - -static void -egg_tray_icon_get_property (GObject *object, - guint prop_id, - GValue *value, - GParamSpec *pspec) -{ - EggTrayIcon *icon = EGG_TRAY_ICON (object); - - switch (prop_id) - { - case PROP_ORIENTATION: - g_value_set_enum (value, icon->orientation); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -#ifdef GDK_WINDOWING_X11 - -static Display * -egg_tray_icon_get_x_display(EggTrayIcon *icon) -{ - Display *xdisplay = NULL; - - GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (icon)); - if (!GDK_IS_DISPLAY (display)) - display = gdk_display_get_default (); - - xdisplay = GDK_DISPLAY_XDISPLAY (display); - - return xdisplay; -} - -static void -egg_tray_icon_get_orientation_property (EggTrayIcon *icon) -{ - Display *xdisplay; - Atom type; - int format; - union { - gulong *prop; - guchar *prop_ch; - } prop = { NULL }; - gulong nitems; - gulong bytes_after; - int error, result; - - g_assert (icon->manager_window != None); - - xdisplay = egg_tray_icon_get_x_display(icon); - if (xdisplay == NULL) - return; - - gdk_error_trap_push (); - type = None; - result = XGetWindowProperty (xdisplay, - icon->manager_window, - icon->orientation_atom, - 0, G_MAXLONG, FALSE, - XA_CARDINAL, - &type, &format, &nitems, - &bytes_after, &(prop.prop_ch)); - error = gdk_error_trap_pop (); - - if (error || result != Success) - return; - - if (type == XA_CARDINAL) - { - GtkOrientation orientation; - - orientation = (prop.prop [0] == SYSTEM_TRAY_ORIENTATION_HORZ) ? - GTK_ORIENTATION_HORIZONTAL : - GTK_ORIENTATION_VERTICAL; - - if (icon->orientation != orientation) - { - icon->orientation = orientation; - - g_object_notify (G_OBJECT (icon), "orientation"); - } - } - - if (prop.prop) - XFree (prop.prop); -} - -static GdkFilterReturn -egg_tray_icon_manager_filter (GdkXEvent *xevent, GdkEvent *event, gpointer user_data) -{ - EggTrayIcon *icon = user_data; - XEvent *xev = (XEvent *)xevent; - - if (xev->xany.type == ClientMessage && - xev->xclient.message_type == icon->manager_atom && - xev->xclient.data.l[1] == icon->selection_atom) - { - egg_tray_icon_update_manager_window (icon, TRUE); - } - else if (xev->xany.window == icon->manager_window) - { - if (xev->xany.type == PropertyNotify && - xev->xproperty.atom == icon->orientation_atom) - { - egg_tray_icon_get_orientation_property (icon); - } - if (xev->xany.type == DestroyNotify) - { - egg_tray_icon_manager_window_destroyed (icon); - } - } - return GDK_FILTER_CONTINUE; -} - -#endif - -static void -egg_tray_icon_unrealize (GtkWidget *widget) -{ -#ifdef GDK_WINDOWING_X11 - EggTrayIcon *icon = EGG_TRAY_ICON (widget); - GdkWindow *root_window; - - if (icon->manager_window != None) - { - GdkWindow *gdkwin; - - gdkwin = gdk_window_lookup_for_display (gtk_widget_get_display (widget), - icon->manager_window); - - gdk_window_remove_filter (gdkwin, egg_tray_icon_manager_filter, icon); - } - - root_window = gdk_screen_get_root_window (gtk_widget_get_screen (widget)); - - gdk_window_remove_filter (root_window, egg_tray_icon_manager_filter, icon); - - if (GTK_WIDGET_CLASS (parent_class)->unrealize) - (* GTK_WIDGET_CLASS (parent_class)->unrealize) (widget); -#endif -} - -#ifdef GDK_WINDOWING_X11 - -static void -egg_tray_icon_send_manager_message (EggTrayIcon *icon, - long message, - Window window, - long data1, - long data2, - long data3) -{ - XClientMessageEvent ev; - Display *display; - - ev.type = ClientMessage; - ev.window = window; - ev.message_type = icon->system_tray_opcode_atom; - ev.format = 32; - ev.data.l[0] = gdk_x11_get_server_time (GTK_WIDGET (icon)->window); - ev.data.l[1] = message; - ev.data.l[2] = data1; - ev.data.l[3] = data2; - ev.data.l[4] = data3; - - display = egg_tray_icon_get_x_display(icon); - - if (display == NULL) - return; - - gdk_error_trap_push (); - XSendEvent (display, - icon->manager_window, False, NoEventMask, (XEvent *)&ev); - XSync (display, False); - gdk_error_trap_pop (); -} - -static void -egg_tray_icon_send_dock_request (EggTrayIcon *icon) -{ - egg_tray_icon_send_manager_message (icon, - SYSTEM_TRAY_REQUEST_DOCK, - icon->manager_window, - gtk_plug_get_id (GTK_PLUG (icon)), - 0, 0); -} - -static void -egg_tray_icon_update_manager_window (EggTrayIcon *icon, - gboolean dock_if_realized) -{ - Display *xdisplay; - - if (icon->manager_window != None) - return; - - xdisplay = egg_tray_icon_get_x_display(icon); - - if (xdisplay == NULL) - return; - - XGrabServer (xdisplay); - - icon->manager_window = XGetSelectionOwner (xdisplay, - icon->selection_atom); - - if (icon->manager_window != None) - XSelectInput (xdisplay, - icon->manager_window, StructureNotifyMask|PropertyChangeMask); - - XUngrabServer (xdisplay); - XFlush (xdisplay); - - if (icon->manager_window != None) - { - GdkWindow *gdkwin; - - gdkwin = gdk_window_lookup_for_display (gtk_widget_get_display (GTK_WIDGET (icon)), - icon->manager_window); - - gdk_window_add_filter (gdkwin, egg_tray_icon_manager_filter, icon); - - if (dock_if_realized && GTK_WIDGET_REALIZED (icon)) - egg_tray_icon_send_dock_request (icon); - - egg_tray_icon_get_orientation_property (icon); - } -} - -static gboolean -transparent_expose_event (GtkWidget *widget, GdkEventExpose *event, gpointer user_data) -{ - gdk_window_clear_area (widget->window, event->area.x, event->area.y, - event->area.width, event->area.height); - return FALSE; -} - -static void -make_transparent_again (GtkWidget *widget, GtkStyle *previous_style, - gpointer user_data) -{ - gdk_window_set_back_pixmap (widget->window, NULL, TRUE); -} - -static void -make_transparent (GtkWidget *widget, gpointer user_data) -{ - if (GTK_WIDGET_NO_WINDOW (widget) || GTK_WIDGET_APP_PAINTABLE (widget)) - return; - - gtk_widget_set_app_paintable (widget, TRUE); - gtk_widget_set_double_buffered (widget, FALSE); - gdk_window_set_back_pixmap (widget->window, NULL, TRUE); - g_signal_connect (widget, "expose_event", - G_CALLBACK (transparent_expose_event), NULL); - g_signal_connect_after (widget, "style_set", - G_CALLBACK (make_transparent_again), NULL); -} - -static void -egg_tray_icon_manager_window_destroyed (EggTrayIcon *icon) -{ - GdkWindow *gdkwin; - - g_return_if_fail (icon->manager_window != None); - - gdkwin = gdk_window_lookup_for_display (gtk_widget_get_display (GTK_WIDGET (icon)), - icon->manager_window); - - gdk_window_remove_filter (gdkwin, egg_tray_icon_manager_filter, icon); - - icon->manager_window = None; - - egg_tray_icon_update_manager_window (icon, TRUE); -} - -#endif - -static void -egg_tray_icon_realize (GtkWidget *widget) -{ -#ifdef GDK_WINDOWING_X11 - EggTrayIcon *icon = EGG_TRAY_ICON (widget); - GdkScreen *screen; - Display *xdisplay; - char buffer[256]; - GdkWindow *root_window; - - if (GTK_WIDGET_CLASS (parent_class)->realize) - GTK_WIDGET_CLASS (parent_class)->realize (widget); - - make_transparent (widget, NULL); - - xdisplay = egg_tray_icon_get_x_display(icon); - - if (xdisplay == NULL) - return; - - screen = gtk_widget_get_screen (widget); - - /* Now see if there's a manager window around */ - g_snprintf (buffer, sizeof (buffer), - "_NET_SYSTEM_TRAY_S%d", - gdk_screen_get_number (screen)); - - icon->selection_atom = XInternAtom (xdisplay, buffer, False); - - icon->manager_atom = XInternAtom (xdisplay, "MANAGER", False); - - icon->system_tray_opcode_atom = XInternAtom (xdisplay, - "_NET_SYSTEM_TRAY_OPCODE", - False); - - icon->orientation_atom = XInternAtom (xdisplay, - "_NET_SYSTEM_TRAY_ORIENTATION", - False); - - egg_tray_icon_update_manager_window (icon, FALSE); - egg_tray_icon_send_dock_request (icon); - - root_window = gdk_screen_get_root_window (screen); - - /* Add a root window filter so that we get changes on MANAGER */ - gdk_window_add_filter (root_window, - egg_tray_icon_manager_filter, icon); -#endif -} - -static void -egg_tray_icon_add (GtkContainer *container, GtkWidget *widget) -{ - g_signal_connect (widget, "realize", - G_CALLBACK (make_transparent), NULL); - GTK_CONTAINER_CLASS (parent_class)->add (container, widget); -} - -EggTrayIcon * -egg_tray_icon_new_for_screen (GdkScreen *screen, const char *name) -{ - g_return_val_if_fail (GDK_IS_SCREEN (screen), NULL); - - return g_object_new (EGG_TYPE_TRAY_ICON, "screen", screen, "title", name, NULL); -} - -EggTrayIcon* -egg_tray_icon_new (const gchar *name) -{ - return g_object_new (EGG_TYPE_TRAY_ICON, "title", name, NULL); -} - -guint -egg_tray_icon_send_message (EggTrayIcon *icon, - gint timeout, - const gchar *message, - gint len) -{ - guint stamp; - - g_return_val_if_fail (EGG_IS_TRAY_ICON (icon), 0); - g_return_val_if_fail (timeout >= 0, 0); - g_return_val_if_fail (message != NULL, 0); - -#ifdef GDK_WINDOWING_X11 - if (icon->manager_window == None) - return 0; -#endif - - if (len < 0) - len = strlen (message); - - stamp = icon->stamp++; - -#ifdef GDK_WINDOWING_X11 - /* Get ready to send the message */ - egg_tray_icon_send_manager_message (icon, SYSTEM_TRAY_BEGIN_MESSAGE, - (Window)gtk_plug_get_id (GTK_PLUG (icon)), - timeout, len, stamp); - - /* Now to send the actual message */ - gdk_error_trap_push (); - while (len > 0) - { - XClientMessageEvent ev; - Display *xdisplay; - - xdisplay = egg_tray_icon_get_x_display(icon); - - if (xdisplay == NULL) - return 0; - - ev.type = ClientMessage; - ev.window = (Window)gtk_plug_get_id (GTK_PLUG (icon)); - ev.format = 8; - ev.message_type = XInternAtom (xdisplay, - "_NET_SYSTEM_TRAY_MESSAGE_DATA", False); - if (len > 20) - { - memcpy (&ev.data, message, 20); - len -= 20; - message += 20; - } - else - { - memcpy (&ev.data, message, len); - len = 0; - } - - XSendEvent (xdisplay, - icon->manager_window, False, StructureNotifyMask, (XEvent *)&ev); - XSync (xdisplay, False); - } - gdk_error_trap_pop (); -#endif - - return stamp; -} - -void -egg_tray_icon_cancel_message (EggTrayIcon *icon, - guint id) -{ - g_return_if_fail (EGG_IS_TRAY_ICON (icon)); - g_return_if_fail (id > 0); -#ifdef GDK_WINDOWING_X11 - egg_tray_icon_send_manager_message (icon, SYSTEM_TRAY_CANCEL_MESSAGE, - (Window)gtk_plug_get_id (GTK_PLUG (icon)), - id, 0, 0); -#endif -} - -GtkOrientation -egg_tray_icon_get_orientation (EggTrayIcon *icon) -{ - g_return_val_if_fail (EGG_IS_TRAY_ICON (icon), GTK_ORIENTATION_HORIZONTAL); - - return icon->orientation; -} diff --git a/src/eggtrayicon.h b/src/eggtrayicon.h deleted file mode 100644 index 557fdb20c..000000000 --- a/src/eggtrayicon.h +++ /dev/null @@ -1,80 +0,0 @@ -/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ -/* eggtrayicon.h - * Copyright (C) 2002 Anders Carlsson - * - * 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 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. - */ - -#ifndef __EGG_TRAY_ICON_H__ -#define __EGG_TRAY_ICON_H__ - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -G_BEGIN_DECLS - -#define EGG_TYPE_TRAY_ICON (egg_tray_icon_get_type ()) -#define EGG_TRAY_ICON(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), EGG_TYPE_TRAY_ICON, EggTrayIcon)) -#define EGG_TRAY_ICON_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), EGG_TYPE_TRAY_ICON, EggTrayIconClass)) -#define EGG_IS_TRAY_ICON(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EGG_TYPE_TRAY_ICON)) -#define EGG_IS_TRAY_ICON_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), EGG_TYPE_TRAY_ICON)) -#define EGG_TRAY_ICON_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EGG_TYPE_TRAY_ICON, EggTrayIconClass)) - -typedef struct _EggTrayIcon EggTrayIcon; -typedef struct _EggTrayIconClass EggTrayIconClass; - -struct _EggTrayIcon -{ - GtkPlug parent_instance; - - guint stamp; - -#ifdef GDK_WINDOWING_X11 - Atom selection_atom; - Atom manager_atom; - Atom system_tray_opcode_atom; - Atom orientation_atom; - Window manager_window; -#endif - GtkOrientation orientation; -}; - -struct _EggTrayIconClass -{ - GtkPlugClass parent_class; -}; - -GType egg_tray_icon_get_type (void); - -EggTrayIcon *egg_tray_icon_new_for_screen (GdkScreen *screen, - const gchar *name); - -EggTrayIcon *egg_tray_icon_new (const gchar *name); - -guint egg_tray_icon_send_message (EggTrayIcon *icon, - gint timeout, - const char *message, - gint len); -void egg_tray_icon_cancel_message (EggTrayIcon *icon, - guint id); - -GtkOrientation egg_tray_icon_get_orientation (EggTrayIcon *icon); - -G_END_DECLS - -#endif /* __EGG_TRAY_ICON_H__ */ diff --git a/src/features_window.py b/src/features_window.py index 609959e65..d784fc50a 100644 --- a/src/features_window.py +++ b/src/features_window.py @@ -33,7 +33,9 @@ from common import helpers from common import kwalletbinding class FeaturesWindow: - '''Class for features window''' + """ + Class for features window + """ def __init__(self): self.xml = gtkgui_helpers.get_glade('features_window.glade') @@ -83,10 +85,6 @@ class FeaturesWindow: _('Passive popups notifying for new events.'), _('Requires python-notify or instead python-dbus in conjunction with notification-daemon.'), _('Feature not available under Windows.')), - _('Trayicon'): (self.trayicon_available, - _('A icon in systemtray reflecting the current presence.'), - _('Requires python-gnome2-extras or compiled trayicon module from Gajim sources.'), - _('Requires PyGTK >= 2.10.')), _('Automatic status'): (self.idle_available, _('Ability to measure idle time, in order to set auto status.'), _('Requires libxss library.'), @@ -240,15 +238,6 @@ class FeaturesWindow: return False return True - def trayicon_available(self): - if os.name == 'nt': - return True - try: - import systray - except Exception: - return False - return True - def idle_available(self): from common import sleepy return sleepy.SUPPORTED diff --git a/src/filetransfers_window.py b/src/filetransfers_window.py index cae4549e6..d3d23a156 100644 --- a/src/filetransfers_window.py +++ b/src/filetransfers_window.py @@ -134,7 +134,9 @@ class FileTransfersWindow: self.xml.signal_autoconnect(self) def find_transfer_by_jid(self, account, jid): - ''' find all transfers with peer 'jid' that belong to 'account' ''' + """ + Find all transfers with peer 'jid' that belong to 'account' + """ active_transfers = [[],[]] # ['senders', 'receivers'] # 'account' is the sender @@ -155,7 +157,9 @@ class FileTransfersWindow: return active_transfers def show_completed(self, jid, file_props): - ''' show a dialog saying that file (file_props) has been transferred''' + """ + Show a dialog saying that file (file_props) has been transferred + """ def on_open(widget, file_props): dialog.destroy() if 'file-name' not in file_props: @@ -207,14 +211,16 @@ class FileTransfersWindow: dialog.show_all() def show_request_error(self, file_props): - ''' show error dialog to the recipient saying that transfer - has been canceled''' + """ + Show error dialog to the recipient saying that transfer has been canceled + """ dialogs.InformationDialog(_('File transfer cancelled'), _('Connection with peer cannot be established.')) self.tree.get_selection().unselect_all() def show_send_error(self, file_props): - ''' show error dialog to the sender saying that transfer - has been canceled''' + """ + Show error dialog to the sender saying that transfer has been canceled + """ dialogs.InformationDialog(_('File transfer cancelled'), _('Connection with peer cannot be established.')) self.tree.get_selection().unselect_all() @@ -273,7 +279,9 @@ _('Connection with peer cannot be established.')) desc_hbox.show_all() def send_file(self, account, contact, file_path, file_desc=''): - ''' start the real transfer(upload) of the file ''' + """ + Start the real transfer(upload) of the file + """ if gtkgui_helpers.file_is_locked(file_path): pritext = _('Gajim cannot access this file') sextext = _('This file is being used by another process.') @@ -303,8 +311,10 @@ _('Connection with peer cannot be established.')) gajim.connections[account].send_file_approval(file_props) def show_file_request(self, account, contact, file_props): - ''' show dialog asking for comfirmation and store location of new - file requested by a contact''' + """ + Show dialog asking for comfirmation and store location of new file + requested by a contact + """ if file_props is None or 'name' not in file_props: return sec_text = '\t' + _('File: %s') % gobject.markup_escape_text( @@ -394,7 +404,9 @@ _('Connection with peer cannot be established.')) self.window.render_icon(self.icons[ident], gtk.ICON_SIZE_MENU)) def set_status(self, typ, sid, status): - ''' change the status of a transfer to state 'status' ''' + """ + Change the status of a transfer to state 'status' + """ iter_ = self.get_iter_by_sid(typ, sid) if iter_ is None: return @@ -409,8 +421,10 @@ _('Connection with peer cannot be established.')) self.select_func(path) def _format_percent(self, percent): - ''' add extra spaces from both sides of the percent, so that - progress string has always a fixed size''' + """ + Add extra spaces from both sides of the percent, so that progress string + has always a fixed size + """ _str = ' ' if percent != 100.: _str += ' ' @@ -481,7 +495,9 @@ _('Connection with peer cannot be established.')) del(file_props) def set_progress(self, typ, sid, transfered_size, iter_ = None): - ''' change the progress of a transfer with new transfered size''' + """ + Change the progress of a transfer with new transfered size + """ if sid not in self.files_props[typ]: return file_props = self.files_props[typ][sid] @@ -546,8 +562,10 @@ _('Connection with peer cannot be established.')) self.select_func(path) def get_iter_by_sid(self, typ, sid): - '''returns iter to the row, which holds file transfer, identified by the - session id''' + """ + Return iter to the row, which holds file transfer, identified by the + session id + """ iter_ = self.model.get_iter_root() while iter_: if typ + sid == self.model[iter_][C_SID].decode('utf-8'): @@ -555,9 +573,10 @@ _('Connection with peer cannot be established.')) iter_ = self.model.iter_next(iter_) def get_send_file_props(self, account, contact, file_path, file_name, - file_desc=''): - ''' create new file_props dict and set initial file transfer - properties in it''' + file_desc=''): + """ + Create new file_props dict and set initial file transfer properties in it + """ file_props = {'file-name' : file_path, 'name' : file_name, 'type' : 's', 'desc' : file_desc} if os.path.isfile(file_path): @@ -582,7 +601,9 @@ _('Connection with peer cannot be established.')) return file_props def add_transfer(self, account, contact, file_props): - ''' add new transfer to FT window and show the FT window ''' + """ + Add new transfer to FT window and show the FT window + """ self.on_transfers_list_leave_notify_event(None) if file_props is None: return @@ -686,15 +707,19 @@ _('Connection with peer cannot be established.')) return True def set_cleanup_sensitivity(self): - ''' check if there are transfer rows and set cleanup_button - sensitive, or insensitive if model is empty''' + """ + Check if there are transfer rows and set cleanup_button sensitive, or + insensitive if model is empty + """ if len(self.model) == 0: self.cleanup_button.set_sensitive(False) else: self.cleanup_button.set_sensitive(True) def set_all_insensitive(self): - ''' make all buttons/menuitems insensitive ''' + """ + Make all buttons/menuitems insensitive + """ self.pause_button.set_sensitive(False) self.pause_menuitem.set_sensitive(False) self.continue_menuitem.set_sensitive(False) @@ -705,8 +730,10 @@ _('Connection with peer cannot be established.')) self.set_cleanup_sensitivity() def set_buttons_sensitive(self, path, is_row_selected): - ''' make buttons/menuitems sensitive as appropriate to - the state of file transfer located at path 'path' ''' + """ + Make buttons/menuitems sensitive as appropriate to the state of file + transfer located at path 'path' + """ if path is None: self.set_all_insensitive() return @@ -743,8 +770,9 @@ _('Connection with peer cannot be established.')) return True def selection_changed(self, args): - ''' selection has changed - change the sensitivity of the - buttons/menuitems''' + """ + Selection has changed - change the sensitivity of the buttons/menuitems + """ selection = args selected = selection.get_selected_rows() if selected[1] != []: @@ -881,7 +909,9 @@ _('Connection with peer cannot be established.')) event_button, event.time) def on_transfers_list_key_press_event(self, widget, event): - '''when a key is pressed in the treeviews''' + """ + When a key is pressed in the treeviews + """ self.tooltip.hide_tooltip() iter_ = None try: @@ -963,5 +993,4 @@ _('Connection with peer cannot be established.')) if event.keyval == gtk.keysyms.Escape: # ESCAPE self.window.hide() - # vim: se ts=3: diff --git a/src/gajim-remote.py b/src/gajim-remote.py index 8926cf341..09db604d0 100644 --- a/src/gajim-remote.py +++ b/src/gajim-remote.py @@ -60,7 +60,6 @@ INTERFACE = 'org.gajim.dbus.RemoteInterface' SERVICE = 'org.gajim.dbus' BASENAME = 'gajim-remote' - class GajimRemote: def __init__(self): @@ -327,7 +326,9 @@ class GajimRemote: self.print_result(res) def print_result(self, res): - ''' Print retrieved result to the output ''' + """ + Print retrieved result to the output + """ if res is not None: if self.command in ('open_chat', 'send_chat_message', 'send_single_message', 'start_chat'): if self.command in ('send_message', 'send_single_message'): @@ -382,8 +383,9 @@ class GajimRemote: return test def init_connection(self): - ''' create the onnection to the session dbus, - or exit if it is not possible ''' + """ + Create the onnection to the session dbus, or exit if it is not possible + """ try: self.sbus = dbus.SessionBus() except Exception: @@ -398,8 +400,10 @@ class GajimRemote: self.method = interface.__getattr__(self.command) def make_arguments_row(self, args): - ''' return arguments list. Mandatory arguments are enclosed with: - '<', '>', optional arguments - with '[', ']' ''' + """ + Return arguments list. Mandatory arguments are enclosed with: + '<', '>', optional arguments - with '[', ']' + """ s = '' for arg in args: if arg[2]: @@ -409,7 +413,9 @@ class GajimRemote: return s def help_on_command(self, command): - ''' return help message for a given command ''' + """ + Return help message for a given command + """ if command in self.commands: command_props = self.commands[command] arguments_str = self.make_arguments_row(command_props[1]) @@ -424,7 +430,9 @@ class GajimRemote: send_error(_('%s not found') % command) def compose_help(self): - ''' print usage, and list available commands ''' + """ + Print usage, and list available commands + """ s = _('Usage: %s command [arguments]\nCommand is one of:\n' ) % BASENAME for command in sorted(self.commands): s += ' ' + command @@ -437,7 +445,9 @@ class GajimRemote: return s def print_info(self, level, prop_dict, encode_return = False): - ''' return formated string from data structure ''' + """ + Return formated string from data structure + """ if prop_dict is None or not isinstance(prop_dict, (dict, list, tuple)): return '' ret_str = '' @@ -486,7 +496,9 @@ class GajimRemote: return ret_str def check_arguments(self): - ''' Make check if all necessary arguments are given ''' + """ + Make check if all necessary arguments are given + """ argv_len = self.argv_len - 2 args = self.commands[self.command][1] if len(args) < argv_len: @@ -559,7 +571,9 @@ class GajimRemote: sys.exit(0) def call_remote_method(self): - ''' calls self.method with arguments from sys.argv[2:] ''' + """ + Calls self.method with arguments from sys.argv[2:] + """ args = [i.decode(PREFERRED_ENCODING) for i in self.arguments] args = [dbus.String(i) for i in args] try: diff --git a/src/gajim_themes_window.py b/src/gajim_themes_window.py index ebaebc8d6..e6c23929a 100644 --- a/src/gajim_themes_window.py +++ b/src/gajim_themes_window.py @@ -275,7 +275,9 @@ class GajimThemesWindow: self._set_font() def _set_color(self, state, widget, option): - ''' set color value in prefs and update the UI ''' + """ + Set color value in prefs and update the UI + """ if state: color = widget.get_color() color_string = gtkgui_helpers.make_color_string(color) @@ -297,7 +299,9 @@ class GajimThemesWindow: gajim.interface.save_config() def _set_font(self): - ''' set font value in prefs and update the UI ''' + """ + Set font value in prefs and update the UI + """ state = self.textfont_checkbutton.get_active() if state: font_string = self.text_fontbutton.get_font_name() @@ -317,13 +321,16 @@ class GajimThemesWindow: gajim.interface.save_config() def _toggle_font_widgets(self, font_props): - ''' toggle font buttons with the bool values of font_props tuple''' + """ + Toggle font buttons with the bool values of font_props tuple + """ self.bold_togglebutton.set_active(font_props[0]) self.italic_togglebutton.set_active(font_props[1]) def _get_font_description(self): - ''' return a FontDescription from togglebuttons - states''' + """ + Return a FontDescription from togglebuttons states + """ fd = pango.FontDescription() if self.bold_togglebutton.get_active(): fd.set_weight(pango.WEIGHT_BOLD) @@ -332,8 +339,10 @@ class GajimThemesWindow: return fd def _set_font_widgets(self, font_attrs): - ''' set the correct toggle state of font style buttons by - a font string of type 'BI' ''' + """ + Set the correct toggle state of font style buttons by a font string of + type 'BI' + """ font_props = [False, False, False] if font_attrs: if font_attrs.find('B') != -1: @@ -343,7 +352,9 @@ class GajimThemesWindow: self._toggle_font_widgets(font_props) def _get_font_attrs(self): - ''' get a string with letters of font attribures: 'BI' ''' + """ + Get a string with letters of font attribures: 'BI' + """ attrs = '' if self.bold_togglebutton.get_active(): attrs += 'B' @@ -353,7 +364,9 @@ class GajimThemesWindow: def _get_font_props(self, font_name): - ''' get tuple of font properties: Weight, Style ''' + """ + Get tuple of font properties: weight, style + """ font_props = [False, False, False] font_description = pango.FontDescription(font_name) if font_description.get_weight() != pango.WEIGHT_NORMAL: diff --git a/src/groupchat_control.py b/src/groupchat_control.py index e71eefe32..6db3398f5 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -29,6 +29,7 @@ import os import time +import locale import gtk import pango import gobject @@ -63,7 +64,9 @@ C_AVATAR, # avatar of the contact ) = range(5) def set_renderer_color(treeview, renderer, set_background=True): - '''set style for group row, using PRELIGHT system color''' + """ + Set style for group row, using PRELIGHT system color + """ if set_background: bgcolor = treeview.style.bg[gtk.STATE_PRELIGHT] renderer.set_property('cell-background-gdk', bgcolor) @@ -140,7 +143,9 @@ class PrivateChatControl(ChatControl): self.TYPE_ID = 'pm' def send_message(self, message, xhtml=None, process_commands=True): - '''call this function to send our message''' + """ + Call this method to send the message + """ if not message: return @@ -317,6 +322,7 @@ class GroupchatControl(ChatControlBase): id_ = self.list_treeview.connect('style-set', self.on_list_treeview_style_set) self.handlers[id_] = self.list_treeview + self.resize_from_another_muc = False # we want to know when the the widget resizes, because that is # an indication that the hpaned has moved... # FIXME: Find a better indicator that the hpaned has moved. @@ -381,7 +387,9 @@ class GroupchatControl(ChatControlBase): self.widget.show_all() def tree_compare_iters(self, model, iter1, iter2): - '''Compare two iters to sort them''' + """ + Compare two iters to sort them + """ type1 = model[iter1][C_TYPE] type2 = model[iter2][C_TYPE] if not type1 or not type2: @@ -393,9 +401,7 @@ class GroupchatControl(ChatControlBase): nick1 = nick1.decode('utf-8') nick2 = nick2.decode('utf-8') if type1 == 'role': - if nick1 < nick2: - return -1 - return 1 + return locale.strcoll(nick1, nick2) if type1 == 'contact': gc_contact1 = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick1) @@ -419,15 +425,13 @@ class GroupchatControl(ChatControlBase): # We compare names name1 = gc_contact1.get_shown_name() name2 = gc_contact2.get_shown_name() - if name1.lower() < name2.lower(): - return -1 - if name2.lower() < name1.lower(): - return 1 - return 0 + return locale.strcoll(name1.lower(), name2.lower()) def on_msg_textview_populate_popup(self, textview, menu): - '''we override the default context menu and we prepend Clear - and the ability to insert a nick''' + """ + Override the default context menu and we prepend Clear + and the ability to insert a nick + """ ChatControlBase.on_msg_textview_populate_popup(self, textview, menu) item = gtk.SeparatorMenuItem() menu.prepend(item) @@ -446,20 +450,36 @@ class GroupchatControl(ChatControlBase): menu.show_all() + def resize_occupant_treeview(self, position): + self.resize_from_another_muc = True + self.hpaned.set_position(position) + def reset_flag(): + self.resize_from_another_muc = False + # Reset the flag when everything will be redrawn, and in particular when + # on_treeview_size_allocate will have been called. + gobject.idle_add(reset_flag) + def on_treeview_size_allocate(self, widget, allocation): - '''The MUC treeview has resized. Move the hpaned in all tabs to match''' + """ + The MUC treeview has resized. Move the hpaned in all tabs to match + """ + if self.resize_from_another_muc: + # Don't send the event to other MUC + return hpaned_position = self.hpaned.get_position() for account in gajim.gc_connected: for room_jid in [i for i in gajim.gc_connected[account] if \ - gajim.gc_connected[account][i]]: + gajim.gc_connected[account][i] and i != self.room_jid]: ctrl = gajim.interface.msg_win_mgr.get_gc_control(room_jid, account) if not ctrl: ctrl = gajim.interface.minimized_controls[account][room_jid] if ctrl: - ctrl.hpaned.set_position(hpaned_position) + ctrl.resize_occupant_treeview(hpaned_position) def iter_contact_rows(self): - '''iterate over all contact rows in the tree model''' + """ + Iterate over all contact rows in the tree model + """ model = self.list_treeview.get_model() role_iter = model.get_iter_root() while role_iter: @@ -470,7 +490,9 @@ class GroupchatControl(ChatControlBase): role_iter = model.iter_next(role_iter) def on_list_treeview_style_set(self, treeview, style): - '''When style (theme) changes, redraw all contacts''' + """ + When style (theme) changes, redraw all contacts + """ # Get the room_jid from treeview for contact in self.iter_contact_rows(): nick = contact[C_NICK].decode('utf-8') @@ -492,10 +514,11 @@ class GroupchatControl(ChatControlBase): self.draw_contact(nick, selected=True, focus=True) def get_tab_label(self, chatstate): - '''Markup the label if necessary. Returns a tuple such as: - (new_label_str, color) - either of which can be None - if chatstate is given that means we have HE SENT US a chatstate''' + """ + Markup the label if necessary. Returns a tuple such as: (new_label_str, + color) either of which can be None if chatstate is given that means we + have HE SENT US a chatstate + """ has_focus = self.parent_win.window.get_property('has-toplevel-focus') current_tab = self.parent_win.get_active_control() == self @@ -580,10 +603,10 @@ class GroupchatControl(ChatControlBase): banner_status_img.set_from_pixbuf(scaled_pix) def get_continued_conversation_name(self): - '''Get the name of a continued conversation. - Will return Continued Conversation if there isn't any other - contact in the room - ''' + """ + Get the name of a continued conversation. Will return Continued + Conversation if there isn't any other contact in the room + """ nicks = [] for nick in gajim.contacts.get_nick_list(self.account, self.room_jid): @@ -597,9 +620,10 @@ class GroupchatControl(ChatControlBase): return title def draw_banner_text(self): - '''Draw the text in the fat line at the top of the window that - houses the room jid, subject. - ''' + """ + Draw the text in the fat line at the top of the window that houses the + room jid, subject + """ self.name_label.set_ellipsize(pango.ELLIPSIZE_END) self.banner_status_label.set_ellipsize(pango.ELLIPSIZE_END) font_attrs, font_attrs_small = self.get_font_attrs() @@ -633,7 +657,9 @@ class GroupchatControl(ChatControlBase): self.banner_status_label.set_markup(subject_text) def prepare_context_menu(self, hide_buttonbar_items=False): - '''sets sensitivity state for configure_room''' + """ + Set sensitivity state for configure_room + """ xml = gtkgui_helpers.get_glade('gc_control_popup_menu.glade') menu = xml.get_widget('gc_control_popup_menu') @@ -737,7 +763,7 @@ class GroupchatControl(ChatControlBase): return menu def destroy_menu(self, menu, change_nick_menuitem, change_subject_menuitem, - bookmark_room_menuitem, history_menuitem): + bookmark_room_menuitem, history_menuitem): # destroy accelerators ag = gtk.accel_groups_from_object(self.parent_win.window)[0] change_nick_menuitem.remove_accelerator(ag, gtk.keysyms.n, @@ -752,7 +778,7 @@ class GroupchatControl(ChatControlBase): menu.destroy() def on_message(self, nick, msg, tim, has_timestamp=False, xhtml=None, - status_code=[]): + status_code=[]): if '100' in status_code: # Room is not anonymous self.is_anonymous = False @@ -768,8 +794,8 @@ class GroupchatControl(ChatControlBase): else: self.print_conversation(msg, nick, tim, xhtml) - def on_private_message(self, nick, msg, tim, xhtml, session, - msg_id=None, encrypted=False): + def on_private_message(self, nick, msg, tim, xhtml, session, msg_id=None, + encrypted=False): # Do we have a queue? fjid = self.room_jid + '/' + nick no_queue = len(gajim.events.get_events(self.account, fjid)) == 0 @@ -827,8 +853,7 @@ class GroupchatControl(ChatControlBase): fin = True return None - def print_old_conversation(self, text, contact='', tim=None, - xhtml = None): + def print_old_conversation(self, text, contact='', tim=None, xhtml = None): if isinstance(text, str): text = unicode(text, 'utf-8') if contact: @@ -847,11 +872,14 @@ class GroupchatControl(ChatControlBase): small_attr + ['restored_message'], count_as_new=False, xhtml=xhtml) def print_conversation(self, text, contact='', tim=None, xhtml=None, - graphics=True): - '''Print a line in the conversation: - if contact is set: it's a message from someone or an info message (contact - = 'info' in such a case) - if contact is not set: it's a message from the server or help''' + graphics=True): + """ + Print a line in the conversation + + If contact is set: it's a message from someone or an info message + (contact = 'info' in such a case). + If contact is not set: it's a message from the server or help. + """ if isinstance(text, str): text = unicode(text, 'utf-8') other_tags_for_name = [] @@ -930,8 +958,10 @@ class GroupchatControl(ChatControlBase): return nb def highlighting_for_message(self, text, tim): - '''Returns a 2-Tuple. The first says whether or not to highlight the - text, the second, what sound to play.''' + """ + Returns a 2-Tuple. The first says whether or not to highlight the text, + the second, what sound to play + """ highlight, sound = (None, None) # Are any of the defined highlighting words in the text? @@ -953,10 +983,11 @@ class GroupchatControl(ChatControlBase): return (highlight, sound) def check_and_possibly_add_focus_out_line(self): - '''checks and possibly adds focus out line for room_jid if it needs it - and does not already have it as last event. If it goes to add this line - it removes previous line first''' - + """ + Check and possibly add focus out line for room_jid if it needs it and + does not already have it as last event. If it goes to add this line + - remove previous line first + """ win = gajim.interface.msg_win_mgr.get_window(self.room_jid, self.account) if win and self.room_jid == win.get_active_jid() and\ win.window.get_property('has-toplevel-focus') and\ @@ -968,9 +999,10 @@ class GroupchatControl(ChatControlBase): self.conv_textview.show_focus_out_line() def needs_visual_notification(self, text): - '''checks text to see whether any of the words in (muc_highlight_words - and nick) appear.''' - + """ + Check text to see whether any of the words in (muc_highlight_words and + nick) appear + """ special_words = gajim.config.get('muc_highlight_words').split(';') special_words.append(self.nick) # Strip empties: ''.split(';') == [''] and would highlight everything. @@ -1064,9 +1096,11 @@ class GroupchatControl(ChatControlBase): self.list_treeview.columns_autosize() def on_send_pm(self, widget=None, model=None, iter_=None, nick=None, - msg=None): - '''opens a chat window and if msg is not None sends private message to a - contact in a room''' + msg=None): + """ + Open a chat window and if msg is not None - send private message to a + contact in a room + """ if nick is None: nick = model[iter_][C_NICK].decode('utf-8') @@ -1075,7 +1109,9 @@ class GroupchatControl(ChatControlBase): ctrl.send_message(msg) def on_send_file(self, widget, gc_contact): - '''sends a file to a contact in the room''' + """ + Send a file to a contact in the room + """ self._on_send_file(gc_contact) def draw_contact(self, nick, selected=False, focus=False): @@ -1160,8 +1196,10 @@ class GroupchatControl(ChatControlBase): self.draw_role(role) def chg_contact_status(self, nick, show, status, role, affiliation, jid, - reason, actor, statusCode, new_nick, avatar_sha, tim=None): - '''When an occupant changes his or her status''' + reason, actor, statusCode, new_nick, avatar_sha, tim=None): + """ + When an occupant changes his or her status + """ if show == 'invisible': return @@ -1440,7 +1478,7 @@ class GroupchatControl(ChatControlBase): self.print_conversation(st, tim=tim, graphics=False) def add_contact_to_roster(self, nick, show, role, affiliation, status, - jid=''): + jid=''): model = self.list_treeview.get_model() role_name = helpers.get_uf_role(role, plural=True) @@ -1506,7 +1544,9 @@ class GroupchatControl(ChatControlBase): return None def remove_contact(self, nick): - '''Remove a user from the contacts_list''' + """ + Remove a user from the contacts_list + """ model = self.list_treeview.get_model() iter_ = self.get_contact_iter(nick) if not iter_: @@ -1521,7 +1561,9 @@ class GroupchatControl(ChatControlBase): model.remove(parent_iter) def send_message(self, message, xhtml=None, process_commands=True): - '''call this function to send our message''' + """ + Call this function to send our message + """ if not message: return @@ -1744,7 +1786,9 @@ class GroupchatControl(ChatControlBase): _('You may also enter an alternate venue:'), ok_handler=on_ok) def _on_bookmark_room_menuitem_activate(self, widget): - '''bookmark the room, without autojoin and not minimized''' + """ + Bookmark the room, without autojoin and not minimized + """ password = gajim.gc_passwords.get(self.room_jid, '') gajim.interface.add_gc_bookmark(self.account, self.name, self.room_jid, \ '0', '0', password, self.nick) @@ -1767,7 +1811,7 @@ class GroupchatControl(ChatControlBase): gajim.connections[self.account].send_invite(self.room_jid, contact_jid) def handle_message_textview_mykey_press(self, widget, event_keyval, - event_keymod): + event_keymod): # NOTE: handles mykeypress which is custom signal connected to this # CB in new_room(). for this singal see message_textview.py @@ -1896,19 +1940,25 @@ class GroupchatControl(ChatControlBase): return True def on_list_treeview_row_expanded(self, widget, iter_, path): - '''When a row is expanded: change the icon of the arrow''' + """ + When a row is expanded: change the icon of the arrow + """ model = widget.get_model() image = gajim.interface.jabber_state_images['16']['opened'] model[iter_][C_IMG] = image def on_list_treeview_row_collapsed(self, widget, iter_, path): - '''When a row is collapsed: change the icon of the arrow''' + """ + When a row is collapsed: change the icon of the arrow + """ model = widget.get_model() image = gajim.interface.jabber_state_images['16']['closed'] model[iter_][C_IMG] = image def kick(self, widget, nick): - '''kick a user''' + """ + Kick a user + """ def on_ok(reason): gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'none', reason) @@ -1918,7 +1968,9 @@ class GroupchatControl(ChatControlBase): _('You may specify a reason below:'), ok_handler=on_ok) def mk_menu(self, event, iter_): - '''Make contact's popup menu''' + """ + Make contact's popup menu + """ model = self.list_treeview.get_model() nick = model[iter_][C_NICK].decode('utf-8') c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) @@ -2062,8 +2114,10 @@ class GroupchatControl(ChatControlBase): return ctrl def on_row_activated(self, widget, path): - '''When an iter is activated (dubblick or single click if gnome is set - this way''' + """ + When an iter is activated (dubblick or single click if gnome is set this + way + """ model = widget.get_model() if len(path) == 1: # It's a group if (widget.row_expanded(path)): @@ -2075,12 +2129,16 @@ class GroupchatControl(ChatControlBase): self._start_private_message(nick) def on_list_treeview_row_activated(self, widget, path, col=0): - '''When an iter is double clicked: open the chat window''' + """ + When an iter is double clicked: open the chat window + """ if not gajim.single_click: self.on_row_activated(widget, path) def on_list_treeview_button_press_event(self, widget, event): - '''popup user's group's or agent menu''' + """ + Popup user's group's or agent menu + """ # hide tooltip, no matter the button is pressed self.tooltip.hide_tooltip() try: @@ -2191,27 +2249,37 @@ class GroupchatControl(ChatControlBase): self.tooltip.hide_tooltip() def grant_voice(self, widget, nick): - '''grant voice privilege to a user''' + """ + Grant voice privilege to a user + """ gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'participant') def revoke_voice(self, widget, nick): - '''revoke voice privilege to a user''' + """ + Revoke voice privilege to a user + """ gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'visitor') def grant_moderator(self, widget, nick): - '''grant moderator privilege to a user''' + """ + Grant moderator privilege to a user + """ gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'moderator') def revoke_moderator(self, widget, nick): - '''revoke moderator privilege to a user''' + """ + Revoke moderator privilege to a user + """ gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'participant') def ban(self, widget, jid): - '''ban a user''' + """ + Ban a user + """ def on_ok(reason): gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'outcast', reason) @@ -2223,32 +2291,44 @@ class GroupchatControl(ChatControlBase): _('You may specify a reason below:'), ok_handler=on_ok) def grant_membership(self, widget, jid): - '''grant membership privilege to a user''' + """ + Grant membership privilege to a user + """ gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'member') def revoke_membership(self, widget, jid): - '''revoke membership privilege to a user''' + """ + Revoke membership privilege to a user + """ gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'none') def grant_admin(self, widget, jid): - '''grant administrative privilege to a user''' + """ + Grant administrative privilege to a user + """ gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'admin') def revoke_admin(self, widget, jid): - '''revoke administrative privilege to a user''' + """ + Revoke administrative privilege to a user + """ gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'member') def grant_owner(self, widget, jid): - '''grant owner privilege to a user''' + """ + Grant owner privilege to a user + """ gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'owner') def revoke_owner(self, widget, jid): - '''revoke owner privilege to a user''' + """ + Revoke owner privilege to a user + """ gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'admin') diff --git a/src/groups.py b/src/groups.py index 1b78f5708..2588b11a4 100644 --- a/src/groups.py +++ b/src/groups.py @@ -26,7 +26,10 @@ import gtkgui_helpers class GroupsPostWindow: def __init__(self, account, servicejid, groupid): - '''Open new 'create post' window to create message for groupid on servicejid service.''' + """ + Open new 'create post' window to create message for groupid on servicejid + service + """ assert isinstance(servicejid, basestring) assert isinstance(groupid, basestring) @@ -43,11 +46,15 @@ class GroupsPostWindow: self.window.show_all() def on_cancel_button_clicked(self, w): - '''Close window.''' + """ + Close window + """ self.window.destroy() def on_send_button_clicked(self, w): - '''Gather info from widgets and send it as a message.''' + """ + Gather info from widgets and send it as a message + """ # constructing item to publish... that's atom:entry element item = xmpp.Node('entry', {'xmlns':'http://www.w3.org/2005/Atom'}) author = item.addChild('author') diff --git a/src/gtkgui_helpers.py b/src/gtkgui_helpers.py index 6a8b1b1cf..273dc65b7 100644 --- a/src/gtkgui_helpers.py +++ b/src/gtkgui_helpers.py @@ -67,8 +67,10 @@ def get_glade(file_name, root = None): return gtk.glade.XML(file_path, root=root, domain=i18n.APP) def get_completion_liststore(entry): - ''' create a completion model for entry widget - completion list consists of (Pixbuf, Text) rows''' + """ + Create a completion model for entry widget completion list consists of + (Pixbuf, Text) rows + """ completion = gtk.EntryCompletion() liststore = gtk.ListStore(gtk.gdk.Pixbuf, str) @@ -86,7 +88,9 @@ def get_completion_liststore(entry): def popup_emoticons_under_button(menu, button, parent_win): - ''' pops emoticons menu under button, which is in parent_win''' + """ + Popup the emoticons menu under button, which is in parent_win + """ window_x1, window_y1 = parent_win.get_origin() def position_menu_under_button(menu): # inline function, which will not keep refs, when used as CB @@ -115,8 +119,9 @@ def popup_emoticons_under_button(menu, button, parent_win): menu.popup(None, None, position_menu_under_button, 1, 0) def get_theme_font_for_option(theme, option): - '''return string description of the font, stored in - theme preferences''' + """ + Return string description of the font, stored in theme preferences + """ font_name = gajim.config.get_per('themes', theme, option) font_desc = pango.FontDescription() font_prop_str = gajim.config.get_per('themes', theme, option + 'attrs') @@ -130,10 +135,10 @@ def get_theme_font_for_option(theme, option): return fd.to_string() def get_default_font(): - '''Get the desktop setting for application font - first check for GNOME, then Xfce and last KDE - it returns None on failure or else a string 'Font Size' ''' - + """ + Get the desktop setting for application font first check for GNOME, then + Xfce and last KDE it returns None on failure or else a string 'Font Size' + """ try: import gconf # in try because daemon may not be there @@ -206,7 +211,9 @@ def user_runs_xfce(): return False def get_running_processes(): - '''returns running processes or None (if not /proc exists)''' + """ + Return running processes or None (if /proc does not exist) + """ if os.path.isdir('/proc'): # under Linux: checking if 'gnome-session' or # 'startkde' programs were run before gajim, by @@ -241,7 +248,9 @@ def get_running_processes(): return [] def move_window(window, x, y): - '''moves the window but also checks if out of screen''' + """ + Move the window, but also check if out of screen + """ if x < 0: x = 0 if y < 0: @@ -254,7 +263,9 @@ def move_window(window, x, y): window.move(x, y) def resize_window(window, w, h): - '''resizes window but also checks if huge window or negative values''' + """ + Resize window, but also checks if huge window or negative values + """ if not w or not h: return if w > screen_w: @@ -354,8 +365,10 @@ def parse_server_xml(path_to_file): print >> sys.stderr, _('Error parsing file:'), message def set_unset_urgency_hint(window, unread_messages_no): - '''sets/unsets urgency hint in window argument - depending if we have unread messages or not''' + """ + Sets/unset urgency hint in window argument depending if we have unread + messages or not + """ if gajim.config.get('use_urgency_hint'): if unread_messages_no > 0: window.props.urgency_hint = True @@ -363,8 +376,10 @@ def set_unset_urgency_hint(window, unread_messages_no): window.props.urgency_hint = False def get_abspath_for_script(scriptname, want_type = False): - '''checks if we are svn or normal user and returns abspath to asked script - if want_type is True we return 'svn' or 'install' ''' + """ + Check if we are svn or normal user and return abspath to asked script if + want_type is True we return 'svn' or 'install' + """ if os.path.isdir('.svn'): # we are svn user type_ = 'svn' cwd = os.getcwd() # it's always ending with src @@ -403,8 +418,10 @@ def get_abspath_for_script(scriptname, want_type = False): return path_to_script def get_pixbuf_from_data(file_data, want_type = False): - '''Gets image data and returns gtk.gdk.Pixbuf - if want_type is True it also returns 'jpeg', 'png' etc''' + """ + Get image data and returns gtk.gdk.Pixbuf if want_type is True it also + returns 'jpeg', 'png' etc + """ pixbufloader = gtk.gdk.PixbufLoader() try: pixbufloader.write(file_data) @@ -431,8 +448,11 @@ def get_invisible_cursor(): return cursor def get_current_desktop(window): - '''returns the current virtual desktop for given window - NOTE: window is GDK window''' + """ + Return the current virtual desktop for given window + + NOTE: Window is a GDK window. + """ prop = window.property_get('_NET_CURRENT_DESKTOP') if prop is None: # it means it's normal window (not root window) # so we look for it's current virtual desktop in another property @@ -444,9 +464,12 @@ def get_current_desktop(window): return current_virtual_desktop_no def possibly_move_window_in_current_desktop(window): - '''moves GTK window to current virtual desktop if it is not in the - current virtual desktop - window is GTK window''' + """ + Moves GTK window to current virtual desktop if it is not in the current + virtual desktop + + NOTE: Window is a GDK window. + """ if os.name == 'nt': return False @@ -468,7 +491,11 @@ def possibly_move_window_in_current_desktop(window): return False def file_is_locked(path_to_file): - '''returns True if file is locked (WINDOWS ONLY)''' + """ + Return True if file is locked + + NOTE: Windows only. + """ if os.name != 'nt': # just in case return @@ -496,8 +523,10 @@ def file_is_locked(path_to_file): return False def _get_fade_color(treeview, selected, focused): - '''get a gdk color that is between foreground and background in 0.3 - 0.7 respectively colors of the cell for the given treeview''' + """ + Get a gdk color that is between foreground and background in 0.3 + 0.7 respectively colors of the cell for the given treeview + """ style = treeview.style if selected: if focused: # is the window focused? @@ -516,9 +545,10 @@ def _get_fade_color(treeview, selected, focused): int(bg.blue*p + fg.blue*q)) def get_scaled_pixbuf(pixbuf, kind): - '''returns scaled pixbuf, keeping ratio etc or None - kind is either "chat", "roster", "notification", "tooltip", "vcard"''' - + """ + Return scaled pixbuf, keeping ratio etc or None kind is either "chat", + "roster", "notification", "tooltip", "vcard" + """ # resize to a width / height for the avatar not to have distortion # (keep aspect ratio) width = gajim.config.get(kind + '_avatar_width') @@ -544,12 +574,14 @@ def get_scaled_pixbuf(pixbuf, kind): return scaled_buf def get_avatar_pixbuf_from_cache(fjid, is_fake_jid = False, use_local = True): - '''checks if jid has cached avatar and if that avatar is valid image - (can be shown) - returns None if there is no image in vcard - returns 'ask' if cached vcard should not be used (user changed his vcard, - so we have new sha) or if we don't have the vcard''' + """ + Check if jid has cached avatar and if that avatar is valid image (can be + shown) + Returns None if there is no image in vcard/ + Returns 'ask' if cached vcard should not be used (user changed his vcard, so + we have new sha) or if we don't have the vcard + """ jid, nick = gajim.get_room_and_nick_from_fjid(fjid) if gajim.config.get('hide_avatar_of_transport') and\ gajim.jid_is_transport(jid): @@ -588,16 +620,21 @@ def get_avatar_pixbuf_from_cache(fjid, is_fake_jid = False, use_local = True): return pixbuf def make_gtk_month_python_month(month): - '''gtk start counting months from 0, so January is 0 - but python's time start from 1, so align to python - month MUST be integer''' + """ + GTK starts counting months from 0, so January is 0 but Python's time start + from 1, so align to Python + + NOTE: Month MUST be an integer. + """ return month + 1 def make_python_month_gtk_month(month): return month - 1 def make_color_string(color): - '''create #aabbcc color string from gtk color''' + """ + Create #aabbcc color string from gtk color + """ col = '#' for i in ('red', 'green', 'blue'): h = hex(getattr(color, i) / (16*16)).split('x')[1] @@ -612,10 +649,12 @@ def make_pixbuf_grayscale(pixbuf): return pixbuf2 def get_path_to_generic_or_avatar(generic, jid = None, suffix = None): - '''Chooses between avatar image and default image. - Returns full path to the avatar image if it exists, - otherwise returns full path to the image. - generic must be with extension and suffix without''' + """ + Choose between avatar image and default image + + Returns full path to the avatar image if it exists, otherwise returns full + path to the image. generic must be with extension and suffix without + """ if jid: # we want an avatar puny_jid = helpers.sanitize_filename(jid) @@ -632,9 +671,10 @@ def get_path_to_generic_or_avatar(generic, jid = None, suffix = None): return os.path.abspath(generic) def decode_filechooser_file_paths(file_paths): - '''decode as UTF-8 under Windows and - ask sys.getfilesystemencoding() in POSIX - file_paths MUST be LIST''' + """ + Decode as UTF-8 under Windows and ask sys.getfilesystemencoding() in POSIX + file_paths MUST be LIST + """ file_paths_list = list() if os.name == 'nt': # decode as UTF-8 under Windows @@ -655,7 +695,9 @@ def decode_filechooser_file_paths(file_paths): return file_paths_list def possibly_set_gajim_as_xmpp_handler(): - '''registers (by default only the first time) xmmp: to Gajim.''' + """ + Register (by default only the first time) 'xmmp:' to Gajim + """ path_to_dot_kde = os.path.expanduser('~/.kde') if os.path.exists(path_to_dot_kde): path_to_kde_file = os.path.join(path_to_dot_kde, @@ -737,8 +779,10 @@ Description=xmpp dlg.checkbutton.set_active(True) def escape_underscore(s): - '''Escape underlines to prevent them from being interpreted - as keyboard accelerators''' + """ + Escape underlines to prevent them from being interpreted as keyboard + accelerators + """ return s.replace('_', '__') def get_state_image_from_file_path_show(file_path, show): @@ -756,7 +800,9 @@ def get_state_image_from_file_path_show(file_path, show): return image def get_possible_button_event(event): - '''mouse or keyboard caused the event?''' + """ + Mouse or keyboard caused the event? + """ if event.type == gtk.gdk.KEY_PRESS: return 0 # no event.button so pass 0 # BUTTON_PRESS event, so pass event.button @@ -847,7 +893,9 @@ def on_bm_header_changed_state(widget, event): widget.set_state(gtk.STATE_NORMAL) #do not allow selected_state def create_combobox(value_list, selected_value = None): - '''Value_list is [(label1, value1), ]''' + """ + Value_list is [(label1, value1)] + """ liststore = gtk.ListStore(str, str) combobox = gtk.ComboBox(liststore) cell = gtk.CellRendererText() @@ -864,7 +912,9 @@ def create_combobox(value_list, selected_value = None): return combobox def create_list_multi(value_list, selected_values=None): - '''Value_list is [(label1, value1), ]''' + """ + Value_list is [(label1, value1)] + """ liststore = gtk.ListStore(str, str) treeview = gtk.TreeView(liststore) treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) @@ -882,8 +932,10 @@ def create_list_multi(value_list, selected_values=None): return treeview def load_iconset(path, pixbuf2=None, transport=False): - '''load full iconset from the given path, and add - pixbuf2 on top left of each static images''' + """ + Load full iconset from the given path, and add pixbuf2 on top left of each + static images + """ path += '/' if transport: list_ = ('online', 'chat', 'away', 'xa', 'dnd', 'offline', @@ -898,21 +950,27 @@ def load_iconset(path, pixbuf2=None, transport=False): return _load_icon_list(list_, path, pixbuf2) def load_icon(icon_name): - '''load an icon from the iconset in 16x16''' + """ + Load an icon from the iconset in 16x16 + """ iconset = gajim.config.get('iconset') path = os.path.join(helpers.get_iconset_path(iconset), '16x16', '') icon_list = _load_icon_list([icon_name], path) return icon_list[icon_name] def load_mood_icon(icon_name): - '''load an icon from the mood iconset in 16x16''' + """ + Load an icon from the mood iconset in 16x16 + """ iconset = gajim.config.get('mood_iconset') path = os.path.join(helpers.get_mood_iconset_path(iconset), '') icon_list = _load_icon_list([icon_name], path) return icon_list[icon_name] def load_activity_icon(category, activity = None): - '''load an icon from the activity iconset in 16x16''' + """ + Load an icon from the activity iconset in 16x16 + """ iconset = gajim.config.get('activity_iconset') path = os.path.join(helpers.get_activity_iconset_path(iconset), category, '') @@ -922,8 +980,10 @@ def load_activity_icon(category, activity = None): return icon_list[activity] def load_icons_meta(): - '''load and return - AND + small icons to put on top left of an icon - for meta contacts.''' + """ + Load and return - AND + small icons to put on top left of an icon for meta + contacts + """ iconset = gajim.config.get('iconset') path = os.path.join(helpers.get_iconset_path(iconset), '16x16') # try to find opened_meta.png file, else opened.png else nopixbuf merge @@ -945,8 +1005,10 @@ def load_icons_meta(): return pixo, pixc def _load_icon_list(icons_list, path, pixbuf2 = None): - '''load icons in icons_list from the given path, - and add pixbuf2 on top left of each static images''' + """ + Load icons in icons_list from the given path, and add pixbuf2 on top left of + each static images + """ imgs = {} for icon in icons_list: # try to open a pixfile with the correct method @@ -972,7 +1034,9 @@ def _load_icon_list(icons_list, path, pixbuf2 = None): return imgs def make_jabber_state_images(): - '''initialise jabber_state_images dict''' + """ + Initialize jabber_state_images dictionary + """ iconset = gajim.config.get('iconset') if iconset: if helpers.get_iconset_path(iconset): @@ -1002,8 +1066,10 @@ def reload_jabber_state_images(): gajim.interface.roster.update_jabber_state_images() def label_set_autowrap(widget): - '''Make labels automatically re-wrap if their containers are resized. - Accepts label or container widgets.''' + """ + Make labels automatically re-wrap if their containers are resized. + Accepts label or container widgets + """ if isinstance (widget, gtk.Container): children = widget.get_children() for i in xrange (len (children)): @@ -1013,7 +1079,9 @@ def label_set_autowrap(widget): widget.connect_after('size-allocate', __label_size_allocate) def __label_size_allocate(widget, allocation): - '''Callback which re-allocates the size of a label.''' + """ + Callback which re-allocates the size of a label + """ layout = widget.get_layout() lw_old, lh_old = layout.get_size() diff --git a/src/gui_interface.py b/src/gui_interface.py index bff913616..76ab68c0c 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -54,7 +54,6 @@ if dbus_support.supported: import gtkgui_helpers - import dialogs import notify import message_control @@ -92,7 +91,6 @@ config_filename = gajimpaths['CONFIG_FILE'] from common import optparser parser = optparser.OptionsParser(config_filename) - import logging log = logging.getLogger('gajim.interface') @@ -265,10 +263,10 @@ class Interface: def handle_event_new_jid(self, account, data): #('NEW_JID', account, (old_jid, new_jid)) - ''' + """ This event is raised when our JID changed (most probably because we use - anonymous account. We update contact and roster entry in this case. - ''' + anonymous account. We update contact and roster entry in this case + """ self.roster.rename_self_contact(data[0], data[1], account) def edit_own_details(self, account): @@ -1324,6 +1322,7 @@ class Interface: def handle_event_file_request_error(self, account, array): # ('FILE_REQUEST_ERROR', account, (jid, file_props, error_msg)) jid, file_props, errmsg = array + jid = gajim.get_jid_without_resource(jid) ft = self.instances['file_transfers'] ft.set_status(file_props['type'], file_props['sid'], 'stop') errno = file_props['error'] @@ -1353,6 +1352,7 @@ class Interface: def handle_event_file_request(self, account, array): jid = array[0] + jid = gajim.get_jid_without_resource(jid) if jid not in gajim.contacts.get_jid_list(account): keyID = '' attached_keys = gajim.config.get_per('accounts', account, @@ -1519,7 +1519,9 @@ class Interface: contact.resource) def handle_event_signed_in(self, account, empty): - '''SIGNED_IN event is emitted when we sign in, so handle it''' + """ + SIGNED_IN event is emitted when we sign in, so handle it + """ # ('SIGNED_IN', account, ()) # block signed in notifications for 30 seconds gajim.block_signed_in_notifications[account] = True @@ -1826,7 +1828,9 @@ class Interface: dialogs.RosterItemExchangeWindow(account, data[0], data[1], data[2]) def handle_event_unique_room_id_supported(self, account, data): - '''Receive confirmation that unique_room_id are supported''' + """ + Receive confirmation that unique_room_id are supported + """ # ('UNIQUE_ROOM_ID_SUPPORTED', server, instance, room_id) instance = data[1] instance.unique_room_id_supported(data[0], data[2]) @@ -1945,10 +1949,10 @@ class Interface: # ('INSECURE_SSL_CONNECTION', account, (connection, connection_type)) server = gajim.config.get_per('accounts', account, 'hostname') def on_ok(is_checked): - del self.instances[account]['online_dialog']['insecure_ssl'] if not is_checked[0]: on_cancel() return + del self.instances[account]['online_dialog']['insecure_ssl'] if is_checked[1]: gajim.config.set_per('accounts', account, 'warn_when_insecure_ssl_connection', False) @@ -1989,6 +1993,27 @@ class Interface: _('PEP node %(node)s was not removed: %(message)s') % { 'node': data[1], 'message': data[2]}) + def handle_event_pep_received(self, account, data): + # ('PEP_RECEIVED', account, (jid, pep_type)) + jid = data[0] + pep_type = data[1] + ctrl = common.gajim.interface.msg_win_mgr.get_control(jid, account) + + if jid == common.gajim.get_jid_from_account(account): + self.roster.draw_account(account) + + if pep_type == 'nickname': + self.roster.draw_contact(jid, account) + if ctrl: + ctrl.update_ui() + win = ctrl.parent_win + win.redraw_tab(ctrl) + win.show_title() + else: + self.roster.draw_pep(jid, account, pep_type) + if ctrl: + ctrl.update_pep(pep_type) + def handle_event_archiving_changed(self, account, data): if 'archiving_preferences' in self.instances[account]: self.instances[account]['archiving_preferences'].archiving_changed( @@ -2098,16 +2123,16 @@ class Interface: 'JINGLE_CONNECTED': [self.handle_event_jingle_connected], 'JINGLE_DISCONNECTED': [self.handle_event_jingle_disconnected], 'JINGLE_ERROR': [self.handle_event_jingle_error], + 'PEP_RECEIVED': [self.handle_event_pep_received], 'ARCHIVING_CHANGED': [self.handle_event_archiving_changed], 'ARCHIVING_ERROR': [self.handle_event_archiving_error], } - def dispatch(self, event, account, data): - ''' - Dispatches an network event to the event handlers of this class. - - Return true if it could be dispatched to alteast one handler. - ''' + def dispatch(self, event, account, data): + """ + Dispatch an network event to the event handlers of this class. Return + true if it could be dispatched to alteast one handler + """ if event not in self.handlers: log.warning('Unknown event %s dispatched to GUI: %s' % (event, data)) return False @@ -2123,7 +2148,9 @@ class Interface: ################################################################################ def add_event(self, account, jid, type_, event_args): - '''add an event to the gajim.events var''' + """ + Add an event to the gajim.events var + """ # We add it to the gajim.events queue # Do we have a queue? jid = gajim.get_jid_without_resource(jid) @@ -2452,7 +2479,9 @@ class Interface: self.invalid_XML_chars = u'[\x00-\x08]|[\x0b-\x0c]|[\x0e-\x19]|[\ud800-\udfff]|[\ufffe-\uffff]' def popup_emoticons_under_button(self, button, parent_win): - ''' pops emoticons menu under button, located in parent_win''' + """ + Popup the emoticons menu under button, located in parent_win + """ gtkgui_helpers.popup_emoticons_under_button(self.emoticons_menu, button, parent_win) @@ -2560,8 +2589,10 @@ class Interface: ################################################################################ def join_gc_room(self, account, room_jid, nick, password, minimize=False, - is_continued=False): - '''joins the room immediately''' + is_continued=False): + """ + Join the room immediately + """ if not nick: nick = gajim.nicks[account] @@ -2786,33 +2817,27 @@ class Interface: listener.disconnect(self.music_track_changed_signal) self.music_track_changed_signal = None - def music_track_changed(self, unused_listener, music_track_info, account=''): - if account == '': + def music_track_changed(self, unused_listener, music_track_info, account=None): + if not account: accounts = gajim.connections.keys() else: accounts = [account] - if music_track_info is None: - artist = '' - title = '' - source = '' - elif hasattr(music_track_info, 'paused') and music_track_info.paused == 0: - artist = '' - title = '' - source = '' + + is_paused = hasattr(music_track_info, 'paused') and music_track_info.paused == 0 + if not music_track_info or is_paused: + artist = title = source = '' else: artist = music_track_info.artist title = music_track_info.title source = music_track_info.album for acct in accounts: - if acct not in gajim.connections: - continue if not gajim.account_is_connected(acct): continue - if not gajim.connections[acct].pep_supported: + if not gajim.config.get_per('accounts', acct, 'publish_tune'): continue if gajim.connections[acct].music_track_info == music_track_info: continue - pep.user_send_tune(acct, artist, title, source) + gajim.connections[acct].send_tune(artist, title, source) gajim.connections[acct].music_track_info = music_track_info def get_bg_fg_colors(self): @@ -2835,7 +2860,9 @@ class Interface: return (bg_str, fg_str) def read_sleepy(self): - '''Check idle status and change that status if needed''' + """ + Check idle status and change that status if needed + """ if not self.sleeper.poll(): # idle detection is not supported in that OS return False # stop looping in vain @@ -2889,7 +2916,9 @@ class Interface: return True # renew timeout (loop for ever) def autoconnect(self): - '''auto connect at startup''' + """ + Auto connect at startup + """ # dict of account that want to connect sorted by status shows = {} for a in gajim.connections: @@ -2928,7 +2957,9 @@ class Interface: helpers.launch_browser_mailer(kind, url) def process_connections(self): - ''' Called each foo (200) miliseconds. Check for idlequeue timeouts. ''' + """ + Called each foo (200) miliseconds. Check for idlequeue timeouts + """ try: gajim.idlequeue.process() except Exception: @@ -2952,7 +2983,11 @@ class Interface: sys.exit() def save_avatar_files(self, jid, photo, puny_nick = None, local = False): - '''Saves an avatar to a separate file, and generate files for dbus notifications. An avatar can be given as a pixmap directly or as an decoded image.''' + """ + Save an avatar to a separate file, and generate files for dbus + notifications. An avatar can be given as a pixmap directly or as an + decoded image + """ puny_jid = helpers.sanitize_filename(jid) path_to_file = os.path.join(gajim.AVATAR_PATH, puny_jid) if puny_nick: @@ -3005,7 +3040,9 @@ class Interface: (path_to_original_file, str(e))) def remove_avatar_files(self, jid, puny_nick = None, local = False): - '''remove avatar files of a jid''' + """ + Remove avatar files of a jid + """ puny_jid = helpers.sanitize_filename(jid) path_to_file = os.path.join(gajim.AVATAR_PATH, puny_jid) if puny_nick: @@ -3022,7 +3059,9 @@ class Interface: os.remove(path_to_file + '_notif_size_bw' + ext) def auto_join_bookmarks(self, account): - '''autojoin bookmarked GCs that have 'auto join' on for this account''' + """ + Autojoin bookmarked GCs that have 'auto join' on for this account + """ for bm in gajim.connections[account].bookmarks: if bm['autojoin'] in ('1', 'true'): jid = bm['jid'] @@ -3040,8 +3079,10 @@ class Interface: self.roster.add_groupchat(jid, account) def add_gc_bookmark(self, account, name, jid, autojoin, minimize, password, - nick): - '''add a bookmark for this account, sorted in bookmark list''' + nick): + """ + Add a bookmark for this account, sorted in bookmark list + """ bm = { 'name': name, 'jid': jid, @@ -3118,7 +3159,7 @@ class Interface: gajim.ipython_window = window def run(self): - if self.systray_capabilities and gajim.config.get('trayicon') != 'never': + if gajim.config.get('trayicon') != 'never': self.show_systray() self.roster = roster_window.RosterWindow() @@ -3353,21 +3394,9 @@ class Interface: gtkgui_helpers.make_jabber_state_images() self.systray_enabled = False - self.systray_capabilities = False - if (os.name == 'nt'): - import statusicon - self.systray = statusicon.StatusIcon() - self.systray_capabilities = True - else: # use ours, not GTK+ one - # [FIXME: remove this when we migrate to 2.10 and we can do - # cool tooltips somehow and (not dying to keep) animation] - import systray - self.systray_capabilities = systray.HAS_SYSTRAY_CAPABILITIES - if self.systray_capabilities: - self.systray = systray.Systray() - else: - gajim.config.set('trayicon', 'never') + import statusicon + self.systray = statusicon.StatusIcon() path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps', 'gajim.png') pix = gtk.gdk.pixbuf_new_from_file(path_to_file) @@ -3481,13 +3510,9 @@ class PassphraseRequest: class ThreadInterface: def __init__(self, func, func_args, callback, callback_args): - '''Call a function in a thread - - :param func: the function to call in the thread - :param func_args: list or arguments for this function - :param callback: callback to call once function is finished - :param callback_args: list of arguments for this callback - ''' + """ + Call a function in a thread + """ def thread_function(func, func_args, callback, callback_args): output = func(*func_args) gobject.idle_add(callback, output, *callback_args) diff --git a/src/gui_menu_builder.py b/src/gui_menu_builder.py index 0c975d5b6..453677aa2 100644 --- a/src/gui_menu_builder.py +++ b/src/gui_menu_builder.py @@ -28,9 +28,11 @@ from common import helpers from common.xmpp.protocol import NS_COMMANDS, NS_FILE, NS_MUC, NS_ESESSION def build_resources_submenu(contacts, account, action, room_jid=None, -room_account=None, cap=None): - ''' Build a submenu with contact's resources. - room_jid and room_account are for action self.on_invite_to_room ''' + room_account=None, cap=None): + """ + Build a submenu with contact's resources. room_jid and room_account are for + action self.on_invite_to_room + """ roster = gajim.interface.roster sub_menu = gtk.Menu() @@ -61,7 +63,9 @@ room_account=None, cap=None): return sub_menu def build_invite_submenu(invite_menuitem, list_): - '''list_ in a list of (contact, account)''' + """ + list_ in a list of (contact, account) + """ roster = gajim.interface.roster # used if we invite only one contact with several resources contact_list = [] @@ -145,10 +149,12 @@ def build_invite_submenu(invite_menuitem, list_): invite_to_submenu.append(menuitem) def get_contact_menu(contact, account, use_multiple_contacts=True, -show_start_chat=True, show_encryption=False, show_buttonbar_items=True, -control=None): - ''' Build contact popup menu for roster and chat window. - If control is not set, we hide invite_contacts_menuitem''' + show_start_chat=True, show_encryption=False, show_buttonbar_items=True, + control=None): + """ + Build contact popup menu for roster and chat window. If control is not set, + we hide invite_contacts_menuitem + """ if not contact: return @@ -320,7 +326,7 @@ control=None): for item in (send_custom_status_menuitem, send_single_message_menuitem, invite_menuitem, block_menuitem, unblock_menuitem, ignore_menuitem, unignore_menuitem, set_custom_avatar_menuitem, subscription_menuitem, - manage_contact_menuitem, convert_to_gc_menuitems): + manage_contact_menuitem, convert_to_gc_menuitem): item.set_no_show_all(True) item.hide() diff --git a/src/history_manager.py b/src/history_manager.py index 9ab638364..5d171b75c 100644 --- a/src/history_manager.py +++ b/src/history_manager.py @@ -290,11 +290,13 @@ class HistoryManager: self._fill_logs_listview(jid) def _get_jid_id(self, jid): - '''jids table has jid and jid_id + """ + jids table has jid and jid_id logs table has log_id, jid_id, contact_name, time, kind, show, message - so to ask logs we need jid_id that matches our jid in jids table - this method wants jid and returns the jid_id for later sql-ing on logs - ''' + + So to ask logs we need jid_id that matches our jid in jids table this + method wants jid and returns the jid_id for later sql-ing on logs + """ if jid.find('/') != -1: # if it has a / jid_is_from_pm = self._jid_is_from_pm(jid) if not jid_is_from_pm: # it's normal jid with resource @@ -304,22 +306,24 @@ class HistoryManager: return str(jid_id) def _get_jid_from_jid_id(self, jid_id): - '''jids table has jid and jid_id - this method accepts jid_id and returns the jid for later sql-ing on logs - ''' + """ + jids table has jid and jid_id + + This method accepts jid_id and returns the jid for later sql-ing on logs + """ self.cur.execute('SELECT jid FROM jids WHERE jid_id = ?', (jid_id,)) jid = self.cur.fetchone()[0] return jid def _jid_is_from_pm(self, jid): - '''if jid is gajim@conf/nkour it's likely a pm one, how we know - gajim@conf is not a normal guy and nkour is not his resource? - we ask if gajim@conf is already in jids (with type room jid) - this fails if user disables logging for room and only enables for - pm (so higly unlikely) and if we fail we do not go chaos - (user will see the first pm as if it was message in room's public chat) - and after that all okay''' - + """ + If jid is gajim@conf/nkour it's likely a pm one, how we know gajim@conf + is not a normal guy and nkour is not his resource? We ask if gajim@conf + is already in jids (with type room jid). This fails if user disables + logging for room and only enables for pm (so higly unlikely) and if we + fail we do not go chaos (user will see the first pm as if it was message + in room's public chat) and after that everything is ok + """ possible_room_jid = jid.split('/', 1)[0] self.cur.execute('SELECT jid_id FROM jids WHERE jid = ? AND type = ?', @@ -331,8 +335,9 @@ class HistoryManager: return True def _jid_is_room_type(self, jid): - '''returns True/False if given id is room type or not - eg. if it is room''' + """ + Return True/False if given id is room type or not eg. if it is room + """ self.cur.execute('SELECT type FROM jids WHERE jid = ?', (jid,)) row = self.cur.fetchone() if row is None: @@ -343,8 +348,10 @@ class HistoryManager: return False def _fill_logs_listview(self, jid): - '''fill the listview with all messages that user sent to or - received from JID''' + """ + Fill the listview with all messages that user sent to or received from + JID + """ # no need to lower jid in this context as jid is already lowered # as we use those jids from db jid_id = self._get_jid_id(jid) @@ -403,7 +410,9 @@ class HistoryManager: subject, nickname)) def _fill_search_results_listview(self, text): - '''ask db and fill listview with results that match text''' + """ + Ask db and fill listview with results that match text + """ self.search_results_liststore.clear() like_sql = '%' + text + '%' self.cur.execute(''' @@ -610,8 +619,8 @@ class HistoryManager: liststore, list_of_paths)) def on_search_db_button_clicked(self, widget): - text = self.search_entry.get_text() - if text == '': + text = self.search_entry.get_text().decode('utf-8') + if not text: return self.welcome_vbox.hide() diff --git a/src/htmltextview.py b/src/htmltextview.py index 36f3ac131..5c12a049c 100644 --- a/src/htmltextview.py +++ b/src/htmltextview.py @@ -804,6 +804,10 @@ class HtmlTextView(gtk.TextView): self.connect('motion-notify-event', self.__motion_notify_event) self.connect('leave-notify-event', self.__leave_event) self.connect('enter-notify-event', self.__motion_notify_event) + self.connect('realize', self.on_html_text_view_realized) + self.connect('unrealize', self.on_html_text_view_unrealized) + self.connect('copy-clipboard', self.on_html_text_view_copy_clipboard) + self.get_buffer().connect_after('mark-set', self.on_text_buffer_mark_set) self.get_buffer().create_tag('eol', scale = pango.SCALE_XX_SMALL) self.tooltip = tooltips.BaseTooltip() self.config = gajim.config @@ -873,7 +877,43 @@ class HtmlTextView(gtk.TextView): #if not eob.starts_line(): # buffer_.insert(eob, '\n') + def on_html_text_view_copy_clipboard(self, unused_data): + clipboard = self.get_clipboard(gtk.gdk.SELECTION_CLIPBOARD) + clipboard.set_text(self.get_selected_text()) + self.emit_stop_by_name('copy-clipboard') + def on_html_text_view_realized(self, unused_data): + self.get_buffer().remove_selection_clipboard(self.get_clipboard(gtk.gdk.SELECTION_PRIMARY)) + + def on_html_text_view_unrealized(self, unused_data): + self.get_buffer().add_selection_clipboard(self.get_clipboard(gtk.gdk.SELECTION_PRIMARY)) + + def on_text_buffer_mark_set(self, location, mark, unused_data): + bounds = self.get_buffer().get_selection_bounds() + if bounds: + clipboard = self.get_clipboard(gtk.gdk.SELECTION_PRIMARY) + clipboard.set_text(self.get_selected_text()) + + def get_selected_text(self): + bounds = self.get_buffer().get_selection_bounds() + selection = '' + if bounds: + (search_iter, end) = bounds + + while (search_iter.compare(end)): + character = search_iter.get_char() + if character == u'\ufffc': + anchor = search_iter.get_child_anchor() + if anchor: + text = anchor.get_data('plaintext') + if text: + selection+=text + else: + selection+=character + else: + selection+=character + search_iter.forward_char() + return selection change_cursor = None diff --git a/src/message_textview.py b/src/message_textview.py index 40ba81067..236ad1cb0 100644 --- a/src/message_textview.py +++ b/src/message_textview.py @@ -21,15 +21,20 @@ ## along with Gajim. If not, see . ## +import gc + import gtk import gobject import pango + import gtkgui_helpers from common import gajim class MessageTextView(gtk.TextView): - '''Class for the message textview (where user writes new messages) - for chat/groupchat windows''' + """ + Class for the message textview (where user writes new messages) for + chat/groupchat windows + """ __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, # return value @@ -272,13 +277,13 @@ class MessageTextView(gtk.TextView): else: return None - def destroy(self): - import gc - gobject.idle_add(lambda:gc.collect()) + gobject.idle_add(gc.collect) def clear(self, widget = None): - '''clear text in the textview''' + """ + Clear text in the textview + """ buffer_ = self.get_buffer() start, end = buffer_.get_bounds() buffer_.delete(start, end) diff --git a/src/profile_window.py b/src/profile_window.py index a417ede08..d00bfc335 100644 --- a/src/profile_window.py +++ b/src/profile_window.py @@ -68,7 +68,7 @@ class ProfileWindow: return True # loop forever def remove_statusbar(self, message_id): - self.statusbar.remove(self.context_id, message_id) + self.statusbar.remove_message(self.context_id, message_id) self.remove_statusbar_timeout_id = None def on_profile_window_destroy(self, widget): @@ -246,7 +246,7 @@ class ProfileWindow: self.set_value(i + '_entry', vcard_[i]) if self.update_progressbar_timeout_id is not None: if self.message_id: - self.statusbar.remove(self.context_id, self.message_id) + self.statusbar.remove_message(self.context_id, self.message_id) self.message_id = self.statusbar.push(self.context_id, _('Information received')) self.remove_statusbar_timeout_id = gobject.timeout_add_seconds(3, @@ -322,8 +322,7 @@ class ProfileWindow: nick = '' if 'NICKNAME' in vcard_: nick = vcard_['NICKNAME'] - from common import pep - pep.user_send_nickname(self.account, nick) + gajim.connections[self.account].send_nickname(nick) if nick == '': nick = gajim.config.get_per('accounts', self.account, 'name') gajim.nicks[self.account] = nick @@ -342,7 +341,7 @@ class ProfileWindow: def vcard_not_published(self): if self.message_id: - self.statusbar.remove(self.context_id, self.message_id) + self.statusbar.remove_message(self.context_id, self.message_id) self.message_id = self.statusbar.push(self.context_id, _('Information NOT published')) self.remove_statusbar_timeout_id = gobject.timeout_add_seconds(3, diff --git a/src/roster_window.py b/src/roster_window.py index cfdb69cf7..dce525e61 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -38,6 +38,7 @@ import gobject import os import sys import time +import locale import common.sleepy import history_window @@ -1022,55 +1023,21 @@ class RosterWindow: self.model[child_iter][C_NAME] = account_name - if gajim.config.get('show_mood_in_roster') \ - and 'mood' in gajim.connections[account].mood \ - and gajim.connections[account].mood['mood'].strip() in MOODS: - - self.model[child_iter][C_MOOD_PIXBUF] = gtkgui_helpers.load_mood_icon( - gajim.connections[account].mood['mood'].strip()).get_pixbuf() - - elif gajim.config.get('show_mood_in_roster') \ - and 'mood' in gajim.connections[account].mood: - self.model[child_iter][C_MOOD_PIXBUF] = \ - gtkgui_helpers.load_mood_icon('unknown'). \ - get_pixbuf() + pep = gajim.connections[account].pep + if gajim.config.get('show_mood_in_roster') and 'mood' in pep: + self.model[child_iter][C_MOOD_PIXBUF] = pep['mood'].asPixbufIcon() else: self.model[child_iter][C_MOOD_PIXBUF] = None - if gajim.config.get('show_activity_in_roster') \ - and 'activity' in gajim.connections[account].activity \ - and gajim.connections[account].activity['activity'].strip() \ - in ACTIVITIES: - if 'subactivity' in gajim.connections[account].activity \ - and gajim.connections[account].activity['subactivity'].strip() \ - in ACTIVITIES[gajim.connections[account].activity['activity'].strip()]: - self.model[child_iter][C_ACTIVITY_PIXBUF] = \ - gtkgui_helpers.load_activity_icon( - gajim.connections[account].activity['activity'].strip(), - gajim.connections[account].activity['subactivity'].strip()). \ - get_pixbuf() - else: - self.model[child_iter][C_ACTIVITY_PIXBUF] = \ - gtkgui_helpers.load_activity_icon( - gajim.connections[account].activity['activity'].strip()). \ - get_pixbuf() - elif gajim.config.get('show_activity_in_roster') \ - and 'activity' in gajim.connections[account].activity: - self.model[child_iter][C_ACTIVITY_PIXBUF] = \ - gtkgui_helpers.load_activity_icon('unknown'). \ - get_pixbuf() + if gajim.config.get('show_activity_in_roster') and 'activity' in pep: + self.model[child_iter][C_ACTIVITY_PIXBUF] = pep['activity'].asPixbufIcon() else: self.model[child_iter][C_ACTIVITY_PIXBUF] = None - if gajim.config.get('show_tunes_in_roster') \ - and ('artist' in gajim.connections[account].tune \ - or 'title' in gajim.connections[account].tune): - path = os.path.join(gajim.DATA_DIR, 'emoticons', 'static', 'music.png') - self.model[child_iter][C_TUNE_PIXBUF] = \ - gtk.gdk.pixbuf_new_from_file(path) + if gajim.config.get('show_tunes_in_roster') and 'tune' in pep: + self.model[child_iter][C_TUNE_PIXBUF] = pep['tune'].asPixbufIcon() else: self.model[child_iter][C_TUNE_PIXBUF] = None - return False def draw_group(self, group, account): @@ -1276,71 +1243,39 @@ class RosterWindow: return False - - def draw_mood(self, jid, account): + def _is_pep_shown_in_roster(self, pep_type): + if pep_type == 'mood': + return gajim.config.get('show_mood_in_roster') + elif pep_type == 'activity': + return gajim.config.get('show_activity_in_roster') + elif pep_type == 'tune': + return gajim.config.get('show_tunes_in_roster') + else: + return False + + def draw_all_pep_types(self, jid, account): + for pep_type in self._pep_type_to_model_column: + self.draw_pep(jid, account, pep_type) + + def draw_pep(self, jid, account, pep_type): + if pep_type not in self._pep_type_to_model_column: + return + if not self._is_pep_shown_in_roster(pep_type): + return + + model_column = self._pep_type_to_model_column[pep_type] iters = self._get_contact_iter(jid, account, model=self.model) - if not iters or not gajim.config.get('show_mood_in_roster'): + if not iters: return jid = self.model[iters[0]][C_JID] jid = jid.decode('utf-8') contact = gajim.contacts.get_contact(account, jid) - if 'mood' in contact.mood and contact.mood['mood'].strip() in MOODS: - pixbuf = gtkgui_helpers.load_mood_icon( - contact.mood['mood'].strip()).get_pixbuf() - elif 'mood' in contact.mood: - pixbuf = gtkgui_helpers.load_mood_icon( - 'unknown').get_pixbuf() + if pep_type in contact.pep: + pixbuf = contact.pep[pep_type].asPixbufIcon() else: pixbuf = None for child_iter in iters: - self.model[child_iter][C_MOOD_PIXBUF] = pixbuf - return False - - - def draw_activity(self, jid, account): - iters = self._get_contact_iter(jid, account, model=self.model) - if not iters or not gajim.config.get('show_activity_in_roster'): - return - jid = self.model[iters[0]][C_JID] - jid = jid.decode('utf-8') - contact = gajim.contacts.get_contact(account, jid) - if 'activity' in contact.activity \ - and contact.activity['activity'].strip() in ACTIVITIES: - if 'subactivity' in contact.activity \ - and contact.activity['subactivity'].strip() in \ - ACTIVITIES[contact.activity['activity'].strip()]: - pixbuf = gtkgui_helpers.load_activity_icon( - contact.activity['activity'].strip(), - contact.activity['subactivity'].strip()).get_pixbuf() - else: - pixbuf = gtkgui_helpers.load_activity_icon( - contact.activity['activity'].strip()).get_pixbuf() - elif 'activity' in contact.activity: - pixbuf = gtkgui_helpers.load_activity_icon( - 'unknown').get_pixbuf() - else: - pixbuf = None - for child_iter in iters: - self.model[child_iter][C_ACTIVITY_PIXBUF] = pixbuf - return False - - - def draw_tune(self, jid, account): - iters = self._get_contact_iter(jid, account, model=self.model) - if not iters or not gajim.config.get('show_tunes_in_roster'): - return - jid = self.model[iters[0]][C_JID] - jid = jid.decode('utf-8') - contact = gajim.contacts.get_contact(account, jid) - if 'artist' in contact.tune or 'title' in contact.tune: - path = os.path.join(gajim.DATA_DIR, 'emoticons', 'static', 'music.png') - pixbuf = gtk.gdk.pixbuf_new_from_file(path) - else: - pixbuf = None - for child_iter in iters: - self.model[child_iter][C_TUNE_PIXBUF] = pixbuf - return False - + self.model[child_iter][model_column] = pixbuf def draw_avatar(self, jid, account): iters = self._get_contact_iter(jid, account, model=self.model) @@ -1359,9 +1294,7 @@ class RosterWindow: def draw_completely(self, jid, account): self.draw_contact(jid, account) - self.draw_mood(jid, account) - self.draw_activity(jid, account) - self.draw_tune(jid, account) + self.draw_all_pep_types(jid, account) self.draw_avatar(jid, account) def adjust_and_draw_contact_context(self, jid, account): @@ -1626,9 +1559,7 @@ class RosterWindow: account1 = account1.decode('utf-8') account2 = account2.decode('utf-8') if type1 == 'account': - if account1 < account2: - return -1 - return 1 + return locale.strcoll(account1, account2) jid1 = model[iter1][C_JID].decode('utf-8') jid2 = model[iter2][C_JID].decode('utf-8') if type1 == 'contact': @@ -1675,20 +1606,23 @@ class RosterWindow: elif show1 > show2: return 1 # We compare names - if name1.lower() < name2.lower(): + cmp_result = locale.strcoll(name1.lower(), name2.lower()) + if cmp_result < 0: return -1 - if name2.lower() < name1.lower(): + if cmp_result > 0: return 1 if type1 == 'contact' and type2 == 'contact': # We compare account names - if account1.lower() < account2.lower(): + cmp_result = locale.strcoll(account1.lower(), account2.lower()) + if cmp_result < 0: return -1 - if account2.lower() < account1.lower(): + if cmp_result > 0: return 1 # We compare jids - if jid1.lower() < jid2.lower(): + cmp_result = locale.strcoll(jid1.lower(), jid2.lower()) + if cmp_result < 0: return -1 - if jid2.lower() < jid1.lower(): + if cmp_result > 0: return 1 return 0 @@ -1964,36 +1898,36 @@ class RosterWindow: self.send_status_continue(account, status, txt, auto, to) - def send_pep(self, account, pep_dict=None): - '''Sends pep information (activity, mood)''' - if not pep_dict: - return - # activity - if 'activity' in pep_dict and pep_dict['activity'] in pep.ACTIVITIES: - activity = pep_dict['activity'] - if 'subactivity' in pep_dict and \ - pep_dict['subactivity'] in pep.ACTIVITIES[activity]: - subactivity = pep_dict['subactivity'] - else: - subactivity = 'other' - if 'activity_text' in pep_dict: - activity_text = pep_dict['activity_text'] - else: - activity_text = '' - pep.user_send_activity(account, activity, subactivity, activity_text) - else: - pep.user_send_activity(account, '') + def send_pep(self, account, pep_dict): + connection = gajim.connections[account] - # mood - if 'mood' in pep_dict and pep_dict['mood'] in pep.MOODS: - mood = pep_dict['mood'] - if 'mood_text' in pep_dict: - mood_text = pep_dict['mood_text'] - else: - mood_text = '' - pep.user_send_mood(account, mood, mood_text) + if 'activity' in pep_dict: + activity = pep_dict['activity'] + subactivity = pep_dict.get('subactivity', None) + activity_text = pep_dict.get('activity_text', None) + connection.send_activity(activity, subactivity, activity_text) else: - pep.user_send_mood(account, '') + connection.retract_activity() + + if 'mood' in pep_dict: + mood = pep_dict['mood'] + mood_text = pep_dict.get('mood_text', None) + connection.send_mood(mood, mood_text) + else: + connection.retract_mood() + + def delete_pep(self, jid, account): + if jid == gajim.get_jid_from_account(account): + gajim.connections[account].pep = {} + self.draw_account(account) + + for contact in gajim.contacts.get_contacts(account, jid): + contact.pep = {} + + self.draw_all_pep_types(jid, account) + ctrl = gajim.interface.msg_win_mgr.get_control(jid, account) + if ctrl: + ctrl.update_all_pep_types() def send_status_continue(self, account, status, txt, auto, to): if gajim.account_is_connected(account) and not to: @@ -2008,8 +1942,7 @@ class RosterWindow: gajim.connections[account].send_custom_status(status, txt, to) else: if status in ('invisible', 'offline'): - pep.delete_pep(gajim.get_jid_from_account(account), \ - account) + self.delete_pep(gajim.get_jid_from_account(account), account) was_invisible = gajim.connections[account].connected == \ gajim.SHOW_LIST.index('invisible') gajim.connections[account].change_status(status, txt, auto) @@ -2087,7 +2020,7 @@ class RosterWindow: contact_instances) if not keep_pep and contact.jid != gajim.get_jid_from_account(account) \ and not contact.is_groupchat(): - pep.delete_pep(contact.jid, account) + self.delete_pep(contact.jid, account) # Redraw everything and select the sender self.adjust_and_draw_contact_context(contact.jid, account) @@ -2509,11 +2442,10 @@ class RosterWindow: account_name = account if gajim.account_is_connected(account): account_name += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total)) - contact = gajim.contacts.create_contact(jid=jid, account=account, name=account_name, - show=connection.get_status(), sub='', status=connection.status, - resource=connection.server_resource, - priority=connection.priority, mood=connection.mood, - tune=connection.tune, activity=connection.activity) + contact = gajim.contacts.create_self_contact(jid=jid, account=account, + name=account_name, show=connection.get_status(), + status=connection.status, resource=connection.server_resource, + priority=connection.priority) if gajim.connections[account].gpg: contact.keyID = gajim.config.get_per('accounts', connection.name, 'keyid') @@ -3396,23 +3328,19 @@ class RosterWindow: gajim.interface.instances['preferences'] = config.PreferencesWindow() def on_publish_tune_toggled(self, widget, account): - act = widget.get_active() - gajim.config.set_per('accounts', account, 'publish_tune', act) - if act: + active = widget.get_active() + gajim.config.set_per('accounts', account, 'publish_tune', active) + if active: gajim.interface.enable_music_listener() else: - # disable it only if no other account use it - for acct in gajim.connections: - if gajim.config.get_per('accounts', acct, 'publish_tune'): + gajim.connections[account].retract_tune() + # disable music listener only if no other account uses it + for acc in gajim.connections: + if gajim.config.get_per('accounts', acc, 'publish_tune'): break else: gajim.interface.disable_music_listener() - if gajim.connections[account].pep_supported: - # As many implementations don't support retracting items, we send a - # "Stopped" event first - pep.user_send_tune(account, '') - pep.user_retract_tune(account) helpers.update_optional_features(account) def on_pep_services_menuitem_activate(self, widget, account): @@ -4489,9 +4417,9 @@ class RosterWindow: renderer.set_property('xpad', 8) - def _fill_mood_pixbuf_renderer(self, column, renderer, model, titer, - data = None): - '''When a row is added, set properties for avatar renderer''' + def _fill_pep_pixbuf_renderer(self, column, renderer, model, titer, + data=None): + '''When a row is added, draw the respective pep icon''' theme = gajim.config.get('roster_theme') type_ = model[titer][C_TYPE] if type_ == 'group': @@ -4499,155 +4427,37 @@ class RosterWindow: return # allocate space for the icon only if needed - if model[titer][C_MOOD_PIXBUF]: + if model[titer][data]: renderer.set_property('visible', True) else: renderer.set_property('visible', False) if type_ == 'account': - color = gajim.config.get_per('themes', theme, - 'accountbgcolor') + color = gajim.config.get_per('themes', theme, 'accountbgcolor') if color: renderer.set_property('cell-background', color) else: - self.set_renderer_color(renderer, - gtk.STATE_ACTIVE) + self.set_renderer_color(renderer, gtk.STATE_ACTIVE) # align pixbuf to the right) renderer.set_property('xalign', 1) # prevent type_ = None, see http://trac.gajim.org/ticket/2534 elif type_: - if not model[titer][C_JID] \ - or not model[titer][C_ACCOUNT]: + if not model[titer][C_JID] or not model[titer][C_ACCOUNT]: # This can append at the moment we add the row return jid = model[titer][C_JID].decode('utf-8') account = model[titer][C_ACCOUNT].decode('utf-8') if jid in gajim.newly_added[account]: - renderer.set_property('cell-background', - gajim.config.get( + renderer.set_property('cell-background', gajim.config.get( 'just_connected_bg_color')) elif jid in gajim.to_be_removed[account]: - renderer.set_property('cell-background', - gajim.config.get( + renderer.set_property('cell-background', gajim.config.get( 'just_disconnected_bg_color')) else: - color = gajim.config.get_per('themes', - theme, 'contactbgcolor') - if color: - renderer.set_property( - 'cell-background', color) - else: - renderer.set_property( - 'cell-background', None) + color = gajim.config.get_per('themes', theme, 'contactbgcolor') + renderer.set_property('cell-background', color if color else None) # align pixbuf to the right renderer.set_property('xalign', 1) - - def _fill_activity_pixbuf_renderer(self, column, renderer, model, titer, - data = None): - '''When a row is added, set properties for avatar renderer''' - theme = gajim.config.get('roster_theme') - type_ = model[titer][C_TYPE] - if type_ == 'group': - renderer.set_property('visible', False) - return - - # allocate space for the icon only if needed - if model[titer][C_ACTIVITY_PIXBUF]: - renderer.set_property('visible', True) - else: - renderer.set_property('visible', False) - if type_ == 'account': - color = gajim.config.get_per('themes', theme, - 'accountbgcolor') - if color: - renderer.set_property('cell-background', color) - else: - self.set_renderer_color(renderer, - gtk.STATE_ACTIVE) - # align pixbuf to the right) - renderer.set_property('xalign', 1) - # prevent type_ = None, see http://trac.gajim.org/ticket/2534 - elif type_: - if not model[titer][C_JID] \ - or not model[titer][C_ACCOUNT]: - # This can append at the moment we add the row - return - jid = model[titer][C_JID].decode('utf-8') - account = model[titer][C_ACCOUNT].decode('utf-8') - if jid in gajim.newly_added[account]: - renderer.set_property('cell-background', - gajim.config.get( - 'just_connected_bg_color')) - elif jid in gajim.to_be_removed[account]: - renderer.set_property('cell-background', - gajim.config.get( - 'just_disconnected_bg_color')) - else: - color = gajim.config.get_per('themes', - theme, 'contactbgcolor') - if color: - renderer.set_property( - 'cell-background', color) - else: - renderer.set_property( - 'cell-background', None) - # align pixbuf to the right - renderer.set_property('xalign', 1) - - - def _fill_tune_pixbuf_renderer(self, column, renderer, model, titer, - data = None): - '''When a row is added, set properties for avatar renderer''' - theme = gajim.config.get('roster_theme') - type_ = model[titer][C_TYPE] - if type_ == 'group': - renderer.set_property('visible', False) - return - - # allocate space for the icon only if needed - if model[titer][C_TUNE_PIXBUF]: - renderer.set_property('visible', True) - else: - renderer.set_property('visible', False) - if type_ == 'account': - color = gajim.config.get_per('themes', theme, - 'accountbgcolor') - if color: - renderer.set_property('cell-background', color) - else: - self.set_renderer_color(renderer, - gtk.STATE_ACTIVE) - # align pixbuf to the right) - renderer.set_property('xalign', 1) - # prevent type_ = None, see http://trac.gajim.org/ticket/2534 - elif type_: - if not model[titer][C_JID] \ - or not model[titer][C_ACCOUNT]: - # This can append at the moment we add the row - return - jid = model[titer][C_JID].decode('utf-8') - account = model[titer][C_ACCOUNT].decode('utf-8') - if jid in gajim.newly_added[account]: - renderer.set_property('cell-background', - gajim.config.get( - 'just_connected_bg_color')) - elif jid in gajim.to_be_removed[account]: - renderer.set_property('cell-background', - gajim.config.get( - 'just_disconnected_bg_color')) - else: - color = gajim.config.get_per('themes', - theme, 'contactbgcolor') - if color: - renderer.set_property( - 'cell-background', color) - else: - renderer.set_property( - 'cell-background', None) - # align pixbuf to the right - renderer.set_property('xalign', 1) - - def _fill_avatar_pixbuf_renderer(self, column, renderer, model, titer, data = None): '''When a row is added, set properties for avatar renderer''' @@ -5948,19 +5758,23 @@ class RosterWindow: col.pack_start(render_pixbuf, expand=False) col.add_attribute(render_pixbuf, 'pixbuf', C_MOOD_PIXBUF) col.set_cell_data_func(render_pixbuf, - self._fill_mood_pixbuf_renderer, None) + self._fill_pep_pixbuf_renderer, C_MOOD_PIXBUF) render_pixbuf = gtk.CellRendererPixbuf() col.pack_start(render_pixbuf, expand=False) col.add_attribute(render_pixbuf, 'pixbuf', C_ACTIVITY_PIXBUF) col.set_cell_data_func(render_pixbuf, - self._fill_activity_pixbuf_renderer, None) + self._fill_pep_pixbuf_renderer, C_ACTIVITY_PIXBUF) render_pixbuf = gtk.CellRendererPixbuf() col.pack_start(render_pixbuf, expand=False) col.add_attribute(render_pixbuf, 'pixbuf', C_TUNE_PIXBUF) col.set_cell_data_func(render_pixbuf, - self._fill_tune_pixbuf_renderer, None) + self._fill_pep_pixbuf_renderer, C_TUNE_PIXBUF) + + self._pep_type_to_model_column = {'mood': C_MOOD_PIXBUF, + 'activity': C_ACTIVITY_PIXBUF, + 'tune': C_TUNE_PIXBUF} if gajim.config.get('avatar_position_in_roster') == 'right': add_avatar_renderer() @@ -6010,10 +5824,8 @@ class RosterWindow: if gajim.config.get('show_roster_on_startup'): self.window.show_all() else: - if not gajim.config.get('trayicon') or not \ - gajim.interface.systray_capabilities: - # cannot happen via GUI, but I put this incase user touches - # config. without trayicon, he or she should see the roster! + if gajim.config.get('trayicon') != 'always': + # Without trayicon, user should see the roster! self.window.show_all() gajim.config.set('show_roster_on_startup', True) diff --git a/src/session.py b/src/session.py index a3693d130..614bb88ef 100644 --- a/src/session.py +++ b/src/session.py @@ -119,6 +119,7 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): log_type = 'chat_msg_recv' else: log_type = 'single_msg_recv' + if self.is_loggable() and msgtxt: try: msg_id = gajim.logger.write(log_type, full_jid_with_resource, diff --git a/src/statusicon.py b/src/statusicon.py index 90bdb8c91..e28b67cfd 100644 --- a/src/statusicon.py +++ b/src/statusicon.py @@ -25,26 +25,62 @@ import sys import gtk -import systray +import gobject +import os + +import dialogs +import config +import tooltips +import gtkgui_helpers +import tooltips from common import gajim from common import helpers +from common import pep -class StatusIcon(systray.Systray): +class StatusIcon: '''Class for the notification area icon''' - #NOTE: gtk api does NOT allow: - # leave, enter motion notify - # and can't do cool tooltips we use def __init__(self): - systray.Systray.__init__(self) + self.single_message_handler_id = None + self.new_chat_handler_id = None + # click somewhere else does not popdown menu. workaround this. + self.added_hide_menuitem = False + self.status = 'offline' + self.xml = gtkgui_helpers.get_glade('systray_context_menu.glade') + self.systray_context_menu = self.xml.get_widget('systray_context_menu') + self.xml.signal_autoconnect(self) + self.popup_menus = [] self.status_icon = None + self.tooltip = tooltips.NotificationAreaTooltip() + + def subscribe_events(self): + '''Register listeners to the events class''' + gajim.events.event_added_subscribe(self.on_event_added) + gajim.events.event_removed_subscribe(self.on_event_removed) + + def unsubscribe_events(self): + '''Unregister listeners to the events class''' + gajim.events.event_added_unsubscribe(self.on_event_added) + gajim.events.event_removed_unsubscribe(self.on_event_removed) + + def on_event_added(self, event): + '''Called when an event is added to the event list''' + if event.show_in_systray: + self.set_img() + + def on_event_removed(self, event_list): + '''Called when one or more events are removed from the event list''' + self.set_img() def show_icon(self): if not self.status_icon: self.status_icon = gtk.StatusIcon() + self.status_icon.set_property('has-tooltip', True) self.status_icon.connect('activate', self.on_status_icon_left_clicked) self.status_icon.connect('popup-menu', self.on_status_icon_right_clicked) + self.status_icon.connect('query-tooltip', + self.on_status_icon_query_tooltip) self.set_img() self.status_icon.set_visible(True) @@ -53,6 +89,11 @@ class StatusIcon(systray.Systray): def on_status_icon_right_clicked(self, widget, event_button, event_time): self.make_menu(event_button, event_time) + def on_status_icon_query_tooltip(self, widget, x, y, keyboard_mode, tooltip): + self.tooltip.populate() + tooltip.set_custom(self.tooltip.hbox) + return True + def hide_icon(self): self.status_icon.set_visible(False) self.unsubscribe_events() @@ -64,22 +105,315 @@ class StatusIcon(systray.Systray): '''apart from image, we also update tooltip text here''' if not gajim.interface.systray_enabled: return - text = helpers.get_notification_icon_tooltip_text() - self.status_icon.set_tooltip(text) if gajim.events.get_nb_systray_events(): - state = 'event' self.status_icon.set_blinking(True) else: - state = self.status self.status_icon.set_blinking(False) - #FIXME: do not always use 16x16 (ask actually used size and use that) - image = gajim.interface.jabber_state_images['16'][state] + # FIXME: do not always use 16x16 (ask actually used size and use that) + image = gajim.interface.jabber_state_images['16'][self.status] if image.get_storage_type() == gtk.IMAGE_PIXBUF: self.status_icon.set_from_pixbuf(image.get_pixbuf()) - #FIXME: oops they forgot to support GIF animation? - #or they were lazy to get it to work under Windows! WTF! - #elif image.get_storage_type() == gtk.IMAGE_ANIMATION: + # FIXME: oops they forgot to support GIF animation? + # or they were lazy to get it to work under Windows! WTF! + elif image.get_storage_type() == gtk.IMAGE_ANIMATION: + self.status_icon.set_from_pixbuf( + image.get_animation().get_static_image()) # self.img_tray.set_from_animation(image.get_animation()) + def change_status(self, global_status): + ''' set tray image to 'global_status' ''' + # change image and status, only if it is different + if global_status is not None and self.status != global_status: + self.status = global_status + self.set_img() + + def start_chat(self, widget, account, jid): + contact = gajim.contacts.get_first_contact_from_jid(account, jid) + if gajim.interface.msg_win_mgr.has_window(jid, account): + gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab( + jid, account) + elif contact: + gajim.interface.new_chat(contact, account) + gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab( + jid, account) + + def on_single_message_menuitem_activate(self, widget, account): + dialogs.SingleMessageWindow(account, action='send') + + def on_new_chat(self, widget, account): + dialogs.NewChatDialog(account) + + def make_menu(self, event_button, event_time): + '''create chat with and new message (sub) menus/menuitems''' + for m in self.popup_menus: + m.destroy() + + chat_with_menuitem = self.xml.get_widget('chat_with_menuitem') + single_message_menuitem = self.xml.get_widget( + 'single_message_menuitem') + status_menuitem = self.xml.get_widget('status_menu') + join_gc_menuitem = self.xml.get_widget('join_gc_menuitem') + sounds_mute_menuitem = self.xml.get_widget('sounds_mute_menuitem') + + if self.single_message_handler_id: + single_message_menuitem.handler_disconnect( + self.single_message_handler_id) + self.single_message_handler_id = None + if self.new_chat_handler_id: + chat_with_menuitem.disconnect(self.new_chat_handler_id) + self.new_chat_handler_id = None + + sub_menu = gtk.Menu() + self.popup_menus.append(sub_menu) + status_menuitem.set_submenu(sub_menu) + + gc_sub_menu = gtk.Menu() # gc is always a submenu + join_gc_menuitem.set_submenu(gc_sub_menu) + + # We need our own set of status icons, let's make 'em! + iconset = gajim.config.get('iconset') + path = os.path.join(helpers.get_iconset_path(iconset), '16x16') + state_images = gtkgui_helpers.load_iconset(path) + + if 'muc_active' in state_images: + join_gc_menuitem.set_image(state_images['muc_active']) + + for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): + uf_show = helpers.get_uf_show(show, use_mnemonic = True) + item = gtk.ImageMenuItem(uf_show) + item.set_image(state_images[show]) + sub_menu.append(item) + item.connect('activate', self.on_show_menuitem_activate, show) + + item = gtk.SeparatorMenuItem() + sub_menu.append(item) + + item = gtk.ImageMenuItem(_('_Change Status Message...')) + path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'kbd_input.png') + img = gtk.Image() + img.set_from_file(path) + item.set_image(img) + sub_menu.append(item) + item.connect('activate', self.on_change_status_message_activate) + + connected_accounts = gajim.get_number_of_connected_accounts() + if connected_accounts < 1: + item.set_sensitive(False) + + connected_accounts_with_private_storage = 0 + + item = gtk.SeparatorMenuItem() + sub_menu.append(item) + + uf_show = helpers.get_uf_show('offline', use_mnemonic = True) + item = gtk.ImageMenuItem(uf_show) + item.set_image(state_images['offline']) + sub_menu.append(item) + item.connect('activate', self.on_show_menuitem_activate, 'offline') + + iskey = connected_accounts > 0 and not (connected_accounts == 1 and + gajim.connections[gajim.connections.keys()[0]].is_zeroconf) + chat_with_menuitem.set_sensitive(iskey) + single_message_menuitem.set_sensitive(iskey) + join_gc_menuitem.set_sensitive(iskey) + + accounts_list = sorted(gajim.contacts.get_accounts()) + # items that get shown whether an account is zeroconf or not + if connected_accounts > 1: # 2 or more connections? make submenus + account_menu_for_chat_with = gtk.Menu() + chat_with_menuitem.set_submenu(account_menu_for_chat_with) + self.popup_menus.append(account_menu_for_chat_with) + + for account in accounts_list: + if gajim.account_is_connected(account): + # for chat_with + item = gtk.MenuItem(_('using account %s') % account) + account_menu_for_chat_with.append(item) + item.connect('activate', self.on_new_chat, account) + + elif connected_accounts == 1: # one account + # one account connected, no need to show 'as jid' + for account in gajim.connections: + if gajim.connections[account].connected > 1: + # for start chat + self.new_chat_handler_id = chat_with_menuitem.connect( + 'activate', self.on_new_chat, account) + break # No other connected account + + # menu items that don't apply to zeroconf connections + if connected_accounts == 1 or (connected_accounts == 2 and \ + gajim.zeroconf_is_connected()): + # only one 'real' (non-zeroconf) account is connected, don't need + # submenus + for account in gajim.connections: + if gajim.account_is_connected(account) and \ + not gajim.config.get_per('accounts', account, 'is_zeroconf'): + if gajim.connections[account].private_storage_supported: + connected_accounts_with_private_storage += 1 + + # for single message + single_message_menuitem.remove_submenu() + self.single_message_handler_id = single_message_menuitem.\ + connect('activate', + self.on_single_message_menuitem_activate, account) + # join gc + gajim.interface.roster.add_bookmarks_list(gc_sub_menu, + account) + break # No other account connected + else: + # 2 or more 'real' accounts are connected, make submenus + account_menu_for_single_message = gtk.Menu() + single_message_menuitem.set_submenu( + account_menu_for_single_message) + self.popup_menus.append(account_menu_for_single_message) + + for account in accounts_list: + if gajim.connections[account].is_zeroconf or \ + not gajim.account_is_connected(account): + continue + if gajim.connections[account].private_storage_supported: + connected_accounts_with_private_storage += 1 + # for single message + item = gtk.MenuItem(_('using account %s') % account) + item.connect('activate', + self.on_single_message_menuitem_activate, account) + account_menu_for_single_message.append(item) + + # join gc + gc_item = gtk.MenuItem(_('using account %s') % account, False) + gc_sub_menu.append(gc_item) + gc_menuitem_menu = gtk.Menu() + gajim.interface.roster.add_bookmarks_list(gc_menuitem_menu, + account) + gc_item.set_submenu(gc_menuitem_menu) + gc_sub_menu.show_all() + + newitem = gtk.SeparatorMenuItem() # separator + gc_sub_menu.append(newitem) + newitem = gtk.ImageMenuItem(_('_Manage Bookmarks...')) + img = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) + newitem.set_image(img) + newitem.connect('activate', + gajim.interface.roster.on_manage_bookmarks_menuitem_activate) + gc_sub_menu.append(newitem) + if connected_accounts_with_private_storage == 0: + newitem.set_sensitive(False) + + sounds_mute_menuitem.set_active(not gajim.config.get('sounds_on')) + + if os.name == 'nt': + if self.added_hide_menuitem is False: + self.systray_context_menu.prepend(gtk.SeparatorMenuItem()) + item = gtk.MenuItem(_('Hide this menu')) + self.systray_context_menu.prepend(item) + self.added_hide_menuitem = True + + self.systray_context_menu.show_all() + self.systray_context_menu.popup(None, None, None, 0, + event_time) + + def on_show_all_events_menuitem_activate(self, widget): + events = gajim.events.get_systray_events() + for account in events: + for jid in events[account]: + for event in events[account][jid]: + gajim.interface.handle_event(account, jid, event.type_) + + def on_sounds_mute_menuitem_activate(self, widget): + gajim.config.set('sounds_on', not widget.get_active()) + gajim.interface.save_config() + + def on_show_roster_menuitem_activate(self, widget): + win = gajim.interface.roster.window + win.present() + + def on_preferences_menuitem_activate(self, widget): + if 'preferences' in gajim.interface.instances: + gajim.interface.instances['preferences'].window.present() + else: + gajim.interface.instances['preferences'] = config.PreferencesWindow() + + def on_quit_menuitem_activate(self, widget): + gajim.interface.roster.on_quit_request() + + def on_left_click(self): + win = gajim.interface.roster.window + if len(gajim.events.get_systray_events()) == 0: + # No pending events, so toggle visible/hidden for roster window + if not win.iconify_initially and (win.get_property( + 'has-toplevel-focus') or os.name == 'nt'): + # visible in ANY virtual desktop? + + # we could be in another VD right now. eg vd2 + # and we want to show it in vd2 + if not gtkgui_helpers.possibly_move_window_in_current_desktop(win): + win.set_property('skip-taskbar-hint', False) + win.iconify() # else we hide it from VD that was visible in + win.set_property('skip-taskbar-hint', True) + else: + win.deiconify() + if not gajim.config.get('roster_window_skip_taskbar'): + win.set_property('skip-taskbar-hint', False) + win.present_with_time(gtk.get_current_event_time()) + else: + self.handle_first_event() + + def handle_first_event(self): + account, jid, event = gajim.events.get_first_systray_event() + if not event: + return + gajim.interface.handle_event(account, jid, event.type_) + + def on_middle_click(self): + '''middle click raises window to have complete focus (fe. get kbd events) + but if already raised, it hides it''' + win = gajim.interface.roster.window + if win.is_active(): # is it fully raised? (eg does it receive kbd events?) + win.hide() + else: + win.present() + + def on_clicked(self, widget, event): + self.on_tray_leave_notify_event(widget, None) + if event.type != gtk.gdk.BUTTON_PRESS: + return + if event.button == 1: # Left click + self.on_left_click() + elif event.button == 2: # middle click + self.on_middle_click() + elif event.button == 3: # right click + self.make_menu(event.button, event.time) + + def on_show_menuitem_activate(self, widget, show): + # we all add some fake (we cannot select those nor have them as show) + # but this helps to align with roster's status_combobox index positions + l = ['online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'SEPARATOR', + 'CHANGE_STATUS_MSG_MENUITEM', 'SEPARATOR', 'offline'] + index = l.index(show) + if not helpers.statuses_unified(): + gajim.interface.roster.status_combobox.set_active(index + 2) + return + current = gajim.interface.roster.status_combobox.get_active() + if index != current: + gajim.interface.roster.status_combobox.set_active(index) + + def on_change_status_message_activate(self, widget): + model = gajim.interface.roster.status_combobox.get_model() + active = gajim.interface.roster.status_combobox.get_active() + status = model[active][2].decode('utf-8') + def on_response(message, pep_dict): + if message is None: # None if user press Cancel + return + accounts = gajim.connections.keys() + for acct in accounts: + if not gajim.config.get_per('accounts', acct, + 'sync_with_global_status'): + continue + show = gajim.SHOW_LIST[gajim.connections[acct].connected] + gajim.interface.roster.send_status(acct, show, message) + gajim.interface.roster.send_pep(acct, pep_dict) + dlg = dialogs.ChangeStatusMessageDialog(on_response, status) + dlg.dialog.present() + # vim: se ts=3: diff --git a/src/systray.py b/src/systray.py deleted file mode 100644 index e2c09331b..000000000 --- a/src/systray.py +++ /dev/null @@ -1,459 +0,0 @@ -# -*- coding:utf-8 -*- -## src/systray.py -## -## Copyright (C) 2003-2005 Vincent Hanquez -## Copyright (C) 2003-2008 Yann Leboulanger -## Copyright (C) 2005 Norman Rasmussen -## Copyright (C) 2005-2006 Dimitur Kirov -## Travis Shirk -## Copyright (C) 2005-2007 Nikos Kouremenos -## Copyright (C) 2006 Stefan Bethge -## Copyright (C) 2006-2008 Jean-Marie Traissard -## Copyright (C) 2007 Lukas Petrovicky -## Julien Pivotto -## -## This file is part of Gajim. -## -## Gajim is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published -## by the Free Software Foundation; version 3 only. -## -## Gajim is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with Gajim. If not, see . -## - -import gtk -import gobject -import os - -import dialogs -import config -import tooltips -import gtkgui_helpers - -from common import gajim -from common import helpers -from common import pep - -HAS_SYSTRAY_CAPABILITIES = True - -try: - import egg.trayicon as trayicon # gnomepythonextras trayicon -except Exception: - try: - import trayicon # our trayicon - except Exception: - gajim.log.debug('No trayicon module available') - HAS_SYSTRAY_CAPABILITIES = False - - -class Systray: - '''Class for icon in the notification area - This class is both base class (for statusicon.py) and normal class - for trayicon in GNU/Linux''' - - def __init__(self): - self.single_message_handler_id = None - self.new_chat_handler_id = None - self.t = None - # click somewhere else does not popdown menu. workaround this. - self.added_hide_menuitem = False - self.img_tray = gtk.Image() - self.status = 'offline' - self.xml = gtkgui_helpers.get_glade('systray_context_menu.glade') - self.systray_context_menu = self.xml.get_widget('systray_context_menu') - self.xml.signal_autoconnect(self) - self.popup_menus = [] - - def subscribe_events(self): - '''Register listeners to the events class''' - gajim.events.event_added_subscribe(self.on_event_added) - gajim.events.event_removed_subscribe(self.on_event_removed) - - def unsubscribe_events(self): - '''Unregister listeners to the events class''' - gajim.events.event_added_unsubscribe(self.on_event_added) - gajim.events.event_removed_unsubscribe(self.on_event_removed) - - def on_event_added(self, event): - '''Called when an event is added to the event list''' - if event.show_in_systray: - self.set_img() - - def on_event_removed(self, event_list): - '''Called when one or more events are removed from the event list''' - self.set_img() - - def set_img(self): - if not gajim.interface.systray_enabled: - return - if gajim.events.get_nb_systray_events(): - state = 'event' - else: - state = self.status - if state != 'event' and gajim.config.get('trayicon') == 'on_event': - self.t.hide() - else: - self.t.show() - image = gajim.interface.jabber_state_images['16'][state] - if image.get_storage_type() == gtk.IMAGE_ANIMATION: - self.img_tray.set_from_animation(image.get_animation()) - elif image.get_storage_type() == gtk.IMAGE_PIXBUF: - self.img_tray.set_from_pixbuf(image.get_pixbuf()) - - def change_status(self, global_status): - ''' set tray image to 'global_status' ''' - # change image and status, only if it is different - if global_status is not None and self.status != global_status: - self.status = global_status - self.set_img() - - def start_chat(self, widget, account, jid): - contact = gajim.contacts.get_first_contact_from_jid(account, jid) - if gajim.interface.msg_win_mgr.has_window(jid, account): - gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab( - jid, account) - elif contact: - gajim.interface.new_chat(contact, account) - gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab( - jid, account) - - def on_single_message_menuitem_activate(self, widget, account): - dialogs.SingleMessageWindow(account, action = 'send') - - def on_new_chat(self, widget, account): - dialogs.NewChatDialog(account) - - def make_menu(self, event_button, event_time): - '''create chat with and new message (sub) menus/menuitems''' - for m in self.popup_menus: - m.destroy() - - chat_with_menuitem = self.xml.get_widget('chat_with_menuitem') - single_message_menuitem = self.xml.get_widget( - 'single_message_menuitem') - status_menuitem = self.xml.get_widget('status_menu') - join_gc_menuitem = self.xml.get_widget('join_gc_menuitem') - sounds_mute_menuitem = self.xml.get_widget('sounds_mute_menuitem') - - if self.single_message_handler_id: - single_message_menuitem.handler_disconnect( - self.single_message_handler_id) - self.single_message_handler_id = None - if self.new_chat_handler_id: - chat_with_menuitem.disconnect(self.new_chat_handler_id) - self.new_chat_handler_id = None - - sub_menu = gtk.Menu() - self.popup_menus.append(sub_menu) - status_menuitem.set_submenu(sub_menu) - - gc_sub_menu = gtk.Menu() # gc is always a submenu - join_gc_menuitem.set_submenu(gc_sub_menu) - - # We need our own set of status icons, let's make 'em! - iconset = gajim.config.get('iconset') - path = os.path.join(helpers.get_iconset_path(iconset), '16x16') - state_images = gtkgui_helpers.load_iconset(path) - - if 'muc_active' in state_images: - join_gc_menuitem.set_image(state_images['muc_active']) - - for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): - uf_show = helpers.get_uf_show(show, use_mnemonic = True) - item = gtk.ImageMenuItem(uf_show) - item.set_image(state_images[show]) - sub_menu.append(item) - item.connect('activate', self.on_show_menuitem_activate, show) - - item = gtk.SeparatorMenuItem() - sub_menu.append(item) - - item = gtk.ImageMenuItem(_('_Change Status Message...')) - path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'kbd_input.png') - img = gtk.Image() - img.set_from_file(path) - item.set_image(img) - sub_menu.append(item) - item.connect('activate', self.on_change_status_message_activate) - - connected_accounts = gajim.get_number_of_connected_accounts() - if connected_accounts < 1: - item.set_sensitive(False) - - connected_accounts_with_private_storage = 0 - - item = gtk.SeparatorMenuItem() - sub_menu.append(item) - - uf_show = helpers.get_uf_show('offline', use_mnemonic = True) - item = gtk.ImageMenuItem(uf_show) - item.set_image(state_images['offline']) - sub_menu.append(item) - item.connect('activate', self.on_show_menuitem_activate, 'offline') - - iskey = connected_accounts > 0 and not (connected_accounts == 1 and - gajim.connections[gajim.connections.keys()[0]].is_zeroconf) - chat_with_menuitem.set_sensitive(iskey) - single_message_menuitem.set_sensitive(iskey) - join_gc_menuitem.set_sensitive(iskey) - - accounts_list = sorted(gajim.contacts.get_accounts()) - # items that get shown whether an account is zeroconf or not - if connected_accounts > 1: # 2 or more connections? make submenus - account_menu_for_chat_with = gtk.Menu() - chat_with_menuitem.set_submenu(account_menu_for_chat_with) - self.popup_menus.append(account_menu_for_chat_with) - - for account in accounts_list: - if gajim.account_is_connected(account): - # for chat_with - item = gtk.MenuItem(_('using account %s') % account) - account_menu_for_chat_with.append(item) - item.connect('activate', self.on_new_chat, account) - - elif connected_accounts == 1: # one account - # one account connected, no need to show 'as jid' - for account in gajim.connections: - if gajim.connections[account].connected > 1: - # for start chat - self.new_chat_handler_id = chat_with_menuitem.connect( - 'activate', self.on_new_chat, account) - break # No other connected account - - # menu items that don't apply to zeroconf connections - if connected_accounts == 1 or (connected_accounts == 2 and \ - gajim.zeroconf_is_connected()): - # only one 'real' (non-zeroconf) account is connected, don't need - # submenus - for account in gajim.connections: - if gajim.account_is_connected(account) and \ - not gajim.config.get_per('accounts', account, 'is_zeroconf'): - if gajim.connections[account].private_storage_supported: - connected_accounts_with_private_storage += 1 - - # for single message - single_message_menuitem.remove_submenu() - self.single_message_handler_id = single_message_menuitem.\ - connect('activate', - self.on_single_message_menuitem_activate, account) - # join gc - gajim.interface.roster.add_bookmarks_list(gc_sub_menu, - account) - break # No other account connected - else: - # 2 or more 'real' accounts are connected, make submenus - account_menu_for_single_message = gtk.Menu() - single_message_menuitem.set_submenu( - account_menu_for_single_message) - self.popup_menus.append(account_menu_for_single_message) - - for account in accounts_list: - if gajim.connections[account].is_zeroconf or \ - not gajim.account_is_connected(account): - continue - if gajim.connections[account].private_storage_supported: - connected_accounts_with_private_storage += 1 - # for single message - item = gtk.MenuItem(_('using account %s') % account) - item.connect('activate', - self.on_single_message_menuitem_activate, account) - account_menu_for_single_message.append(item) - - # join gc - gc_item = gtk.MenuItem(_('using account %s') % account, False) - gc_sub_menu.append(gc_item) - gc_menuitem_menu = gtk.Menu() - gajim.interface.roster.add_bookmarks_list(gc_menuitem_menu, - account) - gc_item.set_submenu(gc_menuitem_menu) - gc_sub_menu.show_all() - - newitem = gtk.SeparatorMenuItem() # separator - gc_sub_menu.append(newitem) - newitem = gtk.ImageMenuItem(_('_Manage Bookmarks...')) - img = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) - newitem.set_image(img) - newitem.connect('activate', - gajim.interface.roster.on_manage_bookmarks_menuitem_activate) - gc_sub_menu.append(newitem) - if connected_accounts_with_private_storage == 0: - newitem.set_sensitive(False) - - sounds_mute_menuitem.set_active(not gajim.config.get('sounds_on')) - - if os.name == 'nt': - if self.added_hide_menuitem is False: - self.systray_context_menu.prepend(gtk.SeparatorMenuItem()) - item = gtk.MenuItem(_('Hide this menu')) - self.systray_context_menu.prepend(item) - self.added_hide_menuitem = True - - self.systray_context_menu.show_all() - self.systray_context_menu.popup(None, None, None, 0, - event_time) - - def on_show_all_events_menuitem_activate(self, widget): - events = gajim.events.get_systray_events() - for account in events: - for jid in events[account]: - for event in events[account][jid]: - gajim.interface.handle_event(account, jid, event.type_) - - def on_sounds_mute_menuitem_activate(self, widget): - gajim.config.set('sounds_on', not widget.get_active()) - gajim.interface.save_config() - - def on_show_roster_menuitem_activate(self, widget): - win = gajim.interface.roster.window - win.present() - - def on_preferences_menuitem_activate(self, widget): - if 'preferences' in gajim.interface.instances: - gajim.interface.instances['preferences'].window.present() - else: - gajim.interface.instances['preferences'] = config.PreferencesWindow() - - def on_quit_menuitem_activate(self, widget): - gajim.interface.roster.on_quit_request() - - def on_left_click(self): - win = gajim.interface.roster.window - if len(gajim.events.get_systray_events()) == 0: - # No pending events, so toggle visible/hidden for roster window - if not win.iconify_initially and (win.get_property( - 'has-toplevel-focus') or os.name == 'nt'): - # visible in ANY virtual desktop? - - # we could be in another VD right now. eg vd2 - # and we want to show it in vd2 - if not gtkgui_helpers.possibly_move_window_in_current_desktop(win): - win.set_property('skip-taskbar-hint', False) - win.iconify() # else we hide it from VD that was visible in - win.set_property('skip-taskbar-hint', True) - else: - win.deiconify() - if not gajim.config.get('roster_window_skip_taskbar'): - win.set_property('skip-taskbar-hint', False) - win.present_with_time(gtk.get_current_event_time()) - else: - self.handle_first_event() - - def handle_first_event(self): - account, jid, event = gajim.events.get_first_systray_event() - if not event: - return - gajim.interface.handle_event(account, jid, event.type_) - - def on_middle_click(self): - '''middle click raises window to have complete focus (fe. get kbd events) - but if already raised, it hides it''' - win = gajim.interface.roster.window - if win.is_active(): # is it fully raised? (eg does it receive kbd events?) - win.hide() - else: - win.present() - - def on_clicked(self, widget, event): - self.on_tray_leave_notify_event(widget, None) - if event.type != gtk.gdk.BUTTON_PRESS: - return - if event.button == 1: # Left click - self.on_left_click() - elif event.button == 2: # middle click - self.on_middle_click() - elif event.button == 3: # right click - self.make_menu(event.button, event.time) - - def on_show_menuitem_activate(self, widget, show): - # we all add some fake (we cannot select those nor have them as show) - # but this helps to align with roster's status_combobox index positions - l = ['online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'SEPARATOR', - 'CHANGE_STATUS_MSG_MENUITEM', 'SEPARATOR', 'offline'] - index = l.index(show) - if not helpers.statuses_unified(): - gajim.interface.roster.status_combobox.set_active(index + 2) - return - current = gajim.interface.roster.status_combobox.get_active() - if index != current: - gajim.interface.roster.status_combobox.set_active(index) - - def on_change_status_message_activate(self, widget): - model = gajim.interface.roster.status_combobox.get_model() - active = gajim.interface.roster.status_combobox.get_active() - status = model[active][2].decode('utf-8') - def on_response(message, pep_dict): - if message is None: # None if user press Cancel - return - accounts = gajim.connections.keys() - for acct in accounts: - if not gajim.config.get_per('accounts', acct, - 'sync_with_global_status'): - continue - show = gajim.SHOW_LIST[gajim.connections[acct].connected] - gajim.interface.roster.send_status(acct, show, message) - gajim.interface.roster.send_pep(acct, pep_dict) - dlg = dialogs.ChangeStatusMessageDialog(on_response, status) - dlg.dialog.present() - - def show_tooltip(self, widget): - position = widget.window.get_origin() - if self.tooltip.id == position: - size = widget.window.get_size() - self.tooltip.show_tooltip('', size[1], position[1]) - - def on_tray_motion_notify_event(self, widget, event): - position = widget.window.get_origin() - if self.tooltip.timeout > 0: - if self.tooltip.id != position: - self.tooltip.hide_tooltip() - if self.tooltip.timeout == 0 and \ - self.tooltip.id != position: - self.tooltip.id = position - self.tooltip.timeout = gobject.timeout_add(500, - self.show_tooltip, widget) - - def on_tray_leave_notify_event(self, widget, event): - position = widget.window.get_origin() - if self.tooltip.timeout > 0 and \ - self.tooltip.id == position: - self.tooltip.hide_tooltip() - - def on_tray_destroyed(self, widget): - '''re-add trayicon when systray is destroyed''' - self.t = None - if gajim.interface.systray_enabled: - self.show_icon() - - def show_icon(self): - if not self.t: - self.t = trayicon.TrayIcon('Gajim') - self.t.connect('destroy', self.on_tray_destroyed) - eb = gtk.EventBox() - # avoid draw seperate bg color in some gtk themes - eb.set_visible_window(False) - eb.set_events(gtk.gdk.POINTER_MOTION_MASK) - eb.connect('button-press-event', self.on_clicked) - eb.connect('motion-notify-event', self.on_tray_motion_notify_event) - eb.connect('leave-notify-event', self.on_tray_leave_notify_event) - self.tooltip = tooltips.NotificationAreaTooltip() - - self.img_tray = gtk.Image() - eb.add(self.img_tray) - self.t.add(eb) - self.set_img() - self.subscribe_events() - self.t.show_all() - - def hide_icon(self): - if self.t: - self.t.destroy() - self.t = None - self.unsubscribe_events() - -# vim: se ts=3: diff --git a/src/tooltips.py b/src/tooltips.py index fa96cbd8c..5e55166c0 100644 --- a/src/tooltips.py +++ b/src/tooltips.py @@ -269,7 +269,7 @@ class NotificationAreaTooltip(BaseTooltip, StatusTable): for line in acct['event_lines']: self.add_text_row(' ' + line, 1) - def populate(self, data): + def populate(self, data=''): self.create_window() self.create_table() @@ -280,7 +280,7 @@ class NotificationAreaTooltip(BaseTooltip, StatusTable): self.table.set_property('column-spacing', 1) self.hbox.add(self.table) - self.win.add(self.hbox) + self.hbox.show_all() class GCTooltip(BaseTooltip): ''' Tooltip that is shown in the GC treeview ''' @@ -579,65 +579,19 @@ class RosterTooltip(NotificationAreaTooltip): Append Tune, Mood, Activity information of the specified contact to the given property list. ''' - if 'mood' in contact.mood: - mood = contact.mood['mood'].strip() - mood = MOODS.get(mood, mood) - mood = gobject.markup_escape_text(mood) - mood_string = _('Mood:') + ' %s' % mood - if 'text' in contact.mood \ - and contact.mood['text'] != '': - mood_text = contact.mood['text'].strip() - mood_text = \ - gobject.markup_escape_text(mood_text) - mood_string += ' (%s)' % mood_text + if 'mood' in contact.pep: + mood = contact.pep['mood'].asMarkupText() + mood_string = _('Mood:') + ' %s' % mood properties.append((mood_string, None)) - if 'activity' in contact.activity: - activity = act_plain = \ - contact.activity['activity'].strip() - activity = gobject.markup_escape_text(activity) - if act_plain in ACTIVITIES: - activity = ACTIVITIES[activity]['category'] - activity_string = _('Activity:') + ' %s' % activity - if 'subactivity' in contact.activity: - activity_sub = \ - contact.activity['subactivity'].strip() - if act_plain in ACTIVITIES and activity_sub in \ - ACTIVITIES[act_plain]: - activity_sub = ACTIVITIES[act_plain][activity_sub] - activity_sub = \ - gobject.markup_escape_text(activity_sub) - activity_string += ': %s' % activity_sub - else: - activity_string += '' - if 'text' in contact.activity: - activity_text = contact.activity['text'].strip() - activity_text = gobject.markup_escape_text( - activity_text) - activity_string += ' (%s)' % activity_text + if 'activity' in contact.pep: + activity = contact.pep['activity'].asMarkupText() + activity_string = _('Activity:') + ' %s' % activity properties.append((activity_string, None)) - if 'artist' in contact.tune \ - or 'title' in contact.tune: - if 'artist' in contact.tune: - artist = contact.tune['artist'].strip() - artist = gobject.markup_escape_text(artist) - else: - artist = _('Unknown Artist') - if 'title' in contact.tune: - title = contact.tune['title'].strip() - title = gobject.markup_escape_text(title) - else: - title = _('Unknown Title') - if 'source' in contact.tune: - source = contact.tune['source'].strip() - source = gobject.markup_escape_text(source) - else: - source = _('Unknown Source') - tune_string = _('Tune:') + ' ' + \ - _('"%(title)s" by %(artist)s\n' - 'from %(source)s') % {'title': title, - 'artist': artist, 'source': source} + if 'tune' in contact.pep: + tune = contact.pep['tune'].asMarkupText() + tune_string = _('Tune:') + ' %s' % tune properties.append((tune_string, None)) diff --git a/src/trayicon.defs b/src/trayicon.defs deleted file mode 100644 index 6d253cf83..000000000 --- a/src/trayicon.defs +++ /dev/null @@ -1,59 +0,0 @@ -;; -*- scheme -*- - -; object definitions ... -(define-object TrayIcon - (in-module "Egg") - (parent "GtkPlug") - (c-name "EggTrayIcon") - (gtype-id "EGG_TYPE_TRAY_ICON") -) - -;; Enumerations and flags ... - - -;; From eggtrayicon.h - -(define-function egg_tray_icon_get_type - (c-name "egg_tray_icon_get_type") - (return-type "GType") -) - -(define-function egg_tray_icon_new_for_screen - (c-name "egg_tray_icon_new_for_screen") - (return-type "EggTrayIcon*") - (parameters - '("GdkScreen*" "screen") - '("const-gchar*" "name") - ) -) - -(define-function egg_tray_icon_new - (c-name "egg_tray_icon_new") - (is-constructor-of "EggTrayIcon") - (return-type "EggTrayIcon*") - (parameters - '("const-gchar*" "name") - ) -) - -(define-method send_message - (of-object "EggTrayIcon") - (c-name "egg_tray_icon_send_message") - (return-type "guint") - (parameters - '("gint" "timeout") - '("const-char*" "message") - '("gint" "len") - ) -) - -(define-method cancel_message - (of-object "EggTrayIcon") - (c-name "egg_tray_icon_cancel_message") - (return-type "none") - (parameters - '("guint" "id") - ) -) - - diff --git a/src/trayicon.override b/src/trayicon.override deleted file mode 100644 index 75bd7ed8f..000000000 --- a/src/trayicon.override +++ /dev/null @@ -1,47 +0,0 @@ -/* -*- Mode: C; c-basic-offset: 4 -*- - * src/trayicon.override - * - * Copyright (C) 2004-2005 Yann Leboulanger - * - * This file is part of Gajim. - * - * Gajim is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published - * by the Free Software Foundation; version 3 only. - * - * Gajim is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Gajim. If not, see . - */ -%% -headers -#include - -#include "pygobject.h" -#include "eggtrayicon.h" -%% -modulename trayicon -%% -import gtk.Plug as PyGtkPlug_Type -import gtk.gdk.Screen as PyGdkScreen_Type -%% -ignore-glob - *_get_type -%% -override egg_tray_icon_send_message kwargs -static PyObject* -_wrap_egg_tray_icon_send_message(PyGObject *self, PyObject *args, PyObject *kwargs) -{ - static char *kwlist[] = {"timeout", "message", NULL}; - int timeout, len, ret; - char *message; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "is#:TrayIcon.send_message", kwlist, &timeout, &message, &len)) - return NULL; - ret = egg_tray_icon_send_message(EGG_TRAY_ICON(self->obj), timeout, message, len); - return PyInt_FromLong(ret); -} diff --git a/src/trayiconmodule.c b/src/trayiconmodule.c deleted file mode 100644 index 7a2b1206f..000000000 --- a/src/trayiconmodule.c +++ /dev/null @@ -1,43 +0,0 @@ -/* -*- Mode: C; c-basic-offset: 4 -*- - * src/trayiconmodule.c - * - * Copyright (C) 2004-2005 Yann Leboulanger - * - * This file is part of Gajim. - * - * Gajim is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published - * by the Free Software Foundation; version 3 only. - * - * Gajim is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Gajim. If not, see . - */ - -/* include this first, before NO_IMPORT_PYGOBJECT is defined */ -#include - -void trayicon_register_classes (PyObject *d); - -extern PyMethodDef trayicon_functions[]; - -DL_EXPORT(void) -inittrayicon(void) -{ - PyObject *m, *d; - - init_pygobject (); - - m = Py_InitModule ("trayicon", trayicon_functions); - d = PyModule_GetDict (m); - - trayicon_register_classes (d); - - if (PyErr_Occurred ()) { - Py_FatalError ("can't initialise module trayicon :("); - } -} diff --git a/test/integration/test_xmpp_transports_nb.py b/test/integration/test_xmpp_transports_nb.py index 6e56ddf94..ef9908903 100644 --- a/test/integration/test_xmpp_transports_nb.py +++ b/test/integration/test_xmpp_transports_nb.py @@ -254,12 +254,12 @@ class TestNonBlockingHTTP(AbstractTransportTest): def test_receive_http_message_in_chunks(self): ''' Let _on_receive handle some chunked http messages ''' transport = self._get_transport(self.bosh_http_dict) - - header = ("HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-8\r\n" + - "Content-Length: 88\r\n\r\n") - payload = "Please don't fail!" + + payload = "Please don't fail!\n\n" body = "%s" \ % payload + header = "HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-8\r\n" +\ + "Content-Length: %i\r\n\r\n" % len(body) message = "%s%s" % (header, body) chunk1, chunk2, chunk3, chunk4 = message[:20], message[20:73], \ diff --git a/test/lib/gajim_mocks.py b/test/lib/gajim_mocks.py index d45e488e6..9607a77c9 100644 --- a/test/lib/gajim_mocks.py +++ b/test/lib/gajim_mocks.py @@ -14,9 +14,7 @@ class MockConnection(Mock, ConnectionHandlersBase): self.name = account self.connected = 2 - self.mood = {} - self.activity = {} - self.tune = {} + self.pep = {} self.blocked_contacts = {} self.blocked_groups = {} self.sessions = {}