#! /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()
