1 """
2 Low level *Skype for Mac OS X* interface implemented using *Carbon
3 distributed notifications*. Uses direct *Carbon*/*CoreFoundation*
4 calls through the *ctypes* module.
5
6 This module handles the options that you can pass to
7 `Skype.__init__` for *Mac OS X* machines.
8
9 - ``RunMainLoop`` (bool) - If set to False, Skype4Py won't start the Carbon event
10 loop. Otherwise it is started in a separate thread. The loop must be running for
11 Skype4Py events to work properly. Set this option to False if you plan to run the
12 loop yourself or if, for example, your GUI framework does it for you.
13
14 Thanks to **Eion Robb** for reversing *Skype for Mac* API protocol.
15 """
16 __docformat__ = 'restructuredtext en'
17
18
19 import sys
20 from ctypes import *
21 from ctypes.util import find_library
22 import threading
23 import time
24 import logging
25
26 from Skype4Py.api import Command, SkypeAPIBase, \
27 timeout2float, finalize_opts
28 from Skype4Py.errors import SkypeAPIError
29 from Skype4Py.enums import *
30
31
32 __all__ = ['SkypeAPI']
36 """Fundamental type for all CoreFoundation types.
37
38 :see: http://developer.apple.com/documentation/CoreFoundation/Reference/CFTypeRef/
39 """
40
42 self.owner = True
43 if isinstance(init, CFType):
44
45 self.handle = init.get_handle()
46 coref.CFRetain(self)
47 elif isinstance(init, c_void_p):
48 self.handle = init
49 else:
50 raise TypeError('illegal init type: %s' % type(init))
51
52 @classmethod
54 if isinstance(handle, (int, long)):
55 handle = c_void_p(handle)
56 elif not isinstance(handle, c_void_p):
57 raise TypeError('illegal handle type: %s' % type(handle))
58 obj = cls(handle)
59 obj.owner = False
60 return obj
61
63 if not coref:
64 return
65 if self.owner:
66 coref.CFRelease(self)
67
69 return '%s(handle=%s)' % (self.__class__.__name__, repr(self.handle))
70
72 if not self.owner:
73 coref.CFRetain(self)
74 self.owner = True
75
77 return coref.CFGetRetainCount(self)
78
81
82
83 _as_parameter_ = property(get_handle)
84
87 """CoreFoundation string type.
88
89 Supports Python unicode type only. String is immutable.
90
91 :see: http://developer.apple.com/documentation/CoreFoundation/Reference/CFStringRef/
92 """
93
95 if isinstance(init, (str, unicode)):
96 s = unicode(init).encode('utf-8')
97 init = c_void_p(coref.CFStringCreateWithBytes(None,
98 s, len(s), 0x08000100, False))
99 CFType.__init__(self, init)
100
102 i = coref.CFStringGetLength(self)
103 size = c_long()
104 if coref.CFStringGetBytes(self, 0, i, 0x08000100, 0, False, None, 0, byref(size)) > 0:
105 buf = create_string_buffer(size.value)
106 coref.CFStringGetBytes(self, 0, i, 0x08000100, 0, False, buf, size, None)
107 return buf.value
108 else:
109 raise UnicodeError('CFStringGetBytes() failed')
110
112 return self.__str__().decode('utf-8')
113
115 return coref.CFStringGetLength(self)
116
118 return 'CFString(%s)' % repr(unicode(self))
119
122 """CoreFoundation number type.
123
124 Supports Python int type only. Number is immutable.
125
126 :see: http://developer.apple.com/documentation/CoreFoundation/Reference/CFNumberRef/
127 """
128
130 if isinstance(init, (int, long)):
131 init = c_void_p(coref.CFNumberCreate(None, 3, byref(c_int(int(init)))))
132 CFType.__init__(self, init)
133
135 n = c_int()
136 if coref.CFNumberGetValue(self, 3, byref(n)):
137 return n.value
138 return 0
139
141 return 'CFNumber(%s)' % repr(int(self))
142
145 """CoreFoundation immutable dictionary type.
146
147 :see: http://developer.apple.com/documentation/CoreFoundation/Reference/CFDictionaryRef/
148 """
149
151 if isinstance(init, dict):
152 d = dict(init)
153 keys = (c_void_p * len(d))()
154 values = (c_void_p * len(d))()
155 for i, (k, v) in enumerate(d.items()):
156 keys[i] = k.get_handle()
157 values[i] = v.get_handle()
158 init = c_void_p(coref.CFDictionaryCreate(None, keys, values, len(d),
159 coref.kCFTypeDictionaryKeyCallBacks, coref.kCFTypeDictionaryValueCallBacks))
160 CFType.__init__(self, init)
161
163 n = len(self)
164 keys = (c_void_p * n)()
165 values = (c_void_p * n)()
166 coref.CFDictionaryGetKeysAndValues(self, keys, values)
167 d = dict()
168 for i in xrange(n):
169 d[CFType.from_handle(keys[i])] = CFType.from_handle(values[i])
170 return d
171
173 return CFType.from_handle(coref.CFDictionaryGetValue(self, key))
174
176 return coref.CFDictionaryGetCount(self)
177
180 """CoreFoundation distributed notification center type.
181
182 :see: http://developer.apple.com/documentation/CoreFoundation/Reference/CFNotificationCenterRef/
183 """
184
185 CFNOTIFICATIONCALLBACK = CFUNCTYPE(None, c_void_p, c_void_p, c_void_p, c_void_p, c_void_p)
186
188 CFType.__init__(self, c_void_p(coref.CFNotificationCenterGetDistributedCenter()))
189
190 self.owner = False
191 self.callbacks = {}
192 self._c_callback = self.CFNOTIFICATIONCALLBACK(self._callback)
193
194 - def _callback(self, center, observer, name, obj, userInfo):
195 observer = CFString.from_handle(observer)
196 name = CFString.from_handle(name)
197 if obj:
198 obj = CFString.from_handle(obj)
199 userInfo = CFDictionary.from_handle(userInfo)
200 callback = self.callbacks[(unicode(observer), unicode(name))]
201 callback(self, observer, name, obj, userInfo)
202
203 - def add_observer(self, observer, callback, name=None, obj=None,
204 drop=False, coalesce=False, hold=False, immediate=False):
205 if not callable(callback):
206 raise TypeError('callback must be callable')
207 observer = CFString(observer)
208 self.callbacks[(unicode(observer), unicode(name))] = callback
209 if name is not None:
210 name = CFString(name)
211 if obj is not None:
212 obj = CFString(obj)
213 if drop:
214 behaviour = 1
215 elif coalesce:
216 behaviour = 2
217 elif hold:
218 behaviour = 3
219 elif immediate:
220 behaviour = 4
221 else:
222 behaviour = 0
223 coref.CFNotificationCenterAddObserver(self, observer,
224 self._c_callback, name, obj, behaviour)
225
227 observer = CFString(observer)
228 if name is not None:
229 name = CFString(name)
230 if obj is not None:
231 obj = CFString(obj)
232 coref.CFNotificationCenterRemoveObserver(self, observer, name, obj)
233 try:
234 del self.callbacks[(unicode(observer), unicode(name))]
235 except KeyError:
236 pass
237
238 - def post_notification(self, name, obj=None, userInfo=None, immediate=False):
239 name = CFString(name)
240 if obj is not None:
241 obj = CFString(obj)
242 if userInfo is not None:
243 userInfo = CFDictionary(userInfo)
244 coref.CFNotificationCenterPostNotification(self, name, obj, userInfo, immediate)
245
248 """Carbon event loop object for the current thread.
249
250 The Carbon reference documentation seems to be gone from developer.apple.com, the following
251 link points to a mirror I found. I don't know how long until this one is gone too.
252
253 :see: http://www.monen.nl/DevDoc/documentation/Carbon/Reference/Carbon_Event_Manager_Ref/index.html
254 """
255
257 self.handle = c_void_p(carbon.GetCurrentEventLoop())
258
259 @staticmethod
260 - def run(timeout=-1):
261
262
263 return (carbon.RunCurrentEventLoop(timeout) == -9876)
264
266 carbon.QuitEventLoop(self.handle)
267
268
269
270
271 if not getattr(sys, 'skype4py_setup', False):
272
273 path = find_library('Carbon')
274 if path is None:
275 raise ImportError('Could not find Carbon.framework')
276 carbon = cdll.LoadLibrary(path)
277 carbon.RunCurrentEventLoop.argtypes = (c_double,)
278
279 path = find_library('CoreFoundation')
280 if path is None:
281 raise ImportError('Could not find CoreFoundation.framework')
282 coref = cdll.LoadLibrary(path)
286 """
287 :note: Code based on Pidgin Skype Plugin source
288 (http://code.google.com/p/skype4pidgin/).
289 Permission to use granted by the author.
290 """
291
293 self.logger = logging.getLogger('Skype4Py.api.darwin.SkypeAPI')
294 SkypeAPIBase.__init__(self)
295 self.run_main_loop = opts.pop('RunMainLoop', True)
296 finalize_opts(opts)
297 self.center = CFDistributedNotificationCenter()
298 self.is_available = False
299 self.client_id = -1
300
302 self.logger.info('thread started')
303 if self.run_main_loop:
304 self.loop = EventLoop()
305 EventLoop.run()
306 self.logger.info('thread finished')
307
309 if hasattr(self, 'loop'):
310 self.loop.stop()
311 self.client_id = -1
312 SkypeAPIBase.close(self)
313
320
321 - def attach(self, timeout, wait=True):
322 if self.attachment_status in (apiAttachPendingAuthorization, apiAttachSuccess):
323 return
324 self.acquire()
325 try:
326 try:
327 self.start()
328 except AssertionError:
329 pass
330 t = threading.Timer(timeout2float(timeout), lambda: setattr(self, 'wait', False))
331 try:
332 self.init_observer()
333 self.client_id = -1
334 self.set_attachment_status(apiAttachPendingAuthorization)
335 self.post('SKSkypeAPIAttachRequest')
336 self.wait = True
337 if wait:
338 t.start()
339 while self.wait and self.attachment_status == apiAttachPendingAuthorization:
340 if self.run_main_loop:
341 time.sleep(1.0)
342 else:
343 EventLoop.run(1.0)
344 finally:
345 t.cancel()
346 if not self.wait:
347 self.set_attachment_status(apiAttachUnknown)
348 raise SkypeAPIError('Skype attach timeout')
349 finally:
350 self.release()
351 command = Command('PROTOCOL %s' % self.protocol, Blocking=True)
352 self.send_command(command)
353 self.protocol = int(command.Reply.rsplit(None, 1)[-1])
354
356 try:
357 self.start()
358 except AssertionError:
359 pass
360 self.init_observer()
361 self.is_available = False
362 self.post('SKSkypeAPIAvailabilityRequest')
363 time.sleep(1.0)
364 return self.is_available
365
366 - def startup(self, minimized, nosplash):
367 if not self.is_running():
368 from subprocess import Popen
369 nul = file('/dev/null')
370 Popen(['/Applications/Skype.app/Contents/MacOS/Skype'], stdin=nul, stdout=nul, stderr=nul)
371
373 if not self.attachment_status == apiAttachSuccess:
374 self.attach(command.Timeout)
375 self.push_command(command)
376 self.notifier.sending_command(command)
377 cmd = u'#%d %s' % (command.Id, command.Command)
378 if command.Blocking:
379 if self.run_main_loop:
380 command._event = event = threading.Event()
381 else:
382 command._loop = EventLoop()
383 else:
384 command._timer = timer = threading.Timer(command.timeout2float(), self.pop_command, (command.Id,))
385
386 self.logger.debug('sending %s', repr(cmd))
387 userInfo = CFDictionary({CFString('SKYPE_API_COMMAND'): CFString(cmd),
388 CFString('SKYPE_API_CLIENT_ID'): CFNumber(self.client_id)})
389 self.post('SKSkypeAPICommand', userInfo)
390
391 if command.Blocking:
392 if self.run_main_loop:
393 event.wait(command.timeout2float())
394 if not event.isSet():
395 raise SkypeAPIError('Skype command timeout')
396 else:
397 if not EventLoop.run(command.timeout2float()):
398 raise SkypeAPIError('Skype command timeout')
399 else:
400 timer.start()
401
403 if self.has_observer():
404 self.delete_observer()
405 self.observer = CFString(self.friendly_name)
406 self.center.add_observer(self.observer, self.SKSkypeAPINotification, 'SKSkypeAPINotification', immediate=True)
407 self.center.add_observer(self.observer, self.SKSkypeWillQuit, 'SKSkypeWillQuit', immediate=True)
408 self.center.add_observer(self.observer, self.SKSkypeBecameAvailable, 'SKSkypeBecameAvailable', immediate=True)
409 self.center.add_observer(self.observer, self.SKAvailabilityUpdate, 'SKAvailabilityUpdate', immediate=True)
410 self.center.add_observer(self.observer, self.SKSkypeAttachResponse, 'SKSkypeAttachResponse', immediate=True)
411
413 if not self.has_observer():
414 return
415 self.center.remove_observer(self.observer, 'SKSkypeAPINotification')
416 self.center.remove_observer(self.observer, 'SKSkypeWillQuit')
417 self.center.remove_observer(self.observer, 'SKSkypeBecameAvailable')
418 self.center.remove_observer(self.observer, 'SKAvailabilityUpdate')
419 self.center.remove_observer(self.observer, 'SKSkypeAttachResponse')
420 del self.observer
421
423 return hasattr(self, 'observer')
424
425 - def post(self, name, userInfo=None):
426 if not self.has_observer():
427 self.init_observer()
428 self.center.post_notification(name, self.observer, userInfo, immediate=True)
429
431 client_id = int(CFNumber(userInfo[CFString('SKYPE_API_CLIENT_ID')]))
432 if client_id != 999 and (client_id == 0 or client_id != self.client_id):
433 return
434 cmd = unicode(CFString(userInfo[CFString('SKYPE_API_NOTIFICATION_STRING')]))
435 self.logger.debug('received %s', repr(cmd))
436
437 if cmd.startswith(u'#'):
438 p = cmd.find(u' ')
439 command = self.pop_command(int(cmd[1:p]))
440 if command is not None:
441 command.Reply = cmd[p + 1:]
442 if command.Blocking:
443 if self.run_main_loop:
444 command._event.set()
445 else:
446 command._loop.stop()
447 else:
448 command._timer.cancel()
449 self.notifier.reply_received(command)
450 else:
451 self.notifier.notification_received(cmd[p + 1:])
452 else:
453 self.notifier.notification_received(cmd)
454
458
460 self.logger.debug('received SKSkypeBecameAvailable')
461 self.set_attachment_status(apiAttachAvailable)
462
464 self.logger.debug('received SKAvailabilityUpdate')
465 self.is_available = not not int(CFNumber(userInfo[CFString('SKYPE_API_AVAILABILITY')]))
466
468 self.logger.debug('received SKSkypeAttachResponse')
469
470
471 if unicode(CFString(userInfo[CFString('SKYPE_API_CLIENT_NAME')])) == self.friendly_name:
472 response = int(CFNumber(userInfo[CFString('SKYPE_API_ATTACH_RESPONSE')]))
473 if response and self.client_id == -1:
474 self.client_id = response
475 self.set_attachment_status(apiAttachSuccess)
476