handle outgoing messages with events. Fixes #6743
[gajim.git] / src / common / pep.py
blob07b2a6161256a2788aa2556219a6e5c7546d6811
1 # -*- coding:utf-8 -*-
2 ## src/common/pep.py
3 ##
4 ## Copyright (C) 2007 Piotr Gaczkowski <doomhammerng AT gmail.com>
5 ## Copyright (C) 2007-2010 Yann Leboulanger <asterix AT lagaule.org>
6 ## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
7 ## Jean-Marie Traissard <jim AT lapin.org>
8 ## Jonathan Schleifer <js-common.gajim AT webkeks.org>
9 ## Stephan Erb <steve-e AT h3c.de>
11 ## This file is part of Gajim.
13 ## Gajim is free software; you can redistribute it and/or modify
14 ## it under the terms of the GNU General Public License as published
15 ## by the Free Software Foundation; version 3 only.
17 ## Gajim is distributed in the hope that it will be useful,
18 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ## GNU General Public License for more details.
22 ## You should have received a copy of the GNU General Public License
23 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
26 MOODS = {
27 'afraid': _('Afraid'),
28 'amazed': _('Amazed'),
29 'amorous': _('Amorous'),
30 'angry': _('Angry'),
31 'annoyed': _('Annoyed'),
32 'anxious': _('Anxious'),
33 'aroused': _('Aroused'),
34 'ashamed': _('Ashamed'),
35 'bored': _('Bored'),
36 'brave': _('Brave'),
37 'calm': _('Calm'),
38 'cautious': _('Cautious'),
39 'cold': _('Cold'),
40 'confident': _('Confident'),
41 'confused': _('Confused'),
42 'contemplative': _('Contemplative'),
43 'contented': _('Contented'),
44 'cranky': _('Cranky'),
45 'crazy': _('Crazy'),
46 'creative': _('Creative'),
47 'curious': _('Curious'),
48 'dejected': _('Dejected'),
49 'depressed': _('Depressed'),
50 'disappointed': _('Disappointed'),
51 'disgusted': _('Disgusted'),
52 'dismayed': _('Dismayed'),
53 'distracted': _('Distracted'),
54 'embarrassed': _('Embarrassed'),
55 'envious': _('Envious'),
56 'excited': _('Excited'),
57 'flirtatious': _('Flirtatious'),
58 'frustrated': _('Frustrated'),
59 'grateful': _('Grateful'),
60 'grieving': _('Grieving'),
61 'grumpy': _('Grumpy'),
62 'guilty': _('Guilty'),
63 'happy': _('Happy'),
64 'hopeful': _('Hopeful'),
65 'hot': _('Hot'),
66 'humbled': _('Humbled'),
67 'humiliated': _('Humiliated'),
68 'hungry': _('Hungry'),
69 'hurt': _('Hurt'),
70 'impressed': _('Impressed'),
71 'in_awe': _('In Awe'),
72 'in_love': _('In Love'),
73 'indignant': _('Indignant'),
74 'interested': _('Interested'),
75 'intoxicated': _('Intoxicated'),
76 'invincible': _('Invincible'),
77 'jealous': _('Jealous'),
78 'lonely': _('Lonely'),
79 'lost': _('Lost'),
80 'lucky': _('Lucky'),
81 'mean': _('Mean'),
82 'moody': _('Moody'),
83 'nervous': _('Nervous'),
84 'neutral': _('Neutral'),
85 'offended': _('Offended'),
86 'outraged': _('Outraged'),
87 'playful': _('Playful'),
88 'proud': _('Proud'),
89 'relaxed': _('Relaxed'),
90 'relieved': _('Relieved'),
91 'remorseful': _('Remorseful'),
92 'restless': _('Restless'),
93 'sad': _('Sad'),
94 'sarcastic': _('Sarcastic'),
95 'satisfied': _('Satisfied'),
96 'serious': _('Serious'),
97 'shocked': _('Shocked'),
98 'shy': _('Shy'),
99 'sick': _('Sick'),
100 'sleepy': _('Sleepy'),
101 'spontaneous': _('Spontaneous'),
102 'stressed': _('Stressed'),
103 'strong': _('Strong'),
104 'surprised': _('Surprised'),
105 'thankful': _('Thankful'),
106 'thirsty': _('Thirsty'),
107 'tired': _('Tired'),
108 'undefined': _('Undefined'),
109 'weak': _('Weak'),
110 'worried': _('Worried')}
112 ACTIVITIES = {
113 'doing_chores': {'category': _('Doing Chores'),
114 'buying_groceries': _('Buying Groceries'),
115 'cleaning': _('Cleaning'),
116 'cooking': _('Cooking'),
117 'doing_maintenance': _('Doing Maintenance'),
118 'doing_the_dishes': _('Doing the Dishes'),
119 'doing_the_laundry': _('Doing the Laundry'),
120 'gardening': _('Gardening'),
121 'running_an_errand': _('Running an Errand'),
122 'walking_the_dog': _('Walking the Dog')},
123 'drinking': {'category': _('Drinking'),
124 'having_a_beer': _('Having a Beer'),
125 'having_coffee': _('Having Coffee'),
126 'having_tea': _('Having Tea')},
127 'eating': {'category': _('Eating'),
128 'having_a_snack': _('Having a Snack'),
129 'having_breakfast': _('Having Breakfast'),
130 'having_dinner': _('Having Dinner'),
131 'having_lunch': _('Having Lunch')},
132 'exercising': {'category': _('Exercising'),
133 'cycling': _('Cycling'),
134 'dancing': _('Dancing'),
135 'hiking': _('Hiking'),
136 'jogging': _('Jogging'),
137 'playing_sports': _('Playing Sports'),
138 'running': _('Running'),
139 'skiing': _('Skiing'),
140 'swimming': _('Swimming'),
141 'working_out': _('Working out')},
142 'grooming': {'category': _('Grooming'),
143 'at_the_spa': _('At the Spa'),
144 'brushing_teeth': _('Brushing Teeth'),
145 'getting_a_haircut': _('Getting a Haircut'),
146 'shaving': _('Shaving'),
147 'taking_a_bath': _('Taking a Bath'),
148 'taking_a_shower': _('Taking a Shower')},
149 'having_appointment': {'category': _('Having an Appointment')},
150 'inactive': {'category': _('Inactive'),
151 'day_off': _('Day Off'),
152 'hanging_out': _('Hanging out'),
153 'hiding': _('Hiding'),
154 'on_vacation': _('On Vacation'),
155 'praying': _('Praying'),
156 'scheduled_holiday': _('Scheduled Holiday'),
157 'sleeping': _('Sleeping'),
158 'thinking': _('Thinking')},
159 'relaxing': {'category': _('Relaxing'),
160 'fishing': _('Fishing'),
161 'gaming': _('Gaming'),
162 'going_out': _('Going out'),
163 'partying': _('Partying'),
164 'reading': _('Reading'),
165 'rehearsing': _('Rehearsing'),
166 'shopping': _('Shopping'),
167 'smoking': _('Smoking'),
168 'socializing': _('Socializing'),
169 'sunbathing': _('Sunbathing'),
170 'watching_tv': _('Watching TV'),
171 'watching_a_movie': _('Watching a Movie')},
172 'talking': {'category': _('Talking'),
173 'in_real_life': _('In Real Life'),
174 'on_the_phone': _('On the Phone'),
175 'on_video_phone': _('On Video Phone')},
176 'traveling': {'category': _('Traveling'),
177 'commuting': _('Commuting'),
178 'cycling': _('Cycling'),
179 'driving': _('Driving'),
180 'in_a_car': _('In a Car'),
181 'on_a_bus': _('On a Bus'),
182 'on_a_plane': _('On a Plane'),
183 'on_a_train': _('On a Train'),
184 'on_a_trip': _('On a Trip'),
185 'walking': _('Walking')},
186 'working': {'category': _('Working'),
187 'coding': _('Coding'),
188 'in_a_meeting': _('In a Meeting'),
189 'studying': _('Studying'),
190 'writing': _('Writing')}}
192 TUNE_DATA = ['artist', 'title', 'source', 'track', 'length']
194 LOCATION_DATA = ['accuracy', 'alt', 'area', 'bearing', 'building', 'country',
195 'countrycode', 'datum', 'description', 'error', 'floor', 'lat',
196 'locality', 'lon', 'postalcode', 'region', 'room', 'speed', 'street',
197 'text', 'timestamp', 'uri']
199 import gobject
200 import gtk
202 import logging
203 log = logging.getLogger('gajim.c.pep')
205 from common import helpers
206 from common import xmpp
207 from common import gajim
209 import gtkgui_helpers
212 class AbstractPEP(object):
214 type = ''
215 namespace = ''
217 @classmethod
218 def get_tag_as_PEP(cls, jid, account, event_tag):
219 items = event_tag.getTag('items', {'node': cls.namespace})
220 if items:
221 log.debug("Received PEP 'user %s' from %s" % (cls.type, jid))
222 return cls(jid, account, items)
223 else:
224 return None
226 def __init__(self, jid, account, items):
227 self._pep_specific_data, self._retracted = self._extract_info(items)
229 self._update_contacts(jid, account)
230 if jid == gajim.get_jid_from_account(account):
231 self._update_account(account)
233 def _extract_info(self, items):
234 '''To be implemented by subclasses'''
235 raise NotImplementedError
237 def _update_contacts(self, jid, account):
238 for contact in gajim.contacts.get_contacts(account, jid):
239 if self._retracted:
240 if self.type in contact.pep:
241 del contact.pep[self.type]
242 else:
243 contact.pep[self.type] = self
245 def _update_account(self, account):
246 acc = gajim.connections[account]
247 if self._retracted:
248 if self.type in acc.pep:
249 del acc.pep[self.type]
250 else:
251 acc.pep[self.type] = self
253 def asPixbufIcon(self):
254 '''SHOULD be implemented by subclasses'''
255 return None
257 def asMarkupText(self):
258 '''SHOULD be implemented by subclasses'''
259 return ''
262 class UserMoodPEP(AbstractPEP):
263 '''XEP-0107: User Mood'''
265 type = 'mood'
266 namespace = xmpp.NS_MOOD
268 def _extract_info(self, items):
269 mood_dict = {}
271 for item in items.getTags('item'):
272 mood_tag = item.getTag('mood')
273 if mood_tag:
274 for child in mood_tag.getChildren():
275 name = child.getName().strip()
276 if name == 'text':
277 mood_dict['text'] = child.getData()
278 else:
279 mood_dict['mood'] = name
281 retracted = items.getTag('retract') or not 'mood' in mood_dict
282 return (mood_dict, retracted)
284 def asPixbufIcon(self):
285 assert not self._retracted
286 received_mood = self._pep_specific_data['mood']
287 mood = received_mood if received_mood in MOODS else 'unknown'
288 pixbuf = gtkgui_helpers.load_mood_icon(mood).get_pixbuf()
289 return pixbuf
291 def asMarkupText(self):
292 assert not self._retracted
293 untranslated_mood = self._pep_specific_data['mood']
294 mood = self._translate_mood(untranslated_mood)
295 markuptext = '<b>%s</b>' % gobject.markup_escape_text(mood)
296 if 'text' in self._pep_specific_data:
297 text = self._pep_specific_data['text']
298 markuptext += ' (%s)' % gobject.markup_escape_text(text)
299 return markuptext
301 def _translate_mood(self, mood):
302 if mood in MOODS:
303 return MOODS[mood]
304 else:
305 return mood
308 class UserTunePEP(AbstractPEP):
309 '''XEP-0118: User Tune'''
311 type = 'tune'
312 namespace = xmpp.NS_TUNE
314 def _extract_info(self, items):
315 tune_dict = {}
317 for item in items.getTags('item'):
318 tune_tag = item.getTag('tune')
319 if tune_tag:
320 for child in tune_tag.getChildren():
321 name = child.getName().strip()
322 data = child.getData().strip()
323 if child.getName() in TUNE_DATA:
324 tune_dict[name] = data
326 retracted = items.getTag('retract') or not ('artist' in tune_dict or
327 'title' in tune_dict)
328 return (tune_dict, retracted)
330 def asPixbufIcon(self):
331 import os
332 path = os.path.join(gajim.DATA_DIR, 'emoticons', 'static', 'music.png')
333 return gtk.gdk.pixbuf_new_from_file(path)
335 def asMarkupText(self):
336 assert not self._retracted
337 tune = self._pep_specific_data
339 artist = tune.get('artist', _('Unknown Artist'))
340 artist = gobject.markup_escape_text(artist)
342 title = tune.get('title', _('Unknown Title'))
343 title = gobject.markup_escape_text(title)
345 source = tune.get('source', _('Unknown Source'))
346 source = gobject.markup_escape_text(source)
348 tune_string = _('<b>"%(title)s"</b> by <i>%(artist)s</i>\n'
349 'from <i>%(source)s</i>') % {'title': title,
350 'artist': artist, 'source': source}
351 return tune_string
354 class UserActivityPEP(AbstractPEP):
355 '''XEP-0108: User Activity'''
357 type = 'activity'
358 namespace = xmpp.NS_ACTIVITY
360 def _extract_info(self, items):
361 activity_dict = {}
363 for item in items.getTags('item'):
364 activity_tag = item.getTag('activity')
365 if activity_tag:
366 for child in activity_tag.getChildren():
367 name = child.getName().strip()
368 data = child.getData().strip()
369 if name == 'text':
370 activity_dict['text'] = data
371 else:
372 activity_dict['activity'] = name
373 for subactivity in child.getChildren():
374 subactivity_name = subactivity.getName().strip()
375 activity_dict['subactivity'] = subactivity_name
377 retracted = items.getTag('retract') or not 'activity' in activity_dict
378 return (activity_dict, retracted)
380 def asPixbufIcon(self):
381 assert not self._retracted
382 pep = self._pep_specific_data
383 activity = pep['activity']
385 has_known_activity = activity in ACTIVITIES
386 has_known_subactivity = (has_known_activity and ('subactivity' in pep)
387 and (pep['subactivity'] in ACTIVITIES[activity]))
389 if has_known_activity:
390 if has_known_subactivity:
391 subactivity = pep['subactivity']
392 return gtkgui_helpers.load_activity_icon(activity, subactivity).get_pixbuf()
393 else:
394 return gtkgui_helpers.load_activity_icon(activity).get_pixbuf()
395 else:
396 return gtkgui_helpers.load_activity_icon('unknown').get_pixbuf()
398 def asMarkupText(self):
399 assert not self._retracted
400 pep = self._pep_specific_data
401 activity = pep['activity']
402 subactivity = pep['subactivity'] if 'subactivity' in pep else None
403 text = pep['text'] if 'text' in pep else None
405 if activity in ACTIVITIES:
406 # Translate standard activities
407 if subactivity in ACTIVITIES[activity]:
408 subactivity = ACTIVITIES[activity][subactivity]
409 activity = ACTIVITIES[activity]['category']
411 markuptext = '<b>' + gobject.markup_escape_text(activity)
412 if subactivity:
413 markuptext += ': ' + gobject.markup_escape_text(subactivity)
414 markuptext += '</b>'
415 if text:
416 markuptext += ' (%s)' % gobject.markup_escape_text(text)
417 return markuptext
420 class UserNicknamePEP(AbstractPEP):
421 '''XEP-0172: User Nickname'''
423 type = 'nickname'
424 namespace = xmpp.NS_NICK
426 def _extract_info(self, items):
427 nick = ''
428 for item in items.getTags('item'):
429 child = item.getTag('nick')
430 if child:
431 nick = child.getData()
432 break
434 retracted = items.getTag('retract') or not nick
435 return (nick, retracted)
437 def _update_contacts(self, jid, account):
438 nick = '' if self._retracted else self._pep_specific_data
439 for contact in gajim.contacts.get_contacts(account, jid):
440 contact.contact_name = nick
442 def _update_account(self, account):
443 if self._retracted:
444 gajim.nicks[account] = gajim.config.get_per('accounts', account, 'name')
445 else:
446 gajim.nicks[account] = self._pep_specific_data
449 class UserLocationPEP(AbstractPEP):
450 '''XEP-0080: User Location'''
452 type = 'location'
453 namespace = xmpp.NS_LOCATION
455 def _extract_info(self, items):
456 location_dict = {}
458 for item in items.getTags('item'):
459 location_tag = item.getTag('geoloc')
460 if location_tag:
461 for child in location_tag.getChildren():
462 name = child.getName().strip()
463 data = child.getData().strip()
464 if child.getName() in LOCATION_DATA:
465 location_dict[name] = data
467 retracted = items.getTag('retract') or not location_dict
468 return (location_dict, retracted)
470 def _update_account(self, account):
471 AbstractPEP._update_account(self, account)
472 con = gajim.connections[account].location_info = \
473 self._pep_specific_data
475 def asPixbufIcon(self):
476 path = gtkgui_helpers.get_icon_path('gajim-earth')
477 return gtk.gdk.pixbuf_new_from_file(path)
479 def asMarkupText(self):
480 assert not self._retracted
481 location = self._pep_specific_data
482 location_string = ''
484 for entry in location.keys():
485 text = location[entry]
486 text = gobject.markup_escape_text(text)
487 location_string += '\n<b>%(tag)s</b>: %(text)s' % \
488 {'tag': entry.capitalize(), 'text': text}
490 return location_string.strip()
493 SUPPORTED_PERSONAL_USER_EVENTS = [UserMoodPEP, UserTunePEP, UserActivityPEP,
494 UserNicknamePEP, UserLocationPEP]
496 from common.connection_handlers_events import PEPReceivedEvent
498 class ConnectionPEP(object):
500 def __init__(self, account, dispatcher, pubsub_connection):
501 self._account = account
502 self._dispatcher = dispatcher
503 self._pubsub_connection = pubsub_connection
504 self.reset_awaiting_pep()
506 def pep_change_account_name(self, new_name):
507 self._account = new_name
509 def reset_awaiting_pep(self):
510 self.to_be_sent_activity = None
511 self.to_be_sent_mood = None
512 self.to_be_sent_tune = None
513 self.to_be_sent_nick = None
514 self.to_be_sent_location = None
516 def send_awaiting_pep(self):
518 Send pep info that were waiting for connection
520 if self.to_be_sent_activity:
521 self.send_activity(*self.to_be_sent_activity)
522 if self.to_be_sent_mood:
523 self.send_mood(*self.to_be_sent_mood)
524 if self.to_be_sent_tune:
525 self.send_tune(*self.to_be_sent_tune)
526 if self.to_be_sent_nick:
527 self.send_nick(self.to_be_sent_nick)
528 if self.to_be_sent_location:
529 self.send_location(self.to_be_sent_location)
530 self.reset_awaiting_pep()
532 def _pubsubEventCB(self, xmpp_dispatcher, msg):
533 ''' Called when we receive <message /> with pubsub event. '''
534 gajim.nec.push_incoming_event(PEPReceivedEvent(None, conn=self,
535 stanza=msg))
537 def send_activity(self, activity, subactivity=None, message=None):
538 if self.connected == 1:
539 # We are connecting, keep activity in mem and send it when we'll be
540 # connected
541 self.to_be_sent_activity = (activity, subactivity, message)
542 return
543 if not self.pep_supported:
544 return
545 item = xmpp.Node('activity', {'xmlns': xmpp.NS_ACTIVITY})
546 if activity:
547 i = item.addChild(activity)
548 if subactivity:
549 i.addChild(subactivity)
550 if message:
551 i = item.addChild('text')
552 i.addData(message)
553 self._pubsub_connection.send_pb_publish('', xmpp.NS_ACTIVITY, item, '0')
555 def retract_activity(self):
556 if not self.pep_supported:
557 return
558 self.send_activity(None)
559 # not all client support new XEP, so we still retract
560 self._pubsub_connection.send_pb_retract('', xmpp.NS_ACTIVITY, '0')
562 def send_mood(self, mood, message=None):
563 if self.connected == 1:
564 # We are connecting, keep mood in mem and send it when we'll be
565 # connected
566 self.to_be_sent_mood = (mood, message)
567 return
568 if not self.pep_supported:
569 return
570 item = xmpp.Node('mood', {'xmlns': xmpp.NS_MOOD})
571 if mood:
572 item.addChild(mood)
573 if message:
574 i = item.addChild('text')
575 i.addData(message)
576 self._pubsub_connection.send_pb_publish('', xmpp.NS_MOOD, item, '0')
578 def retract_mood(self):
579 if not self.pep_supported:
580 return
581 self.send_mood(None)
582 # not all client support new XEP, so we still retract
583 self._pubsub_connection.send_pb_retract('', xmpp.NS_MOOD, '0')
585 def send_tune(self, artist='', title='', source='', track=0, length=0,
586 items=None):
587 if self.connected == 1:
588 # We are connecting, keep tune in mem and send it when we'll be
589 # connected
590 self.to_be_sent_tune = (artist, title, source, track, length, items)
591 return
592 if not self.pep_supported:
593 return
594 item = xmpp.Node('tune', {'xmlns': xmpp.NS_TUNE})
595 if artist:
596 i = item.addChild('artist')
597 i.addData(artist)
598 if title:
599 i = item.addChild('title')
600 i.addData(title)
601 if source:
602 i = item.addChild('source')
603 i.addData(source)
604 if track:
605 i = item.addChild('track')
606 i.addData(track)
607 if length:
608 i = item.addChild('length')
609 i.addData(length)
610 if items:
611 item.addChild(payload=items)
612 self._pubsub_connection.send_pb_publish('', xmpp.NS_TUNE, item, '0')
614 def retract_tune(self):
615 if not self.pep_supported:
616 return
617 self.send_tune(None)
618 # not all client support new XEP, so we still retract
619 self._pubsub_connection.send_pb_retract('', xmpp.NS_TUNE, '0')
621 def send_nickname(self, nick):
622 if self.connected == 1:
623 # We are connecting, keep nick in mem and send it when we'll be
624 # connected
625 self.to_be_sent_nick = nick
626 return
627 if not self.pep_supported:
628 return
629 item = xmpp.Node('nick', {'xmlns': xmpp.NS_NICK})
630 item.addData(nick)
631 self._pubsub_connection.send_pb_publish('', xmpp.NS_NICK, item, '0')
633 def retract_nickname(self):
634 if not self.pep_supported:
635 return
636 self.send_nickname(None)
637 # not all client support new XEP, so we still retract
638 self._pubsub_connection.send_pb_retract('', xmpp.NS_NICK, '0')
640 def send_location(self, info):
641 if self.connected == 1:
642 # We are connecting, keep location in mem and send it when we'll be
643 # connected
644 self.to_be_sent_location = info
645 return
646 if not self.pep_supported:
647 return
648 item = xmpp.Node('geoloc', {'xmlns': xmpp.NS_LOCATION})
649 for field in LOCATION_DATA:
650 if info.get(field, None):
651 i = item.addChild(field)
652 i.addData(info[field])
653 self._pubsub_connection.send_pb_publish('', xmpp.NS_LOCATION, item, '0')
655 def retract_location(self):
656 if not self.pep_supported:
657 return
658 self.send_location({})
659 # not all client support new XEP, so we still retract
660 self._pubsub_connection.send_pb_retract('', xmpp.NS_LOCATION, '0')