Package papyon :: Module conversation

Source Code for Module papyon.conversation

  1  # -*- coding: utf-8 -*- 
  2  # 
  3  # papyon - a python client library for Msn 
  4  # 
  5  # Copyright (C) 2005-2007 Ali Sabil <ali.sabil@gmail.com> 
  6  # Copyright (C) 2007 Johann Prieur <johann.prieur@gmail.com> 
  7  # 
  8  # This program is free software; you can redistribute it and/or modify 
  9  # it under the terms of the GNU General Public License as published by 
 10  # the Free Software Foundation; either version 2 of the License, or 
 11  # (at your option) any later version. 
 12  # 
 13  # This program is distributed in the hope that it will be useful, 
 14  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 15  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 16  # GNU General Public License for more details. 
 17  # 
 18  # You should have received a copy of the GNU General Public License 
 19  # along with this program; if not, write to the Free Software 
 20  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 21   
 22  """Conversation 
 23   
 24  This module contains the classes needed to have a conversation with a 
 25  contact.""" 
 26   
 27  import msnp 
 28  import p2p 
 29  from switchboard_manager import SwitchboardClient 
 30  from papyon.event import EventsDispatcher 
 31  from papyon.profile import NetworkID 
 32   
 33  import logging 
 34  import gobject 
 35  from urllib import quote, unquote 
 36   
 37  __all__ = ['Conversation', 'ConversationInterface', 'ConversationMessage', 'TextFormat'] 
 38   
 39  logger = logging.getLogger('conversation') 
