#! /usr/bin/env python # -*- coding: utf-8 -*- # # Sascha Stoeter, www.stoeter.com # 2007-05-29 # # To do: a ton of error checking, debug handling, functionality, prettyfying ... pretty much anything! # import objc from Foundation import * from AppKit import * from PyObjCTools import AppHelper import time import sys import re import sets import datetime from time import sleep from libgmail import GmailAccount, GmailLoginFailure from optparse import OptionParser options = None def removeMarkup(s): """ Remove markup around interesting info. """ try: leftDelim = "\u003cb\>" l = s.index(leftDelim) + len(leftDelim) rightDelim = "\u003c/b\>" r = s.index(rightDelim, l) except ValueError: print "not found" raise SystemExit return s[l:r] def shorten(s, length): """ Shorten to given number of characters; append ellipses if necessary """ continuationIndicator = "..." if len(s) > length: return s[:length-len(continuationIndicator)] + continuationIndicator else: return s def sendSkypeSms(recipient, text): if SkypeAPI.isSkypeAvailable(): cmd = "CREATE SMS OUTGOING " + recipient print "> " + cmd response = SkypeAPI.sendSkypeCommand_(cmd) print "<", response m = re.search('[0-9]+', response) print "id:", m.group() time.sleep(1) # [todo: wait for proper async message before continuing] cmd = "SET SMS " + m.group() + " BODY \"" + text + "\"" print "> " + cmd response = SkypeAPI.sendSkypeCommand_(cmd) print "<", response time.sleep(1) cmd = "ALTER SMS " + m.group() + " SEND" #cmd = "DELETE SMS " + m.group() # uncomment this to not send and delete the sms print "> " + cmd response = SkypeAPI.sendSkypeCommand_(cmd) print "<", response time.sleep(1) class PeriodicAction(NSObject): def init(self): self = super(PeriodicAction, self).init() if self is None: return None self.shutdownRequested = False self.running = False self.reportedActiveIds = set() # threads that have already been reported return self def run_(self, arg): pool = NSAutoreleasePool.alloc().init() actionPeriod = datetime.timedelta(minutes=1) actionTime = datetime.datetime.now() while not self.shutdownRequested: if self.running: if actionTime <= datetime.datetime.now(): self.action() actionTime = datetime.datetime.now() + actionPeriod time.sleep(1) pool.release() def shutdown(self): self.shutdownRequested = True def action(self): msgLength = 160 # restrict sms to this many characters. note that skype may increase the message length. g = GmailAccount(options.username, options.password) try: g.login() except GmailLoginFailure: print "Login failed" #print "Check at ", datetime.datetime.isoformat(datetime.datetime.utcnow()) # obtain current unread threads from inbox. may contain new messages, # and previously reported messages. messages may also have been moved # since the last check. threads = g.getMessagesByQuery('is:unread label:Inbox') #print "Number of threads: ", len(threads) currentIds = set() for thread in threads: # ensure no duplicate notification; # if thread has a new message it is reported again as the thread's id is changed to that of new message # really need to check thread for multiple new messages received during sleep; could have messages slip through # ... of course I can also define notification to report only the latest new message of a thread print "thread ID:", thread.id currentIds.add(thread.id) if thread.id not in self.reportedActiveIds: item = removeMarkup(thread.authors) + ", " + removeMarkup(thread.subject) + ", " + removeMarkup(thread.date) + "." + thread.snippet #print len(item), item shortItem = shorten(item, msgLength) print len(shortItem), shortItem print sendSkypeSms(options.target, shortItem) self.reportedActiveIds = currentIds class SkypeClient(NSObject): def init(self): self.lui = None return self def applicationDidFinishLaunching_(self, aNotification): print "applicationDidFinishLaunching_" self.lui = PeriodicAction.alloc().init() NSThread.detachNewThreadSelector_toTarget_withObject_('run:', self.lui, 42) def clientApplicationName(self): return "Gmail SMS Notifier" # what gets displayed to Skype UI def skypeNotificationReceived_(self, aString): print "< " + aString + " [async]" # just print whatever we got from Skype as message # inform thread, skype is really running if re.match(r'SKYPEVERSION', aString) is not None: self.lui.running = True def skypeAttachResponse_(aNotification): # print "SkypeAttachResponse: ", aNotification pass # could have some meaningful code here to be notified of Skype availability # events... but this first one only points to myself (delegate), and when I # tried to have extra parameters, it crashed with "bus error" :( def skypeBecameAvailable_(self, aNotification): print "SkypeBecameAvailable" def skypeBecameUnavailable_(self, aNotification): print "SkypeBecameUnvailable end" ### BEGIN - example code from stdinreader.py... kinda bloated just for reading stdin? ### but provides us nice asynchronous notifications class FileObserver(NSObject): def initWithFileDescriptor_readCallback_errorCallback_(self, fileDescriptor, readCallback, errorCallback): self = self.init() self.readCallback = readCallback self.errorCallback = errorCallback self.fileHandle = NSFileHandle.alloc().initWithFileDescriptor_( fileDescriptor) self.nc = NSNotificationCenter.defaultCenter() self.nc.addObserver_selector_name_object_( self, 'fileHandleReadCompleted:', NSFileHandleReadCompletionNotification, self.fileHandle) self.fileHandle.readInBackgroundAndNotify() return self def fileHandleReadCompleted_(self, aNotification): ui = aNotification.userInfo() newData = ui.objectForKey_(NSFileHandleNotificationDataItem) if newData is None: if self.errorCallback is not None: self.errorCallback(self, ui.objectForKey_(NSFileHandleError)) self.close() else: self.fileHandle.readInBackgroundAndNotify() if self.readCallback is not None: self.readCallback(self, str(newData)) def close(self): self.nc.removeObserver_(self) if self.fileHandle is not None: self.fileHandle.closeFile() self.fileHandle = None # break cycles in case these functions are closed over # an instance of us self.readCallback = None self.errorCallback = None def __del__(self): # Without this, if a notification fires after we are GC'ed # then the app will crash because NSNotificationCenter # doesn't retain observers. In this example, it doesn't # matter, but it's worth pointing out. self.close() def prompt(): sys.stdout.write("write something: ") sys.stdout.flush() # rewritten to handle Skype API def gotLine(observer, aLine): if aLine: if SkypeAPI.isSkypeAvailable(): print "> " + aLine.rstrip() response = SkypeAPI.sendSkypeCommand_(aLine.rstrip()) #if type(response) is types.NoneType: if response is not None: print len(response), "< " + response else: print "[no synchronous response for command]" else: print "Cannot send command, Skype not available: " + aLine.rstrip() else: print "" AppHelper.stopEventLoop() def gotError(observer, err): print "error:", err AppHelper.stopEventLoop() ### END - example code from stdinreader def main(): parser = OptionParser() parser.add_option("-u", "--username", dest="username", help="Gmail username") parser.add_option("-p", "--password", dest="password", help="Gmail password") parser.add_option("-t", "--target", dest="target", help="SMS target") global options (options, args) = parser.parse_args() app = NSApplication.sharedApplication() # we must keep a reference to the delegate object ourselves, # NSApp.setDelegate_() doesn't retain it. A local variable is # enough here. client = SkypeClient.alloc().init() NSApp().setDelegate_(client) objc.loadBundle("SkypeAPI", globals(), bundle_path = objc.pathForFramework(u'/Applications/Skype.app/Contents/Frameworks/Skype.framework')) SkypeAPI.setSkypeDelegate_(client) SkypeAPI.connect() AppHelper.installMachInterrupt() # install ctrl-c handler observer = FileObserver.alloc().initWithFileDescriptor_readCallback_errorCallback_(sys.stdin.fileno(), gotLine, gotError) print "Press Ctrl-C to quit.\n" AppHelper.runEventLoop() # there are actually many ways to run the eventloop :) here are some more examples # that I got from various places. not sure which one is the most correct... # app.run() # AppHelper.runConsoleEventLoop(installInterrupt=True) if __name__ == '__main__' : main()