1
# -*- coding: utf-8 -*-
3
# pymsn - a python client library for Msn
5
# Copyright (C) 2005-2007 Ali Sabil <ali.sabil@gmail.com>
6
# Copyright (C) 2007 Johann Prieur <johann.prieur@gmail.com>
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.
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.
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
24
This module contains the classes needed to have a conversation with a
29
from switchboard_manager import SwitchboardClient
30
from pymsn.event import EventsDispatcher
31
from pymsn.profile import NetworkID
35
from urllib import quote, unquote
37
__all__ = ['Conversation', 'ConversationInterface', 'ConversationMessage', 'TextFormat']
39
logger = logging.getLogger('conversation')
42
def Conversation(client, contacts):
43
"""Factory function used to create the appropriate conversation with the
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.
51
@param contacts: The list of contacts to invite into the conversation
52
@type contacts: [L{Contact<pymsn.profile.Contact>}, ...]
54
@returns: a Conversation object implementing L{ConversationInterface<pymsn.conversation.ConversationInterface>}
55
@rtype: L{ConversationInterface<pymsn.conversation.ConversationInterface>}
57
msn_contacts = set([contact for contact in contacts \
58
if contact.network_id == NetworkID.MSN])
59
external_contacts = set(contacts) - msn_contacts
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)
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"""
77
def send_text_message(self, message):
78
"""Send a message to all persons in this conversation.
80
@param message: the message to send to the users on this conversation
81
@type message: L{Contact<pymsn.profile.Contact>}"""
82
raise NotImplementedError
85
"""Sends a nudge to the contacts on this conversation."""
86
raise NotImplementedError
88
def send_typing_notification(self):
89
"""Sends an user typing notification to the contacts on this
91
raise NotImplementedError
93
def invite_user(self, contact):
94
"""Request a contact to join in the conversation.
96
@param contact: the contact to invite.
97
@type contact: L{Contact<pymsn.profile.Contact>}"""
98
raise NotImplementedError
101
"""Leave the conversation."""
102
raise NotImplementedError
105
class ConversationMessage(object):
106
"""A Conversation message sent or received
108
@ivar display_name: the display name to show for the sender of this message
109
@type display_name: utf-8 encoded string
111
@ivar content: the content of the message
112
@type content: utf-8 encoded string
114
@ivar formatting: the formatting for this message
115
@type formatting: L{TextFormat<pymsn.conversation.TextFormat>}
117
@ivar msn_objects: a dictionary mapping smileys
118
to an L{MSNObject<pymsn.p2p.MSNObject>}
119
@type msn_objects: {smiley: string => L{MSNObject<pymsn.p2p.MSNObject>}}
121
def __init__(self, content, formatting=None, msn_objects={}):
124
@param content: the content of the message
125
@type content: utf-8 encoded string
127
@param formatting: the formatting for this message
128
@type formatting: L{TextFormat<pymsn.conversation.TextFormat>}
130
@param msn_objects: a dictionary mapping smileys
131
to an L{MSNObject<pymsn.p2p.MSNObject>}
132
@type msn_objects: {smiley: string => L{MSNObject<pymsn.p2p.MSNObject>}}"""
133
self.display_name = None
134
self.content = content
135
self.formatting = formatting
136
self.msn_objects = msn_objects
138
class TextFormat(object):
140
DEFAULT_FONT = 'MS Sans Serif'
151
DEFAULT_CHARSET = '1'
154
SHIFTJIS_CHARSET = '80'
155
HANGEUL_CHARSET = '81'
157
GB2312_CHARSET = '86'
158
CHINESEBIG5_CHARSET = '88'
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'
167
EASTEUROPE_CHARSET = 'ee'
185
text_format = TextFormat()
186
text_format.__parse(format)
202
def right_alignment(self):
203
return self._right_alignment
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):
223
self._charset = charset
225
self._family = family
226
self._right_alignment = right_alignment
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)]
234
self._font = unquote(value)
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
243
value = value.zfill(6)
244
self._color = ''.join((value[4:6], value[2:4], value[0:2]))
247
self._charset = value
250
value = value.zfill(2)
251
self._family = int(value[0])
252
self._pitch = int(value[1])
255
if value == '1': self._right_alignement = True
259
if self._style & TextFormat.BOLD == TextFormat.BOLD:
261
if self._style & TextFormat.ITALIC == TextFormat.ITALIC:
263
if self._style & TextFormat.UNDERLINE == TextFormat.UNDERLINE:
265
if self._style & TextFormat.STRIKETHROUGH == TextFormat.STRIKETHROUGH:
268
color = '%s%s%s' % (self._color[4:6], self._color[2:4], self._color[0:2])
270
format = 'FN=%s; EF=%s; CO=%s; CS=%s; PF=%d%d' % (quote(self._font),
275
if self._right_alignment: format += '; RL=1'
283
class AbstractConversation(ConversationInterface, EventsDispatcher):
284
def __init__(self, client):
285
self._client = client
286
ConversationInterface.__init__(self)
287
EventsDispatcher.__init__(self)
289
self.__last_received_msn_objects = {}
291
def send_text_message(self, message):
292
if len(message.msn_objects) > 0:
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))
302
content_type = ("text/plain","utf-8")
303
body = message.content.encode("utf-8")
304
ack = msnp.MessageAcknowledgement.HALF
306
if message.formatting is not None:
307
headers["X-MMS-IM-Format"] = str(message.formatting)
309
self._send_message(content_type, body, headers, ack)
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)
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)
324
def invite_user(self, contact):
325
raise NotImplementedError
328
raise NotImplementedError
330
def _send_message(self, content_type, body, headers={},
331
ack=msnp.MessageAcknowledgement.HALF):
332
raise NotImplementedError
334
def _on_contact_joined(self, contact):
335
self._dispatch("on_conversation_user_joined", contact)
337
def _on_contact_left(self, contact):
338
self._dispatch("on_conversation_user_left", contact)
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]
345
message_formatting = message.get_header('X-MMS-IM-Format')
347
message_formatting = '='
349
if message_type == 'text/plain':
350
msg = ConversationMessage(unicode(message.body, message_encoding),
351
TextFormat.parse(message_formatting),
352
self.__last_received_msn_objects)
354
display_name = message.get_header('P4-Context')
356
display_name = sender.display_name
357
msg.display_name = display_name
358
self._dispatch("on_conversation_message_received", sender, msg)
359
self.__last_received_msn_objects = {}
360
elif message_type == 'text/x-msmsgscontrol':
361
self._dispatch("on_conversation_user_typing", sender)
362
elif message_type in ['text/x-mms-emoticon',
363
'text/x-mms-animemoticon']:
365
parts = message.body.split('\t')
367
for i in [i for i in range(len(parts)) if not i % 2]:
368
if parts[i] == '': break
369
msn_objects[parts[i]] = p2p.MSNObject.parse(self._client,
371
self.__last_received_msn_objects = msn_objects
372
elif message_type == 'text/x-msnmsgr-datacast' and \
373
message.body.strip() == "ID: 1":
374
self._dispatch("on_conversation_nudge_received", sender)
376
def _on_message_sent(self, message):
379
def _on_error(self, error_type, error):
380
self._dispatch("on_conversation_error", error_type, error)
383
class ExternalNetworkConversation(AbstractConversation):
384
def __init__(self, client, contacts):
385
AbstractConversation.__init__(self, client)
386
self.participants = set(contacts)
387
client._register_external_conversation(self)
388
gobject.idle_add(self._open)
391
for contact in self.participants:
392
self._on_contact_joined(contact)
395
def invite_user(self, contact):
396
raise NotImplementedError("The protocol doesn't allow multiuser " \
397
"conversations for external contacts")
400
self._client._unregister_external_conversation(self)
402
def _send_message(self, content_type, body, headers={},
403
ack=msnp.MessageAcknowledgement.HALF):
404
if content_type[0] in ['text/x-mms-emoticon',
405
'text/x-mms-animemoticon']:
407
message = msnp.Message(self._client.profile)
408
for key, value in headers.iteritems():
409
message.add_header(key, value)
410
message.content_type = content_type
412
for contact in self.participants:
413
self._client._protocol.\
414
send_unmanaged_message(contact, message)
417
class SwitchboardConversation(AbstractConversation, SwitchboardClient):
418
def __init__(self, client, contacts):
419
SwitchboardClient.__init__(self, client, contacts, priority=0)
420
AbstractConversation.__init__(self, client)
423
def _can_handle_message(message, switchboard_client=None):
424
content_type = message.content_type[0]
425
if switchboard_client is None:
426
return content_type in ('text/plain', 'text/x-msnmsgr-datacast')
427
# FIXME : we need to not filter those 'text/x-mms-emoticon', 'text/x-mms-animemoticon'
428
return content_type in ('text/plain', 'text/x-msmsgscontrol',
429
'text/x-msnmsgr-datacast', 'text/x-mms-emoticon',
430
'text/x-mms-animemoticon')
432
def invite_user(self, contact):
433
"""Request a contact to join in the conversation.
435
@param contact: the contact to invite.
436
@type contact: L{profile.Contact}"""
437
SwitchboardClient._invite_user(self, contact)
440
"""Leave the conversation."""
441
SwitchboardClient._leave(self)
443
def _send_message(self, content_type, body, headers={},
444
ack=msnp.MessageAcknowledgement.HALF):
445
SwitchboardClient._send_message(self, content_type, body, headers, ack)