40 41 42 -def Conversation(client, contacts):
43 """Factory function used to create the appropriate conversation with the 44 given contacts. 45 46 This is the method you need to use to start a conversation with both MSN 47 users and Yahoo! users. 48 @attention: you can only talk to one Yahoo! contact at a time, and you 49 cannot have multi-user conversations with both MSN and Yahoo! contacts. 50 51 @param contacts: The list of contacts to invite into the conversation 52 @type contacts: [L{Contact<papyon.profile.Contact>}, ...] 53 54 @returns: a Conversation object implementing L{ConversationInterface<papyon.conversation.ConversationInterface>} 55 @rtype: L{ConversationInterface<papyon.conversation.ConversationInterface>} 56 """ 57 msn_contacts = set([contact for contact in contacts \ 58 if contact.network_id == NetworkID.MSN]) 59 external_contacts = set(contacts) - msn_contacts 60 61 if len(external_contacts) == 0: 62 return SwitchboardConversation(client, contacts) 63 elif len(msn_contacts) != 0: 64 raise NotImplementedError("The protocol doesn't allow mixing " \ 65 "contacts from different networks in a single conversation") 66 elif len(external_contacts) > 1: 67 raise NotImplementedError("The protocol doesn't allow having " \ 68 "more than one external contact in a conversation") 69 elif len(external_contacts) == 1: 70 return ExternalNetworkConversation(client, contacts)
71
72 73 -class ConversationInterface(object):
74 """Interface implemented by all the Conversation objects, a Conversation 75 object allows the user to communicate with one or more peers""" 76
77 - def send_text_message(self, message):
78 """Send a message to all persons in this conversation. 79 80 @param message: the message to send to the users on this conversation 81 @type message: L{Contact<papyon.profile.Contact>}""" 82 raise NotImplementedError
83
84 - def send_nudge(self):
85 """Sends a nudge to the contacts on this conversation.""" 86 raise NotImplementedError
87
88 - def send_typing_notification(self):
89 """Sends an user typing notification to the contacts on this 90 conversation.""" 91 raise NotImplementedError
92
93 - def invite_user(self, contact):
94 """Request a contact to join in the conversation. 95 96 @param contact: the contact to invite. 97 @type contact: L{Contact<papyon.profile.Contact>}""" 98 raise NotImplementedError
99
100 - def leave(self):
101 """Leave the conversation.""" 102 raise NotImplementedError
103
104 105 -class ConversationMessage(object):
106 """A Conversation message sent or received 107 108 @ivar display_name: the display name to show for the sender of this message 109 @type display_name: utf-8 encoded string 110 111 @ivar content: the content of the message 112 @type content: utf-8 encoded string 113 114 @ivar formatting: the formatting for this message 115 @type formatting: L{TextFormat<papyon.conversation.TextFormat>} 116 117 @ivar msn_objects: a dictionary mapping smileys 118 to an L{MSNObject<papyon.p2p.MSNObject>} 119 @type msn_objects: {smiley: string => L{MSNObject<papyon.p2p.MSNObject>}} 120 """
121 - def __init__(self, content, formatting=None, msn_objects={}):
122 """Initializer 123 124 @param content: the content of the message 125 @type content: utf-8 encoded string 126 127 @param formatting: the formatting for this message 128 @type formatting: L{TextFormat<papyon.conversation.TextFormat>} 129 130 @param msn_objects: a dictionary mapping smileys 131 to an L{MSNObject<papyon.p2p.MSNObject>} 132 @type msn_objects: {smiley: string => L{MSNObject<papyon.p2p.MSNObject>}}""" 133 self.display_name = None 134 self.content = content 135 self.formatting = formatting 136 self.msn_objects = msn_objects
137
138 -class TextFormat(object):
139 140 DEFAULT_FONT = 'MS Sans Serif' 141 142 # effects 143 NO_EFFECT = 0 144 BOLD = 1 145 ITALIC = 2 146 UNDERLINE = 4 147 STRIKETHROUGH = 8 148 149 # charset 150 ANSI_CHARSET = '0' 151 DEFAULT_CHARSET = '1' 152 SYMBOL_CHARSET = '2' 153 MAC_CHARSETLT = '4d' 154 SHIFTJIS_CHARSET = '80' 155 HANGEUL_CHARSET = '81' 156 JOHAB_CHARSET = '82' 157 GB2312_CHARSET = '86' 158 CHINESEBIG5_CHARSET = '88' 159 GREEK_CHARSET = 'a1' 160 TURKISH_CHARSET = 'a2' 161 VIETNAMESE_CHARSET = 'a3' 162 HEBREW_CHARSET = 'b1' 163 ARABIC_CHARSET = 'b2' 164 BALTIC_CHARSET = 'ba' 165 RUSSIAN_CHARSET_DEFAULT = 'cc' 166 THAI_CHARSET = 'de' 167 EASTEUROPE_CHARSET = 'ee' 168 OEM_DEFAULT = 'ff' 169 170 # family 171 FF_DONTCARE = 0 172 FF_ROMAN = 1 173 FF_SWISS = 2 174 FF_MODERN = 3 175 FF_SCRIPT = 4 176 FF_DECORATIVE = 5 177 178 # pitch 179 DEFAULT_PITCH = 0 180 FIXED_PITCH = 1 181 VARIABLE_PITCH = 2 182 183 @staticmethod
184 - def parse(format):
185 text_format = TextFormat() 186 text_format.__parse(format) 187 return text_format
188 189 @property
190 - def font(self):
191 return self._font
192 193 @property
194 - def style(self):
195 return self._style
196 197 @property
198 - def color(self):
199 return self._color
200 201 @property
202 - def right_alignment(self):
203 return self._right_alignment
204 205 @property
206 - def charset(self):
207 return self._charset
208 209 @property
210 - def pitch(self):
211 return self._pitch
212 213 @property
214 - def family(self):
215 return self._family
216
217 - def __init__(self, font=DEFAULT_FONT, style=NO_EFFECT, color='0', 218 charset=DEFAULT_CHARSET, family=FF_DONTCARE, 219 pitch=DEFAULT_PITCH, right_alignment=False):
220 self._font = font 221 self._style = style 222 self._color = color 223 self._charset = charset 224 self._pitch = pitch 225 self._family = family 226 self._right_alignment = right_alignment
227
228 - def __parse(self, format):
229 for property in format.split(';'): 230 key, value = [p.strip(' \t|').upper() \ 231 for p in property.split('=', 1)] 232 if key == 'FN': 233 # Font 234 self._font = unquote(value) 235 elif key == 'EF': 236 # Effects 237 if 'B' in value: self._style |= TextFormat.BOLD 238 if 'I' in value: self._style |= TextFormat.ITALIC 239 if 'U' in value: self._style |= TextFormat.UNDERLINE 240 if 'S' in value: self._style |= TextFormat.STRIKETHROUGH 241 elif key == 'CO': 242 # Color 243 value = value.zfill(6) 244 self._color = ''.join((value[4:6], value[2:4], value[0:2])) 245 elif key == 'CS': 246 # Charset 247 self._charset = value 248 elif key == 'PF': 249 # Family and pitch 250 value = value.zfill(2) 251 self._family = int(value[0]) 252 self._pitch = int(value[1]) 253 elif key == 'RL': 254 # Right alignment 255 if value == '1': self._right_alignement = True
256
257 - def __str__(self):
258 style = '' 259 if self._style & TextFormat.BOLD == TextFormat.BOLD: 260 style += 'B' 261 if self._style & TextFormat.ITALIC == TextFormat.ITALIC: 262 style += 'I' 263 if self._style & TextFormat.UNDERLINE == TextFormat.UNDERLINE: 264 style += 'U' 265 if self._style & TextFormat.STRIKETHROUGH == TextFormat.STRIKETHROUGH: 266 style += 'S' 267 268 color = '%s%s%s' % (self._color[4:6], self._color[2:4], self._color[0:2]) 269 270 format = 'FN=%s; EF=%s; CO=%s; CS=%s; PF=%d%d' % (quote(self._font), 271 style, color, 272 self._charset, 273 self._family, 274 self._pitch) 275 if self._right_alignment: format += '; RL=1' 276 277 return format
278
279 - def __repr__(self):
280 return __str__(self)
281
282 283 -class AbstractConversation(ConversationInterface, EventsDispatcher):
284 - def __init__(self, client):
285 self._client = client 286 ConversationInterface.__init__(self) 287 EventsDispatcher.__init__(self) 288 289 self.__last_received_msn_objects = {}
290
291 - def send_text_message(self, message):
292 if len(message.msn_objects) > 0: 293 body = [] 294 for alias, msn_object in message.msn_objects.iteritems(): 295 self._client._msn_object_store.publish(msn_object) 296 body.append(alias.encode("utf-8")) 297 body.append(str(msn_object)) 298 # FIXME : we need to distinguish animemoticon and emoticons 299 # and send the related msn objects in separated messages 300 self._send_message(("text/x-mms-animemoticon",), '\t'.join(body)) 301 302 content_type = ("text/plain","utf-8") 303 body = message.content.encode("utf-8") 304 ack = msnp.MessageAcknowledgement.HALF 305 headers = {} 306 if message.formatting is not None: 307 headers["X-MMS-IM-Format"] = str(message.formatting) 308 309 self._send_message(content_type, body, headers, ack)
310
311 - def send_nudge(self):
312 content_type = "text/x-msnmsgr-datacast" 313 body = "ID: 1\r\n\r\n".encode('UTF-8') #FIXME: we need to figure out the datacast objects :D 314 ack = msnp.MessageAcknowledgement.NONE 315 self._send_message(content_type, body, ack=ack)
316
317 - def send_typing_notification(self):
318 content_type = "text/x-msmsgscontrol" 319 body = "\r\n\r\n".encode('UTF-8') 320 headers = { "TypingUser" : self._client.profile.account.encode('UTF_8') } 321 ack = msnp.MessageAcknowledgement.NONE 322 self._send_message(content_type, body, headers, ack)
323
324 - def invite_user(self, contact):
325 raise NotImplementedError
326
327 - def leave(self):
328 raise NotImplementedError
329
330 - def _send_message(self, content_type, body, headers={}, 331 ack=msnp.MessageAcknowledgement.HALF):
332 raise NotImplementedError
333
334 - def _on_contact_joined(self, contact):
335 self._dispatch("on_conversation_user_joined", contact)
336
337 - def _on_contact_left(self, contact):
338 self._dispatch("on_conversation_user_left", contact)
339
340 - def _on_message_received(self, message):
341 sender = message.sender 342 message_type = message.content_type[0] 343 message_encoding = message.content_type[1] 344 try: 345 message_formatting = message.get_header('X-MMS-IM-Format') 346 if not message_formatting: 347 message_formatting = '=' 348 except KeyError: 349 message_formatting = '=' 350 351 if message_type == 'text/plain': 352 msg = ConversationMessage(unicode(message.body, message_encoding), 353 TextFormat.parse(message_formatting), 354 self.__last_received_msn_objects) 355 try: 356 display_name = message.get_header('P4-Context') 357 except KeyError: 358 display_name = sender.display_name 359 msg.display_name = display_name 360 self._dispatch("on_conversation_message_received", sender, msg) 361 self.__last_received_msn_objects = {} 362 elif message_type == 'text/x-msmsgscontrol': 363 self._dispatch("on_conversation_user_typing", sender) 364 elif message_type in ['text/x-mms-emoticon', 365 'text/x-mms-animemoticon']: 366 msn_objects = {} 367 parts = message.body.split('\t') 368 logger.debug(parts) 369 for i in [i for i in range(len(parts)) if not i % 2]: 370 if parts[i] == '': break 371 msn_objects[parts[i]] = p2p.MSNObject.parse(self._client, 372 parts[i+1]) 373 self.__last_received_msn_objects = msn_objects 374 elif message_type == 'text/x-msnmsgr-datacast' and \ 375 message.body.strip() == "ID: 1": 376 self._dispatch("on_conversation_nudge_received", sender)
377
378 - def _on_message_sent(self, message):
379 pass
380
381 - def _on_error(self, error_type, error):
382 self._dispatch("on_conversation_error", error_type, error)
383
384 385 -class ExternalNetworkConversation(AbstractConversation):
386 - def __init__(self, client, contacts):
387 AbstractConversation.__init__(self, client) 388 self.participants = set(contacts) 389 client._register_external_conversation(self) 390 gobject.idle_add(self._open)
391
392 - def _open(self):
393 for contact in self.participants: 394 self._on_contact_joined(contact) 395 return False
396
397 - def invite_user(self, contact):
398 raise NotImplementedError("The protocol doesn't allow multiuser " \ 399 "conversations for external contacts")
400
401 - def leave(self):
402 self._client._unregister_external_conversation(self)
403
404 - def _send_message(self, content_type, body, headers={}, 405 ack=msnp.MessageAcknowledgement.HALF):
406 if content_type[0] in ['text/x-mms-emoticon', 407 'text/x-mms-animemoticon']: 408 return 409 message = msnp.Message(self._client.profile) 410 for key, value in headers.iteritems(): 411 message.add_header(key, value) 412 message.content_type = content_type 413 message.body = body 414 for contact in self.participants: 415 self._client._protocol.\ 416 send_unmanaged_message(contact, message)
417
418 419 -class SwitchboardConversation(AbstractConversation, SwitchboardClient):
420 - def __init__(self, client, contacts):
421 SwitchboardClient.__init__(self, client, contacts, priority=0) 422 AbstractConversation.__init__(self, client)
423 424 @staticmethod
425 - def _can_handle_message(message, switchboard_client=None):
426 content_type = message.content_type[0] 427 if switchboard_client is None: 428 return content_type in ('text/plain', 'text/x-msnmsgr-datacast') 429 # FIXME : we need to not filter those 'text/x-mms-emoticon', 'text/x-mms-animemoticon' 430 return content_type in ('text/plain', 'text/x-msmsgscontrol', 431 'text/x-msnmsgr-datacast', 'text/x-mms-emoticon', 432 'text/x-mms-animemoticon')
433
434 - def invite_user(self, contact):
435 """Request a contact to join in the conversation. 436 437 @param contact: the contact to invite. 438 @type contact: L{profile.Contact}""" 439 SwitchboardClient._invite_user(self, contact)
440
441 - def leave(self):
442 """Leave the conversation.""" 443 SwitchboardClient._leave(self)
444
445 - def _send_message(self, content_type, body, headers={}, 446 ack=msnp.MessageAcknowledgement.HALF):
447 SwitchboardClient._send_message(self, content_type, body, headers, ack)
448