diff --git a/data/glade/message_window.glade b/data/glade/message_window.glade index b3e2c3cc321f5a959c7151746e1e5f12c193b5c9..bafc686cef30fe0a242d0095e9bb7e715f487f09 100644 --- a/data/glade/message_window.glade +++ b/data/glade/message_window.glade @@ -1,7 +1,7 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd"> -<!--*- mode: xml -*--> +<?xml version="1.0"?> <glade-interface> + <!-- interface-requires gtk+ 2.14 --> + <!-- interface-naming-policy toplevel-contextual --> <widget class="GtkWindow" id="message_window"> <property name="default_width">480</property> <property name="default_height">440</property> @@ -40,6 +40,7 @@ <property name="expand">False</property> <property name="fill">False</property> <property name="padding">5</property> + <property name="position">0</property> </packing> </child> <child> @@ -54,6 +55,9 @@ <property name="label"><span weight="heavy" size="large">Contact name</span></property> <property name="use_markup">True</property> </widget> + <packing> + <property name="position">0</property> + </packing> </child> <child> <placeholder/> @@ -71,14 +75,17 @@ <widget class="GtkImage" id="mood_image"> <property name="no_show_all">True</property> <property name="stock">None</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> + <packing> + <property name="position">0</property> + </packing> </child> <child> <widget class="GtkImage" id="activity_image"> <property name="no_show_all">True</property> <property name="stock">None</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> <packing> <property name="position">1</property> @@ -87,13 +94,33 @@ <child> <widget class="GtkImage" id="tune_image"> <property name="no_show_all">True</property> - <property name="pixbuf">../emoticons/static/music.png</property> - <property name="icon_size">1</property> + <property name="pixbuf">../emoticons/static/music.png</property> + <property name="icon-size">1</property> </widget> <packing> <property name="position">2</property> </packing> </child> + <child> + <widget class="GtkImage" id="audio_banner_image"> + <property name="visible">True</property> + <property name="stock">None</property> + <property name="icon-size">1</property> + </widget> + <packing> + <property name="position">3</property> + </packing> + </child> + <child> + <widget class="GtkImage" id="video_banner_image"> + <property name="visible">True</property> + <property name="stock">None</property> + <property name="icon-size">1</property> + </widget> + <packing> + <property name="position">4</property> + </packing> + </child> <child> <widget class="GtkAlignment" id="alignment3"> <property name="width_request">11</property> @@ -103,7 +130,7 @@ </child> </widget> <packing> - <property name="position">3</property> + <property name="position">5</property> </packing> </child> </widget> @@ -138,6 +165,7 @@ <packing> <property name="expand">False</property> <property name="fill">False</property> + <property name="position">0</property> </packing> </child> <child> @@ -148,35 +176,40 @@ <property name="height_request">60</property> <property name="can_focus">True</property> <property name="border_width">3</property> - <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property> - <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> - <property name="shadow_type">GTK_SHADOW_IN</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> <child> <placeholder/> </child> </widget> + <packing> + <property name="position">0</property> + </packing> </child> <child> - <widget class="GtkHBox" id="hbox1"> + <widget class="GtkHBox" id="hbox"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <child> <widget class="GtkButton" id="authentication_button"> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="no_show_all">True</property> - <property name="relief">GTK_RELIEF_NONE</property> + <property name="relief">none</property> <property name="focus_on_click">False</property> - <property name="response_id">0</property> <child> <widget class="GtkImage" id="lock_image"> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="stock">gtk-dialog-authentication</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> <packing> <property name="expand">False</property> + <property name="position">0</property> </packing> </child> <child> @@ -184,9 +217,9 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="border_width">3</property> - <property name="hscrollbar_policy">GTK_POLICY_NEVER</property> - <property name="vscrollbar_policy">GTK_POLICY_NEVER</property> - <property name="shadow_type">GTK_SHADOW_IN</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">never</property> + <property name="shadow_type">in</property> <child> <placeholder/> </child> @@ -212,37 +245,40 @@ <child> <widget class="GtkButton" id="emoticons_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="tooltip" translatable="yes">Show a list of emoticons (Alt+M)</property> - <property name="relief">GTK_RELIEF_NONE</property> + <property name="relief">none</property> <property name="focus_on_click">False</property> - <property name="response_id">0</property> <child> <widget class="GtkImage" id="emoticons_button_image"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="stock">gtk-missing-image</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> <packing> <property name="expand">False</property> + <property name="position">0</property> </packing> </child> <child> <widget class="GtkButton" id="formattings_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="tooltip" translatable="yes">Show a list of formattings</property> - <property name="relief">GTK_RELIEF_NONE</property> + <property name="relief">none</property> <property name="focus_on_click">False</property> - <property name="response_id">0</property> <child> <widget class="GtkImage" id="image10"> <property name="visible">True</property> <property name="stock">gtk-bold</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> @@ -268,14 +304,13 @@ <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="no_show_all">True</property> <property name="tooltip" translatable="yes">Add this contact to roster (Ctrl+D)</property> - <property name="relief">GTK_RELIEF_NONE</property> - <property name="response_id">0</property> + <property name="relief">none</property> <child> <widget class="GtkImage" id="image9"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="stock">gtk-add</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> @@ -288,16 +323,17 @@ <child> <widget class="GtkButton" id="send_file_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="tooltip" translatable="yes">Send a file (Ctrl+F)</property> - <property name="relief">GTK_RELIEF_NONE</property> + <property name="relief">none</property> <property name="focus_on_click">False</property> - <property name="response_id">0</property> <child> <widget class="GtkImage" id="image3"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> @@ -306,70 +342,111 @@ <property name="position">4</property> </packing> </child> + <child> + <widget class="GtkToggleButton" id="audio_togglebutton"> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip" translatable="yes">Toggle audio session</property> + <property name="relief">none</property> + <child> + <widget class="GtkImage" id="audio_image"> + <property name="visible">True</property> + <property name="stock">gtk-missing-image</property> + <property name="icon-size">1</property> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">5</property> + </packing> + </child> + <child> + <widget class="GtkToggleButton" id="video_togglebutton"> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip" translatable="yes">Toggle video session</property> + <property name="relief">none</property> + <child> + <widget class="GtkImage" id="video_image"> + <property name="visible">True</property> + <property name="stock">gtk-missing-image</property> + <property name="icon-size">1</property> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="position">6</property> + </packing> + </child> <child> <widget class="GtkButton" id="convert_to_gc_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="tooltip" translatable="yes">Invite contacts to the conversation (Ctrl+G)</property> - <property name="relief">GTK_RELIEF_NONE</property> + <property name="relief">none</property> <property name="focus_on_click">False</property> - <property name="response_id">0</property> <child> <widget class="GtkImage" id="convert_to_gc_button_image"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="stock">gtk-missing-image</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> <packing> <property name="expand">False</property> - <property name="position">5</property> + <property name="position">7</property> </packing> </child> <child> <widget class="GtkButton" id="contact_information_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="tooltip" translatable="yes">Show the contact's profile (Ctrl+I)</property> - <property name="relief">GTK_RELIEF_NONE</property> + <property name="relief">none</property> <property name="focus_on_click">False</property> - <property name="response_id">0</property> <child> <widget class="GtkImage" id="image2"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="stock">gtk-info</property> - <property name="icon_size">2</property> + <property name="icon-size">2</property> </widget> </child> </widget> <packing> <property name="expand">False</property> - <property name="position">6</property> + <property name="position">8</property> </packing> </child> <child> <widget class="GtkButton" id="history_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="tooltip" translatable="yes">Browse the chat history (Ctrl+H)</property> - <property name="relief">GTK_RELIEF_NONE</property> + <property name="relief">none</property> <property name="focus_on_click">False</property> - <property name="response_id">0</property> <child> <widget class="GtkImage" id="image5"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="stock">gtk-justify-fill</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> <packing> <property name="expand">False</property> - <property name="position">7</property> + <property name="position">9</property> </packing> </child> <child> @@ -379,29 +456,30 @@ </widget> <packing> <property name="expand">False</property> - <property name="position">8</property> + <property name="position">10</property> </packing> </child> <child> <widget class="GtkButton" id="message_window_actions_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="tooltip" translatable="yes">Show a menu of advanced functions (Alt+A)</property> - <property name="relief">GTK_RELIEF_NONE</property> + <property name="relief">none</property> <property name="focus_on_click">False</property> - <property name="response_id">0</property> <child> <widget class="GtkImage" id="image1"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="stock">gtk-execute</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> <packing> <property name="expand">False</property> - <property name="position">9</property> + <property name="position">11</property> </packing> </child> <child> @@ -413,14 +491,14 @@ </child> </widget> <packing> - <property name="position">10</property> + <property name="position">12</property> </packing> </child> <child> <widget class="GtkButton" id="send_button"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="response_id">0</property> + <property name="receives_default">False</property> <child> <widget class="GtkAlignment" id="alignment102"> <property name="visible">True</property> @@ -438,6 +516,7 @@ <packing> <property name="expand">False</property> <property name="fill">False</property> + <property name="position">0</property> </packing> </child> <child> @@ -459,7 +538,7 @@ </widget> <packing> <property name="expand">False</property> - <property name="position">11</property> + <property name="position">13</property> </packing> </child> </widget> @@ -486,6 +565,7 @@ <packing> <property name="expand">False</property> <property name="fill">False</property> + <property name="position">0</property> </packing> </child> <child> @@ -493,7 +573,7 @@ <property name="visible">True</property> <property name="xalign">0</property> <property name="use_markup">True</property> - <property name="ellipsize">PANGO_ELLIPSIZE_END</property> + <property name="ellipsize">end</property> </widget> <packing> <property name="position">1</property> @@ -505,14 +585,14 @@ <property name="height_request">20</property> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="relief">GTK_RELIEF_NONE</property> - <property name="response_id">0</property> + <property name="receives_default">False</property> + <property name="relief">none</property> <child> <widget class="GtkImage" id="image1329"> <property name="visible">True</property> <property name="ypad">6</property> <property name="stock">gtk-close</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> @@ -526,8 +606,8 @@ </child> </widget> <packing> - <property name="type">tab</property> <property name="tab_fill">False</property> + <property name="type">tab</property> </packing> </child> <child> @@ -555,6 +635,7 @@ <property name="expand">False</property> <property name="fill">False</property> <property name="padding">5</property> + <property name="position">0</property> </packing> </child> <child> @@ -569,6 +650,9 @@ <property name="label"><span weight="heavy" size="large">room jid</span></property> <property name="use_markup">True</property> </widget> + <packing> + <property name="position">0</property> + </packing> </child> <child> <placeholder/> @@ -586,6 +670,7 @@ <packing> <property name="expand">False</property> <property name="fill">False</property> + <property name="position">0</property> </packing> </child> <child> @@ -609,21 +694,24 @@ <property name="height_request">60</property> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property> - <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> - <property name="shadow_type">GTK_SHADOW_IN</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> <child> <placeholder/> </child> </widget> + <packing> + <property name="position">0</property> + </packing> </child> <child> <widget class="GtkScrolledWindow" id="message_scrolledwindow"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="hscrollbar_policy">GTK_POLICY_NEVER</property> - <property name="vscrollbar_policy">GTK_POLICY_NEVER</property> - <property name="shadow_type">GTK_SHADOW_IN</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">never</property> + <property name="shadow_type">in</property> <child> <placeholder/> </child> @@ -634,6 +722,9 @@ </packing> </child> </widget> + <packing> + <property name="position">0</property> + </packing> </child> </widget> <packing> @@ -646,9 +737,9 @@ <property name="width_request">100</property> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="hscrollbar_policy">GTK_POLICY_NEVER</property> - <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> - <property name="shadow_type">GTK_SHADOW_IN</property> + <property name="hscrollbar_policy">never</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> <child> <widget class="GtkTreeView" id="list_treeview"> <property name="visible">True</property> @@ -677,41 +768,45 @@ <child> <widget class="GtkButton" id="emoticons_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="tooltip" translatable="yes">Show a list of emoticons (Alt+M)</property> - <property name="relief">GTK_RELIEF_NONE</property> - <property name="response_id">0</property> + <property name="relief">none</property> <child> <widget class="GtkImage" id="emoticons_button_image"> <property name="visible">True</property> <property name="stock">gtk-missing-image</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> <packing> <property name="expand">False</property> <property name="fill">False</property> + <property name="position">0</property> </packing> </child> </widget> <packing> <property name="expand">False</property> + <property name="position">0</property> </packing> </child> <child> <widget class="GtkButton" id="formattings_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="tooltip" translatable="yes">Show a list of formattings</property> - <property name="relief">GTK_RELIEF_NONE</property> + <property name="relief">none</property> <property name="focus_on_click">False</property> - <property name="response_id">0</property> <child> <widget class="GtkImage" id="image11"> <property name="visible">True</property> <property name="stock">gtk-bold</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> @@ -733,16 +828,17 @@ <child> <widget class="GtkButton" id="change_nick_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="tooltip" translatable="yes">Change your nickname (Ctrl+N)</property> - <property name="relief">GTK_RELIEF_NONE</property> - <property name="response_id">0</property> + <property name="relief">none</property> <child> <widget class="GtkImage" id="image4"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="stock">gtk-edit</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> @@ -755,16 +851,17 @@ <child> <widget class="GtkButton" id="change_subject_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="tooltip" translatable="yes">Change the room's subject (Alt+T)</property> - <property name="relief">GTK_RELIEF_NONE</property> - <property name="response_id">0</property> + <property name="relief">none</property> <child> <widget class="GtkImage" id="image6"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="stock">gtk-properties</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> @@ -777,17 +874,18 @@ <child> <widget class="GtkButton" id="bookmark_button"> <property name="visible">True</property> - <property name="no_show_all">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="no_show_all">True</property> <property name="tooltip" translatable="yes">Bookmark this room (Ctrl+B)</property> - <property name="relief">GTK_RELIEF_NONE</property> - <property name="response_id">0</property> + <property name="relief">none</property> <child> <widget class="GtkImage" id="image7"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="stock">gtk-add</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> @@ -800,16 +898,17 @@ <child> <widget class="GtkButton" id="history_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="tooltip" translatable="yes">Browse the chat history (Ctrl+H)</property> - <property name="relief">GTK_RELIEF_NONE</property> - <property name="response_id">0</property> + <property name="relief">none</property> <child> <widget class="GtkImage" id="image8"> <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="stock">gtk-justify-fill</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> @@ -832,11 +931,12 @@ <child> <widget class="GtkButton" id="muc_window_actions_button"> <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <property name="tooltip" translatable="yes">Show a menu of advanced functions (Alt+A)</property> - <property name="relief">GTK_RELIEF_NONE</property> + <property name="relief">none</property> <property name="focus_on_click">False</property> - <property name="response_id">0</property> <child> <widget class="GtkAlignment" id="alignment104"> <property name="visible">True</property> @@ -846,7 +946,7 @@ <widget class="GtkImage" id="image1344"> <property name="visible">True</property> <property name="stock">gtk-execute</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> @@ -874,7 +974,7 @@ <widget class="GtkButton" id="send_button"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="response_id">0</property> + <property name="receives_default">False</property> <child> <widget class="GtkAlignment" id="alignment105"> <property name="visible">True</property> @@ -892,6 +992,7 @@ <packing> <property name="expand">False</property> <property name="fill">False</property> + <property name="position">0</property> </packing> </child> <child> @@ -944,6 +1045,7 @@ <packing> <property name="expand">False</property> <property name="fill">False</property> + <property name="position">0</property> </packing> </child> <child> @@ -963,14 +1065,14 @@ <property name="height_request">20</property> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="relief">GTK_RELIEF_NONE</property> - <property name="response_id">0</property> + <property name="receives_default">False</property> + <property name="relief">none</property> <child> <widget class="GtkImage" id="image1347"> <property name="visible">True</property> <property name="ypad">6</property> <property name="stock">gtk-close</property> - <property name="icon_size">1</property> + <property name="icon-size">1</property> </widget> </child> </widget> @@ -984,9 +1086,9 @@ </child> </widget> <packing> - <property name="type">tab</property> <property name="position">1</property> <property name="tab_fill">False</property> + <property name="type">tab</property> </packing> </child> </widget> diff --git a/data/glade/voip_call_received_dialog.glade b/data/glade/voip_call_received_dialog.glade new file mode 100644 index 0000000000000000000000000000000000000000..b4314fe2232c14d6cbeb7a8e54552830e6e7a6a5 --- /dev/null +++ b/data/glade/voip_call_received_dialog.glade @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<glade-interface> + <!-- interface-requires gtk+ 2.14 --> + <!-- interface-naming-policy toplevel-contextual --> + <widget class="GtkMessageDialog" id="voip_call_received_messagedialog"> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="border_width">5</property> + <property name="resizable">False</property> + <property name="window_position">center-on-parent</property> + <property name="type_hint">dialog</property> + <property name="skip_taskbar_hint">True</property> + <property name="message_type">question</property> + <property name="buttons">yes-no</property> + <property name="text"><b><big>Incoming call</big></b></property> + <property name="use_markup">True</property> + <signal name="destroy" handler="on_voip_call_received_messagedialog_destroy"/> + <signal name="close" handler="on_voip_call_received_messagedialog_close"/> + <signal name="response" handler="on_voip_call_received_messagedialog_response"/> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox4"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area4"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="layout_style">end</property> + </widget> + <packing> + <property name="expand">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </widget> + </child> + </widget> +</glade-interface> diff --git a/src/chat_control.py b/src/chat_control.py index 29e74c814290c5bc9289b084442f869dc41fd470..dc5c4a40fdde14789cd5230e429cab9dc9cb9eb8 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -51,6 +51,7 @@ from common.logger import constants from common.pep import MOODS, ACTIVITIES from common.xmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION +from common.xmpp.protocol import NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_JINGLE_ICE_UDP from command_system.implementation.middleware import ChatCommandProcessor from command_system.implementation.middleware import CommandTools @@ -1173,6 +1174,15 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): ################################################################################ class ChatControl(ChatControlBase): '''A control for standard 1-1 chat''' + ( + JINGLE_STATE_NOT_AVAILABLE, + JINGLE_STATE_AVAILABLE, + JINGLE_STATE_CONNECTING, + JINGLE_STATE_CONNECTION_RECEIVED, + JINGLE_STATE_CONNECTED, + JINGLE_STATE_ERROR + ) = range(6) + TYPE_ID = message_control.TYPE_CHAT old_msg_kind = None # last kind of the printed message @@ -1200,6 +1210,14 @@ class ChatControl(ChatControlBase): self._on_add_to_roster_menuitem_activate) self.handlers[id_] = self._add_to_roster_button + self._audio_button = self.xml.get_widget('audio_togglebutton') + id_ = self._audio_button.connect('toggled', self.on_audio_button_toggled) + self.handlers[id_] = self._audio_button + + self._video_button = self.xml.get_widget('video_togglebutton') + id_ = self._video_button.connect('toggled', self.on_video_button_toggled) + self.handlers[id_] = self._video_button + self._send_file_button = self.xml.get_widget('send_file_button') # add a special img for send file button path_to_upload_img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'upload.png') @@ -1241,6 +1259,13 @@ class ChatControl(ChatControlBase): img.set_from_pixbuf(gtkgui_helpers.load_icon( 'muc_active').get_pixbuf()) + self._audio_banner_image = self.xml.get_widget('audio_banner_image') + self._video_banner_image = self.xml.get_widget('video_banner_image') + self.audio_sid = None + self.audio_state = self.JINGLE_STATE_NOT_AVAILABLE + self.video_sid = None + self.video_state = self.JINGLE_STATE_NOT_AVAILABLE + self.update_toolbar() self._mood_image = self.xml.get_widget('mood_image') @@ -1345,6 +1370,38 @@ class ChatControl(ChatControlBase): else: self._add_to_roster_button.hide() + # Jingle detection + if gajim.capscache.is_supported(self.contact, NS_JINGLE_ICE_UDP) and \ + gajim.HAVE_FARSIGHT and self.contact.resource: + if gajim.capscache.is_supported(self.contact, NS_JINGLE_RTP_AUDIO): + if self.audio_state == self.JINGLE_STATE_NOT_AVAILABLE: + self.set_audio_state('available') + else: + self.set_audio_state('not_available') + + if gajim.capscache.is_supported(self.contact, NS_JINGLE_RTP_VIDEO): + if self.video_state == self.JINGLE_STATE_NOT_AVAILABLE: + self.set_video_state('available') + else: + self.set_video_state('not_available') + else: + if self.audio_state != self.JINGLE_STATE_NOT_AVAILABLE: + self.set_audio_state('not_available') + if self.video_state != self.JINGLE_STATE_NOT_AVAILABLE: + self.set_video_state('not_available') + + # Audio buttons + if self.audio_state == self.JINGLE_STATE_NOT_AVAILABLE: + self._audio_button.set_sensitive(False) + else: + self._audio_button.set_sensitive(True) + + # Video buttons + if self.video_state == self.JINGLE_STATE_NOT_AVAILABLE: + self._video_button.set_sensitive(False) + else: + self._video_button.set_sensitive(True) + # Send file if gajim.capscache.is_supported(self.contact, NS_FILE) and \ self.contact.resource: @@ -1477,6 +1534,34 @@ class ChatControl(ChatControlBase): else: self._tune_image.hide() + def _update_jingle(self, jingle_type): + if jingle_type not in ('audio', 'video'): + return + if self.__dict__[jingle_type + '_state'] in ( + self.JINGLE_STATE_NOT_AVAILABLE, self.JINGLE_STATE_AVAILABLE): + self.__dict__['_' + jingle_type + '_banner_image'].hide() + else: + self.__dict__['_' + jingle_type + '_banner_image'].show() + if self.audio_state == self.JINGLE_STATE_CONNECTING: + self.__dict__['_' + jingle_type + '_banner_image'].set_from_stock( + gtk.STOCK_CONVERT, 1) + elif self.audio_state == self.JINGLE_STATE_CONNECTION_RECEIVED: + self.__dict__['_' + jingle_type + '_banner_image'].set_from_stock( + gtk.STOCK_NETWORK, 1) + elif self.audio_state == self.JINGLE_STATE_CONNECTED: + self.__dict__['_' + jingle_type + '_banner_image'].set_from_stock( + gtk.STOCK_CONNECT, 1) + elif self.audio_state == self.JINGLE_STATE_ERROR: + self.__dict__['_' + jingle_type + '_banner_image'].set_from_stock( + gtk.STOCK_DIALOG_WARNING, 1) + self.update_toolbar() + + def update_audio(self): + self._update_jingle('audio') + + def update_video(self): + self._update_jingle('video') + def change_resource(self, resource): old_full_jid = self.get_full_jid() self.resource = resource @@ -1490,6 +1575,52 @@ class ChatControl(ChatControlBase): # update MessageWindow._controls self.parent_win.change_jid(self.account, old_full_jid, new_full_jid) + def _set_jingle_state(self, jingle_type, state, sid=None, reason=None): + if jingle_type not in ('audio', 'video'): + return + if state in ('connecting', 'connected', 'stop') and reason: + str = _('%(type)s state : %(state)s, reason: %(reason)s') % { + 'type': jingle_type.capitalize(), 'state': state, 'reason': reason} + self.print_conversation(str, 'info') + + states = {'not_available': self.JINGLE_STATE_NOT_AVAILABLE, + 'available': self.JINGLE_STATE_AVAILABLE, + 'connecting': self.JINGLE_STATE_CONNECTING, + 'connection_received': self.JINGLE_STATE_CONNECTION_RECEIVED, + 'connected': self.JINGLE_STATE_CONNECTED, + 'stop': self.JINGLE_STATE_AVAILABLE, + 'error': self.JINGLE_STATE_ERROR} + + if state in states: + jingle_state = states[state] + if self.__dict__[jingle_type + '_state'] == jingle_state: + return + self.__dict__[jingle_type + '_state'] = jingle_state + + # Destroy existing session with the user when he signs off + # We need to do that before modifying the sid + if state == 'not_available': + gajim.connections[self.account].delete_jingle_session( + self.contact.get_full_jid(), self.__dict__[jingle_type + '_sid']) + + if state in ('not_available', 'available', 'stop'): + self.__dict__[jingle_type + '_sid'] = None + if state in ('connection_received', 'connecting'): + self.__dict__[jingle_type + '_sid'] = sid + + if state in ('connecting', 'connected', 'connection_received'): + self.__dict__['_' + jingle_type + '_button'].set_active(True) + elif state in ('not_available', 'stop'): + self.__dict__['_' + jingle_type + '_button'].set_active(False) + + eval('self.update_' + jingle_type)() + + def set_audio_state(self, state, sid=None, reason=None): + self._set_jingle_state('audio', state, sid=sid, reason=reason) + + def set_video_state(self, state, sid=None, reason=None): + 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 @@ -1688,6 +1819,34 @@ class ChatControl(ChatControlBase): banner_name_label.set_markup(label_text) banner_name_label.set_tooltip_text(label_tooltip) + def on_audio_button_toggled(self, widget): + if widget.get_active(): + if self.audio_state == self.JINGLE_STATE_AVAILABLE: + sid = gajim.connections[self.account].startVoIP( + self.contact.get_full_jid()) + self.set_audio_state('connecting', sid) + else: + session = gajim.connections[self.account].get_jingle_session( + self.contact.get_full_jid(), self.audio_sid) + if session: + content = session.get_content('audio') + if content: + session.remove_content(content.creator, content.name) + + def on_video_button_toggled(self, widget): + if widget.get_active(): + if self.video_state == self.JINGLE_STATE_AVAILABLE: + sid = gajim.connections[self.account].startVideoIP( + self.contact.get_full_jid()) + self.set_video_state('connecting', sid) + else: + session = gajim.connections[self.account].get_jingle_session( + self.contact.get_full_jid(), self.video_sid) + if session: + content = session.get_content('video') + if content: + session.remove_content(content.creator, content.name) + def _toggle_gpg(self): if not self.gpg_is_active and not self.contact.keyID: dialogs.ErrorDialog(_('No GPG key assigned'), diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index f902812462810d651a87d7d40cad90fceee78bf4..f48a98d0a1362d9f21a53ccb930f39f9f43a4436 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -52,6 +52,14 @@ from common import exceptions from common.commands import ConnectionCommands from common.pubsub import ConnectionPubSub from common.caps import ConnectionCaps +if gajim.HAVE_FARSIGHT: + from common.jingle import ConnectionJingle +else: + class ConnectionJingle(): + def __init__(self): + pass + def _JingleCB(self, con, stanza): + pass from common import dbus_support if dbus_support.supported: @@ -1445,12 +1453,13 @@ sent a message to.''' return sess -class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionCaps, ConnectionHandlersBase): +class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionCaps, ConnectionHandlersBase, ConnectionJingle): def __init__(self): ConnectionVcard.__init__(self) ConnectionBytestream.__init__(self) ConnectionCommands.__init__(self) ConnectionPubSub.__init__(self) + ConnectionJingle.__init__(self) ConnectionHandlersBase.__init__(self) self.gmail_url = None @@ -2807,6 +2816,10 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, common.xmpp.NS_PRIVACY) con.RegisterHandler('iq', self._PubSubCB, 'result') con.RegisterHandler('iq', self._PubSubErrorCB, 'error') + con.RegisterHandler('iq', self._JingleCB, 'result') + con.RegisterHandler('iq', self._JingleCB, 'error') + con.RegisterHandler('iq', self._JingleCB, 'set', + common.xmpp.NS_JINGLE) con.RegisterHandler('iq', self._ErrorCB, 'error') con.RegisterHandler('iq', self._IqCB) con.RegisterHandler('iq', self._StanzaArrivedCB) diff --git a/src/common/events.py b/src/common/events.py index 82017112aa9047418b5494e89eaab4d2b1004653..c69fc27d1bdcc4a94d6ad9ca3f535a697abf4a56 100644 --- a/src/common/events.py +++ b/src/common/events.py @@ -33,7 +33,7 @@ class Event: ''' type_ in chat, normal, file-request, file-error, file-completed, file-request-error, file-send-error, file-stopped, gc_msg, pm, printed_chat, printed_gc_msg, printed_marked_gc_msg, printed_pm, - gc-invitation, subscription_request, unsubscribed + gc-invitation, subscription_request, unsubscribedm jingle-incoming parameters is (per type_): chat, normal, pm: [message, subject, kind, time, encrypted, resource, msg_id] @@ -46,6 +46,7 @@ class Event: gc-invitation: [room_jid, reason, password, is_continued] subscription_request: [text, nick] unsubscribed: contact + jingle-incoming: (fulljid, sessionid, content_types) ''' self.type_ = type_ self.time_ = time_ diff --git a/src/common/gajim.py b/src/common/gajim.py index 3511fe2d9fbacc0020b9905f1442a7a3a832008c..0f340114a98674e630a43ba71802bd69bb8914b0 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -189,6 +189,11 @@ try: except ImportError: HAVE_INDICATOR = False +HAVE_FARSIGHT = True +try: + import farsight, gst +except ImportError: + HAVE_FARSIGHT = False gajim_identity = {'type': 'pc', 'category': 'client', 'name': 'Gajim'} gajim_common_features = [xmpp.NS_BYTESTREAM, xmpp.NS_SI, xmpp.NS_FILE, xmpp.NS_MUC, xmpp.NS_MUC_USER, xmpp.NS_MUC_ADMIN, xmpp.NS_MUC_OWNER, diff --git a/src/common/helpers.py b/src/common/helpers.py index 106b0ee344c81dff9bf0c2c2bc7c44a5ec13925d..1282e6e42bff95e5ca33757aad6c6fcf548505cd 100644 --- a/src/common/helpers.py +++ b/src/common/helpers.py @@ -1357,6 +1357,12 @@ def update_optional_features(account = None): gajim.gajim_optional_features[a].append(xmpp.NS_ESESSION) if gajim.config.get_per('accounts', a, 'answer_receipts'): gajim.gajim_optional_features[a].append(xmpp.NS_RECEIPTS) + if gajim.HAVE_FARSIGHT: + gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE) + gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP) + gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_AUDIO) + gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_VIDEO) + gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_ICE_UDP) gajim.caps_hash[a] = compute_caps_hash([gajim.gajim_identity], gajim.gajim_common_features + gajim.gajim_optional_features[a]) # re-send presence with new hash diff --git a/src/common/jingle.py b/src/common/jingle.py new file mode 100644 index 0000000000000000000000000000000000000000..f058865334c1ce48e603eb318143692fa904fae4 --- /dev/null +++ b/src/common/jingle.py @@ -0,0 +1,1165 @@ +## +## 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 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 +# * XEP 0177 (raw udp) + +# * UI: +# - make state and codec informations available to the user +# - video integration +# * config: +# - 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 + +# * 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 <service-unavailable/> + # 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 <content/> element to <jingle/> element, + with (full=True) or without (full=False) <content/> + children. ''' + jingle.addChild('content', + attrs={'name': content.name, 'creator': content.creator}) + + def __append_contents(self, jingle): + ''' Append all <content/> elements to <jingle/>.''' + # 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) + + +class ConnectionJingle(object): + ''' This object depends on that it is a part of Connection class. ''' + def __init__(self): + # dictionary: (jid, sessionid) => JingleSession object + self.__sessions = {} + + # dictionary: (jid, iq stanza id) => JingleSession object, + # one time callbacks + self.__iq_responses = {} + + def add_jingle(self, jingle): + ''' Add a jingle session to a jingle stanza dispatcher + jingle - a JingleSession object. + ''' + self.__sessions[(jingle.peerjid, jingle.sid)] = jingle + + def delete_jingle_session(self, peerjid, sid): + ''' Remove a jingle session from a jingle stanza dispatcher ''' + key = (peerjid, sid) + if key in self.__sessions: + #FIXME: Move this elsewhere? + for content in self.__sessions[key].contents.values(): + content.destroy() + self.__sessions[key].callbacks = [] + del self.__sessions[key] + + def _JingleCB(self, con, stanza): + ''' The jingle stanza dispatcher. + Route jingle stanza to proper JingleSession object, + or create one if it is a new session. + TODO: Also check if the stanza isn't an error stanza, if so + route it adequatelly.''' + + # get data + jid = helpers.get_full_jid_from_iq(stanza) + id = stanza.getID() + + if (jid, id) in self.__iq_responses.keys(): + self.__iq_responses[(jid, id)].stanzaCB(stanza) + del self.__iq_responses[(jid, id)] + raise xmpp.NodeProcessed + + jingle = stanza.getTag('jingle') + if not jingle: return + sid = jingle.getAttr('sid') + + # do we need to create a new jingle object + if (jid, sid) not in self.__sessions: + #TODO: tie-breaking and other things... + newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid) + self.add_jingle(newjingle) + + # we already have such session in dispatcher... + self.__sessions[(jid, sid)].stanzaCB(stanza) + + raise xmpp.NodeProcessed + + def startVoIP(self, jid): + if self.get_jingle_session(jid, media='audio'): + 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)) + else: + jingle = JingleSession(self, weinitiate=True, jid=jid) + self.add_jingle(jingle) + jingle.add_content('voice', JingleVoIP(jingle)) + jingle.start_session() + return jingle.sid + + def startVideoIP(self, jid): + if self.get_jingle_session(jid, media='video'): + return self.get_jingle_session(jid, media='video').sid + jingle = self.get_jingle_session(jid, media='audio') + if jingle: + jingle.add_content('video', JingleVideo(jingle)) + else: + jingle = JingleSession(self, weinitiate=True, jid=jid) + self.add_jingle(jingle) + jingle.add_content('video', JingleVideo(jingle)) + jingle.start_session() + return jingle.sid + + def get_jingle_session(self, jid, sid=None, media=None): + if sid: + if (jid, sid) in self.__sessions: + return self.__sessions[(jid, sid)] + else: + return None + elif media: + if media not in ('audio', 'video'): + return None + for session in self.__sessions.values(): + if session.peerjid == jid and session.get_content(media): + return session + + return None diff --git a/src/common/meta.py b/src/common/meta.py new file mode 100644 index 0000000000000000000000000000000000000000..153c2e952283bab6b036dd4ea2631f1e326d7e6d --- /dev/null +++ b/src/common/meta.py @@ -0,0 +1,36 @@ +#!/usr/bin/python + +import types + +class VerboseClassType(type): + indent = '' + + def __init__(cls, name, bases, dict): + super(VerboseClassType, cls).__init__(cls, name, bases, dict) + new = {} + print 'Initializing new class %s:' % cls + for fname, fun in dict.iteritems(): + wrap = hasattr(fun, '__call__') + print '%s%s is %s, we %s wrap it.' % \ + (cls.__class__.indent, fname, fun, wrap and 'will' or "won't") + if not wrap: continue + setattr(cls, fname, cls.wrap(name, fname, fun)) + + def wrap(cls, name, fname, fun): + def verbose(*a, **b): + args = ', '.join(map(repr, a)+map(lambda x:'%s=%r'%x, b.iteritems())) + print '%s%s.%s(%s):' % (cls.__class__.indent, name, fname, args) + cls.__class__.indent += '| ' + r = fun(*a, **b) + cls.__class__.indent = cls.__class__.indent[:-4] + print '%s+=%r' % (cls.__class__.indent, r) + return r + verbose.__name__ = fname + return verbose + +def nested_property(f): + ret = f() + p = {} + for v in ('fget', 'fset', 'fdel', 'doc'): + if v in ret: p[v]=ret[v] + return property(**p) diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index 3d0debca588ec0306e6ce723af109ad204a37757..6197714098d959d5b36b6763c759a89dc51bae07 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -63,6 +63,13 @@ NS_HTTP_BIND ='http://jabber.org/protocol/httpbind' # XEP-0124 NS_IBB ='http://jabber.org/protocol/ibb' NS_INVISIBLE ='presence-invisible' # Jabberd2 NS_IQ ='iq' # Jabberd2 +NS_JINGLE ='urn:xmpp:jingle:1' # XEP-0166 +NS_JINGLE_ERRORS='urn:xmpp:jingle:errors:1' # XEP-0166 +NS_JINGLE_RTP ='urn:xmpp:jingle:apps:rtp:1' # XEP-0167 +NS_JINGLE_RTP_AUDIO='urn:xmpp:jingle:apps:rtp:audio' # XEP-0167 +NS_JINGLE_RTP_VIDEO='urn:xmpp:jingle:apps:rtp:video' # XEP-0167 +NS_JINGLE_RAW_UDP='urn:xmpp:jingle:transports:raw-udp:1' # XEP-0177 +NS_JINGLE_ICE_UDP='urn:xmpp:jingle:transports:ice-udp:1' # XEP-0176 NS_LAST ='jabber:iq:last' NS_MESSAGE ='message' # Jabberd2 NS_MOOD ='http://jabber.org/protocol/mood' # XEP-0107 diff --git a/src/common/xmpp/simplexml.py b/src/common/xmpp/simplexml.py index a658189024479b368432682e770669ae4d9278c3..f47142c46ece339a5242146b33aabca7710a2e6d 100644 --- a/src/common/xmpp/simplexml.py +++ b/src/common/xmpp/simplexml.py @@ -297,6 +297,9 @@ class Node(object): def __delitem__(self,item): ''' Deletes node's attribute "item". ''' return self.delAttr(item) + def __contains__(self,item): + """ Checks if node has attribute "item" """ + return self.has_attr(item) def __getattr__(self,attr): ''' Reduce memory usage caused by T/NT classes - use memory only when needed. ''' if attr=='T': diff --git a/src/dialogs.py b/src/dialogs.py index f4927ce18534222460c3762bd5c7ab1e1065389a..a0fc9f9b7d39175f209c957015bf02051fd9428b 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -32,6 +32,7 @@ import gtk import gobject import os +from weakref import WeakValueDictionary import gtkgui_helpers import vcard @@ -4514,6 +4515,8 @@ class GPGInfoWindow: def on_close_button_clicked(self, widget): self.window.destroy() + + class ResourceConflictDialog(TimeoutDialog, InputDialog): def __init__(self, title, text, resource, ok_handler): TimeoutDialog.__init__(self, 15, self.on_timeout) @@ -4525,4 +4528,106 @@ class ResourceConflictDialog(TimeoutDialog, InputDialog): def on_timeout(self): self.on_okbutton_clicked(None) + + +class VoIPCallReceivedDialog(object): + instances = {} + def __init__(self, account, contact_jid, sid, content_types): + self.instances[(contact_jid, sid)] = self + self.account = account + self.fjid = contact_jid + self.sid = sid + self.content_types = content_types + + xml = gtkgui_helpers.get_glade('voip_call_received_dialog.glade') + xml.signal_autoconnect(self) + + jid = gajim.get_jid_without_resource(self.fjid) + contact = gajim.contacts.get_first_contact_from_jid(account, jid) + if contact and contact.name: + self.contact_text = '%s (%s)' % (contact.name, jid) + else: + self.contact_text = contact_jid + + self.dialog = xml.get_widget('voip_call_received_messagedialog') + self.set_secondary_text() + + self.dialog.show_all() + + @classmethod + def get_dialog(cls, jid, sid): + if (jid, sid) in cls.instances: + return cls.instances[(jid, sid)] + else: + return None + + def set_secondary_text(self): + if 'audio' in self.content_types and 'video' in self.content_types: + types_text = _('an audio and video') + elif 'audio' in self.content_types: + types_text = _('an audio') + elif 'video' in self.content_types: + types_text = _('a video') + + # do the substitution + self.dialog.set_property('secondary-text', + _('%(contact)s wants to start %(type)s session with you. Do you want ' + 'to answer the call?') % {'contact': self.contact_text, 'type': types_text}) + + def add_contents(self, content_types): + for type_ in content_types: + if type_ not in self.content_types: + self.content_types.add(type_) + self.set_secondary_text() + + def on_voip_call_received_messagedialog_destroy(self, dialog): + if (self.fjid, self.sid) in self.instances: + del self.instances[(self.fjid, self.sid)] + + def on_voip_call_received_messagedialog_close(self, dialog): + return self.on_voip_call_received_messagedialog_response(dialog, + gtk.RESPONSE_NO) + + def on_voip_call_received_messagedialog_response(self, dialog, response): + # we've got response from user, either stop connecting or accept the call + session = gajim.connections[self.account].get_jingle_session(self.fjid, + self.sid) + if not session: + return + if response == gtk.RESPONSE_YES: + #TODO: Ensure that ctrl.contact.resource == resource + jid = gajim.get_jid_without_resource(self.fjid) + resource = gajim.get_resource_from_jid(self.fjid) + ctrl = gajim.interface.msg_win_mgr.get_control(self.fjid, self.account) + if not ctrl: + ctrl = gajim.interface.msg_win_mgr.get_control(jid, self.account) + if not ctrl: + # open chat control + contact = gajim.contacts.get_contact(self.account, jid, resource) + if not contact: + contact = gajim.contacts.get_contact(self.account, jid) + if not contact: + return + ctrl = gajim.interface.new_chat(contact, self.account) + # Chat control opened, update content's status + if session.get_content('audio'): + ctrl.set_audio_state('connecting', self.sid) + if session.get_content('video'): + ctrl.set_video_state('connecting', self.sid) + # Now, accept the content/sessions. + # This should be done after the chat control is running + if not session.accepted: + session.approve_session() + for content in self.content_types: + session.approve_content(content) + else: # response==gtk.RESPONSE_NO + if not session.accepted: + session.decline_session() + else: + for content in self.content_types: + session.reject_content(content) + + dialog.destroy() + + # vim: se ts=3: diff --git a/src/features_window.py b/src/features_window.py index ebe00155808d24ca5142ebf044968044b0129fe2..609959e65539970281109c34caf5620e25b893f6 100644 --- a/src/features_window.py +++ b/src/features_window.py @@ -107,6 +107,10 @@ class FeaturesWindow: _('Ability to have clickable URLs in chat and groupchat window banners.'), _('Requires python-sexy.'), _('Requires python-sexy.')), + _('Audio / Video'): (self.farsight_available, + _('Ability to start audio and video chat.'), + _('Requires python-farsight.'), + _('Feature not available under Windows.')), } # name, supported @@ -265,4 +269,7 @@ class FeaturesWindow: def pysexy_available(self): return gajim.HAVE_PYSEXY + def farsight_available(self): + return gajim.HAVE_FARSIGHT + # vim: se ts=3: diff --git a/src/gajim.py b/src/gajim.py index 3dd191d37776e33b3918268e89ecd412acddff07..9f17ae7ae0a8c45f7b1d394d23bbac621a76f65a 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -2107,6 +2107,102 @@ class Interface: _('You are already connected to this account with the same resource. ' 'Please type a new one'), resource=proposed_resource, ok_handler=on_ok) + def handle_event_jingle_incoming(self, account, data): + # ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type, + # data...)) + # TODO: conditional blocking if peer is not in roster + + # unpack data + peerjid, sid, contents = data + content_types = set(c[0] for c in contents) + + # check type of jingle session + if 'audio' in content_types or 'video' in content_types: + # a voip session... + # we now handle only voip, so the only thing we will do here is + # not to return from function + pass + else: + # unknown session type... it should be declined in common/jingle.py + return + + jid = gajim.get_jid_without_resource(peerjid) + resource = gajim.get_resource_from_jid(peerjid) + ctrl = self.msg_win_mgr.get_control(peerjid, account) + if not ctrl: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + if 'audio' in content_types: + ctrl.set_audio_state('connection_received', sid) + if 'video' in content_types: + ctrl.set_video_state('connection_received', sid) + + dlg = dialogs.VoIPCallReceivedDialog.get_dialog(peerjid, sid) + if dlg: + dlg.add_contents(content_types) + return + + if helpers.allow_popup_window(account): + dialogs.VoIPCallReceivedDialog(account, peerjid, sid, content_types) + return + + self.add_event(account, peerjid, 'jingle-incoming', (peerjid, sid, + content_types)) + + if helpers.allow_showing_notification(account): + # TODO: we should use another pixmap ;-) + img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', + 'ft_request.png') + txt = _('%s wants to start a voice chat.') % gajim.get_name_from_jid( + account, peerjid) + path = gtkgui_helpers.get_path_to_generic_or_avatar(img) + event_type = _('Voice Chat Request') + notify.popup(event_type, peerjid, account, 'jingle-incoming', + path_to_image = path, title = event_type, text = txt) + + def handle_event_jingle_connected(self, account, data): + # ('JINGLE_CONNECTED', account, (peerjid, sid, media)) + peerjid, sid, media = data + if media in ('audio', 'video'): + jid = gajim.get_jid_without_resource(peerjid) + resource = gajim.get_resource_from_jid(peerjid) + ctrl = self.msg_win_mgr.get_control(peerjid, account) + if not ctrl: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + if media == 'audio': + ctrl.set_audio_state('connected', sid) + else: + ctrl.set_video_state('connected', sid) + + def handle_event_jingle_disconnected(self, account, data): + # ('JINGLE_DISCONNECTED', account, (peerjid, sid, reason)) + peerjid, sid, media, reason = data + jid = gajim.get_jid_without_resource(peerjid) + resource = gajim.get_resource_from_jid(peerjid) + ctrl = self.msg_win_mgr.get_control(peerjid, account) + if not ctrl: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + if media in ('audio', None): + ctrl.set_audio_state('stop', sid=sid, reason=reason) + if media in ('video', None): + ctrl.set_video_state('stop', sid=sid, reason=reason) + dialog = dialogs.VoIPCallReceivedDialog.get_dialog(peerjid, sid) + if dialog: + dialog.dialog.destroy() + + def handle_event_jingle_error(self, account, data): + # ('JINGLE_ERROR', account, (peerjid, sid, reason)) + peerjid, sid, reason = data + jid = gajim.get_jid_without_resource(peerjid) + resource = gajim.get_resource_from_jid(peerjid) + ctrl = self.msg_win_mgr.get_control(peerjid, account) + if not ctrl: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + ctrl.set_audio_state('error', reason=reason) + def handle_event_pep_config(self, account, data): # ('PEP_CONFIG', account, (node, form)) if 'pep_services' in self.instances[account]: @@ -2364,6 +2460,10 @@ class Interface: 'INSECURE_SSL_CONNECTION': self.handle_event_insecure_ssl_connection, 'PUBSUB_NODE_REMOVED': self.handle_event_pubsub_node_removed, 'PUBSUB_NODE_NOT_REMOVED': self.handle_event_pubsub_node_not_removed, + 'JINGLE_INCOMING': self.handle_event_jingle_incoming, + 'JINGLE_CONNECTED': self.handle_event_jingle_connected, + 'JINGLE_DISCONNECTED': self.handle_event_jingle_disconnected, + 'JINGLE_ERROR': self.handle_event_jingle_error, } def dispatch(self, event, account, data): @@ -2388,7 +2488,7 @@ class Interface: jid = gajim.get_jid_without_resource(jid) no_queue = len(gajim.events.get_events(account, jid)) == 0 # type_ can be gc-invitation file-send-error file-error file-request-error - # file-request file-completed file-stopped + # file-request file-completed file-stopped jingle-incoming # event_type can be in advancedNotificationWindow.events_list event_types = {'file-request': 'ft_request', 'file-completed': 'ft_finished'} @@ -2548,6 +2648,11 @@ class Interface: self.show_unsubscribed_dialog(account, contact) gajim.events.remove_events(account, jid, event) self.roster.draw_contact(jid, account) + elif type_ == 'jingle-incoming': + event = gajim.events.get_first_event(account, jid, type_) + peerjid, sid, content_types = event.parameters + dialogs.VoIPCallReceivedDialog(account, peerjid, sid, content_types) + gajim.events.remove_events(account, jid, event) if w: w.set_active_tab(ctrl) w.window.window.focus(gtk.get_current_event_time())