1 ## plugins/whiteboard/plugin.py
3 ## Copyright (C) 2009 Jeff Ling <jeff.ummu AT gmail.com>
4 ## Copyright (C) 2010 Yann Leboulanger <asterix AT lagaule.org>
6 ## This file is part of Gajim.
8 ## Gajim is free software; you can redistribute it and/or modify
9 ## it under the terms of the GNU General Public License as published
10 ## by the Free Software Foundation; version 3 only.
12 ## Gajim is distributed in the hope that it will be useful,
13 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 ## GNU General Public License for more details.
17 ## You should have received a copy of the GNU General Public License
18 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
24 :author: Yann Leboulanger <asterix@lagaule.org>
25 :since: 1st November 2010
26 :copyright: Copyright (2010) Yann Leboulanger <asterix@lagaule.org>
31 from common
import helpers
32 from common
import gajim
33 from plugins
import GajimPlugin
34 from plugins
.plugin
import GajimPluginException
35 from plugins
.helpers
import log_calls
, log
39 from common
import ged
40 from common
.jingle_session
import JingleSession
41 from common
.jingle_content
import JingleContent
42 from common
.jingle_transport
import JingleTransport
, TransportType
44 from whiteboard_widget
import Whiteboard
, HAS_GOOCANVAS
45 from common
import xmpp
46 from common
import caps_cache
48 NS_JINGLE_XHTML
= 'urn:xmpp:tmp:jingle:apps:xhtml'
49 NS_JINGLE_SXE
= 'urn:xmpp:tmp:jingle:transports:sxe'
50 NS_SXE
= 'urn:xmpp:sxe:0'
52 class WhiteboardPlugin(GajimPlugin
):
53 @log_calls('WhiteboardPlugin')
55 self
.config_dialog
= None
56 self
.events_handlers
= {
57 'jingle-request-received': (ged
.GUI1
, self
._nec
_jingle
_received
),
58 'jingle-connected-received': (ged
.GUI1
, self
._nec
_jingle
_connected
),
59 'jingle-disconnected-received': (ged
.GUI1
,
60 self
._nec
_jingle
_disconnected
),
61 'raw-message-received': (ged
.GUI1
, self
._nec
_raw
_message
),
63 self
.gui_extension_points
= {
64 'chat_control_base' : (self
.connect_with_chat_control
,
65 self
.disconnect_from_chat_control
),
66 'chat_control_base_update_toolbar': (self
.update_button_state
,
72 @log_calls('WhiteboardPlugin')
73 def _compute_caps_hash(self
):
74 for a
in gajim
.connections
:
75 gajim
.caps_hash
[a
] = caps_cache
.compute_caps_hash([
76 gajim
.gajim_identity
], gajim
.gajim_common_features
+ \
77 gajim
.gajim_optional_features
[a
])
78 # re-send presence with new hash
79 connected
= gajim
.connections
[a
].connected
80 if connected
> 1 and gajim
.SHOW_LIST
[connected
] != 'invisible':
81 gajim
.connections
[a
].change_status(gajim
.SHOW_LIST
[connected
],
82 gajim
.connections
[a
].status
)
84 @log_calls('WhiteboardPlugin')
87 raise GajimPluginException('python-pygoocanvas is missing!')
88 if NS_JINGLE_SXE
not in gajim
.gajim_common_features
:
89 gajim
.gajim_common_features
.append(NS_JINGLE_SXE
)
90 if NS_SXE
not in gajim
.gajim_common_features
:
91 gajim
.gajim_common_features
.append(NS_SXE
)
92 self
._compute
_caps
_hash
()
94 @log_calls('WhiteboardPlugin')
96 if NS_JINGLE_SXE
in gajim
.gajim_common_features
:
97 gajim
.gajim_common_features
.remove(NS_JINGLE_SXE
)
98 if NS_SXE
in gajim
.gajim_common_features
:
99 gajim
.gajim_common_features
.remove(NS_SXE
)
100 self
._compute
_caps
_hash
()
102 @log_calls('WhiteboardPlugin')
103 def connect_with_chat_control(self
, control
):
104 if isinstance(control
, chat_control
.ChatControl
):
105 base
= Base(self
, control
)
106 self
.controls
.append(base
)
108 @log_calls('WhiteboardPlugin')
109 def disconnect_from_chat_control(self
, chat_control
):
110 for base
in self
.controls
:
111 base
.disconnect_from_chat_control()
114 @log_calls('WhiteboardPlugin')
115 def update_button_state(self
, control
):
116 for base
in self
.controls
:
117 if base
.chat_control
== control
:
118 if control
.contact
.supports(NS_JINGLE_SXE
) and \
119 control
.contact
.supports(NS_SXE
):
120 base
.button
.set_sensitive(True)
122 base
.button
.set_sensitive(False)
124 @log_calls('WhiteboardPlugin')
125 def show_request_dialog(self
, account
, fjid
, jid
, sid
, content_types
):
127 session
= gajim
.connections
[account
].get_jingle_session(fjid
, sid
)
128 self
.sid
= session
.sid
129 if not session
.accepted
:
130 session
.approve_session()
131 for content
in content_types
:
132 session
.approve_content(content
)
133 for _jid
in (fjid
, jid
):
134 ctrl
= gajim
.interface
.msg_win_mgr
.get_control(_jid
, account
)
139 gajim
.interface
.new_chat_from_jid(account
, jid
)
140 ctrl
= gajim
.interface
.msg_win_mgr
.get_control(jid
, account
)
141 session
= session
.contents
[('initiator', 'xhtml')]
142 ctrl
.draw_whiteboard(session
)
145 session
= gajim
.connections
[account
].get_jingle_session(fjid
, sid
)
146 session
.decline_session()
148 contact
= gajim
.contacts
.get_first_contact_from_jid(account
, jid
)
150 name
= contact
.get_shown_name()
153 pritext
= _('Incoming Whiteboard')
154 sectext
= _('%(name)s (%(jid)s) wants to start a whiteboard with '
155 'you. Do you want to accept?') % {'name': name
, 'jid': jid
}
156 dialog
= dialogs
.NonModalConfirmationDialog(pritext
, sectext
=sectext
,
157 on_response_ok
=on_ok
, on_response_cancel
=on_cancel
)
160 @log_calls('WhiteboardPlugin')
161 def _nec_jingle_received(self
, obj
):
162 if not HAS_GOOCANVAS
:
164 content_types
= set(c
[0] for c
in obj
.contents
)
165 if 'xhtml' not in content_types
:
167 self
.show_request_dialog(obj
.conn
.name
, obj
.fjid
, obj
.jid
, obj
.sid
,
170 @log_calls('WhiteboardPlugin')
171 def _nec_jingle_connected(self
, obj
):
172 if not HAS_GOOCANVAS
:
174 account
= obj
.conn
.name
175 ctrl
= (gajim
.interface
.msg_win_mgr
.get_control(obj
.fjid
, account
)
176 or gajim
.interface
.msg_win_mgr
.get_control(obj
.jid
, account
))
179 session
= gajim
.connections
[obj
.conn
.name
].get_jingle_session(obj
.fjid
,
182 if ('initiator', 'xhtml') not in session
.contents
:
185 session
= session
.contents
[('initiator', 'xhtml')]
186 ctrl
.draw_whiteboard(session
)
188 @log_calls('WhiteboardPlugin')
189 def _nec_jingle_disconnected(self
, obj
):
190 for base
in self
.controls
:
191 if base
.sid
== obj
.sid
:
192 base
.stop_whiteboard(reason
= obj
.reason
)
194 @log_calls('WhiteboardPlugin')
195 def _nec_raw_message(self
, obj
):
196 if not HAS_GOOCANVAS
:
198 if obj
.stanza
.getTag('sxe', namespace
=NS_SXE
):
199 account
= obj
.conn
.name
202 fjid
= helpers
.get_full_jid_from_iq(obj
.stanza
)
203 except helpers
.InvalidFormat
:
204 obj
.conn
.dispatch('ERROR', (_('Invalid Jabber ID'),
205 _('A message from a non-valid JID arrived, it has been '
208 jid
= gajim
.get_jid_without_resource(fjid
)
209 ctrl
= (gajim
.interface
.msg_win_mgr
.get_control(fjid
, account
)
210 or gajim
.interface
.msg_win_mgr
.get_control(jid
, account
))
213 sxe
= obj
.stanza
.getTag('sxe')
216 sid
= sxe
.getAttr('session')
217 if (jid
, sid
) not in obj
.conn
._sessions
:
219 # newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid)
220 # self.addJingle(newjingle)
222 # we already have such session in dispatcher...
223 session
= obj
.conn
.get_jingle_session(fjid
, sid
)
224 cn
= session
.contents
[('initiator', 'xhtml')]
225 error
= obj
.stanza
.getTag('error')
231 cn
.on_stanza(obj
.stanza
, sxe
, error
, action
)
232 # def __editCB(self, stanza, content, error, action):
233 #new_tags = sxe.getTags('new')
234 #remove_tags = sxe.getTags('remove')
236 #if new_tags is not None:
237 ## Process new elements
238 #for tag in new_tags:
239 #if tag.getAttr('type') == 'element':
240 #ctrl.whiteboard.recieve_element(tag)
241 #elif tag.getAttr('type') == 'attr':
242 #ctrl.whiteboard.recieve_attr(tag)
243 #ctrl.whiteboard.apply_new()
245 #if remove_tags is not None:
247 #for tag in remove_tags:
248 #target = tag.getAttr('target')
249 #ctrl.whiteboard.image.del_rid(target)
251 # Stop propagating this event, it's handled
256 def __init__(self
, plugin
, chat_control
):
258 self
.chat_control
= chat_control
259 self
.chat_control
.draw_whiteboard
= self
.draw_whiteboard
260 self
.contact
= self
.chat_control
.contact
261 self
.account
= self
.chat_control
.account
262 self
.jid
= self
.contact
.get_full_jid()
263 self
.create_buttons()
264 self
.whiteboard
= None
267 def create_buttons(self
):
268 # create juick button
269 actions_hbox
= self
.chat_control
.xml
.get_object('actions_hbox')
270 self
.button
= gtk
.ToggleButton(label
=None, use_underline
=True)
271 self
.button
.set_property('relief', gtk
.RELIEF_NONE
)
272 self
.button
.set_property('can-focus', False)
274 img_path
= self
.plugin
.local_file_path('whiteboard.png')
275 pixbuf
= gtk
.gdk
.pixbuf_new_from_file(img_path
)
276 iconset
= gtk
.IconSet(pixbuf
=pixbuf
)
277 factory
= gtk
.IconFactory()
278 factory
.add('whiteboard', iconset
)
279 img_path
= self
.plugin
.local_file_path('brush_tool.png')
280 pixbuf
= gtk
.gdk
.pixbuf_new_from_file(img_path
)
281 iconset
= gtk
.IconSet(pixbuf
=pixbuf
)
282 factory
.add('brush_tool', iconset
)
283 img_path
= self
.plugin
.local_file_path('line_tool.png')
284 pixbuf
= gtk
.gdk
.pixbuf_new_from_file(img_path
)
285 iconset
= gtk
.IconSet(pixbuf
=pixbuf
)
286 factory
.add('line_tool', iconset
)
287 img_path
= self
.plugin
.local_file_path('oval_tool.png')
288 pixbuf
= gtk
.gdk
.pixbuf_new_from_file(img_path
)
289 iconset
= gtk
.IconSet(pixbuf
=pixbuf
)
290 factory
.add('oval_tool', iconset
)
291 factory
.add_default()
292 img
.set_from_stock('whiteboard', gtk
.ICON_SIZE_BUTTON
)
293 self
.button
.set_image(img
)
294 send_button
= self
.chat_control
.xml
.get_object('send_button')
295 send_button_pos
= actions_hbox
.child_get_property(send_button
,
297 actions_hbox
.add_with_properties(self
.button
, 'position',
298 send_button_pos
- 1, 'expand', False)
299 id_
= self
.button
.connect('toggled', self
.on_whiteboard_button_toggled
)
300 self
.chat_control
.handlers
[id_
] = self
.button
303 def draw_whiteboard(self
, content
):
304 hbox
= self
.chat_control
.xml
.get_object('chat_control_hbox')
305 if len(hbox
.get_children()) == 1:
306 self
.whiteboard
= Whiteboard(self
.account
, self
.contact
, content
,
309 self
.whiteboard
.hbox
.set_size_request(300, 0)
310 hbox
.pack_start(self
.whiteboard
.hbox
, expand
=False, fill
=False)
311 self
.whiteboard
.hbox
.show_all()
312 self
.button
.set_active(True)
313 content
.control
= self
314 self
.sid
= content
.session
.sid
316 def on_whiteboard_button_toggled(self
, widget
):
320 if widget
.get_active():
321 if not self
.whiteboard
:
322 self
.start_whiteboard()
324 self
.stop_whiteboard()
326 def start_whiteboard(self
):
327 conn
= gajim
.connections
[self
.chat_control
.account
]
328 jingle
= JingleSession(conn
, weinitiate
=True, jid
=self
.jid
)
329 self
.sid
= jingle
.sid
330 conn
._sessions
[jingle
.sid
] = jingle
331 content
= JingleWhiteboard(jingle
)
332 content
.control
= self
333 jingle
.add_content('xhtml', content
)
334 jingle
.start_session()
336 def stop_whiteboard(self
, reason
=None):
337 conn
= gajim
.connections
[self
.chat_control
.account
]
339 session
= conn
.get_jingle_session(self
.jid
, media
='xhtml')
341 session
.end_session()
342 self
.button
.set_active(False)
344 txt
= _('Whiteboard stopped: %(reason)s') % {'reason': reason
}
345 self
.chat_control
.print_conversation(txt
, 'info')
346 if not self
.whiteboard
:
348 hbox
= self
.chat_control
.xml
.get_object('chat_control_hbox')
349 if self
.whiteboard
.hbox
in hbox
.get_children():
350 if hasattr(self
.whiteboard
, 'hbox'):
351 hbox
.remove(self
.whiteboard
.hbox
)
352 self
.whiteboard
= None
354 def disconnect_from_chat_control(self
):
355 actions_hbox
= self
.chat_control
.xml
.get_object('actions_hbox')
356 actions_hbox
.remove(self
.button
)
358 class JingleWhiteboard(JingleContent
):
359 ''' Jingle Whiteboard sessions consist of xhtml content'''
360 def __init__(self
, session
, transport
=None):
362 transport
= JingleTransportSXE()
363 JingleContent
.__init
__(self
, session
, transport
)
365 self
.negotiated
= True # there is nothing to negotiate
367 self
.callbacks
['session-accept'] += [self
._sessionAcceptCB
]
368 self
.callbacks
['session-terminate'] += [self
._stop
]
369 self
.callbacks
['session-terminate-sent'] += [self
._stop
]
370 self
.callbacks
['edit'] = [self
._EditCB
]
372 def _EditCB(self
, stanza
, content
, error
, action
):
373 new_tags
= content
.getTags('new')
374 remove_tags
= content
.getTags('remove')
376 if new_tags
is not None:
377 # Process new elements
379 if tag
.getAttr('type') == 'element':
380 self
.control
.whiteboard
.recieve_element(tag
)
381 elif tag
.getAttr('type') == 'attr':
382 self
.control
.whiteboard
.recieve_attr(tag
)
383 self
.control
.whiteboard
.apply_new()
385 if remove_tags
is not None:
387 for tag
in remove_tags
:
388 target
= tag
.getAttr('target')
389 self
.control
.whiteboard
.image
.del_rid(target
)
391 def _sessionAcceptCB(self
, stanza
, content
, error
, action
):
392 log
.debug('session accepted')
393 self
.session
.connection
.dispatch('WHITEBOARD_ACCEPTED',
394 (self
.session
.peerjid
, self
.session
.sid
))
396 def generate_rids(self
, x
):
397 # generates x number of rids and returns in list
400 rids
.append(str(self
.last_rid
))
404 def send_whiteboard_node(self
, items
, rids
):
405 # takes int rid and dict items and sends it as a node
407 jid
= self
.session
.peerjid
408 sid
= self
.session
.sid
409 message
= xmpp
.Message(to
=jid
)
410 sxe
= message
.addChild(name
='sxe', attrs
={'session': sid
},
414 if items
[x
]['type'] == 'element':
417 'name': items
[x
]['data'][0].getName(),
418 'type': items
[x
]['type']}
419 sxe
.addChild(name
='new', attrs
=attrs
)
420 if items
[x
]['type'] == 'attr':
421 attr_name
= items
[x
]['data']
422 chdata
= items
[parent
]['data'][0].getAttr(attr_name
)
425 'type': items
[x
]['type'],
428 sxe
.addChild(name
='new', attrs
=attrs
)
429 self
.session
.connection
.connection
.send(message
)
431 def delete_whiteboard_node(self
, rids
):
432 message
= xmpp
.Message(to
=self
.session
.peerjid
)
433 sxe
= message
.addChild(name
='sxe', attrs
={'session': self
.session
.sid
},
437 sxe
.addChild(name
='remove', attrs
= {'target': x
})
438 self
.session
.connection
.connection
.send(message
)
440 def send_items(self
, items
, rids
):
441 # recieves dict items and a list of rids of items to send
442 # TODO: is there a less clumsy way that doesn't involve passing
444 self
.send_whiteboard_node(items
, rids
)
446 def del_item(self
, rids
):
447 self
.delete_whiteboard_node(rids
)
449 def encode(self
, xml
):
450 # encodes it sendable string
451 return 'data:text/xml,' + urllib
.quote(xml
)
453 def _fill_content(self
, content
):
454 content
.addChild(NS_JINGLE_XHTML
+ ' description')
456 def _stop(self
, *things
):
462 def get_content(desc
):
463 return JingleWhiteboard
465 common
.jingle_content
.contents
[NS_JINGLE_XHTML
] = get_content
467 class JingleTransportSXE(JingleTransport
):
469 JingleTransport
.__init
__(self
, TransportType
.streaming
)
471 def make_transport(self
, candidates
=None):
472 transport
= JingleTransport
.make_transport(self
, candidates
)
473 transport
.setNamespace(NS_JINGLE_SXE
)
474 transport
.setTagData('host', 'TODO')
477 common
.jingle_transport
.transports
[NS_JINGLE_SXE
] = JingleTransportSXE