~ubuntu-branches/ubuntu/precise/pymsn/precise

« back to all changes in this revision

Viewing changes to pymsn/conversation.py

  • Committer: Bazaar Package Importer
  • Author(s): Laurent Bigonville, Sjoerd Simons, Laurent Bigonville, Jonny Lamb
  • Date: 2008-01-17 18:23:14 UTC
  • mfrom: (1.1.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20080117182314-lwymmpnk2ut3rvr1
Tags: 0.3.1-0ubuntu1
[ Sjoerd Simons ]
* debian/rules: remove dh_python, it's no longer needed

[ Laurent Bigonville ]
* New upstream release (0.3.1)
* debian/control:
  - Add myself as an Uploaders
  - Add python:Provides for binary package
  - Add python-ctypes and python-crypto to build-deps/deps
* debian/rules: remove binary-install rule
* Add watch file
* remove pycompat file, not needed anymore
* Modify Maintainer value to match the DebianMaintainerField
  specification.

[ Jonny Lamb ]
* Added python-adns to build-deps/deps.
* Added python-pyopenssl to build-deps/deps.
* Updated copyright.
* Upped Standards-Version to 3.7.3.
* Added "XS-Dm-Upload-Allowed: yes" under the request of Sjoerd Simons.
* Added myself to Uploaders.
* Added Homepage to control.
* Added Vcs-Bzr to control.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
#
 
3
# pymsn - 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 pymsn.event import EventsDispatcher
 
31
from pymsn.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<pymsn.profile.Contact>}, ...]
 
53
 
 
54
        @returns: a Conversation object implementing L{ConversationInterface<pymsn.conversation.ConversationInterface>}
 
55
        @rtype: L{ConversationInterface<pymsn.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<pymsn.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<pymsn.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<pymsn.conversation.TextFormat>}
 
116
 
 
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>}}
 
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<pymsn.conversation.TextFormat>}
 
129
 
 
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
 
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
        except KeyError:
 
347
            message_formatting = '='
 
348
 
 
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)
 
353
            try:
 
354
                display_name = message.get_header('P4-Context')
 
355
            except KeyError:
 
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']:
 
364
            msn_objects = {}
 
365
            parts = message.body.split('\t')
 
366
            logger.debug(parts)
 
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,
 
370
                        parts[i+1])
 
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)
 
375
 
 
376
    def _on_message_sent(self, message):
 
377
        pass
 
378
 
 
379
    def _on_error(self, error_type, error):
 
380
        self._dispatch("on_conversation_error", error_type, error)
 
381
 
 
382
 
 
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)
 
389
    
 
390
    def _open(self):
 
391
        for contact in self.participants:
 
392
            self._on_contact_joined(contact)
 
393
        return False
 
394
 
 
395
    def invite_user(self, contact):
 
396
        raise NotImplementedError("The protocol doesn't allow multiuser " \
 
397
                "conversations for external contacts")
 
398
 
 
399
    def leave(self):
 
400
        self._client._unregister_external_conversation(self)
 
401
 
 
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']:
 
406
            return
 
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
 
411
        message.body = body
 
412
        for contact in self.participants:
 
413
            self._client._protocol.\
 
414
                    send_unmanaged_message(contact, message)
 
415
 
 
416
 
 
417
class SwitchboardConversation(AbstractConversation, SwitchboardClient):
 
418
    def __init__(self, client, contacts):
 
419
        SwitchboardClient.__init__(self, client, contacts, priority=0)
 
420
        AbstractConversation.__init__(self, client)
 
421
    
 
422
    @staticmethod
 
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')
 
431
 
 
432
    def invite_user(self, contact):
 
433
        """Request a contact to join in the conversation.
 
434
            
 
435
            @param contact: the contact to invite.
 
436
            @type contact: L{profile.Contact}"""
 
437
        SwitchboardClient._invite_user(self, contact)
 
438
 
 
439
    def leave(self):
 
440
        """Leave the conversation."""
 
441
        SwitchboardClient._leave(self)
 
442
 
 
443
    def _send_message(self, content_type, body, headers={},
 
444
            ack=msnp.MessageAcknowledgement.HALF):
 
445
        SwitchboardClient._send_message(self, content_type, body, headers, ack)
 
446
 
 
447