├── .gitignore ├── resources ├── mail.png ├── Icon.icns ├── guess.png ├── circle.acorn ├── spinner.gif ├── disclosure-down.png ├── disclosure-right.png ├── MainMenu.nib │ └── keyedobjects.nib ├── Credits.html └── style.css ├── Sparkle.framework ├── Sparkle └── Versions │ └── A │ ├── Sparkle │ ├── Resources │ ├── fr_CA.lproj │ ├── fr.lproj │ │ ├── fr.lproj │ │ ├── Sparkle.strings │ │ ├── SUUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ ├── SUAutomaticUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ └── SUUpdatePermissionPrompt.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ ├── relaunch │ ├── de.lproj │ │ ├── Sparkle.strings │ │ ├── SUUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ ├── SUAutomaticUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ └── SUUpdatePermissionPrompt.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ ├── en.lproj │ │ ├── Sparkle.strings │ │ ├── SUUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ ├── SUAutomaticUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ └── SUUpdatePermissionPrompt.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ ├── es.lproj │ │ ├── Sparkle.strings │ │ ├── SUUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ ├── SUAutomaticUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ └── SUUpdatePermissionPrompt.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ ├── it.lproj │ │ ├── Sparkle.strings │ │ ├── SUUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ ├── SUAutomaticUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ └── SUUpdatePermissionPrompt.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ ├── nl.lproj │ │ ├── Sparkle.strings │ │ ├── SUUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ ├── SUAutomaticUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ └── SUUpdatePermissionPrompt.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ ├── ru.lproj │ │ ├── Sparkle.strings │ │ ├── SUUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ ├── SUAutomaticUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ └── SUUpdatePermissionPrompt.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ ├── sv.lproj │ │ ├── Sparkle.strings │ │ ├── SUUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ ├── SUAutomaticUpdateAlert.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ │ └── SUUpdatePermissionPrompt.nib │ │ │ ├── keyedobjects.nib │ │ │ ├── info.nib │ │ │ └── classes.nib │ ├── SUStatus.nib │ │ ├── keyedobjects.nib │ │ ├── info.nib │ │ └── classes.nib │ ├── Info.plist │ ├── License.txt │ └── SUModelTranslation.plist │ └── Headers │ ├── Sparkle.h │ ├── SUAppcast.h │ ├── SUVersionComparisonProtocol.h │ ├── SUAppcastItem.h │ └── SUUpdater.h ├── HACKING ├── LICENSE ├── Makefile ├── extractors ├── ComAppleAddressBook.py ├── ComAppleMail.py ├── ComSkypeSkype.py ├── ComRancheroNetNewsWire.py ├── ComIconfactoryTwitterrific.py ├── ComAdiumXAdiumX.py ├── OrgMozillaFirefox.py ├── ComAppleIChat.py ├── ComGoogleChrome.py ├── ComAppleSafari.py └── Extractor.py ├── PyShelfApplication.py ├── README ├── relmeparser.py ├── main.py ├── setup.py ├── providers ├── BasicProvider.py ├── TwitterProvider.py ├── FlickrProvider.py ├── Provider.py ├── SpotlightProvider.py ├── DopplrProvider.py ├── LastFmProvider.py └── FeedProvider.py ├── TODO ├── lib ├── autorss.py └── microformatparser.py ├── Utilities.py ├── Cache.py ├── ChangeLog ├── dev_appserver_login.py ├── Clue.py └── PyShelfWindowController.py /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /resources/mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/resources/mail.png -------------------------------------------------------------------------------- /resources/Icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/resources/Icon.icns -------------------------------------------------------------------------------- /resources/guess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/resources/guess.png -------------------------------------------------------------------------------- /resources/circle.acorn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/resources/circle.acorn -------------------------------------------------------------------------------- /resources/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/resources/spinner.gif -------------------------------------------------------------------------------- /Sparkle.framework/Sparkle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Sparkle -------------------------------------------------------------------------------- /resources/disclosure-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/resources/disclosure-down.png -------------------------------------------------------------------------------- /resources/disclosure-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/resources/disclosure-right.png -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Sparkle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Sparkle -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/fr_CA.lproj: -------------------------------------------------------------------------------- 1 | /Users/andym/Development/Build Products/Release/Sparkle.framework/Resources/fr.lproj -------------------------------------------------------------------------------- /resources/MainMenu.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/resources/MainMenu.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/fr.lproj/fr.lproj: -------------------------------------------------------------------------------- 1 | /Users/andym/Development/Build Products/Release/Sparkle.framework/Resources/fr.lproj -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/relaunch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/relaunch -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/de.lproj/Sparkle.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/de.lproj/Sparkle.strings -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/es.lproj/Sparkle.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/es.lproj/Sparkle.strings -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/fr.lproj/Sparkle.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/fr.lproj/Sparkle.strings -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/it.lproj/Sparkle.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/it.lproj/Sparkle.strings -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/nl.lproj/Sparkle.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/nl.lproj/Sparkle.strings -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/ru.lproj/Sparkle.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/ru.lproj/Sparkle.strings -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/sv.lproj/Sparkle.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/sv.lproj/Sparkle.strings -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/SUStatus.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/SUStatus.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/de.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/de.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/es.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/es.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/fr.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/fr.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/it.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/it.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/nl.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/nl.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/ru.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/ru.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/sv.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/sv.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tominsam/shelf-python/HEAD/Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdatePermissionPrompt.nib/keyedobjects.nib -------------------------------------------------------------------------------- /resources/Credits.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Shelf. It's insane.

4 | 5 |

http://jerakeen.org/code/shelf/

6 | 7 |

by Tom Insam

8 | 9 |

Icon by Rui Carmo

10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /HACKING: -------------------------------------------------------------------------------- 1 | Building (for development) 2 | 3 | python setup.py py2app -A 4 | 5 | Building (for deployment) 6 | 7 | python setup.py py2app 8 | 9 | Notes: 10 | 11 | Don't be fooled by the fact that the files are in folders. When packaged, 12 | they're all in one big flat folder. They're only in folders in the source 13 | so that I can organise them a little. 14 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 629 7 | IBOldestOS 8 | 5 9 | IBOpenObjects 10 | 11 | IBSystem Version 12 | 9E17 13 | targetFramework 14 | IBCocoaFramework 15 | 16 | 17 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 629 7 | IBOldestOS 8 | 5 9 | IBOpenObjects 10 | 11 | IBSystem Version 12 | 9E17 13 | targetFramework 14 | IBCocoaFramework 15 | 16 | 17 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/fr.lproj/SUAutomaticUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 629 7 | IBOldestOS 8 | 5 9 | IBOpenObjects 10 | 11 | IBSystem Version 12 | 9D34 13 | targetFramework 14 | IBCocoaFramework 15 | 16 | 17 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdatePermissionPrompt.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 629 7 | IBOldestOS 8 | 5 9 | IBOpenObjects 10 | 11 | IBSystem Version 12 | 9E17 13 | targetFramework 14 | IBCocoaFramework 15 | 16 | 17 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdatePermissionPrompt.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 629 7 | IBOldestOS 8 | 5 9 | IBOpenObjects 10 | 11 | IBSystem Version 12 | 9E17 13 | targetFramework 14 | IBCocoaFramework 15 | 16 | 17 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/nl.lproj/SUAutomaticUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 629 7 | IBOldestOS 8 | 5 9 | IBOpenObjects 10 | 11 | 6 12 | 13 | IBSystem Version 14 | 9D34 15 | targetFramework 16 | IBCocoaFramework 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Shelf source code repository includes imported snapshots of the following libraries: 2 | 3 | autorss.py - Copyright 2002, Mark Pilgrim, licensed under the Python license. 4 | 5 | feedparser.py - Copyright 2002-2006, Mark Pilgrim, licensed under something that looks like the 3-clause BSD licence with the no-endorsement clause removed 6 | 7 | microformatparser.py - Copyright 2005, Phil Dawes, 'Distributed under a New BSD style license' 8 | 9 | Given all this, Shelf needs a license. Python? Still thinking about it. Please 10 | don't submit any patches unless you're willing to have them licensed under something 11 | random that I think of later. 12 | 13 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Headers/Sparkle.h: -------------------------------------------------------------------------------- 1 | // 2 | // Sparkle.h 3 | // Sparkle 4 | // 5 | // Created by Andy Matuschak on 3/16/06. (Modified by CDHW on 23/12/07) 6 | // Copyright 2006 Andy Matuschak. All rights reserved. 7 | // 8 | 9 | #ifndef SPARKLE_H 10 | #define SPARKLE_H 11 | 12 | // This list should include the shared headers. It doesn't matter if some of them aren't shared (unless 13 | // there are name-space collisions) so we can list all of them to start with: 14 | 15 | #import 16 | 17 | #import 18 | #import 19 | #import 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdatePermissionPrompt.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 670 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | IBSystem Version 14 | 9E17 15 | targetFramework 16 | IBCocoaFramework 17 | 18 | 19 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/SUStatus.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 670 7 | IBLastKnownRelativeProjectPath 8 | Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 10A96 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # wrapper Makefile for py2app invocation and cleaning 2 | 3 | PYTHON ?= python 4 | 5 | # EVIL EVIL EVIL 6 | VERSION = $(shell grep 'version =' setup.py | cut -d'"' -f 2) 7 | 8 | .PHONY: all 9 | all: dist 10 | @ : 11 | 12 | .PHONY: dev 13 | dev: 14 | @echo - 15 | @echo - dev build will not work under Snow Leopard unless you\'ve fixed your local build!! 16 | @echo - 17 | $(PYTHON) setup.py py2app -A 18 | 19 | .PHONY: dist 20 | dist: 21 | $(PYTHON) setup.py py2app 22 | 23 | .PHONY: zip 24 | zip: 25 | cd dist && rm -f Shelf-$(VERSION).zip 26 | cd dist && zip -r9 Shelf-$(VERSION).zip Shelf.app/ 27 | du -k dist/*.zip 28 | 29 | .PHONY: clean 30 | clean: 31 | rm -rf build dist 32 | -------------------------------------------------------------------------------- /extractors/ComAppleAddressBook.py: -------------------------------------------------------------------------------- 1 | from ScriptingBridge import * 2 | from Extractor import * 3 | from AddressBook import * 4 | from Utilities import * 5 | 6 | class ComAppleAddressBook( Extractor ): 7 | 8 | def __init__(self): 9 | super( ComAppleAddressBook, self ).__init__() 10 | self.ab = SBApplication.applicationWithBundleIdentifier_("com.apple.AddressBook") 11 | 12 | def clues(self): 13 | selection = self.ab.selection() 14 | if selection.count() == 0: return [] 15 | 16 | selected_id = selection[0].id() 17 | 18 | person = self.addressBook.recordForUniqueId_( selected_id ) 19 | self.addClues( [ Clue.forPerson( person ) ] ) 20 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 667 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 9D34 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 667 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 9D34 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 667 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 9D34 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 670 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 9E17 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBDocumentLocation 6 | 69 14 356 240 0 0 1280 778 7 | IBFramework Version 8 | 489.0 9 | IBLastKnownRelativeProjectPath 10 | ../Sparkle.xcodeproj 11 | IBOldestOS 12 | 5 13 | IBSystem Version 14 | 9D34 15 | targetFramework 16 | IBCocoaFramework 17 | 18 | 19 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 670 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 18 14 | 15 | IBSystem Version 16 | 10A96 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/de.lproj/SUAutomaticUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 667 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 9D34 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdatePermissionPrompt.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 667 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 9D34 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 658 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 9C7010 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/es.lproj/SUAutomaticUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 667 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 9D34 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/it.lproj/SUAutomaticUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 667 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 9D34 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdatePermissionPrompt.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 667 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 5 14 | 15 | IBSystem Version 16 | 9D34 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/ru.lproj/SUAutomaticUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 670 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 9E17 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/sv.lproj/SUAutomaticUpdateAlert.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 670 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 10A96 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdatePermissionPrompt.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 667 7 | IBLastKnownRelativeProjectPath 8 | ../../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 9D34 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdatePermissionPrompt.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 670 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 15 | IBSystem Version 16 | 10A96 17 | targetFramework 18 | IBCocoaFramework 19 | 20 | 21 | -------------------------------------------------------------------------------- /extractors/ComAppleMail.py: -------------------------------------------------------------------------------- 1 | from ScriptingBridge import * 2 | from Extractor import * 3 | from Utilities import * 4 | 5 | class ComAppleMail( Extractor ): 6 | 7 | def __init__(self): 8 | super( ComAppleMail, self ).__init__() 9 | # handily, this persists across Mail.app restarts 10 | self.mail = SBApplication.applicationWithBundleIdentifier_("com.apple.mail") 11 | 12 | def clues(self): 13 | # are we looking at a message viewer 14 | if self.mail.windows()[0].id() in map( lambda v: v.window().id(), self.mail.messageViewers() ): 15 | messages = self.mail.selection() 16 | if messages.count() > 0: 17 | self.clues_from_email( messages[0].sender() ) 18 | -------------------------------------------------------------------------------- /extractors/ComSkypeSkype.py: -------------------------------------------------------------------------------- 1 | from ScriptingBridge import * 2 | from Extractor import * 3 | from Utilities import * 4 | 5 | class ComSkypeSkype(Extractor): 6 | 7 | def __init__(self): 8 | super( ComSkypeSkype, self ).__init__() 9 | self.skype = SBApplication.applicationWithBundleIdentifier_("com.skype.skype") 10 | 11 | def clues(self): 12 | # first window is an invisible emoticons tihng 13 | 14 | if not self.skype.windows().count() > 1: 15 | return 16 | chat = self.skype.windows()[1] 17 | if not chat.exists(): return [] 18 | 19 | # seriously, this is the best we can do? 20 | name = chat.name() 21 | self.clues_from_name( name ) 22 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/info.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBFramework Version 6 | 670 7 | IBLastKnownRelativeProjectPath 8 | ../Sparkle.xcodeproj 9 | IBOldestOS 10 | 5 11 | IBOpenObjects 12 | 13 | 6 14 | 41 15 | 16 | IBSystem Version 17 | 10A96 18 | targetFramework 19 | IBCocoaFramework 20 | 21 | 22 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Headers/SUAppcast.h: -------------------------------------------------------------------------------- 1 | // 2 | // SUAppcast.h 3 | // Sparkle 4 | // 5 | // Created by Andy Matuschak on 3/12/06. 6 | // Copyright 2006 Andy Matuschak. All rights reserved. 7 | // 8 | 9 | #ifndef SUAPPCAST_H 10 | #define SUAPPCAST_H 11 | 12 | @class SUAppcastItem; 13 | @interface SUAppcast : NSObject { 14 | NSArray *items; 15 | NSString *userAgentString; 16 | id delegate; 17 | NSMutableData *incrementalData; 18 | } 19 | 20 | - (void)fetchAppcastFromURL:(NSURL *)url; 21 | - (void)setDelegate:delegate; 22 | - (void)setUserAgentString:(NSString *)userAgentString; 23 | 24 | - (NSArray *)items; 25 | 26 | @end 27 | 28 | @interface NSObject (SUAppcastDelegate) 29 | - (void)appcastDidFinishLoading:(SUAppcast *)appcast; 30 | - (void)appcast:(SUAppcast *)appcast failedToLoadWithError:(NSError *)error; 31 | @end 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /extractors/ComRancheroNetNewsWire.py: -------------------------------------------------------------------------------- 1 | from ScriptingBridge import * 2 | from Extractor import * 3 | from Utilities import * 4 | 5 | class ComRancheroNetNewsWire(Extractor): 6 | 7 | def __init__(self): 8 | super( ComRancheroNetNewsWire, self ).__init__() 9 | self.nnw = SBApplication.applicationWithBundleIdentifier_("com.ranchero.NetNewsWire") 10 | 11 | def clues(self): 12 | selected = self.nnw.selectedHeadline() 13 | if not selected.exists(): return 14 | 15 | if selected.objectDescription(): 16 | self.clues_from_html( selected.objectDescription(), selected.URL() ) 17 | if self.done: return 18 | 19 | self.clues_from_url( selected.URL() ) 20 | if self.done: return 21 | 22 | self.clues_from_url( selected.subscription().homeURL() ) 23 | if self.done: return 24 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | Sparkle 9 | CFBundleIdentifier 10 | org.andymatuschak.Sparkle 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Sparkle 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.5 Beta 6 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 313 23 | 24 | 25 | -------------------------------------------------------------------------------- /extractors/ComIconfactoryTwitterrific.py: -------------------------------------------------------------------------------- 1 | from ScriptingBridge import * 2 | from Extractor import * 3 | from Utilities import * 4 | 5 | class ComIconfactoryTwitterrific(Extractor): 6 | 7 | def __init__(self): 8 | super( ComIconfactoryTwitterrific, self ).__init__() 9 | self.twitterific = SBApplication.applicationWithBundleIdentifier_("com.iconfactory.Twitterrific") 10 | 11 | def clues(self): 12 | try: 13 | if not self.twitterific.tweets().count(): return 14 | except AttributeError: 15 | # old twitteriffic 16 | return 17 | 18 | url = self.twitterific.selection().userUrl() 19 | self.clues_from_url( url ) 20 | if self.done: return 21 | 22 | username = self.twitterific.selection().screenName() 23 | self.clues_from_url("http://twitter.com/%s"%( username ) ) 24 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // SUVersionComparisonProtocol.h 3 | // Sparkle 4 | // 5 | // Created by Andy Matuschak on 12/21/07. 6 | // Copyright 2007 Andy Matuschak. All rights reserved. 7 | // 8 | 9 | #ifndef SUVERSIONCOMPARISONPROTOCOL_H 10 | #define SUVERSIONCOMPARISONPROTOCOL_H 11 | 12 | /*! 13 | @protocol 14 | @abstract Implement this protocol to provide version comparison facilities for Sparkle. 15 | */ 16 | @protocol SUVersionComparison 17 | 18 | /*! 19 | @method 20 | @abstract An abstract method to compare two version strings. 21 | @discussion Should return NSOrderedAscending if b > a, NSOrderedDescending if b < a, and NSOrderedSame if they are equivalent. 22 | */ 23 | - (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB; 24 | 25 | @end 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /extractors/ComAdiumXAdiumX.py: -------------------------------------------------------------------------------- 1 | from ScriptingBridge import * 2 | from Extractor import * 3 | from Utilities import * 4 | 5 | class ComAdiumXAdiumX(Extractor): 6 | 7 | def __init__(self): 8 | super( ComAdiumXAdiumX, self ).__init__() 9 | self.adium = SBApplication.applicationWithBundleIdentifier_("com.adiumX.adiumX") 10 | 11 | def clues(self): 12 | chat = self.adium.activeChat() 13 | if not chat.exists(): return [] 14 | account_type = chat.ID().split(".")[0].lower() 15 | username = ".".join( chat.ID().split(".")[1:] ) 16 | if account_type in ['aim', 'mac']: 17 | self.clues_from_aim( username ) 18 | elif account_type in ['jabber', 'gtalk', 'livejournal']: 19 | self.clues_from_jabber( username ) 20 | elif account_type in ['yahoo!']: 21 | self.clues_from_yahoo( username ) 22 | -------------------------------------------------------------------------------- /extractors/OrgMozillaFirefox.py: -------------------------------------------------------------------------------- 1 | from Extractor import * 2 | from AppKit import * 3 | from Utilities import * 4 | 5 | class OrgMozillaFirefox( Extractor ): 6 | 7 | # Firefox completely refuses to cooperate with the scripting 8 | # bridge. Annoying as hell. 9 | 10 | def __init__(self): 11 | super( OrgMozillaFirefox, self ).__init__() 12 | 13 | # thanks, mark 14 | # DANGER - UTF8 here! 15 | script = u""" 16 | tell application "Firefox" 17 | get \u00ABclass curl\u00BB of front window 18 | end tell 19 | """ 20 | 21 | self.ascript = NSAppleScript.alloc().initWithSource_( script ) 22 | 23 | def clues(self): 24 | 25 | [ ret, error ] = self.ascript.executeAndReturnError_( None ) 26 | if error: 27 | print("ERROR: %s"%repr(error)) 28 | return 29 | if ret: 30 | url = ret.stringValue() 31 | self.clues_from_url( url ) 32 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006 Andy Matuschak 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | { 2 | IBClasses = ( 3 | { 4 | CLASS = FirstResponder; 5 | LANGUAGE = ObjC; 6 | SUPERCLASS = NSObject; 7 | }, 8 | { 9 | CLASS = NSApplication; 10 | LANGUAGE = ObjC; 11 | SUPERCLASS = NSResponder; 12 | }, 13 | { 14 | CLASS = NSObject; 15 | LANGUAGE = ObjC; 16 | }, 17 | { 18 | ACTIONS = { 19 | installUpdate = id; 20 | remindMeLater = id; 21 | skipThisVersion = id; 22 | }; 23 | CLASS = SUUpdateAlert; 24 | LANGUAGE = ObjC; 25 | OUTLETS = { 26 | delegate = id; 27 | description = NSTextField; 28 | releaseNotesView = WebView; 29 | }; 30 | SUPERCLASS = SUWindowController; 31 | }, 32 | { 33 | CLASS = SUWindowController; 34 | LANGUAGE = ObjC; 35 | SUPERCLASS = NSWindowController; 36 | } 37 | ); 38 | IBVersion = 1; 39 | } -------------------------------------------------------------------------------- /PyShelfApplication.py: -------------------------------------------------------------------------------- 1 | from Foundation import * 2 | from AppKit import * 3 | 4 | # based on example at http://svn.red-bean.com/pyobjc/trunk/pyobjc/pyobjc-framework-Cocoa/Examples/AppKit/HotKeyPython/ 5 | 6 | #from Carbon.CarbonEvt import RegisterEventHotKey, GetApplicationEventTarget 7 | #from Carbon.Events import cmdKey, controlKey 8 | 9 | kEventHotKeyPressedSubtype = 6 10 | kEventHotKeyReleasedSubtype = 9 11 | 12 | class PyShelfApplication(NSApplication): 13 | def finishLaunching(self): 14 | super(PyShelfApplication, self).finishLaunching() 15 | # register cmd-control-J 16 | #if NSUserDefaults.standardUserDefaults().boolForKey_("useHotkey"): 17 | # self.hotKeyRef = RegisterEventHotKey(38, cmdKey | controlKey, (0, 0), GetApplicationEventTarget(), 0) 18 | 19 | def sendEvent_(self, theEvent): 20 | if theEvent.type() == NSSystemDefined and theEvent.subtype() == kEventHotKeyPressedSubtype: 21 | if NSUserDefaults.standardUserDefaults().boolForKey_("useHotkey"): 22 | #self.activateIgnoringOtherApps_(True) 23 | if self.delegate(): 24 | self.delegate().hotKeyPressed() 25 | 26 | super(PyShelfApplication, self).sendEvent_(theEvent) 27 | 28 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Headers/SUAppcastItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // SUAppcastItem.h 3 | // Sparkle 4 | // 5 | // Created by Andy Matuschak on 3/12/06. 6 | // Copyright 2006 Andy Matuschak. All rights reserved. 7 | // 8 | 9 | #ifndef SUAPPCASTITEM_H 10 | #define SUAPPCASTITEM_H 11 | 12 | @interface SUAppcastItem : NSObject { 13 | NSString *title; 14 | NSDate *date; 15 | NSString *itemDescription; 16 | 17 | NSURL *releaseNotesURL; 18 | 19 | NSString *DSASignature; 20 | NSString *minimumSystemVersion; 21 | 22 | NSURL *fileURL; 23 | NSString *versionString; 24 | NSString *displayVersionString; 25 | 26 | NSDictionary *propertiesDictionary; 27 | } 28 | 29 | // Initializes with data from a dictionary provided by the RSS class. 30 | - initWithDictionary:(NSDictionary *)dict; 31 | 32 | - (NSString *)title; 33 | - (NSString *)versionString; 34 | - (NSString *)displayVersionString; 35 | - (NSDate *)date; 36 | - (NSString *)itemDescription; 37 | - (NSURL *)releaseNotesURL; 38 | - (NSURL *)fileURL; 39 | - (NSString *)DSASignature; 40 | - (NSString *)minimumSystemVersion; 41 | 42 | // Returns the dictionary provided in initWithDictionary; this might be useful later for extensions. 43 | - (NSDictionary *)propertiesDictionary; 44 | 45 | @end 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | http://code.movieos.org/shelf/ 2 | 3 | Shelf is an app for MacOS that looks at the current foreground application, and tries to figure out if what you’re 4 | looking at corresponds to a person in your Address Book. Then it’ll tell you things about them. 5 | 6 | ## Using Shelf 7 | 8 | Just run it. It’ll sit in the background, and watch the foreground application. If it can tie something you’re looking 9 | at (the current url in your web browser, for instance, or the target of an open chat) to a person in your Address Book, 10 | it’ll open a window and show you their name and picture, and it’ll try to fetch RSS feeds for any URLs in their address 11 | card. 12 | 13 | It’s possible that you don’t have a very deep address book (most people just have email addresses, the URL field is 14 | hidden by default in Address Book.app). If you want a demo, just download my VCard file and import it. Then when you 15 | look at this page Shelf should figure out that you’re looking at me, and show you my recent Flickr photos, blog entries, 16 | etc. 17 | 18 | ## Building Shelf 19 | 20 | In the root of the checkout, just run (assuming a recent macos) 21 | 22 | make dist 23 | 24 | This will write out a Shelf.app in the local dist folder. Don't use the -A flag to py2app under Snow Leopard, it doesn't 25 | work for some reason. 26 | -------------------------------------------------------------------------------- /relmeparser.py: -------------------------------------------------------------------------------- 1 | # slightly based on http://phildawes.net/microformats/microformatparser.html 2 | 3 | from sgmllib import SGMLParser, SGMLParseError 4 | import urlparse 5 | 6 | class RelMeProcessor(SGMLParser): 7 | 8 | def __init__(self, *stuff, **other): 9 | SGMLParser.__init__(self, *stuff, **other) 10 | self.rels = [] 11 | 12 | def _getattr(self,name,attrs): 13 | for attr in attrs: 14 | if name == attr[0]: return attr[1] 15 | 16 | def start_a(self, attrs): 17 | if self._getattr('rel',attrs) == 'me': 18 | self.rels.append( self._getattr('href',attrs) ) 19 | 20 | def parse(inp, base = "http://dummy/url"): 21 | m = RelMeProcessor(base) 22 | try: 23 | str = inp.read() 24 | except AttributeError: 25 | str = inp 26 | if not str: 27 | return [] 28 | try: 29 | m.feed(str) 30 | m.close() 31 | return map( lambda u: urlparse.urljoin( base, u ), m.rels ) 32 | except SGMLParseError: 33 | return [] 34 | 35 | if __name__ == "__main__": 36 | import urllib, sys 37 | if len(sys.argv) == 1: 38 | print "Usage:",sys.argv[0],"" 39 | sys.exit(0) 40 | else: 41 | for url in sys.argv[1:]: 42 | print(repr(parse(urllib.urlopen(url),url))) 43 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # 2 | # main.py 3 | # PyShelf 4 | # 5 | # Created by Tom Insam on 06/01/2008. 6 | # Copyright __MyCompanyName__ 2008. All rights reserved. 7 | # 8 | 9 | #import modules required by application 10 | import objc 11 | import Foundation 12 | import AppKit 13 | import os 14 | from AppKit import * 15 | from PyObjCTools import AppHelper 16 | 17 | # put external deps here where py2app can find them 18 | import urllib, urllib2 19 | import sgmllib 20 | import cgi 21 | import xml.dom.minidom 22 | import HTMLParser 23 | import ScriptingBridge 24 | import urlparse 25 | import json 26 | import WebKit 27 | 28 | # import sparkle framework 29 | base_path = os.path.join(os.path.dirname(os.getcwd()), 'Frameworks') 30 | bundle_path = os.path.abspath(os.path.join(base_path, 'Sparkle.framework')) 31 | #objc.loadBundle('Sparkle', globals(), bundle_path=bundle_path) 32 | 33 | NSUserDefaults.standardUserDefaults().registerDefaults_({ 34 | 'googleSocial':False, 35 | 'googleSocialContext':False, 36 | 'bringAppForward':True, 37 | 'alwaysOnTop':True, 38 | 'firstRun':True, 39 | 'debug':False 40 | }) 41 | 42 | # import modules containing classes required to start application and load MainMenu.nib 43 | import PyShelfApplication 44 | import PyShelfWindowController 45 | 46 | # pass control to AppKit 47 | AppHelper.runEventLoop() 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Script for building the example. 4 | 5 | Usage: 6 | python setup.py py2app 7 | """ 8 | from distutils.core import setup 9 | import py2app 10 | from glob import glob 11 | 12 | version = "0.0.15" # update in Cache.py as well, for the User-Agent string 13 | 14 | plist = dict( 15 | CFBundleName="Shelf", 16 | NSMainNibFile="MainMenu", 17 | NSPrincipalClass='PyShelfApplication', 18 | CFBundleIdentifier="org.jerakeen.pyshelf", # historical 19 | CFBundleShortVersionString=version, 20 | CFBundleVersion=version, 21 | NSHumanReadableCopyright="Copyright 2008 Tom Insam", 22 | 23 | NSAppleScriptEnabled=True, 24 | CFBundleURLTypes=[ 25 | dict( 26 | CFBundleURLName='Shelf callback', 27 | CFBundleURLSchemes=['shelf'], 28 | ) 29 | ], 30 | 31 | # sparkle appcast url, for auto-updates 32 | SUFeedURL="http://code.movieos.org/shelf/appcast/" # doesn't exist, but it's a thing at least. 33 | ) 34 | 35 | setup( 36 | app=["main.py",], 37 | data_files= glob("resources/*.nib") + glob("resources/*.html") + glob("resources/*.gif") + glob("*.py") + glob("*/*.py") + glob("resources/*.css") + glob("resources/*.png"), 38 | options=dict(py2app=dict( 39 | plist=plist, 40 | iconfile="resources/Icon.icns", 41 | frameworks=glob("*.framework"), 42 | )), 43 | ) 44 | 45 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/de.lproj/SUAutomaticUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | doNotInstall 19 | id 20 | installLater 21 | id 22 | installNow 23 | id 24 | 25 | CLASS 26 | SUAutomaticUpdateAlert 27 | LANGUAGE 28 | ObjC 29 | SUPERCLASS 30 | SUWindowController 31 | 32 | 33 | CLASS 34 | FirstResponder 35 | LANGUAGE 36 | ObjC 37 | SUPERCLASS 38 | NSObject 39 | 40 | 41 | CLASS 42 | NSObject 43 | LANGUAGE 44 | ObjC 45 | 46 | 47 | IBVersion 48 | 1 49 | 50 | 51 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | doNotInstall 19 | id 20 | installLater 21 | id 22 | installNow 23 | id 24 | 25 | CLASS 26 | SUAutomaticUpdateAlert 27 | LANGUAGE 28 | ObjC 29 | SUPERCLASS 30 | SUWindowController 31 | 32 | 33 | CLASS 34 | FirstResponder 35 | LANGUAGE 36 | ObjC 37 | SUPERCLASS 38 | NSObject 39 | 40 | 41 | CLASS 42 | NSObject 43 | LANGUAGE 44 | ObjC 45 | 46 | 47 | IBVersion 48 | 1 49 | 50 | 51 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/es.lproj/SUAutomaticUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | doNotInstall 19 | id 20 | installLater 21 | id 22 | installNow 23 | id 24 | 25 | CLASS 26 | SUAutomaticUpdateAlert 27 | LANGUAGE 28 | ObjC 29 | SUPERCLASS 30 | SUWindowController 31 | 32 | 33 | CLASS 34 | FirstResponder 35 | LANGUAGE 36 | ObjC 37 | SUPERCLASS 38 | NSObject 39 | 40 | 41 | CLASS 42 | NSObject 43 | LANGUAGE 44 | ObjC 45 | 46 | 47 | IBVersion 48 | 1 49 | 50 | 51 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/fr.lproj/SUAutomaticUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | doNotInstall 19 | id 20 | installLater 21 | id 22 | installNow 23 | id 24 | 25 | CLASS 26 | SUAutomaticUpdateAlert 27 | LANGUAGE 28 | ObjC 29 | SUPERCLASS 30 | SUWindowController 31 | 32 | 33 | CLASS 34 | FirstResponder 35 | LANGUAGE 36 | ObjC 37 | SUPERCLASS 38 | NSObject 39 | 40 | 41 | CLASS 42 | NSObject 43 | LANGUAGE 44 | ObjC 45 | 46 | 47 | IBVersion 48 | 1 49 | 50 | 51 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/it.lproj/SUAutomaticUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | doNotInstall 19 | id 20 | installLater 21 | id 22 | installNow 23 | id 24 | 25 | CLASS 26 | SUAutomaticUpdateAlert 27 | LANGUAGE 28 | ObjC 29 | SUPERCLASS 30 | SUWindowController 31 | 32 | 33 | CLASS 34 | FirstResponder 35 | LANGUAGE 36 | ObjC 37 | SUPERCLASS 38 | NSObject 39 | 40 | 41 | CLASS 42 | NSObject 43 | LANGUAGE 44 | ObjC 45 | 46 | 47 | IBVersion 48 | 1 49 | 50 | 51 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/nl.lproj/SUAutomaticUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | doNotInstall 19 | id 20 | installLater 21 | id 22 | installNow 23 | id 24 | 25 | CLASS 26 | SUAutomaticUpdateAlert 27 | LANGUAGE 28 | ObjC 29 | SUPERCLASS 30 | SUWindowController 31 | 32 | 33 | CLASS 34 | FirstResponder 35 | LANGUAGE 36 | ObjC 37 | SUPERCLASS 38 | NSObject 39 | 40 | 41 | CLASS 42 | NSObject 43 | LANGUAGE 44 | ObjC 45 | 46 | 47 | IBVersion 48 | 1 49 | 50 | 51 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/ru.lproj/SUAutomaticUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | doNotInstall 19 | id 20 | installLater 21 | id 22 | installNow 23 | id 24 | 25 | CLASS 26 | SUAutomaticUpdateAlert 27 | LANGUAGE 28 | ObjC 29 | SUPERCLASS 30 | SUWindowController 31 | 32 | 33 | CLASS 34 | FirstResponder 35 | LANGUAGE 36 | ObjC 37 | SUPERCLASS 38 | NSObject 39 | 40 | 41 | CLASS 42 | NSObject 43 | LANGUAGE 44 | ObjC 45 | 46 | 47 | IBVersion 48 | 1 49 | 50 | 51 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/sv.lproj/SUAutomaticUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | doNotInstall 19 | id 20 | installLater 21 | id 22 | installNow 23 | id 24 | 25 | CLASS 26 | SUAutomaticUpdateAlert 27 | LANGUAGE 28 | ObjC 29 | SUPERCLASS 30 | SUWindowController 31 | 32 | 33 | CLASS 34 | FirstResponder 35 | LANGUAGE 36 | ObjC 37 | SUPERCLASS 38 | NSObject 39 | 40 | 41 | CLASS 42 | NSObject 43 | LANGUAGE 44 | ObjC 45 | 46 | 47 | IBVersion 48 | 1 49 | 50 | 51 | -------------------------------------------------------------------------------- /extractors/ComAppleIChat.py: -------------------------------------------------------------------------------- 1 | from ScriptingBridge import * 2 | from Extractor import * 3 | from Utilities import * 4 | 5 | class ComAppleIChat(Extractor): 6 | 7 | def __init__(self): 8 | super( ComAppleIChat, self ).__init__() 9 | self.ichat = SBApplication.applicationWithBundleIdentifier_("com.apple.iChat") 10 | 11 | def clues(self): 12 | if self.ichat.chats().count() == 0: return [] 13 | # iChat sucks. There's no 'active Chat' variable, so I'm going to 14 | # (a) Guess based on the window name, looking for a name, then 15 | # (b) Just use the first element of the chats() array. Which is the 16 | # first opened chat. TODO - I can do better - use the most recently updated 17 | # chat. Not _much_ better... 18 | username = None 19 | title = self.ichat.windows()[0].name() 20 | match = re.search(r'Chat with (.*)', title) # probably won't work with localization 21 | if match: 22 | name = match.group(1) 23 | chats = filter(lambda c: c.participants()[0].fullName() == name, self.ichat.chats()) 24 | if chats: 25 | username = chats[0].participants()[0].handle() 26 | 27 | # give up, use first chat. 28 | if not username: 29 | username = self.ichat.chats()[0].participants()[0].handle() 30 | 31 | if username: 32 | self.clues_from_aim( username ) 33 | self.clues_from_jabber( username ) 34 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/SUStatus.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | CLASS 17 | NSApplication 18 | LANGUAGE 19 | ObjC 20 | SUPERCLASS 21 | NSResponder 22 | 23 | 24 | CLASS 25 | FirstResponder 26 | LANGUAGE 27 | ObjC 28 | SUPERCLASS 29 | NSObject 30 | 31 | 32 | CLASS 33 | NSObject 34 | LANGUAGE 35 | ObjC 36 | 37 | 38 | CLASS 39 | SUStatusController 40 | LANGUAGE 41 | ObjC 42 | OUTLETS 43 | 44 | actionButton 45 | NSButton 46 | progressBar 47 | NSProgressIndicator 48 | 49 | SUPERCLASS 50 | SUWindowController 51 | 52 | 53 | IBVersion 54 | 1 55 | 56 | 57 | -------------------------------------------------------------------------------- /providers/BasicProvider.py: -------------------------------------------------------------------------------- 1 | from Provider import * 2 | from urllib import quote 3 | from Utilities import * 4 | 5 | import time 6 | 7 | class BasicAtom( ProviderAtom ): 8 | 9 | def sortOrder(self): 10 | return MAX_SORT_ORDER 11 | 12 | def content(self): 13 | clue = self.provider.clue 14 | content = "" 15 | 16 | if clue.emails(): 17 | content += "

" 18 | for email in clue.emails(): 19 | content += "%s "%( email, email ) 20 | content += "

" 21 | 22 | if clue.birthday(): 23 | content += "

Born %s

"%time.strftime("%B %d, %Y", clue.birthday()) 24 | 25 | addresses = clue.addresses() 26 | if len(addresses) > 0: 27 | address = addresses[0] 28 | bits = [ address[atom] for atom in filter(lambda a: a in address, ['Street', 'City', 'Zip']) ] 29 | joined = ", ".join(bits) 30 | if bits: 31 | content += '

%s

'%( quote(joined.encode("utf-8")), joined ) 32 | 33 | if clue.ab_urls(): 34 | content += "

" + "
".join(map(lambda url: "%s"%(url, url), clue.ab_urls())) + "

" 35 | 36 | if not content: 37 | content = "

No address book information for %s

"%clue.forename() 38 | 39 | return content 40 | 41 | class BasicProvider( Provider ): 42 | 43 | def atomClass(self): 44 | return BasicAtom 45 | 46 | def provide(self): 47 | self.atoms = [ BasicAtom(self, "") ] 48 | -------------------------------------------------------------------------------- /providers/TwitterProvider.py: -------------------------------------------------------------------------------- 1 | from FeedProvider import * 2 | from Utilities import * 3 | 4 | # cunning subclassing of feedprovider here as a demo. 5 | 6 | class TwitterAtom( FeedAtom ): 7 | 8 | def specialCaseFeedUrl( self, url ): 9 | # deriving the feed url from the username is faster than 10 | # fetching the HTML first. 11 | username = re.search(r'twitter\.com/([^/]+)', url).group(1) 12 | if username == 'home': return "" 13 | return "http://twitter.com/statuses/user_timeline/%s.atom"%(username) 14 | 15 | def username(self): 16 | return NSUserDefaults.standardUserDefaults().stringForKey_("twitterUsername") 17 | 18 | def password(self): 19 | NSUserDefaults.standardUserDefaults().stringForKey_("twitterPassword") 20 | 21 | def htmlForFeed( self, url, feed, stale = False ): 22 | item = feed.entries[0] 23 | html = "" 24 | 25 | date = None 26 | if 'published_parsed' in item: date = item.published_parsed 27 | elif 'updated_parsed' in item: date = item.updated_parsed 28 | if date: 29 | #html += u'%s'%( time.strftime("%b %d", date ) ) 30 | ago = time_ago_in_words(date) + " ago" 31 | html += u'%s'%ago 32 | 33 | html += '

%s

'%( item.title ) 34 | return html 35 | 36 | def timeout(self): 37 | return 180 38 | 39 | class TwitterProvider( FeedProvider ): 40 | 41 | def atomClass(self): 42 | return TwitterAtom 43 | 44 | def urls(self): 45 | return self.clue.takeUrls(r'twitter\.com/.') 46 | 47 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdatePermissionPrompt.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | finishPrompt 19 | id 20 | toggleMoreInfo 21 | id 22 | 23 | CLASS 24 | SUUpdatePermissionPrompt 25 | LANGUAGE 26 | ObjC 27 | OUTLETS 28 | 29 | delegate 30 | id 31 | descriptionTextField 32 | NSTextField 33 | moreInfoButton 34 | NSButton 35 | moreInfoView 36 | NSView 37 | 38 | SUPERCLASS 39 | SUWindowController 40 | 41 | 42 | CLASS 43 | FirstResponder 44 | LANGUAGE 45 | ObjC 46 | SUPERCLASS 47 | NSObject 48 | 49 | 50 | CLASS 51 | NSObject 52 | LANGUAGE 53 | ObjC 54 | 55 | 56 | IBVersion 57 | 1 58 | 59 | 60 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | finishPrompt 19 | id 20 | toggleMoreInfo 21 | id 22 | 23 | CLASS 24 | SUUpdatePermissionPrompt 25 | LANGUAGE 26 | ObjC 27 | OUTLETS 28 | 29 | delegate 30 | id 31 | descriptionTextField 32 | NSTextField 33 | moreInfoButton 34 | NSButton 35 | moreInfoView 36 | NSView 37 | 38 | SUPERCLASS 39 | SUWindowController 40 | 41 | 42 | CLASS 43 | FirstResponder 44 | LANGUAGE 45 | ObjC 46 | SUPERCLASS 47 | NSObject 48 | 49 | 50 | CLASS 51 | NSObject 52 | LANGUAGE 53 | ObjC 54 | 55 | 56 | IBVersion 57 | 1 58 | 59 | 60 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdatePermissionPrompt.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | finishPrompt 19 | id 20 | toggleMoreInfo 21 | id 22 | 23 | CLASS 24 | SUUpdatePermissionPrompt 25 | LANGUAGE 26 | ObjC 27 | OUTLETS 28 | 29 | delegate 30 | id 31 | descriptionTextField 32 | NSTextField 33 | moreInfoButton 34 | NSButton 35 | moreInfoView 36 | NSView 37 | 38 | SUPERCLASS 39 | SUWindowController 40 | 41 | 42 | CLASS 43 | FirstResponder 44 | LANGUAGE 45 | ObjC 46 | SUPERCLASS 47 | NSObject 48 | 49 | 50 | CLASS 51 | NSObject 52 | LANGUAGE 53 | ObjC 54 | 55 | 56 | IBVersion 57 | 1 58 | 59 | 60 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdatePermissionPrompt.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | finishPrompt 19 | id 20 | toggleMoreInfo 21 | id 22 | 23 | CLASS 24 | SUUpdatePermissionPrompt 25 | LANGUAGE 26 | ObjC 27 | OUTLETS 28 | 29 | delegate 30 | id 31 | descriptionTextField 32 | NSTextField 33 | moreInfoButton 34 | NSButton 35 | moreInfoView 36 | NSView 37 | 38 | SUPERCLASS 39 | SUWindowController 40 | 41 | 42 | CLASS 43 | FirstResponder 44 | LANGUAGE 45 | ObjC 46 | SUPERCLASS 47 | NSObject 48 | 49 | 50 | CLASS 51 | NSObject 52 | LANGUAGE 53 | ObjC 54 | 55 | 56 | IBVersion 57 | 1 58 | 59 | 60 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdatePermissionPrompt.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | finishPrompt 19 | id 20 | toggleMoreInfo 21 | id 22 | 23 | CLASS 24 | SUUpdatePermissionPrompt 25 | LANGUAGE 26 | ObjC 27 | OUTLETS 28 | 29 | delegate 30 | id 31 | descriptionTextField 32 | NSTextField 33 | moreInfoButton 34 | NSButton 35 | moreInfoView 36 | NSView 37 | 38 | SUPERCLASS 39 | SUWindowController 40 | 41 | 42 | CLASS 43 | FirstResponder 44 | LANGUAGE 45 | ObjC 46 | SUPERCLASS 47 | NSObject 48 | 49 | 50 | CLASS 51 | NSObject 52 | LANGUAGE 53 | ObjC 54 | 55 | 56 | IBVersion 57 | 1 58 | 59 | 60 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdatePermissionPrompt.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | finishPrompt 19 | id 20 | toggleMoreInfo 21 | id 22 | 23 | CLASS 24 | SUUpdatePermissionPrompt 25 | LANGUAGE 26 | ObjC 27 | OUTLETS 28 | 29 | delegate 30 | id 31 | descriptionTextField 32 | NSTextField 33 | moreInfoButton 34 | NSButton 35 | moreInfoView 36 | NSView 37 | 38 | SUPERCLASS 39 | SUWindowController 40 | 41 | 42 | CLASS 43 | FirstResponder 44 | LANGUAGE 45 | ObjC 46 | SUPERCLASS 47 | NSObject 48 | 49 | 50 | CLASS 51 | NSObject 52 | LANGUAGE 53 | ObjC 54 | 55 | 56 | IBVersion 57 | 1 58 | 59 | 60 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdatePermissionPrompt.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | NSObject 10 | LANGUAGE 11 | ObjC 12 | 13 | 14 | CLASS 15 | SUWindowController 16 | LANGUAGE 17 | ObjC 18 | SUPERCLASS 19 | NSWindowController 20 | 21 | 22 | ACTIONS 23 | 24 | finishPrompt 25 | id 26 | toggleMoreInfo 27 | id 28 | 29 | CLASS 30 | SUUpdatePermissionPrompt 31 | LANGUAGE 32 | ObjC 33 | OUTLETS 34 | 35 | delegate 36 | id 37 | descriptionTextField 38 | NSTextField 39 | moreInfoButton 40 | NSButton 41 | moreInfoView 42 | NSView 43 | 44 | SUPERCLASS 45 | SUWindowController 46 | 47 | 48 | CLASS 49 | FirstResponder 50 | LANGUAGE 51 | ObjC 52 | SUPERCLASS 53 | NSObject 54 | 55 | 56 | IBVersion 57 | 1 58 | 59 | 60 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdatePermissionPrompt.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | ACTIONS 17 | 18 | finishPrompt 19 | id 20 | toggleMoreInfo 21 | id 22 | 23 | CLASS 24 | SUUpdatePermissionPrompt 25 | LANGUAGE 26 | ObjC 27 | OUTLETS 28 | 29 | delegate 30 | id 31 | descriptionTextField 32 | NSTextField 33 | moreInfoButton 34 | NSButton 35 | moreInfoView 36 | NSView 37 | 38 | SUPERCLASS 39 | SUWindowController 40 | 41 | 42 | CLASS 43 | FirstResponder 44 | LANGUAGE 45 | ObjC 46 | SUPERCLASS 47 | NSObject 48 | 49 | 50 | CLASS 51 | NSObject 52 | LANGUAGE 53 | ObjC 54 | 55 | 56 | IBVersion 57 | 1 58 | 59 | 60 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | flickr userid / username disambiguation, somehow? Do they result in the same feed url? 2 | 3 | Sort out licenses! 4 | 5 | display online/offline status if possible. 6 | 7 | use gravatar to fetch picture if there's nothing in addressbook 8 | 9 | consider: maybe it's not worth displaying the window if we have nothing useful to say? 10 | 11 | rename 'atom' to 'block' everywhere 12 | 13 | blocks maybe want a little 'refresh' icon on them? 14 | 15 | Guessed blocks have the 'G' symbol. clicking this symbol should offer to add 16 | it to the address book card. 17 | 18 | Expanding links using the google social graph should be turnoffable. And add 19 | a button to do it on demand. 20 | 21 | Display AIM and facebook statues somehow. 22 | 23 | Normalize urls before testing them against the addressbook to catch, eg, deli.cio.us -> delicious.com change 24 | 25 | Follow redirects on urls where possible so we don't repeat sources (eg jerakeen.org/photos appears as a new source, 26 | though it's just a redirect to my flickr page now) 27 | 28 | Google Chrome support is very rough, though this is mostly a limitation of the lousy AppleScript it offers. Keep an 29 | eye on it. 30 | 31 | First display of shelf for a person seems sluggish - is it fetching things from the network before deciding if it needs 32 | to display the card? I'd rather open the window (in the background) as soon as possible to provide feedback that we 33 | might know something. 34 | 35 | If the clue doesn't change, try not to do rediscovery of it. Or at least profile it and verify to myself that it's 36 | cheap. But I'd rather just not do re-discovery. The point is to get active app pings cheap enough that I can drop the 37 | hearbeat to one second. 38 | -------------------------------------------------------------------------------- /resources/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin:0; 3 | padding:0; 4 | } 5 | 6 | body { 7 | font-family: "Lucida Grande"; 8 | font-size: 11px; 9 | overflow-x: hidden; /* supress horizontal scrolling */ 10 | background-color: #dde4eb; 11 | } 12 | 13 | p, img, pre { 14 | margin: 5px 0 0 5px; 15 | } 16 | 17 | /* 18 | Standard mail.app sidebar colors: 19 | #dde4eb - light blue background 20 | #a1afce - border-top of selected item (which turns to white text with 1px vertical shadow) 21 | #b0bedb - top value of gradient 22 | #8194bc - bottom value of gradient 23 | */ 24 | 25 | h3 { 26 | padding: 1px 0 1px 5px; 27 | margin-top: 10px; 28 | border-top: 1px solid a1afce; 29 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#b0bedb), to(#8194bc)); 30 | white-space: nowrap; 31 | } 32 | 33 | h3 a { 34 | color: white; 35 | text-shadow: 0px 1px 2px #808080; 36 | } 37 | 38 | h3,p,a { 39 | text-decoration: none; 40 | } 41 | 42 | p a { 43 | color: black; 44 | } 45 | 46 | h3 a:hover, p a:hover { 47 | text-decoration: underline; 48 | } 49 | 50 | h3 img.spinner { 51 | margin: 0; 52 | padding: 0; 53 | float: right; 54 | margin-right: 5px; 55 | } 56 | 57 | h3 img.guess { 58 | margin: 0; 59 | padding: 0; 60 | float: right; 61 | margin-right: 5px; 62 | } 63 | 64 | img.flickr-image { 65 | width: 40px; 66 | height: 40px; 67 | float: left; 68 | margin-right: 5px; 69 | border: none; 70 | } 71 | 72 | span.feed-date { 73 | float: right; 74 | color: #444; 75 | font-size: 80%; 76 | margin-top: 5px; 77 | } 78 | 79 | p.feed-content { 80 | color: #444; 81 | font-size: 80%; 82 | margin-top: 2px; 83 | } -------------------------------------------------------------------------------- /lib/autorss.py: -------------------------------------------------------------------------------- 1 | """Find RSS feed from site's LINK tag""" 2 | 3 | __author__ = "Mark Pilgrim (f8dy@diveintomark.org)" 4 | __copyright__ = "Copyright 2002, Mark Pilgrim" 5 | __license__ = "Python" 6 | 7 | # messed around with by Tom to do Atom as well. 8 | 9 | import urllib, urlparse 10 | from sgmllib import SGMLParser 11 | 12 | BUFFERSIZE = 1024 13 | 14 | class LinkParser(SGMLParser): 15 | def reset(self): 16 | SGMLParser.reset(self) 17 | self.href = '' 18 | 19 | def do_link(self, attrs): 20 | if not ('rel', 'alternate') in attrs: return 21 | typelist = [e[1] for e in attrs if e[0]=='type'] 22 | if len(typelist) == 0: return 23 | if typelist[0] !='application/rss+xml' and typelist[0] != 'application/atom+xml': return 24 | hreflist = [e[1] for e in attrs if e[0]=='href'] 25 | if not hreflist: return 26 | self.href = hreflist[0] 27 | self.setnomoretags() 28 | 29 | def end_head(self, attrs): 30 | self.setnomoretags() 31 | start_body = end_head 32 | 33 | def getRSSLinkFromHTMLSource(htmlSource): 34 | try: 35 | parser = LinkParser() 36 | parser.feed(htmlSource) 37 | return parser.href 38 | except: 39 | return '' 40 | 41 | def getRSSLink(url): 42 | try: 43 | usock = urllib.urlopen(url) 44 | parser = LinkParser() 45 | while 1: 46 | buffer = usock.read(BUFFERSIZE) 47 | parser.feed(buffer) 48 | if parser.nomoretags: break 49 | if len(buffer) < BUFFERSIZE: break 50 | usock.close() 51 | if parser.href == '': return None 52 | return urlparse.urljoin(url, parser.href) 53 | except: 54 | return None 55 | 56 | if __name__ == '__main__': 57 | import sys 58 | print getRSSLink(sys.argv[1]) 59 | 60 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | CLASS 17 | NSApplication 18 | LANGUAGE 19 | ObjC 20 | SUPERCLASS 21 | NSResponder 22 | 23 | 24 | ACTIONS 25 | 26 | installUpdate 27 | id 28 | remindMeLater 29 | id 30 | skipThisVersion 31 | id 32 | 33 | CLASS 34 | SUUpdateAlert 35 | LANGUAGE 36 | ObjC 37 | OUTLETS 38 | 39 | delegate 40 | id 41 | description 42 | NSTextField 43 | releaseNotesView 44 | WebView 45 | 46 | SUPERCLASS 47 | SUWindowController 48 | 49 | 50 | CLASS 51 | FirstResponder 52 | LANGUAGE 53 | ObjC 54 | SUPERCLASS 55 | NSObject 56 | 57 | 58 | CLASS 59 | NSObject 60 | LANGUAGE 61 | ObjC 62 | 63 | 64 | IBVersion 65 | 1 66 | 67 | 68 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | CLASS 17 | NSApplication 18 | LANGUAGE 19 | ObjC 20 | SUPERCLASS 21 | NSResponder 22 | 23 | 24 | ACTIONS 25 | 26 | installUpdate 27 | id 28 | remindMeLater 29 | id 30 | skipThisVersion 31 | id 32 | 33 | CLASS 34 | SUUpdateAlert 35 | LANGUAGE 36 | ObjC 37 | OUTLETS 38 | 39 | delegate 40 | id 41 | description 42 | NSTextField 43 | releaseNotesView 44 | WebView 45 | 46 | SUPERCLASS 47 | SUWindowController 48 | 49 | 50 | CLASS 51 | FirstResponder 52 | LANGUAGE 53 | ObjC 54 | SUPERCLASS 55 | NSObject 56 | 57 | 58 | CLASS 59 | NSObject 60 | LANGUAGE 61 | ObjC 62 | 63 | 64 | IBVersion 65 | 1 66 | 67 | 68 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | CLASS 17 | NSApplication 18 | LANGUAGE 19 | ObjC 20 | SUPERCLASS 21 | NSResponder 22 | 23 | 24 | ACTIONS 25 | 26 | installUpdate 27 | id 28 | remindMeLater 29 | id 30 | skipThisVersion 31 | id 32 | 33 | CLASS 34 | SUUpdateAlert 35 | LANGUAGE 36 | ObjC 37 | OUTLETS 38 | 39 | delegate 40 | id 41 | description 42 | NSTextField 43 | releaseNotesView 44 | WebView 45 | 46 | SUPERCLASS 47 | SUWindowController 48 | 49 | 50 | CLASS 51 | FirstResponder 52 | LANGUAGE 53 | ObjC 54 | SUPERCLASS 55 | NSObject 56 | 57 | 58 | CLASS 59 | NSObject 60 | LANGUAGE 61 | ObjC 62 | 63 | 64 | IBVersion 65 | 1 66 | 67 | 68 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | CLASS 17 | NSApplication 18 | LANGUAGE 19 | ObjC 20 | SUPERCLASS 21 | NSResponder 22 | 23 | 24 | ACTIONS 25 | 26 | installUpdate 27 | id 28 | remindMeLater 29 | id 30 | skipThisVersion 31 | id 32 | 33 | CLASS 34 | SUUpdateAlert 35 | LANGUAGE 36 | ObjC 37 | OUTLETS 38 | 39 | delegate 40 | id 41 | description 42 | NSTextField 43 | releaseNotesView 44 | WebView 45 | 46 | SUPERCLASS 47 | SUWindowController 48 | 49 | 50 | CLASS 51 | FirstResponder 52 | LANGUAGE 53 | ObjC 54 | SUPERCLASS 55 | NSObject 56 | 57 | 58 | CLASS 59 | NSObject 60 | LANGUAGE 61 | ObjC 62 | 63 | 64 | IBVersion 65 | 1 66 | 67 | 68 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | CLASS 17 | NSApplication 18 | LANGUAGE 19 | ObjC 20 | SUPERCLASS 21 | NSResponder 22 | 23 | 24 | ACTIONS 25 | 26 | installUpdate 27 | id 28 | remindMeLater 29 | id 30 | skipThisVersion 31 | id 32 | 33 | CLASS 34 | SUUpdateAlert 35 | LANGUAGE 36 | ObjC 37 | OUTLETS 38 | 39 | delegate 40 | id 41 | description 42 | NSTextField 43 | releaseNotesView 44 | WebView 45 | 46 | SUPERCLASS 47 | SUWindowController 48 | 49 | 50 | CLASS 51 | FirstResponder 52 | LANGUAGE 53 | ObjC 54 | SUPERCLASS 55 | NSObject 56 | 57 | 58 | CLASS 59 | NSObject 60 | LANGUAGE 61 | ObjC 62 | 63 | 64 | IBVersion 65 | 1 66 | 67 | 68 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | CLASS 17 | NSApplication 18 | LANGUAGE 19 | ObjC 20 | SUPERCLASS 21 | NSResponder 22 | 23 | 24 | ACTIONS 25 | 26 | installUpdate 27 | id 28 | remindMeLater 29 | id 30 | skipThisVersion 31 | id 32 | 33 | CLASS 34 | SUUpdateAlert 35 | LANGUAGE 36 | ObjC 37 | OUTLETS 38 | 39 | delegate 40 | id 41 | description 42 | NSTextField 43 | releaseNotesView 44 | WebView 45 | 46 | SUPERCLASS 47 | SUWindowController 48 | 49 | 50 | CLASS 51 | FirstResponder 52 | LANGUAGE 53 | ObjC 54 | SUPERCLASS 55 | NSObject 56 | 57 | 58 | CLASS 59 | NSObject 60 | LANGUAGE 61 | ObjC 62 | 63 | 64 | IBVersion 65 | 1 66 | 67 | 68 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdateAlert.nib/classes.nib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IBClasses 6 | 7 | 8 | CLASS 9 | SUWindowController 10 | LANGUAGE 11 | ObjC 12 | SUPERCLASS 13 | NSWindowController 14 | 15 | 16 | CLASS 17 | NSApplication 18 | LANGUAGE 19 | ObjC 20 | SUPERCLASS 21 | NSResponder 22 | 23 | 24 | ACTIONS 25 | 26 | installUpdate 27 | id 28 | remindMeLater 29 | id 30 | skipThisVersion 31 | id 32 | 33 | CLASS 34 | SUUpdateAlert 35 | LANGUAGE 36 | ObjC 37 | OUTLETS 38 | 39 | delegate 40 | id 41 | description 42 | NSTextField 43 | releaseNotesView 44 | WebView 45 | 46 | SUPERCLASS 47 | SUWindowController 48 | 49 | 50 | CLASS 51 | FirstResponder 52 | LANGUAGE 53 | ObjC 54 | SUPERCLASS 55 | NSObject 56 | 57 | 58 | CLASS 59 | NSObject 60 | LANGUAGE 61 | ObjC 62 | 63 | 64 | IBVersion 65 | 1 66 | 67 | 68 | -------------------------------------------------------------------------------- /providers/FlickrProvider.py: -------------------------------------------------------------------------------- 1 | from FeedProvider import * 2 | from Utilities import * 3 | 4 | class FlickrAtom( FeedAtom ): 5 | 6 | def htmlForFeed( self, url, feed, stale = False ): 7 | html = "" 8 | 9 | entries = feed.entries 10 | for item in entries[0:4]: 11 | 12 | date = None 13 | if 'published_parsed' in item: date = item.published_parsed 14 | elif 'updated_parsed' in item: date = item.updated_parsed 15 | if date: 16 | #html += u'%s'%( time.strftime("%b %d", date ) ) 17 | ago = time_ago_in_words(date) + " ago" 18 | html += u'%s'%ago 19 | 20 | # ewwwwww 21 | img = re.search(r'"(http://[^"]*_m.jpg)"', item.content[0].value).group(1) 22 | img = re.sub(r'_m.jpg', '_s.jpg', img) 23 | html += ""%( item.link, img ) 24 | html += '

%s

'%( item.link, item.title ) 25 | 26 | if 'content' in item and len(item.content) > 0: 27 | detail = item.content[0].value 28 | elif 'summary' in item and len(item.summary) > 0: 29 | detail = item.summary 30 | if detail: 31 | raw = re.sub(r'<.*?>', '', detail) # strip tags 32 | try: 33 | trimmed = u" ".join( re.split(r'\s+', raw.strip())[0:10] ) 34 | except UnicodeDecodeError: 35 | trimmed = u"invalid unicode content" 36 | html += u'

%s ...

'%( trimmed, item.link ) 37 | 38 | html += '
' 39 | 40 | return html 41 | 42 | class FlickrProvider( FeedProvider ): 43 | 44 | def atomClass(self): 45 | return FlickrAtom 46 | 47 | def urls(self): 48 | return self.clue.takeUrls(r'flickr\.com/(photos|people)/.') 49 | 50 | -------------------------------------------------------------------------------- /extractors/ComGoogleChrome.py: -------------------------------------------------------------------------------- 1 | from ScriptingBridge import * 2 | from Extractor import * 3 | import urlparse 4 | from Utilities import * 5 | 6 | class ComGoogleChrome(Extractor): 7 | 8 | def __init__(self): 9 | super( ComGoogleChrome, self ).__init__() 10 | self.chrome = SBApplication.applicationWithBundleIdentifier_("com.google.Chrome") 11 | 12 | def clues(self): 13 | clues = [] 14 | if not self.chrome.windows().count(): return 15 | 16 | # window 0 is always the foreground window? Seems it. 17 | tab = self.chrome.windows()[ 0 ].activeTab() 18 | 19 | # this is in one version of the chome API, but not others. 20 | if hasattr(tab, "exists"): 21 | if not tab.exists(): return # foreground window is not a browser window. 22 | 23 | self.clues_from_url( tab.URL() ) 24 | if self.done: 25 | return 26 | 27 | # this is in one version of the chome API, but not others. 28 | if hasattr(tab, "source") and tab.source(): 29 | # look for microformats 30 | self.clues_from_html( tab.source(), tab.URL() ) 31 | if self.done: return 32 | 33 | # look for rel="me" links 34 | relme = RelMeParser() 35 | try: 36 | relme.feed( tab.source() ) 37 | except Exception: 38 | # bad page source 39 | return 40 | print_info("Found rel='me' links: %s"%( ",".join(relme.hrefs) ) ) 41 | for link in relme.hrefs: 42 | profile = urlparse.urljoin( tab.URL(), link ) 43 | # not sure what to do here. This might point somewhere 44 | # useful. It might not. In the case of flickr, it points 45 | # to a page that contains enough microformats that we 46 | # might be able to work out who they are, but I really 47 | # don't want to have to _fetch_ the page, not here. 48 | self.clues_from_url( profile ) 49 | 50 | # I'm sure the microformats output format makes sense _somewhere_ 51 | def tree_to_dict(tree): 52 | pass 53 | 54 | 55 | 56 | from sgmllib import SGMLParser 57 | 58 | class RelMeParser(SGMLParser): 59 | def reset(self): 60 | SGMLParser.reset(self) 61 | self.hrefs = [] 62 | 63 | def do_a( self, attrs ): 64 | if not ('rel', 'me') in attrs: return 65 | self.hrefs += filter( lambda l: re.match(r'http', l), [e[1] for e in attrs if e[0]=='href'] ) 66 | 67 | -------------------------------------------------------------------------------- /providers/Provider.py: -------------------------------------------------------------------------------- 1 | from Foundation import * 2 | from AppKit import * 3 | from WebKit import * 4 | from AddressBook import * 5 | 6 | import urllib, urllib2 7 | import base64 8 | import os 9 | import re 10 | from time import time 11 | import traceback 12 | 13 | from Utilities import * 14 | import Cache 15 | 16 | MAX_SORT_ORDER = 999999999999 17 | MIN_SORT_ORDER = 1 18 | 19 | class ProviderAtom( object ): 20 | def __init__(self, provider, url): 21 | self.provider = provider 22 | self.url = url 23 | self.name = url 24 | self.stale = False 25 | self.dead = False 26 | self.error = None 27 | self.guessed = False 28 | 29 | def title(self): 30 | spinner_html = "" 31 | if self.guessed: 32 | spinner_html += ' ' 33 | if self.stale: 34 | spinner_html += self.provider.spinner() 35 | return "

%s%s

"%(self.url,self.name,spinner_html) 36 | 37 | def body(self): 38 | return "" 39 | 40 | def sortOrder(self): 41 | return MIN_SORT_ORDER 42 | 43 | def content(self): 44 | if self.dead: 45 | return "" 46 | elif self.error: 47 | return "" # don't display error self.title() + "
%s
"%html_escape(unicode( self.error )) 48 | else: 49 | body = self.body() 50 | if body or self.stale: 51 | return self.title() + body 52 | return "" 53 | 54 | def changed(self): 55 | self.provider.changed() 56 | 57 | class Provider( object ): 58 | 59 | PROVIDERS = [] 60 | 61 | @classmethod 62 | def addProvider( myClass, classname ): 63 | cls = __import__(classname, globals(), locals(), ['']) 64 | Provider.PROVIDERS.append(getattr(cls, classname)) 65 | 66 | @classmethod 67 | def providers( myClass ): 68 | return Provider.PROVIDERS 69 | 70 | def __init__(self, clue): 71 | #NSLog("** Provider '%s' init"%self.__class__.__name__) 72 | super( Provider, self ).__init__() 73 | self.atoms = [] 74 | self.running = True 75 | self.clue = clue 76 | 77 | def atomClass(self): 78 | return ProviderAtom 79 | 80 | def changed(self): 81 | self.clue.changed() 82 | 83 | def provide( self ): 84 | pass 85 | 86 | def stop(self): 87 | # not enforced, it's just a hint to the processor to stop 88 | NSObject.cancelPreviousPerformRequestsWithTarget_( self ) 89 | self.running = False 90 | 91 | 92 | def spinner(self): 93 | return "" 94 | 95 | -------------------------------------------------------------------------------- /providers/SpotlightProvider.py: -------------------------------------------------------------------------------- 1 | from Provider import * 2 | from urllib import quote 3 | from Utilities import * 4 | 5 | import time 6 | import Cache 7 | 8 | class SpotlightAtom( ProviderAtom ): 9 | def __init__(self, provider, url): 10 | ProviderAtom.__init__( self, provider, url ) 11 | clue = self.provider.clue 12 | self.results = None 13 | if clue.emails(): 14 | if url == 'Recent Messages': 15 | # query messages 16 | predicate = "(kMDItemContentType = 'com.apple.mail.emlx') && (" + \ 17 | '||'.join(["((kMDItemAuthorEmailAddresses = '%s') || (kMDItemRecipientEmailAddresses = '%s'))" % (m, m) for m in clue.emails()]) + \ 18 | ")" 19 | else: 20 | # query attachments 21 | # exclude image, text and html files that are sometimes wrongly attached to emails 22 | exclusions = ['public.image','public.text'] 23 | predicate = "(" + \ 24 | '&&'.join(["(kMDItemContentTypeTree != '%s')" % e for e in exclusions]) + \ 25 | ") && (" + \ 26 | '||'.join(["(kMDItemWhereFroms like '*%s*')" % m for m in clue.emails()]) + \ 27 | ')' 28 | self.proxy = queryProxy.alloc().init() 29 | setattr(self.proxy, 'atom', self) 30 | setattr(self.proxy, 'predicate', predicate) 31 | self.proxy.start() 32 | 33 | def sortOrder(self): 34 | return MAX_SORT_ORDER - 1 35 | 36 | def body(self): 37 | if not self.results: return None 38 | body = [] 39 | for r in self.results[:10]: 40 | body.append('

%s

' % (r.valueForAttribute_('kMDItemPath'),r.valueForAttribute_('kMDItemDisplayName'))) 41 | return ''.join(body) 42 | 43 | # proxy NSObject class to receive notifications 44 | class queryProxy(NSObject): 45 | def init(self): 46 | self = super(queryProxy, self).init() 47 | if not self: return 48 | self.atom = None 49 | self.predicate = None 50 | return self 51 | 52 | def start(self): 53 | self.query = NSMetadataQuery.alloc().init() 54 | self.query.setPredicate_(NSPredicate.predicateWithFormat_(self.predicate)) 55 | self.query.setSortDescriptors_(NSArray.arrayWithObject_(NSSortDescriptor.alloc().initWithKey_ascending_('kMDItemContentCreationDate',False))) 56 | NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(self, self.gotSpotlightData_, NSMetadataQueryDidFinishGatheringNotification, self.query) 57 | self.query.startQuery() 58 | 59 | def gotSpotlightData_(self, notification): 60 | query = notification.object() 61 | print "Got %d results for %s." % (len(query.results()), self.predicate) 62 | self.atom.results = query.results() 63 | self.atom.changed() 64 | 65 | class SpotlightProvider( Provider ): 66 | 67 | def atomClass(self): 68 | return SpotlightAtom 69 | 70 | def provide(self): 71 | self.atoms = [ SpotlightAtom(self, "Recent Messages"), SpotlightAtom(self, "Attachments") ] 72 | -------------------------------------------------------------------------------- /extractors/ComAppleSafari.py: -------------------------------------------------------------------------------- 1 | from ScriptingBridge import * 2 | from Extractor import * 3 | import urlparse 4 | from Utilities import * 5 | from xml.etree.ElementTree import ElementTree 6 | 7 | class ComAppleSafari(Extractor): 8 | 9 | def __init__(self): 10 | super( ComAppleSafari, self ).__init__() 11 | self.safari = SBApplication.applicationWithBundleIdentifier_("com.apple.safari") 12 | 13 | def clues(self): 14 | clues = [] 15 | if not self.safari.windows().count(): return 16 | 17 | # window 0 is always the foreground window? Seems it. 18 | tab = self.safari.windows()[ 0 ].currentTab() 19 | if not tab.exists(): return # foreground window is not a browser window. 20 | 21 | self.clues_from_url( tab.URL() ) 22 | if self.done: return 23 | 24 | if re.match( r'https://www.google.com/reader/', unicode(tab.URL()) ): 25 | # google reader. Try to investigate current item. 26 | js = """ 27 | var link = null; 28 | var n = document.getElementById('entries').childNodes; 29 | for (var i = 0; i < n.length; i++) { 30 | if (/expanded/.test(n[i].className)) { 31 | var l = n[i]; 32 | var a = l.getElementsByClassName("entry-title-link")[0]; 33 | if (a) { 34 | link = a.href; 35 | break; 36 | } 37 | } 38 | } 39 | link; 40 | """ 41 | link = self.safari.doJavaScript_in_(js, self.safari.windows()[0].currentTab()) 42 | if link: 43 | self.clues_from_url( link ) 44 | if self.done: return 45 | 46 | if tab.source(): 47 | # look for microformats 48 | self.clues_from_html( tab.source(), tab.URL() ) 49 | if self.done: return 50 | 51 | # look for rel="me" links 52 | relme = RelMeParser() 53 | try: 54 | relme.feed( tab.source() ) 55 | except Exception: 56 | # bad page source 57 | return 58 | print_info("Found rel='me' links: %s"%( ",".join(relme.hrefs) ) ) 59 | for link in relme.hrefs: 60 | profile = urlparse.urljoin( tab.URL(), link ) 61 | # not sure what to do here. This might point somewhere 62 | # useful. It might not. In the case of flickr, it points 63 | # to a page that contains enough microformats that we 64 | # might be able to work out who they are, but I really 65 | # don't want to have to _fetch_ the page, not here. 66 | self.clues_from_url( profile ) 67 | 68 | # I'm sure the microformats output format makes sense _somewhere_ 69 | def tree_to_dict(tree): 70 | pass 71 | 72 | 73 | 74 | from sgmllib import SGMLParser 75 | 76 | class RelMeParser(SGMLParser): 77 | def reset(self): 78 | SGMLParser.reset(self) 79 | self.hrefs = [] 80 | 81 | def do_a( self, attrs ): 82 | if not ('rel', 'me') in attrs: return 83 | self.hrefs += filter( lambda l: re.match(r'http', l), [e[1] for e in attrs if e[0]=='href'] ) 84 | -------------------------------------------------------------------------------- /providers/DopplrProvider.py: -------------------------------------------------------------------------------- 1 | from Provider import * 2 | import urllib 3 | import re 4 | import json 5 | from datetime import datetime 6 | from time import time, strftime, gmtime 7 | import feedparser # can parse iso8601 dates 8 | 9 | from Utilities import * 10 | import Cache 11 | 12 | class DopplrAtom( ProviderAtom ): 13 | def __init__(self, provider, url): 14 | ProviderAtom.__init__( self, provider, url ) 15 | self.username = re.search(r'/traveller/([^/]+)', self.url).group(1) 16 | self.name = "Dopplr / %s"%self.username 17 | self.response = None 18 | self.fail = None 19 | 20 | self.token = NSUserDefaults.standardUserDefaults().stringForKey_("dopplrToken") 21 | if not self.token: return 22 | 23 | url = "https://www.dopplr.com/api/traveller_info.js?token=%s&traveller=%s"%( self.token, self.username ) 24 | Cache.getContentOfUrlAndCallback( callback = self.gotDopplrData, url = url, timeout = 3600, wantStale = True, failure = self.failed ) 25 | 26 | def failed(self, error): 27 | NSLog("dopplr client fail: %@", error) 28 | self.fail = error 29 | self.changed() 30 | 31 | def gotDopplrData(self, data, stale): 32 | self.stale = stale 33 | try: 34 | doc = json.loads( data ) 35 | self.response = doc['traveller'] 36 | except ValueError, e: 37 | print(e) 38 | self.dead = True 39 | except KeyError, e: 40 | print(e) 41 | self.dead = True 42 | 43 | self.changed() 44 | 45 | def body(self): 46 | if not self.token: 47 | return """

No Dopplr API token defined. click here to get one.

""" 48 | 49 | if self.fail: 50 | return """

Problem talking to Dopplr - maybe the token is invalid? Try re-authorizing..

""" 51 | 52 | if not self.response: return None 53 | 54 | # dopplr api coveniently provides offset from UTC :-) 55 | offset = int(str(self.response['current_city']['utcoffset'])) 56 | readable_time = strftime("%l:%M %p on %A", gmtime( time() + offset )) 57 | 58 | if 'current_trip' in self.response: 59 | start = feedparser._parse_date_iso8601( self.response['current_trip']['start'] ) 60 | finish = feedparser._parse_date_iso8601( self.response['current_trip']['finish'] ) 61 | 62 | body = "

%s is in %s (from %s to %s). Local time is %s.

"%( 63 | self.provider.clue.displayName(), 64 | self.response['current_trip']['city']['name'], 65 | strftime("%B %d", start ), 66 | strftime("%B %d", finish ), 67 | readable_time 68 | ) 69 | else: 70 | body = "

%s is at home in %s (where it is %s).

"%( 71 | self.provider.clue.displayName(), 72 | self.response['current_city']['name'], 73 | readable_time 74 | ) 75 | 76 | return body 77 | 78 | def sortOrder(self): 79 | return MAX_SORT_ORDER - 1 80 | 81 | 82 | class DopplrProvider( Provider ): 83 | 84 | def atomClass(self): 85 | return DopplrAtom 86 | 87 | def provide( self ): 88 | dopplrs = self.clue.takeUrls(r'dopplr\.com/traveller') 89 | 90 | self.atoms = [] 91 | for url in dopplrs: 92 | self.atoms.append( DopplrAtom( self, url ) ) 93 | -------------------------------------------------------------------------------- /providers/LastFmProvider.py: -------------------------------------------------------------------------------- 1 | from FeedProvider import * 2 | from Utilities import * 3 | 4 | from xml.dom.minidom import parseString 5 | from time import time, gmtime 6 | from urllib import quote 7 | 8 | class LastFmAtom( ProviderAtom ): 9 | def __init__(self, *stuff): 10 | ProviderAtom.__init__( self, *stuff ) 11 | 12 | username = re.search(r'user/([^/]+)', self.url).group(1) 13 | self.name = "last.fm / %s"%username 14 | 15 | self.tracks = [] 16 | 17 | recent_url = "http://ws.audioscrobbler.com/1.0/user/%s/recenttracks.xml"%username 18 | Cache.getContentOfUrlAndCallback( callback = self.gotRecentTracks, url = recent_url, timeout = 60, wantStale = True, failure = self.failed ) 19 | 20 | def failed( self, error ): 21 | self.stale = False 22 | self.changed() 23 | 24 | def sortOrder(self): 25 | if len(self.tracks): 26 | return self.tracks[0]['date'] 27 | 28 | def gotRecentTracks(self, xml, stale): 29 | self.stale = stale 30 | dom = parseString( xml ) 31 | self.tracks = [] 32 | def gsv(node, val): 33 | try: 34 | return node.getElementsByTagName(val)[0].childNodes[0].wholeText 35 | except IndexError: 36 | return None 37 | alltracks = dom.getElementsByTagName("track") 38 | 39 | # sort tracks by order of last played 40 | def byPlayed(a,b): 41 | a_played = int(a.getElementsByTagName("date")[0].getAttribute('uts')) 42 | b_played = int(b.getElementsByTagName("date")[0].getAttribute('uts')) 43 | return a_played - b_played 44 | alltracks.sort( byPlayed ) 45 | 46 | for track in alltracks[0:3]: 47 | track.normalize() 48 | played = int(track.getElementsByTagName("date")[0].getAttribute('uts')) 49 | data = { 50 | 'link':gsv(track, "url"), 51 | 'art':"", 52 | 'artist':gsv(track, "artist"), 53 | 'album':gsv(track, "album"), 54 | 'title':gsv(track, "name"), 55 | 'date':played 56 | } 57 | self.tracks.append( data ) 58 | if data['album']: 59 | def updateArtwork(data, stale, trackdata = data): 60 | trackdata['art'] = gsv( parseString(data), 'small' ) 61 | self.changed() 62 | art_url = "http://ws.audioscrobbler.com/1.0/album/%s/%s/info.xml"%( quote(data['artist'].encode('utf-8'),""), quote(data['album'].encode('utf-8'),"") ) 63 | Cache.getContentOfUrlAndCallback( callback = updateArtwork, url = art_url, timeout = 24 * 3600, wantStale = True ) 64 | 65 | self.changed() 66 | 67 | def body(self): 68 | html = "" 69 | 70 | for track in self.tracks: 71 | ago = time_ago_in_words(gmtime(track['date'])) + " ago" 72 | html += u'%s'%ago 73 | 74 | html += ""%( track['link'], track['art'] ) 75 | 76 | html += '

%s

'%( track['link'], track['title'] ) 77 | html += u'

%s

'%( track['artist'] ) 78 | 79 | html += '
' 80 | 81 | return html 82 | 83 | 84 | def timeout(self): 85 | return 180 86 | 87 | 88 | 89 | 90 | 91 | 92 | class LastFmProvider( Provider ): 93 | 94 | def atomClass(self): 95 | return LastFmAtom 96 | 97 | def provide( self ): 98 | urls = self.clue.takeUrls(r'last\.fm/user/.') 99 | self.atoms = [ self.atomClass()( self, url ) for url in urls ] 100 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Headers/SUUpdater.h: -------------------------------------------------------------------------------- 1 | // 2 | // SUUpdater.h 3 | // Sparkle 4 | // 5 | // Created by Andy Matuschak on 1/4/06. 6 | // Copyright 2006 Andy Matuschak. All rights reserved. 7 | // 8 | 9 | #ifndef SUUPDATER_H 10 | #define SUUPDATER_H 11 | 12 | #import 13 | 14 | @class SUUpdateDriver, SUAppcastItem, SUHost, SUAppcast; 15 | @interface SUUpdater : NSObject { 16 | NSTimer *checkTimer; 17 | SUUpdateDriver *driver; 18 | 19 | SUHost *host; 20 | IBOutlet id delegate; 21 | } 22 | 23 | + (SUUpdater *)sharedUpdater; 24 | + (SUUpdater *)updaterForBundle:(NSBundle *)bundle; 25 | - (NSBundle *)hostBundle; 26 | 27 | - (void)setDelegate:(id)delegate; 28 | - delegate; 29 | 30 | - (void)setAutomaticallyChecksForUpdates:(BOOL)automaticallyChecks; 31 | - (BOOL)automaticallyChecksForUpdates; 32 | 33 | - (void)setUpdateCheckInterval:(NSTimeInterval)interval; 34 | - (NSTimeInterval)updateCheckInterval; 35 | 36 | - (void)setFeedURL:(NSURL *)feedURL; 37 | - (NSURL *)feedURL; 38 | 39 | - (void)setSendsSystemProfile:(BOOL)sendsSystemProfile; 40 | - (BOOL)sendsSystemProfile; 41 | 42 | - (void)setAutomaticallyDownloadsUpdates:(BOOL)automaticallyDownloadsUpdates; 43 | - (BOOL)automaticallyDownloadsUpdates; 44 | 45 | // This IBAction is meant for a main menu item. Hook up any menu item to this action, 46 | // and Sparkle will check for updates and report back its findings verbosely. 47 | - (IBAction)checkForUpdates:sender; 48 | 49 | // This kicks off an update meant to be programmatically initiated. That is, it will display no UI unless it actually finds an update, 50 | // in which case it proceeds as usual. If the fully automated updating is turned on, however, this will invoke that behavior, and if an 51 | // update is found, it will be downloaded and prepped for installation. 52 | - (void)checkForUpdatesInBackground; 53 | 54 | // Date of last update check. Returns null if no check has been performed. 55 | - (NSDate*)lastUpdateCheckDate; 56 | 57 | // This begins a "probing" check for updates which will not actually offer to update to that version. The delegate methods, though, 58 | // (up to updater:didFindValidUpdate: and updaterDidNotFindUpdate:), are called, so you can use that information in your UI. 59 | - (void)checkForUpdateInformation; 60 | 61 | // Call this to appropriately schedule or cancel the update checking timer according to the preferences for time interval and automatic checks. This call does not change the date of the next check, but only the internal NSTimer. 62 | - (void)resetUpdateCycle; 63 | 64 | - (BOOL)updateInProgress; 65 | @end 66 | 67 | @interface NSObject (SUUpdaterDelegateInformalProtocol) 68 | // This method allows you to add extra parameters to the appcast URL, potentially based on whether or not Sparkle will also be sending along the system profile. This method should return an array of dictionaries with keys: "key", "value", "displayKey", "displayValue", the latter two being specifically for display to the user. 69 | - (NSArray *)feedParametersForUpdater:(SUUpdater *)updater sendingSystemProfile:(BOOL)sendingProfile; 70 | 71 | // Use this to override the default behavior for Sparkle prompting the user about automatic update checks. 72 | - (BOOL)updaterShouldPromptForPermissionToCheckForUpdates:(SUUpdater *)bundle; 73 | 74 | // Implement this if you want to do some special handling with the appcast once it finishes loading. 75 | - (void)updater:(SUUpdater *)updater didFinishLoadingAppcast:(SUAppcast *)appcast; 76 | 77 | // If you're using special logic or extensions in your appcast, implement this to use your own logic for finding 78 | // a valid update, if any, in the given appcast. 79 | - (SUAppcastItem *)bestValidUpdateInAppcast:(SUAppcast *)appcast forUpdater:(SUUpdater *)bundle; 80 | 81 | // Sent when a valid update is found by the update driver. 82 | - (void)updater:(SUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)update; 83 | 84 | // Sent when a valid update is not found. 85 | - (void)updaterDidNotFindUpdate:(SUUpdater *)update; 86 | 87 | // Sent immediately before installing the specified update. 88 | - (void)updater:(SUUpdater *)updater willInstallUpdate:(SUAppcastItem *)update; 89 | 90 | // Return YES to delay the relaunch until you do some processing; invoke the given NSInvocation to continue. 91 | - (BOOL)updater:(SUUpdater *)updater shouldPostponeRelaunchForUpdate:(SUAppcastItem *)update untilInvoking:(NSInvocation *)invocation; 92 | 93 | // Called immediately before relaunching. 94 | - (void)updaterWillRelaunchApplication:(SUUpdater *)updater; 95 | 96 | // This method allows you to provide a custom version comparator. 97 | // If you don't implement this method or return nil, the standard version comparator will be used. 98 | - (id )versionComparatorForUpdater:(SUUpdater *)updater; 99 | 100 | // Returns the path which is used to relaunch the client after the update is installed. By default, the path of the host bundle. 101 | - (NSString *)pathToRelaunchForUpdater:(SUUpdater *)updater; 102 | 103 | @end 104 | 105 | // Define some minimum intervals to avoid DOS-like checking attacks. These are in seconds. 106 | #ifdef DEBUG 107 | #define SU_MIN_CHECK_INTERVAL 60 108 | #else 109 | #define SU_MIN_CHECK_INTERVAL 60*60 110 | #endif 111 | 112 | #ifdef DEBUG 113 | #define SU_DEFAULT_CHECK_INTERVAL 60 114 | #else 115 | #define SU_DEFAULT_CHECK_INTERVAL 60*60*24 116 | #endif 117 | 118 | #endif 119 | -------------------------------------------------------------------------------- /Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ADP2,1 6 | Developer Transition Kit 7 | MacBook1,1 8 | MacBook (Core Duo) 9 | MacBook2,1 10 | MacBook (Core 2 Duo) 11 | MacBook4,1 12 | MacBook (Core 2 Duo Feb 2008) 13 | MacBookAir1,1 14 | MacBook Air (January 2008) 15 | MacBookPro1,1 16 | MacBook Pro Core Duo (15-inch) 17 | MacBookPro1,2 18 | MacBook Pro Core Duo (17-inch) 19 | MacBookPro2,1 20 | MacBook Pro Core 2 Duo (17-inch) 21 | MacBookPro2,2 22 | MacBook Pro Core 2 Duo (15-inch) 23 | MacBookPro3,1 24 | MacBook Pro Core 2 Duo (15-inch LED, Core 2 Duo) 25 | MacBookPro3,2 26 | MacBook Pro Core 2 Duo (17-inch HD, Core 2 Duo) 27 | MacBookPro4,1 28 | MacBook Pro (Core 2 Duo Feb 2008) 29 | MacPro1,1 30 | Mac Pro (four-core) 31 | MacPro2,1 32 | Mac Pro (eight-core) 33 | MacPro3,1 34 | Mac Pro (January 2008 4- or 8- core "Harpertown") 35 | Macmini1,1 36 | Mac Mini (Core Solo/Duo) 37 | PowerBook1,1 38 | PowerBook G3 39 | PowerBook2,1 40 | iBook G3 41 | PowerBook2,2 42 | iBook G3 (FireWire) 43 | PowerBook2,3 44 | iBook G3 45 | PowerBook2,4 46 | iBook G3 47 | PowerBook3,1 48 | PowerBook G3 (FireWire) 49 | PowerBook3,2 50 | PowerBook G4 51 | PowerBook3,3 52 | PowerBook G4 (Gigabit Ethernet) 53 | PowerBook3,4 54 | PowerBook G4 (DVI) 55 | PowerBook3,5 56 | PowerBook G4 (1GHz / 867MHz) 57 | PowerBook4,1 58 | iBook G3 (Dual USB, Late 2001) 59 | PowerBook4,2 60 | iBook G3 (16MB VRAM) 61 | PowerBook4,3 62 | iBook G3 Opaque 16MB VRAM, 32MB VRAM, Early 2003) 63 | PowerBook5,1 64 | PowerBook G4 (17 inch) 65 | PowerBook5,2 66 | PowerBook G4 (15 inch FW 800) 67 | PowerBook5,3 68 | PowerBook G4 (17-inch 1.33GHz) 69 | PowerBook5,4 70 | PowerBook G4 (15 inch 1.5/1.33GHz) 71 | PowerBook5,5 72 | PowerBook G4 (17-inch 1.5GHz) 73 | PowerBook5,6 74 | PowerBook G4 (15 inch 1.67GHz/1.5GHz) 75 | PowerBook5,7 76 | PowerBook G4 (17-inch 1.67GHz) 77 | PowerBook5,8 78 | PowerBook G4 (Double layer SD, 15 inch) 79 | PowerBook5,9 80 | PowerBook G4 (Double layer SD, 17 inch) 81 | PowerBook6,1 82 | PowerBook G4 (12 inch) 83 | PowerBook6,2 84 | PowerBook G4 (12 inch, DVI) 85 | PowerBook6,3 86 | iBook G4 87 | PowerBook6,4 88 | PowerBook G4 (12 inch 1.33GHz) 89 | PowerBook6,5 90 | iBook G4 (Early-Late 2004) 91 | PowerBook6,7 92 | iBook G4 (Mid 2005) 93 | PowerBook6,8 94 | PowerBook G4 (12 inch 1.5GHz) 95 | PowerMac1,1 96 | Power Macintosh G3 (Blue & White) 97 | PowerMac1,2 98 | Power Macintosh G4 (PCI Graphics) 99 | PowerMac10,1 100 | Mac Mini G4 101 | PowerMac10,2 102 | Mac Mini (Late 2005) 103 | PowerMac11,2 104 | Power Macintosh G5 (Late 2005) 105 | PowerMac12,1 106 | iMac G5 (iSight) 107 | PowerMac2,1 108 | iMac G3 (Slot-loading CD-ROM) 109 | PowerMac2,2 110 | iMac G3 (Summer 2000) 111 | PowerMac3,1 112 | Power Macintosh G4 (AGP Graphics) 113 | PowerMac3,2 114 | Power Macintosh G4 (AGP Graphics) 115 | PowerMac3,3 116 | Power Macintosh G4 (Gigabit Ethernet) 117 | PowerMac3,4 118 | Power Macintosh G4 (Digital Audio) 119 | PowerMac3,5 120 | Power Macintosh G4 (Quick Silver) 121 | PowerMac3,6 122 | Power Macintosh G4 (Mirrored Drive Door) 123 | PowerMac4,1 124 | iMac G3 (Early/Summer 2001) 125 | PowerMac4,2 126 | iMac G4 (Flat Panel) 127 | PowerMac4,4 128 | eMac 129 | PowerMac4,5 130 | iMac G4 (17-inch Flat Panel) 131 | PowerMac5,1 132 | Power Macintosh G4 Cube 133 | PowerMac6,1 134 | iMac G4 (USB 2.0) 135 | PowerMac6,3 136 | iMac G4 (20-inch Flat Panel) 137 | PowerMac6,4 138 | eMac (USB 2.0, 2005) 139 | PowerMac7,2 140 | Power Macintosh G5 141 | PowerMac7,3 142 | Power Macintosh G5 143 | PowerMac8,1 144 | iMac G5 145 | PowerMac8,2 146 | iMac G5 (Ambient Light Sensor) 147 | PowerMac9,1 148 | Power Macintosh G5 (Late 2005) 149 | RackMac1,1 150 | Xserve G4 151 | RackMac1,2 152 | Xserve G4 (slot-loading, cluster node) 153 | RackMac3,1 154 | Xserve G5 155 | Xserve1,1 156 | Xserve (Intel Xeon) 157 | Xserve2,1 158 | Xserve (January 2008 quad-core) 159 | iMac1,1 160 | iMac G3 (Rev A-D) 161 | iMac4,1 162 | iMac (Core Duo) 163 | iMac4,2 164 | iMac for Education (17-inch, Core Duo) 165 | iMac5,1 166 | iMac (Core 2 Duo, 17 or 20 inch, SuperDrive) 167 | iMac5,2 168 | iMac (Core 2 Duo, 17 inch, Combo Drive) 169 | iMac6,1 170 | iMac (Core 2 Duo, 24 inch, SuperDrive) 171 | iMac8,1 172 | iMac (April 2008) 173 | 174 | 175 | -------------------------------------------------------------------------------- /Utilities.py: -------------------------------------------------------------------------------- 1 | from Foundation import * 2 | from AppKit import * 3 | from ScriptingBridge import * 4 | from AddressBook import * 5 | 6 | import re 7 | 8 | def as_dump( obj ): 9 | methods = dir(obj) 10 | for x in dir(object()) + dir(NSObject.alloc().init()): 11 | if x in methods: methods.remove(x) 12 | methods.sort() 13 | print( obj.__class__.__name__ ) 14 | print( "\n".join(map(lambda x: " - %s"%x, methods) ) ) 15 | 16 | def as_app(bundle): 17 | return SBApplication.applicationWithBundleIdentifier_(bundle) 18 | 19 | def print_info(stuff): 20 | if NSUserDefaults.standardUserDefaults().boolForKey_("debug"): 21 | print(stuff) 22 | 23 | def html_escape( s ): 24 | s = re.sub(r"&", "&", s) 25 | s = re.sub(r"<", "<", s) 26 | s = re.sub(r">", ">", s) 27 | return s 28 | 29 | # this is a HACK to normalize urls. not that it's not required to return 30 | # something that's still a valid url! (though it currently does) Don't assume 31 | # that it does 32 | def normalize_url( url ): 33 | url = re.sub(r'/$', '', url) # trailing slash 34 | url = re.sub(r'^\w+://', '', url) # protocol 35 | url = re.sub(r'^www.flickr.', 'flickr.', url) # flickr special casing 36 | return url.lower() # ewwww 37 | 38 | 39 | 40 | 41 | 42 | 43 | # from http://pylonshq.com/WebHelpers/webhelpers/rails/date.py.html on 2008-02-11 44 | 45 | """Date/Time Helpers""" 46 | # Last synced with Rails copy at Revision 6080 on Feb 8th, 2007. 47 | # Note that the select_ tags are purposely not ported as they're very totally useless 48 | # and inefficient beyond comprehension. 49 | 50 | from datetime import datetime 51 | import time 52 | 53 | DEFAULT_PREFIX = 'date' 54 | 55 | def distance_of_time_in_words(from_time, to_time=0, include_seconds=False): 56 | """ 57 | Reports the approximate distance in time between two datetime objects or 58 | integers as seconds. 59 | 60 | Set ``include_seconds`` to True for more more detailed approximations when 61 | distance < 1 min, 29 secs 62 | 63 | Distances are reported based on the following table: 64 | 65 | 0 <-> 29 secs => less than a minute 66 | 30 secs <-> 1 min, 29 secs => 1 minute 67 | 1 min, 30 secs <-> 44 mins, 29 secs => [2..44] minutes 68 | 44 mins, 30 secs <-> 89 mins, 29 secs => about 1 hour 69 | 89 mins, 29 secs <-> 23 hrs, 59 mins, 29 secs => about [2..24] hours 70 | 23 hrs, 59 mins, 29 secs <-> 47 hrs, 59 mins, 29 secs => 1 day 71 | 47 hrs, 59 mins, 29 secs <-> 29 days, 23 hrs, 59 mins, 29 secs => [2..29] days 72 | 29 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs => about 1 month 73 | 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 31 secs => [2..12] months 74 | 1 yr minus 30 secs <-> 2 yrs minus 31 secs => about 1 year 75 | 2 yrs minus 30 secs <-> max time or date => over [2..X] years 76 | 77 | With ``include_seconds`` set to True and the difference < 1 minute 29 78 | seconds: 79 | 80 | 0-4 secs => less than 5 seconds 81 | 5-9 secs => less than 10 seconds 82 | 10-19 secs => less than 20 seconds 83 | 20-39 secs => half a minute 84 | 40-59 secs => less than a minute 85 | 60-89 secs => 1 minute 86 | 87 | Examples: 88 | 89 | >>> from datetime import datetime, timedelta 90 | >>> from_time = datetime.now() 91 | >>> distance_of_time_in_words(from_time, from_time + timedelta(minutes=50)) 92 | 'about 1 hour' 93 | >>> distance_of_time_in_words(from_time, from_time + timedelta(seconds=15)) 94 | 'less than a minute' 95 | >>> distance_of_time_in_words(from_time, from_time + timedelta(seconds=15), include_seconds=True) 96 | 'less than 20 seconds' 97 | 98 | Note: ``distance_of_time_in_words`` calculates one year as 365.25 days. 99 | """ 100 | if isinstance(from_time, int): 101 | from_time = time.time()+from_time 102 | elif isinstance( from_time, time.struct_time ): 103 | from_time = time.mktime(from_time) 104 | else: 105 | from_time = time.mktime(from_time.timetuple()) 106 | if isinstance(to_time, int): 107 | to_time = time.time()+to_time 108 | else: 109 | to_time = time.mktime(to_time.timetuple()) 110 | 111 | distance_in_minutes = int(round(abs(to_time-from_time)/60)) 112 | distance_in_seconds = int(round(abs(to_time-from_time))) 113 | 114 | if distance_in_minutes <= 1: 115 | if include_seconds: 116 | for remainder in [5, 10, 20]: 117 | if distance_in_seconds < remainder: 118 | return "less than %s seconds" % remainder 119 | if distance_in_seconds < 40: 120 | return "half a minute" 121 | elif distance_in_seconds < 60: 122 | return "less than a minute" 123 | else: 124 | return "1 minute" 125 | else: 126 | if distance_in_minutes == 0: 127 | return "less than a minute" 128 | else: 129 | return "1 minute" 130 | elif distance_in_minutes < 45: 131 | return "%s minutes" % distance_in_minutes 132 | elif distance_in_minutes < 90: 133 | return "about 1 hour" 134 | elif distance_in_minutes < 1440: 135 | return "about %d hours" % (round(distance_in_minutes / 60.0)) 136 | elif distance_in_minutes < 2880: 137 | return "1 day" 138 | elif distance_in_minutes < 43220: 139 | return "%d days" % (round(distance_in_minutes / 1440)) 140 | elif distance_in_minutes < 86400: 141 | return "about 1 month" 142 | elif distance_in_minutes < 525600: 143 | return "%d months" % (round(distance_in_minutes / 43200)) 144 | elif distance_in_minutes < 1051200: 145 | return "about 1 year" 146 | else: 147 | return "over %d years" % (round(distance_in_minutes / 525600)) 148 | 149 | def time_ago_in_words(from_time, include_seconds=False): 150 | """ 151 | Like distance_of_time_in_words, but where ``to_time`` is fixed to ``datetime.now()``. 152 | """ 153 | ago = distance_of_time_in_words(from_time, datetime.utcnow(), include_seconds) 154 | # TODO - think about these. The output from the function is Too Damn Long, is all. 155 | ago = re.sub(r'^about ', '~', ago ) 156 | ago = re.sub(r'minute', 'min', ago ) 157 | ago = re.sub(r'second', 'sec', ago ) 158 | return ago 159 | 160 | __all__ = ['as_dump', 'as_app', 'print_info', 'html_escape', 'normalize_url','distance_of_time_in_words', 'time_ago_in_words'] 161 | -------------------------------------------------------------------------------- /Cache.py: -------------------------------------------------------------------------------- 1 | from Foundation import * 2 | from AppKit import * 3 | from WebKit import * 4 | 5 | from time import time, sleep 6 | import base64 7 | import urllib 8 | import os 9 | import os.path 10 | import hashlib 11 | import re 12 | 13 | from Utilities import * 14 | 15 | def keyForUrlUsernamePassword( url, username, password ): 16 | return "%s:::%s:::%s"%( url, username, password ) 17 | 18 | 19 | def filenameForKey( key ): 20 | folder = os.path.join( os.environ['HOME'], "Library", "Application Support", "Shelf", "cache" ) 21 | try: 22 | os.makedirs( folder ) 23 | except OSError: 24 | pass 25 | filename = urllib.quote( key, '' ) 26 | # this can make filenames that are waaaay too long 27 | hasher = hashlib.md5() 28 | hasher.update(filename) 29 | return os.path.join( folder, hasher.hexdigest() ) 30 | 31 | 32 | #LAST_CACHE_CLEAN = 0 33 | def cleanCache(): 34 | #if time() - LAST_CACHE_CLEAN < 60: 35 | # return 36 | folder = os.path.join( os.environ['HOME'], "Library", "Application Support", "Shelf", "cache" ) 37 | try: 38 | files = os.listdir(folder) 39 | except OSError: 40 | return 41 | 42 | for file in os.listdir( folder ): 43 | filename = os.path.join( folder, file ) 44 | # file not looked at in a day 45 | if time() - os.path.getatime( filename ) > 24 * 3600: 46 | print_info("Removing old cache file %s"%filename) 47 | os.unlink( filename ) 48 | #LAST_CACHE_CLEAN = time() 49 | 50 | 51 | # ask Cocoa to query Spotlight and get back to us. 52 | def querySpotlightAndCallback(**params): 53 | cleanCache() 54 | emails = params['emails'] 55 | # exclude image, text and html files that are sometimes wrongly attached to emails 56 | exclusions = ['public.image','public.text'] 57 | query = NSMetadataQuery.alloc().init() 58 | # The easy bit - all e-mails where these addresses are seen 59 | predicate = "((kMDItemContentType = 'com.apple.mail.emlx') && (" + \ 60 | '||'.join(["((kMDItemAuthorEmailAddresses = '%s') || (kMDItemRecipientEmailAddresses = '%s'))" % (m, m) for m in emails]) + \ 61 | ")" 62 | predicate += "|| (" + \ 63 | '&&'.join(["(kMDItemContentTypeTree != '%s')" % e for e in exclusions]) + \ 64 | ") && (" + \ 65 | '||'.join(["(kMDItemWhereFroms like '*%s*')" % m for m in emails]) + \ 66 | '))' 67 | print predicate 68 | query.setPredicate_(NSPredicate.predicateWithFormat_(predicate)) 69 | query.setSortDescriptors_(NSArray.arrayWithObject_(NSSortDescriptor.alloc().initWithKey_ascending_('kMDItemContentCreationDate',False))) 70 | NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(params['observer'], params['callback'], NSMetadataQueryDidFinishGatheringNotification, query) 71 | query.startQuery() 72 | 73 | 74 | # ask Cocoa to download an url and get back to us. It pulls the file to disk 75 | # locally, and uses this as a cache, using mtime. The callback should be a 76 | # function that will be called at some time in the future, with 2 params - 77 | # the data, and a true/false if the data is stale or not. 78 | # 79 | # Calling with wantstale of true will call the callback function right away 80 | # if here is _any_ data, even if it's old, then will fetch the data and call 81 | # the callback _again_. 82 | # 83 | def getContentOfUrlAndCallback( **params ): 84 | cleanCache() 85 | # I have address book entries that are just 'www.foo.com' 86 | if not re.match(r'^\w+://', params['url']): 87 | params['url'] = "http://%s" % params['url'] 88 | 89 | delegate = DownloadDelegate.alloc().init() 90 | for key in params: 91 | setattr(delegate, key, params[key] ) 92 | 93 | delay = 0.1 94 | if 'delay' in params: delay = params['delay'] 95 | delegate.performSelector_withObject_afterDelay_('start', None, delay ) 96 | 97 | 98 | class DownloadDelegate( NSObject ): 99 | 100 | def init(self): 101 | self = super(DownloadDelegate, self).init() 102 | if not self: return 103 | # these are set above via setattr 104 | self.callback = None 105 | self.failure = None 106 | self.url = None 107 | self.username = None 108 | self.password = None 109 | self.timeout = None 110 | self.wantStale = None 111 | return self 112 | 113 | # this is a seperate function so I can call it after a delay 114 | def start(self): 115 | filename = filenameForKey( keyForUrlUsernamePassword( self.url, self.username, self.password ) ) 116 | if os.path.exists(filename): 117 | if time() - os.path.getmtime( filename ) < self.timeout: 118 | self.callback( file( filename ).read(), False ) 119 | return # no need to get the URL 120 | elif self.wantStale: 121 | # call the callback immediately with the stale data. 122 | print_info("We have stale data") 123 | self.callback( file( filename ).read(), True ) 124 | # don't return - we still want to fetch the file 125 | # TODO - if we're already fetching the file on behalf of someone 126 | # else, it would be nice to do the Right Thing here. 127 | 128 | req = NSMutableURLRequest.requestWithURL_( NSURL.URLWithString_( self.url ) ) 129 | if self.username or self.password: 130 | base64string = base64.encodestring('%s:%s' % (self.username, self.password))[:-1] 131 | req.setValue_forHTTPHeaderField_("Basic %s"%base64string, "Authorization") 132 | # Send the right User-Agent. TODO - get the bundle version properly, don't hard-code 133 | req.setValue_forHTTPHeaderField_("Shelf/git.HEAD +https://github.com/rcarmo/shelf/", "User-Agent") 134 | downloader = NSURLDownload.alloc().initWithRequest_delegate_( req, self ) 135 | downloader.setDestination_allowOverwrite_( filename, True ) 136 | 137 | def downloadDidBegin_(self, downloader): 138 | print_info("Begun download of %s"%downloader.request()) 139 | 140 | def download_didCreateDestination_(self, downloader, filename): 141 | self.filename = filename 142 | os.utime( self.filename, None ) 143 | 144 | def downloadDidFinish_(self, downloader): 145 | # the downloader sets the mtime to be the web server's idea of 146 | # when the file was last updated. Which is cute. But useless to us. 147 | # I want to know when I fetched it. 148 | os.utime( self.filename, None ) 149 | data = file( self.filename ).read() 150 | self.callback( data, False ) 151 | 152 | def download_didFailWithError_(self, downloader, error): 153 | #print("error downloading %s: %s"%( downloader.request(), error )) 154 | if self.failure: 155 | self.failure( error ) 156 | 157 | 158 | # incredibly evil - ignore https cert errors (doesn't work!) 159 | #from objc import Category 160 | #class NSURLRequest(Category(NSURLRequest)): 161 | # @classmethod 162 | # def allowsAnyHTTPSCertificateForHost_(cls, host): 163 | # return True 164 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | ### 0.0.15 (2011-02-28) 2 | 3 | Made work under Snow Leopard, Python 2.6, etc (Anil Madhavapeddy) 4 | 5 | Crazy experimental Google Reader support (Safari only) 6 | 7 | Support more versions of Chrome, hopefully. They keep changing the AS support. 8 | 9 | Added a Spotlight provider that attempts to grab e-mail messages and 10 | attachments exchanged with a particular contact (Rui Carmo) 11 | 12 | Tightened up indents and removed blank lines in some files to make it 13 | easier to work on a somewhat cramped screen. 14 | 15 | ### 0.0.14 16 | 17 | last.fm changed their XML order? Anyway, I now explicitly order the tracks 18 | in the client. 19 | 20 | Replaces simlpejson with json - requires Python 2.6, but Snow Leopard has that 21 | and I'm not really interested in supporting anything else. 22 | 23 | ### 0.0.13 (released 2008-03-28) 24 | 25 | All blocks are now sorted by date, rather than just Feed blocks. This means 26 | that if a contact has blogged something more recently than they've twittered 27 | something, the blog block will be above the twitter block. 28 | 29 | The Dopplr provider is back and working again. It now properly uses the 30 | Dopplr auth-bounce code to get itself a token from Dopplr, so should actually 31 | be usable by people other than me. 32 | 33 | Lots of trivial little encoding fixes and catches for annoying 'other apps 34 | suck' failure cases. 35 | 36 | RSS feed items without titles no longer break the Feed provider. 37 | 38 | If you live in a non-GMT timezone, dates now work. Hurrah for going to 39 | conferences and having to suffer under my own bugs. 40 | 41 | ### 0.0.12 (released 2008-02-14) 42 | 43 | Shelf now has a preference to only poll for context and display the window if 44 | a global shortcut key is pressed (currently, this is hard-coded to 45 | Command-Ctrl-J). Turning this on will stop Shelf from polling, it'll manually 46 | display the window when you press the hotkey. BUG - turning this preference 47 | off will require a restart of Shelf before the polling starts again. And yes, 48 | I'll make the key configurable soon. 49 | 50 | Feed entries now display about how long ago they were, rather than absolute 51 | times. Do we like this? Maybe this should be a preference. 52 | 53 | Messed with the flickr photo display style to include titles, and have smaller 54 | thumbnails. I think I prefer this way. 55 | 56 | Special casing for last.fm urls is now in, so you'll see your contact's 57 | recently-played tracks and coverart, if available. 58 | 59 | Special-casing for the few pages I know of that have 60 | [totally broken RSS feeds](http://jerakeen.org/blog/2008/02/irritating-rss-feed-links/) 61 | to supress them from the feedlists. 62 | 63 | Feeds in the display are now ordered with the most recently updated feed at 64 | the top of the list. 'Special' feeds (twitter, flickr, lastfm) are still 65 | ordered above all others. I'll change this soon. 66 | 67 | We now correctly strip CSS styling from feed contents. 68 | 69 | The file cache now cleans itself up. The first run of the app may take a 70 | while... Sorry. 71 | 72 | The feedprovider de-dupes its blocks based on the feed url, rather than 73 | the page url. No more repeated flickr photos blocks because they have more than 74 | one different url to their photos. 75 | 76 | If there are rel="me" links in the current page, and you're using Safari, 77 | I can use them directly to resolve context rather than relying on google 78 | to have spidered the page. 79 | 80 | Blocks that have come from the Google Social Graph or have otherwise been 81 | guessed now have a little 'G' in their title bar. Ugly, but I want a way of 82 | distinguishing them a little. 83 | 84 | Shelf now sends a proper User-Agent to remote servers when fetching feeds. 85 | 86 | Documented large chunks of the source much better. 87 | 88 | Removed the xmltramp library, using simplejson to parse the Dopplr API files 89 | instead. Not that the Dopplr module works. But if it did, I would. 90 | 91 | 92 | 93 | ### 0.0.11 (released 2008-02-06) 94 | 95 | Feed provider now displays the first 10 words of the blog entry under the 96 | heading, and the entry date. 97 | 98 | Add 'birthday' to the list of simple information displayed about a user. 99 | 100 | I now use the Google Social Graph to try to derive a person from a page you're 101 | looking at, if that page isn't already in your address book. Likewise, I use 102 | the Google Social Graph to find out more urls from someone once I have one URL 103 | for them. So if your homepage links to your flickr page, and only your 104 | homepage is in my address book, I'll still see your flickr photos when I visit 105 | your home page, and I'll see you if I visit your flickr page. Magic. 106 | 107 | The Google Social stuff is off by default - there are two preferences for 108 | turning it on. This is because it's a privacy _nightmare_ - it sends every URL 109 | you visit, and every URL belonging to anyone you IM with, to Google. 110 | 111 | There are now some trivial preferences for how you want Shelf to display - the 112 | old default behaviours of 'always come to the foreground when I have something 113 | to tell you' and 'window is always on top if there is context' are now 114 | options. 115 | 116 | #### Known bugs 117 | 118 | The Dopplr module is currently broken, due to the odd SSL certificate Dopplr 119 | use. Sorry. Working on it. 120 | 121 | The RSS feed fetcher shares a cookie store with Safari. This may or may not 122 | be a bug or a feature, depending on your point of view. Anyone care? 123 | 124 | #### Technical changes: 125 | 126 | Converted build process to use py2app rather than xcode, because I know how to 127 | make py2app dance to my tunes. Specifically, I know how to make it pull in 128 | external libraries and bundle them properly. Reorganised source files into 129 | folders so I can keep track of where things are. 130 | 131 | _Huge_ great internal re-write. Many more code-paths are now much more 132 | message-passing based, and can do things in the background. The Google Social 133 | stuff uses this, so no longer blocks the foreground thread. 134 | 135 | 136 | 137 | ### 0.0.10 (released 2008-01-11) 138 | 139 | I've taken a random stab at CSS styling - the thing is a lot prettier now, 140 | though that's not really saying much. Input welcome. 'themes' (spit) are 141 | planned, of course. But till then, at least it isn't just a default webkit 142 | display now. 143 | 144 | The webkit view now has crude click-through, which is useful as the window 145 | is always on top, so of course you want to follow that link. But it _doesn't_ 146 | have hover-through, so you won't see link mouseover effects just yet. 147 | Annoying. 148 | 149 | Companies in the address book are recognised as such, and display the company 150 | name first. They have a different default icon. 151 | 152 | The NetNewsWire clue provider now tries to find a person based on the target of 153 | the currently viewed headline first, then falls back to the feed URL, rather 154 | than just using the feed URL. It also looks for Microformats in the body of 155 | the feed item before considering URLs. I may reconsider the ordering here, 156 | though. 157 | 158 | Hopefully better URL disambiguation now - www. on the beginning of addresses 159 | can be considered optional. 160 | 161 | The display will hang around a little longer if it thinks you're no longer 162 | looking at someone interesting, to try to bridge the gap if you're navigating 163 | to a resource about the same person. 164 | 165 | Mattb had an odd crashing bug that I don't understand, where there seemed to be 166 | no 'active application'. I'm now guarding against this case. 167 | 168 | Shelf now stops correctly when quit. oops. 169 | 170 | 171 | 172 | ### 0.0.8 (released 2008-01-10) 173 | 174 | New icon, thanks Rui. 175 | 176 | I'm sure there was something else. I decide I should start keeping a ChangeLog. 177 | 178 | -------------------------------------------------------------------------------- /providers/FeedProvider.py: -------------------------------------------------------------------------------- 1 | from Provider import * 2 | from urllib import quote 3 | 4 | import feedparser 5 | from autorss import getRSSLinkFromHTMLSource 6 | import urllib, urlparse 7 | import time 8 | 9 | from Utilities import * 10 | import Cache 11 | 12 | class FeedAtom(ProviderAtom): 13 | def __init__(self, *stuff): 14 | ProviderAtom.__init__( self, *stuff ) 15 | self.feed = None 16 | self.feed_url = None 17 | self.refresh( False ) 18 | 19 | def refresh( self, force ): 20 | # TODO - force-refresh should blow the cache 21 | self.getFeedUrl() 22 | 23 | def sortOrder(self): 24 | if not self.feed: 25 | return MIN_SORT_ORDER 26 | if len(self.feed.entries) == 0: 27 | return MIN_SORT_ORDER 28 | if 'updated_parsed' in self.feed.entries[0] and self.feed.entries[0].updated_parsed: 29 | return time.mktime(self.feed.entries[0].updated_parsed) 30 | if "published_parsed" in self.feed.entries[0] and self.feed.entries[0].published_parsed: 31 | return time.mktime(self.feed.entries[0].published_parsed) 32 | return MIN_SORT_ORDER 33 | 34 | def getFeedUrl(self): 35 | # it's very unlikely that the feed source will move 36 | # TODO - check stale cache first. Man, the feed provider is too complicated. 37 | special = self.specialCaseFeedUrl( self.url ) 38 | # return None to mean 'no special case', blank string to mean "no feed here" 39 | if special != None: 40 | if len(special) > 0: 41 | print_info("special-case feed url %s"%special) 42 | self.getFeed( special ) 43 | else: 44 | # bad feed 45 | self.dead = True 46 | self.changed() 47 | return 48 | 49 | Cache.getContentOfUrlAndCallback( callback = self.gotMainPage, url = self.url, timeout = self.timeout() * 10, wantStale = False, failure = self.failed ) # TODO - use stale version somehow 50 | 51 | def specialCaseFeedUrl( self, url ): 52 | print_info("trying to special-case url %s"%url) 53 | if re.match(r'http://search\.cpan\.org/~', url): 54 | print_info("RSS feed is known-bad (search.cpan)") 55 | return "" # bad feed 56 | if re.match(r'http://use\.perl\.org/~\w+/?$', url): # /journal is ok 57 | print_info("RSS feed is known-bad (use.perl)") 58 | return "" # bad feed 59 | 60 | print_info("No special case") 61 | return None 62 | 63 | def gotMainPage( self, data, stale ): 64 | rss = getRSSLinkFromHTMLSource(data) 65 | if rss: 66 | feed_url = urlparse.urljoin( self.url, rss ) 67 | self.getFeed( feed_url ) 68 | else: 69 | self.dead = True 70 | self.changed() 71 | 72 | def username(self): return None 73 | def password(self): return None 74 | 75 | def getFeed(self, feed_url ): 76 | if not self.feed_url and self.provider.isDuplicateFeed( feed_url ): 77 | self.dead = True 78 | self.changed() 79 | else: 80 | self.feed_url = feed_url 81 | # if we have a feed object, then I'm not interested in re-parsing a stale file. 82 | wantStale = not self.feed 83 | Cache.getContentOfUrlAndCallback( callback = self.gotFeed, url = feed_url, username = self.username(), password = self.password(), timeout = self.timeout(), wantStale = wantStale, failure = self.failed ) 84 | 85 | def gotFeed( self, data, stale ): 86 | feed = feedparser.parse( data ) 87 | if feed and 'feed' in feed and 'title' in feed.feed: 88 | self.feed = feed 89 | self.stale = stale 90 | self.name = feed.feed.title 91 | self.changed() 92 | else: 93 | self.dead = True 94 | self.changed() 95 | 96 | def failed( self, error ): 97 | if self.feed: 98 | # never mind, we have _something_ 99 | self.stale = False 100 | else: 101 | # no old feed, just display error 102 | self.error = error 103 | self.stale = False 104 | self.changed() 105 | 106 | def body(self): 107 | if self.feed and self.feed.entries: 108 | return self.htmlForFeed( url = self.url, feed = self.feed, stale = self.stale ) 109 | elif self.feed: 110 | return "" # no entries 111 | else: 112 | return self.htmlForPending( url = self.url, stale = self.stale ) 113 | 114 | def timeout(self): 115 | return 60 * 20 116 | 117 | def htmlForPending( self, url, stale = False ): 118 | return "" 119 | 120 | def htmlForFeed( self, url, feed, stale = False ): 121 | html = u"" 122 | entries = feed.entries 123 | for item in filter( lambda item: "link" in item, entries )[0:4]: 124 | if 'published_parsed' in item: date = item.published_parsed 125 | elif 'updated_parsed' in item: date = item.updated_parsed 126 | else: date = None 127 | 128 | if date: 129 | #html += u'%s'%( time.strftime("%b %d", date ) ) 130 | ago = time_ago_in_words(date) + " ago" 131 | html += u'%s'%ago 132 | title = 'title' in item and item.title or "untitled" 133 | 134 | try: 135 | html += u'

%s

'%( item.link, title ) 136 | except UnicodeDecodeError: 137 | html += u'

invalid unicode title

'%( item.link ) 138 | detail = None 139 | if 'content' in item and len(item.content) > 0: 140 | detail = item.content[0].value 141 | elif 'summary' in item and len(item.summary) > 0: 142 | detail = item.summary 143 | if detail: 144 | raw = re.sub(r'<.*?>', '', detail) # strip tags 145 | try: 146 | trimmed = u" ".join( re.split(r'\s+', raw.strip())[0:10] ) 147 | except UnicodeDecodeError: 148 | trimmed = u"invalid unicode content" 149 | try: 150 | html += u'

%s ...

'%( trimmed, item.link ) 151 | except UnicodeDecodeError: 152 | html += u'

invalid unicode content

' 153 | return html 154 | 155 | 156 | class FeedProvider( Provider ): 157 | 158 | def atomClass(self): 159 | return FeedAtom 160 | 161 | def provide( self ): 162 | todo = self.urls() # if we're claiming from boring_urls, do it first 163 | 164 | # sync atoms to urls 165 | for atom in [x for x in self.atoms]: 166 | if atom.url in todo: 167 | todo.remove( atom.url ) 168 | atom.refresh( False ) 169 | else: 170 | self.atoms.remove(atom) 171 | 172 | for url in todo: 173 | atom = self.atomClass()( self, url ) 174 | atom.guessed = url in self.clue.extra_urls 175 | self.atoms.append( atom ) 176 | 177 | def isDuplicateFeed(self, url): 178 | # called once an atom has a feed url for itself. Rather than 179 | # removing the atom, just mark it as dead, so that the proide() 180 | # function above doesn't re-add it 181 | if not url: return False 182 | return normalize_url(url) in [ normalize_url(a.feed_url) for a in filter(lambda a: a.feed_url, self.atoms) ] 183 | 184 | # override these 185 | def urls(self): 186 | return self.clue.boring_urls 187 | 188 | 189 | -------------------------------------------------------------------------------- /lib/microformatparser.py: -------------------------------------------------------------------------------- 1 | # from http://phildawes.net/microformats/microformatparser.html 2 | # changed by tom to understand multiple classes ina single class="foo bar baz" stanza 3 | 4 | 5 | 6 | #!/usr/bin/env python 7 | # 8 | # Microformat parser hack 9 | # - My lame attempt to build a generic microformat parser engine 10 | # (C) Phil Dawes 2005 11 | # Distributed under a New BSD style license: 12 | # See: http://www.opensource.org/licenses/bsd-license.php 13 | # 14 | # Usage: python ./ 15 | 16 | import sys 17 | import urlparse 18 | from HTMLParser import HTMLParser 19 | import re 20 | import urllib2 21 | 22 | class MicroformatSchema: 23 | 24 | def __init__(self,props,parentprops): 25 | self.props = props 26 | self.parentprops = parentprops 27 | 28 | def isValidProperty(self,prop): 29 | if prop in self.props + self.parentprops: 30 | return True 31 | return False 32 | 33 | def isParentProperty(self,prop): 34 | return prop in self.parentprops 35 | 36 | vcardprops = MicroformatSchema(['fn','family-name', 'given-name', 'additional-name', 'honorific-prefix', 'honorific-suffix', 'nickname', 'sort-string','url','email','type','tel','post-office-box', 'extended-address', 'street-address', 'locality', 'region', 'postal-code', 'country-name', 'label', 'latitude', 'longitude', 'tz', 'photo', 'logo', 'sound', 'bday','title', 'role','organization-name', 'organization-unit','category', 'note','class', 'key', 'mailer', 'uid', 'rev'],['n','email','adr','geo','org','tel']) 37 | 38 | veventprops = MicroformatSchema(["summary","url","dtstart","dtend","location"],[]) 39 | 40 | SCHEMAS= {'vcard':vcardprops,'vevent':veventprops} 41 | 42 | class nodeitem: 43 | def __init__(self,id,tag,predicates,attrs,nested): 44 | self.tag = tag 45 | self.id = id 46 | self.predicates = predicates 47 | self.attrs = attrs 48 | self.nested = nested 49 | 50 | def __repr__(self): 51 | return ""%(self.tag, self.id, self.predicates, 52 | self.attrs,self.nested) 53 | 54 | class MicroformatToStmts(HTMLParser): 55 | def __init__(self,url): 56 | self.url = url 57 | HTMLParser.__init__(self) 58 | self.nodestack = [] 59 | self.nodemap = {} 60 | self.chars = "" 61 | self.tree = [] 62 | self.treestack = [] 63 | 64 | def _getattr(self,name,attrs): 65 | for attr in attrs: 66 | if name == attr[0]: return attr[1] 67 | 68 | def predicateIsAParent(self,pred): 69 | if SCHEMAS[self.currentCompoundFormat].isParentProperty(pred): 70 | return True 71 | return False 72 | 73 | def handle_starttag(self, elementtag, attrs): 74 | self.chars="" 75 | if self.currentlyInAMicroformat(): 76 | try: 77 | preds = self._getattr("class",attrs).split() 78 | except AttributeError: 79 | self.nodestack.append(nodeitem(1,elementtag,None,attrs,False)) 80 | return 81 | 82 | prevpreds = [] 83 | #while 1: 84 | nested = False 85 | while 1: 86 | if prevpreds == preds: 87 | break 88 | prevpreds = preds 89 | if self.predicateIsAParent(preds[0]): 90 | self.openParentProperty(preds[0]) 91 | nested = True 92 | 93 | if elementtag == "img": 94 | self.emitAttributeAsPropertyIfExists('src',attrs, preds) 95 | elif elementtag == "a": 96 | self.emitAttributeAsPropertyIfExists('href',attrs, preds) 97 | self.emitAttributeAsPropertyIfExists('title',attrs, preds) 98 | elif elementtag == "abbr": 99 | self.emitAttributeAsPropertyIfExists('title',attrs, preds) 100 | 101 | self.nodestack.append(nodeitem(1,elementtag,preds,attrs,nested)) 102 | 103 | elif self.nodeStartsAMicroformat(attrs): 104 | 105 | classattrs = self._getattr('class',attrs).split() 106 | for classattr in classattrs: 107 | if classattr in SCHEMAS.keys(): 108 | self.currentCompoundFormat = classattr 109 | break 110 | self.nodestack.append(nodeitem(1,elementtag,[self._getattr('class',attrs)],attrs,True)) 111 | self.tree.append([]) 112 | self.treestack = [self.tree[-1]] # opening tree stack frame 113 | self.openParentProperty(self.currentCompoundFormat) 114 | 115 | def openParentProperty(self,prop): 116 | self.treestack[-1].append((prop,[])) 117 | self.treestack.append(self.treestack[-1][-1][1]) 118 | 119 | def currentlyInAMicroformat(self): 120 | return self.nodestack != [] 121 | 122 | def nodeStartsAMicroformat(self, attrs): 123 | class_attr = self._getattr('class',attrs) 124 | if not class_attr: return False 125 | for a in class_attr.split(): 126 | if a in SCHEMAS.keys(): return True 127 | return False 128 | 129 | def emitAttributeAsPropertyIfExists(self, attrname, attrs, preds): 130 | obj = self._getattr(attrname,attrs) 131 | if obj is not None: 132 | try: 133 | pred = preds[0] 134 | if SCHEMAS[self.currentCompoundFormat].isValidProperty(pred): 135 | if attrname in ("href","src"): 136 | obj = urlparse.urljoin(self.url,obj) 137 | obj = self.makeDatesParsable(pred,obj) 138 | self.addPropertyValueToOutput(pred,obj) 139 | del preds[0] 140 | except IndexError: 141 | pass 142 | 143 | def addPropertyValueToOutput(self,prop,val): 144 | self.treestack[-1].append((prop,val)) 145 | 146 | def handle_endtag(self,tag): 147 | if self.currentlyInAMicroformat(): 148 | while 1: 149 | try: 150 | item = self.nodestack.pop() 151 | except IndexError: 152 | return # no more elements 153 | if item.tag == tag: 154 | break # found it! 155 | 156 | # if there's still predicates, then output the text as object 157 | if item.predicates and item.predicates != [] and self.chars.strip() != "": 158 | #if item.tag == 'a': 159 | # print "ITEM:a",self.treestack 160 | preds = item.predicates 161 | self.treestack[-1].append((preds[0],self.chars)) 162 | del preds[0] 163 | if item.nested == 1: 164 | self.treestack.pop() 165 | self.chars = "" 166 | 167 | # HTMLPARSER interface 168 | def handle_data(self,content): 169 | if self.hasPredicatesPending(): 170 | content = content.strip() 171 | if content == "": 172 | return 173 | self.chars += content 174 | 175 | def hasPredicatesPending(self): 176 | for n in self.nodestack: 177 | if n.predicates != []: 178 | return 1 179 | return 0 180 | 181 | # hack to stop dates like '20051005' being interpreted as floats downstream 182 | def makeDatesParsable(self,p,o): 183 | if p in ["dtstart","dtend"]: 184 | try: 185 | float(o) # can it be interpreted as a float? 186 | o = "%s-%s-%s"%(o[:4],o[4:6],o[6:]) 187 | except ValueError: 188 | pass 189 | return o 190 | 191 | 192 | def printTree(tree,tab=""): 193 | for p,v in tree: 194 | if isinstance(v,list): 195 | print tab + p 196 | printTree(v,tab+" ") 197 | else: 198 | print tab + unicode(p),":",v 199 | 200 | def printTreeStack(treestack,tab=""): 201 | for t in treestack: 202 | if isinstance(t,list): 203 | printTreeStack(t,tab+" ") 204 | else: 205 | print t 206 | 207 | def parse(f,url="http://dummyurl.com/"): 208 | m = MicroformatToStmts(url) 209 | try: 210 | s = f.read() 211 | except AttributeError: 212 | s = f 213 | m.feed(s) 214 | m.close() 215 | 216 | return m.tree 217 | 218 | 219 | if __name__ == "__main__": 220 | import urllib 221 | if len(sys.argv) == 1: 222 | print "Usage:",sys.argv[0],"" 223 | sys.exit(0) 224 | else: 225 | for url in sys.argv[1:]: 226 | trees = parse(urllib.urlopen(url),url) 227 | for tree in trees: 228 | printTree(tree) 229 | -------------------------------------------------------------------------------- /extractors/Extractor.py: -------------------------------------------------------------------------------- 1 | from Foundation import * 2 | from AppKit import * 3 | from WebKit import * 4 | from AddressBook import * 5 | from ScriptingBridge import * 6 | 7 | import re 8 | from email.utils import parseaddr 9 | import microformatparser 10 | import relmeparser 11 | import sgmllib 12 | from HTMLParser import HTMLParseError 13 | import urllib, urlparse, urllib2 14 | from urllib import quote 15 | import json 16 | 17 | import Cache 18 | from Utilities import * 19 | from Clue import * 20 | 21 | class Extractor(object): 22 | 23 | def __init__(self): 24 | #NSLog("** Extractor '%s' init"%self.__class__.__name__) 25 | super( Extractor, self ).__init__() 26 | self.addressBook = ABAddressBook.sharedAddressBook() 27 | 28 | def getClue( self, caller ): 29 | self.done = False 30 | NSObject.cancelPreviousPerformRequestsWithTarget_( self ) 31 | self.caller = caller 32 | self.clues() # implemented in subclasses. Calls addClues 33 | 34 | def addClues( self, clues, more_urls = [] ): 35 | print_info("addClues: %s %s" % (str(clues), str(more_urls))) 36 | if clues and self.caller: 37 | print_info("found a clue!") 38 | clues[0].addExtraUrls( more_urls ) 39 | self.caller.gotClue( clues[0] ) 40 | self.caller = None 41 | self.done = True 42 | NSObject.cancelPreviousPerformRequestsWithTarget_( self ) 43 | 44 | def clues_from_email( self, email, more_urls = [] ): 45 | if self.done: return 46 | # email look like 'Name ' sometimes. 47 | name, email = parseaddr( email ) 48 | print_info("Looking for people with email '%s'"%email) 49 | self.addClues( self._search_for( email, "Email" ), more_urls ) 50 | 51 | def clues_from_url( self, url, more_urls = [] ): 52 | if not url: return 53 | if self.done: return 54 | original = url # preserve 55 | 56 | if re.match(r'xmpp:', url): 57 | self.clues_from_jabber( re.sub(r'xmpp:', '', url) ) 58 | return 59 | 60 | if re.match(r'email:', url): 61 | self.clues_from_email( re.sub(r'email:', '', url) ) 62 | return 63 | 64 | if re.match(r'\w+:', url) and not re.match(r'http', url): 65 | # has a protocol, but isn't http 66 | return 67 | 68 | clues = self._search_for_url( url ) 69 | 70 | while not clues and re.search(r'//', url): 71 | url = re.sub(r'/[^/]*$','',url) 72 | clues += self._search_for_url( url ) 73 | 74 | if clues: 75 | self.addClues( clues, more_urls ) 76 | 77 | elif NSUserDefaults.standardUserDefaults().boolForKey_("googleSocial"): 78 | # this is a background process, calls us back later 79 | # order is a little sensitive for now, as if the cache is good, 80 | # the clues are updated _Before_ this function returns. 81 | # I consider this a bug in the implementation. 82 | self.getSocialGraphFor( original ) 83 | 84 | 85 | def _search_for_url( self, url ): 86 | url = normalize_url( url ) 87 | 88 | print_info("Looking for people with URL '%s'"%url) 89 | 90 | previous = Clue.forUrl( url ) 91 | if previous: return [ previous ] 92 | 93 | # search for url, plus url with trailing slash 94 | clues = self._search_for( url, "URLs", kABSuffixMatchCaseInsensitive ) 95 | clues += self._search_for( url + "/", "URLs", kABSuffixMatchCaseInsensitive ) 96 | return clues 97 | 98 | def clues_from_aim( self, username, more_urls = [] ): 99 | if self.done: return 100 | print_info("Looking for people with AIM %s"%username) 101 | self.addClues( self._search_for( username, kABAIMInstantProperty ), more_urls ) 102 | 103 | def clues_from_jabber( self, username, more_urls = [] ): 104 | if self.done: return 105 | print_info("Looking for people with Jabber %s"%username) 106 | self.addClues( self._search_for( username, kABJabberInstantProperty ), more_urls ) 107 | 108 | def clues_from_yahoo( self, username, more_urls = [] ): 109 | if self.done: return 110 | print_info("Looking for people with Yahoo! %s"%username) 111 | self.addClues( self._search_for( username, kABYahooInstantProperty ), more_urls ) 112 | 113 | def clues_from_name( self, name, more_urls = [] ): 114 | if self.done: return 115 | names = re.split(r'\s+', name) 116 | self.addClues( self.clues_from_names( names[0], names[-1], more_urls ) ) 117 | 118 | def clues_from_names( self, forename, surname, more_urls = [] ): 119 | if self.done: return 120 | print_info("Looking for people called '%s' '%s'"%( forename, surname )) 121 | forename_search = ABPerson.searchElementForProperty_label_key_value_comparison_( kABFirstNameProperty, None, None, forename, kABPrefixMatchCaseInsensitive ) 122 | surname_search = ABPerson.searchElementForProperty_label_key_value_comparison_( kABLastNameProperty, None, None, surname, kABEqualCaseInsensitive ) 123 | se = ABSearchElement.searchElementForConjunction_children_( kABSearchAnd, [ forename_search, surname_search ] ) 124 | self.addClues( map(lambda a: Clue.forPerson(a), self.addressBook.recordsMatchingSearchElement_( se )), more_urls ) 125 | 126 | 127 | def _search_for( self, thing, type, method = kABEqualCaseInsensitive ): 128 | if not thing or len(thing) == 0: 129 | return [] 130 | 131 | se = ABPerson.searchElementForProperty_label_key_value_comparison_( type, None, None, thing, method ) 132 | return map(lambda a: Clue.forPerson(a), self.addressBook.recordsMatchingSearchElement_( se )) 133 | 134 | 135 | def clues_from_html( self, source, url ): 136 | if self.done: return 137 | try: 138 | feeds = microformatparser.parse( source, url ) 139 | except HTMLParseError: 140 | feeds = [] 141 | except UnicodeDecodeError: 142 | feeds = [] 143 | except TypeError: 144 | feeds = [] 145 | 146 | try: 147 | relmes = relmeparser.parse( source, url ) 148 | except HTMLParseError: 149 | relmes = [] 150 | except UnicodeDecodeError: 151 | relmes = [] 152 | 153 | # try all rel="me" links for urls we can deal with. 154 | for relurl in relmes: 155 | self.clues_from_url( relurl, relmes ) 156 | if self.done: return 157 | 158 | if not feeds: return 159 | 160 | # I'm going to assume that the _first_ microformat on the page 161 | # is the person the page is about. I can't really do better 162 | # than that, can I? 163 | # TODO - yes, I can. Look for 'rel="me"' 164 | feed = feeds[0] 165 | 166 | # look for vcard microformats 167 | vcards = [ tree for name, tree in feed if name =='vcard'] 168 | if not vcards: return [] 169 | 170 | card = dict(vcards[0]) 171 | clues = [] 172 | 173 | if 'url' in card: 174 | self.clues_from_url( card['url'], [url] + relmes ) 175 | 176 | if 'email' in card: 177 | if isinstance(card['email'], str) or isinstance(card['email'], unicode): 178 | addrs = [ card['email'] ] 179 | else: 180 | addrs = [ e[1] for e in card['email'] ] 181 | 182 | for addr in addrs: 183 | # bloody flickr 184 | e = re.sub(r'\s*\[\s*at\s*\]\s*', '@', addr) 185 | self.clues_from_email( e, [url] + relmes ) 186 | 187 | if 'family-name' in card and 'given-name' in card: 188 | # TODO - check ordering here for .jp issues? Gah. 189 | self.clues_from_names( card['given-name'], card['family-name'], [url] + relmes ) 190 | 191 | if 'fn' in card: 192 | self.clues_from_name( card['fn'], [url] + relmes ) 193 | 194 | 195 | SOCIAL_GRAPH_CACHE = {} 196 | def getSocialGraphFor( self, url, more_urls = [] ): 197 | if not re.match(r'http', url): return 198 | 199 | if url in Extractor.SOCIAL_GRAPH_CACHE: 200 | print_info("using cached social graph data") 201 | self.addClues( Extractor.SOCIAL_GRAPH_CACHE[url], more_urls ) 202 | return 203 | api = "http://socialgraph.apis.google.com/lookup?pretty=1&fme=1&edo=1&edi=1" 204 | api += "&q=" + quote( url, '' ) 205 | print_info("Social graph API call to " + api ) 206 | # TODO _ respect more_urls here 207 | Cache.getContentOfUrlAndCallback( callback = self.gotSocialGraphData, url = api, timeout = 3600 * 48 ) # huge timeout here 208 | 209 | def gotSocialGraphData( self, raw, isStale ): 210 | try: 211 | data = json.loads( raw ) 212 | except ValueError: 213 | return # meh 214 | original_url = data['canonical_mapping'].keys()[0] 215 | urls = filter( lambda u: len(u) > 4 and re.match(r'http', u), data['nodes'].keys() ) # sometimes it returns '/' as a node. 216 | extra = [] 217 | for u in urls: 218 | if 'unverified_claiming_nodes' in data['nodes'][u]: 219 | extra += data['nodes'][u]['unverified_claiming_nodes'] 220 | urls += extra # TODO _ weed dupes 221 | 222 | for graph_url in urls: 223 | print_info("Google Social Graph URL '%s'"%graph_url) 224 | clues = self._search_for_url( graph_url ) 225 | self.addClues( clues ) 226 | if clues: 227 | Extractor.SOCIAL_GRAPH_CACHE[ original_url ] = clues 228 | return # done 229 | 230 | Extractor.SOCIAL_GRAPH_CACHE[ original_url ] = [] 231 | -------------------------------------------------------------------------------- /dev_appserver_login.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2007 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Helper CGI for logins/logout in the development application server. 18 | 19 | This CGI has these parameters: 20 | 21 | continue: URL to redirect to after a login or logout has completed. 22 | email: Email address to set for the client. 23 | admin: If 'True', the client should be logged in as an admin. 24 | action: What action to take ('Login' or 'Logout'). 25 | 26 | To view the current user information and a form for logging in and out, 27 | supply no parameters. 28 | """ 29 | 30 | 31 | import cgi 32 | import Cookie 33 | import md5 34 | import os 35 | import sys 36 | import urllib 37 | 38 | 39 | CONTINUE_PARAM = 'continue' 40 | EMAIL_PARAM = 'email' 41 | ADMIN_PARAM = 'admin' 42 | ACTION_PARAM = 'action' 43 | 44 | LOGOUT_ACTION = 'Logout' 45 | LOGIN_ACTION = 'Login' 46 | 47 | LOGOUT_PARAM = 'action=%s' % LOGOUT_ACTION 48 | 49 | COOKIE_NAME = 'dev_appserver_login' 50 | 51 | 52 | def GetUserInfo(http_cookie, cookie_name=COOKIE_NAME): 53 | """Get the requestor's user info from the HTTP cookie in the CGI environment. 54 | 55 | Args: 56 | http_cookie: Value of the HTTP_COOKIE environment variable. 57 | cookie_name: Name of the cookie that stores the user info. 58 | 59 | Returns: 60 | Tuple (email, admin) where: 61 | email: The user's email address, if any. 62 | admin: True if the user is an admin; False otherwise. 63 | """ 64 | cookie = Cookie.SimpleCookie(http_cookie) 65 | 66 | cookie_value = '' 67 | if cookie_name in cookie: 68 | cookie_value = cookie[cookie_name].value 69 | 70 | email, admin, user_id = (cookie_value.split(':') + ['', '', ''])[:3] 71 | return email, (admin == 'True'), user_id 72 | 73 | 74 | def CreateCookieData(email, admin): 75 | """Creates cookie payload data. 76 | 77 | Args: 78 | email, admin: Parameters to incorporate into the cookie. 79 | 80 | Returns: 81 | String containing the cookie payload. 82 | """ 83 | admin_string = 'False' 84 | if admin: 85 | admin_string = 'True' 86 | if email: 87 | user_id_digest = md5.new(email.lower()).digest() 88 | user_id = '1' + ''.join(['%02d' % ord(x) for x in user_id_digest])[:20] 89 | else: 90 | user_id = '' 91 | return '%s:%s:%s' % (email, admin_string, user_id) 92 | 93 | 94 | def SetUserInfoCookie(email, admin, cookie_name=COOKIE_NAME): 95 | """Creates a cookie to set the user information for the requestor. 96 | 97 | Args: 98 | email: Email to set for the user. 99 | admin: True if the user should be admin; False otherwise. 100 | cookie_name: Name of the cookie that stores the user info. 101 | 102 | Returns: 103 | 'Set-Cookie' header for setting the user info of the requestor. 104 | """ 105 | cookie_value = CreateCookieData(email, admin) 106 | set_cookie = Cookie.SimpleCookie() 107 | set_cookie[cookie_name] = cookie_value 108 | set_cookie[cookie_name]['path'] = '/' 109 | return '%s\r\n' % set_cookie 110 | 111 | 112 | def ClearUserInfoCookie(cookie_name=COOKIE_NAME): 113 | """Clears the user info cookie from the requestor, logging them out. 114 | 115 | Args: 116 | cookie_name: Name of the cookie that stores the user info. 117 | 118 | Returns: 119 | 'Set-Cookie' header for clearing the user info of the requestor. 120 | """ 121 | set_cookie = Cookie.SimpleCookie() 122 | set_cookie[cookie_name] = '' 123 | set_cookie[cookie_name]['path'] = '/' 124 | set_cookie[cookie_name]['max-age'] = '0' 125 | return '%s\r\n' % set_cookie 126 | 127 | 128 | LOGIN_TEMPLATE = """ 129 | 130 | Login 131 | 132 | 133 | 134 |
136 |
141 |

%(login_message)s

142 |

143 | 144 | 145 |

146 |

147 | 149 | 150 |

151 |

152 | 154 | 156 |

157 |
158 | 159 |
160 | 161 | 162 | 163 | """ 164 | 165 | 166 | def RenderLoginTemplate(login_url, continue_url, email, admin): 167 | """Renders the login page. 168 | 169 | Args: 170 | login_url, continue_url, email, admin: Parameters passed to 171 | LoginCGI. 172 | 173 | Returns: 174 | String containing the contents of the login page. 175 | """ 176 | login_message = 'Not logged in' 177 | if email: 178 | login_message = 'Logged in' 179 | admin_checked = '' 180 | if admin: 181 | admin_checked = 'checked' 182 | 183 | template_dict = { 184 | 185 | 186 | 'email': email or 'test\x40example.com', 187 | 'admin_checked': admin_checked, 188 | 'login_message': login_message, 189 | 'login_url': login_url, 190 | 'continue_url': continue_url 191 | } 192 | 193 | return LOGIN_TEMPLATE % template_dict 194 | 195 | 196 | def LoginRedirect(login_url, 197 | hostname, 198 | port, 199 | relative_url, 200 | outfile): 201 | """Writes a login redirection URL to a user. 202 | 203 | Args: 204 | login_url: Relative URL which should be used for handling user logins. 205 | hostname: Name of the host on which the webserver is running. 206 | port: Port on which the webserver is running. 207 | relative_url: String containing the URL accessed. 208 | outfile: File-like object to which the response should be written. 209 | """ 210 | dest_url = "http://%s:%s%s" % (hostname, port, relative_url) 211 | redirect_url = 'http://%s:%s%s?%s=%s' % (hostname, 212 | port, 213 | login_url, 214 | CONTINUE_PARAM, 215 | urllib.quote(dest_url)) 216 | outfile.write('Status: 302 Requires login\r\n') 217 | outfile.write('Location: %s\r\n\r\n' % redirect_url) 218 | 219 | 220 | def LoginCGI(login_url, 221 | email, 222 | admin, 223 | action, 224 | set_email, 225 | set_admin, 226 | continue_url, 227 | outfile): 228 | """Runs the login CGI. 229 | 230 | This CGI does not care about the method at all. For both POST and GET the 231 | client will be redirected to the continue URL. 232 | 233 | Args: 234 | login_url: URL used to run the CGI. 235 | email: Current email address of the requesting user. 236 | admin: True if the requesting user is an admin; False otherwise. 237 | action: The action used to run the CGI; 'Login' for a login action, 'Logout' 238 | for when a logout should occur. 239 | set_email: Email to set for the user; Empty if no email should be set. 240 | set_admin: True if the user should be an admin; False otherwise. 241 | continue_url: URL to which the user should be redirected when the CGI 242 | finishes loading; defaults to the login_url with no parameters (showing 243 | current status) if not supplied. 244 | outfile: File-like object to which all output data should be written. 245 | """ 246 | redirect_url = '' 247 | output_headers = [] 248 | 249 | if action: 250 | if action.lower() == LOGOUT_ACTION.lower(): 251 | output_headers.append(ClearUserInfoCookie()) 252 | elif set_email: 253 | output_headers.append(SetUserInfoCookie(set_email, set_admin)) 254 | 255 | redirect_url = continue_url or login_url 256 | 257 | if redirect_url: 258 | outfile.write('Status: 302 Redirecting to continue URL\r\n') 259 | for header in output_headers: 260 | outfile.write(header) 261 | outfile.write('Location: %s\r\n' % redirect_url) 262 | outfile.write('\r\n') 263 | else: 264 | outfile.write('Status: 200\r\n') 265 | outfile.write('Content-Type: text/html\r\n') 266 | outfile.write('\r\n') 267 | outfile.write(RenderLoginTemplate(login_url, 268 | continue_url, 269 | email, 270 | admin)) 271 | 272 | 273 | def main(): 274 | """Runs the login and logout CGI script.""" 275 | form = cgi.FieldStorage() 276 | login_url = os.environ['PATH_INFO'] 277 | email = os.environ.get('USER_EMAIL', '') 278 | admin = os.environ.get('USER_IS_ADMIN', '0') == '1' 279 | 280 | action = form.getfirst(ACTION_PARAM) 281 | set_email = form.getfirst(EMAIL_PARAM, '') 282 | set_admin = form.getfirst(ADMIN_PARAM, '') == 'True' 283 | continue_url = form.getfirst(CONTINUE_PARAM, '') 284 | 285 | LoginCGI(login_url, 286 | email, 287 | admin, 288 | action, 289 | set_email, 290 | set_admin, 291 | continue_url, 292 | sys.stdout) 293 | return 0 294 | 295 | 296 | if __name__ == '__main__': 297 | main() 298 | -------------------------------------------------------------------------------- /Clue.py: -------------------------------------------------------------------------------- 1 | import objc 2 | from AddressBook import * 3 | 4 | import re 5 | from time import time, gmtime 6 | from urllib import quote 7 | import json 8 | 9 | from Utilities import * 10 | 11 | from Provider import * 12 | 13 | Provider.addProvider( "BasicProvider" ) 14 | Provider.addProvider( "SpotlightProvider" ) 15 | Provider.addProvider( "TwitterProvider" ) 16 | Provider.addProvider( "DopplrProvider" ) 17 | Provider.addProvider( "LastFmProvider" ) 18 | Provider.addProvider( "FlickrProvider" ) 19 | # Order is important - FeedProvider must be _last_, because it uses all 20 | # urls in the address book card not claimed by another provider 21 | Provider.addProvider( "FeedProvider" ) 22 | 23 | 24 | class Clue(object): 25 | 26 | # Make Clue objects singletons - one Person in the address book, one Clue. 27 | # This way clues can retain their local providers and content, so a clue 28 | # you've seen before will display more quickly. 29 | CACHE = {} 30 | @classmethod 31 | def forPerson( cls, person ): 32 | if person.uniqueId() in Clue.CACHE: 33 | print_info("person is cached") 34 | return Clue.CACHE[ person.uniqueId() ] 35 | print_info("creating new person") 36 | Clue.CACHE[ person.uniqueId() ] = Clue( person ) 37 | return Clue.CACHE[ person.uniqueId() ] 38 | 39 | # sometimes, google will suggest a page for a person because I have 40 | # page A, that lnks to page B, that links to page C. Shelf suggests 41 | # page C as a page for this person. When I visit page C, Google will only 42 | # tell me about page B, and I won't be able to tie it back to that person. 43 | # To work around this, I'll just remember every url I get for a person. 44 | @classmethod 45 | def forUrl( cls, url ): 46 | for clue in Clue.CACHE.values(): 47 | if normalize_url( url) in [ normalize_url(u) for u in clue.urls() ]: 48 | return clue 49 | 50 | def __init__(self, person): 51 | # for now, clues are tied to AddressBook person objects. 52 | # But nothing outside the Clue object knows this - names, etc are 53 | # all extractd from the Clue object using methods on the Clue, not 54 | # on the ABPerson. 55 | # Clues are constructed using Clue.forPerson() from everywhere. Eventually 56 | # I'd like clues to be a little more flexible. 57 | self.person = person 58 | self.delegate = None 59 | self.extra_urls = [] # Urls from google social 60 | 61 | # on the first inflate of this person, ask google for more urls. 62 | if NSUserDefaults.standardUserDefaults().boolForKey_("googleSocialContext"): 63 | self.getMoreUrls() 64 | 65 | # create providers 66 | self.providers = [ cls(self) for cls in Provider.providers() ] 67 | 68 | 69 | def setDelegate_(self, delegate): 70 | self.delegate = delegate 71 | 72 | # Kick off all the providers to start getting information on the person. 73 | # providers call back to this object when they have something. 74 | def start(self): 75 | 76 | # the 'interesting' providers - flickr, twitter, etc - extract urls 77 | # from the boring_urls list based on regular expressions. The FeedProvider 78 | # wakes up right at the end, and turns everything left over into feeds. 79 | self.boring_urls = self.urls() 80 | 81 | # tell every provider to look for clues. 82 | # TODO - we really don't need to do this _incredibly_ often. a 30 second 83 | # timeout would make Shelf a lot less twitchy in terms of looking for feeds 84 | for provider in self.providers: 85 | provider.provide() 86 | 87 | self.changed() 88 | 89 | # use Google Social - ask it to tell us which urls are linked to using 90 | # rel="me" links from any of the urls that we already have for this person. 91 | def getMoreUrls( self ): 92 | if not self.ab_urls(): return # no point 93 | api = "http://socialgraph.apis.google.com/lookup?pretty=1&fme=1&q=" + ",".join([ quote(url) for url in self.ab_urls() ]) 94 | print_info("Social graph API call to " + api ) 95 | Cache.getContentOfUrlAndCallback( callback = self.gotSocialGraphData, url = api, timeout = 3600 * 48, wantStale = False, delay = 2 ) # huge timeout here 96 | 97 | # callback from Google Social call 98 | def gotSocialGraphData( self, raw, isStale ): 99 | try: 100 | data = json.loads( raw ) 101 | except ValueError: 102 | return # meh 103 | urls = data['nodes'].keys() 104 | self.addExtraUrls( urls ) 105 | self.start() # TODO - this kicks everything off again. Too heavy? 106 | 107 | def addExtraUrls( self, urls ): 108 | if not urls: return 109 | 110 | # build hash to dedupe - keys are the normalized url form, 111 | # values are the URLs as they came in. 112 | dedupe = {} 113 | for url in self.extra_urls + urls: 114 | if re.match(r'http', url): # HTTP only 115 | # respect existing normalizartion forms 116 | if not normalize_url( url ) in dedupe: 117 | dedupe[ normalize_url( url ) ] = url 118 | self.extra_urls = dedupe.values() 119 | 120 | # remove all the urls we already know about from the Address Book 121 | known_urls = [ normalize_url( url ) for url in self.ab_urls() ] 122 | for url in [ u for u in self.extra_urls ]: # cheap copy, as we're mutating the array and python doesn't like looping over an array you're changing in place 123 | if normalize_url( url ) in known_urls: 124 | self.extra_urls.remove(url) 125 | 126 | print_info("I have the Address Book urls '%s'"%(", ".join(self.ab_urls()))) 127 | print_info("Google gave me the extra urls '%s'"%(", ".join(self.extra_urls))) 128 | 129 | 130 | # the providers callback to this function when they have something new to say. 131 | # We just pass the message upwards 132 | def changed(self): 133 | if self.delegate: 134 | self.delegate.updateWebContent_fromClue_( self.content(), self ) 135 | 136 | # this method returns the HTML content that should be in the webview for this clue. 137 | def content(self): 138 | # jut cat together the content of our providers. 139 | # TODO - long term, I'd like providers to return smarter objects, with 140 | # contents and date and headings, so the front-end can group them by 141 | # source, or URL, or date, and get a date-sorted list of everything a 142 | # person has done. 143 | atoms = [] 144 | for p in self.providers: 145 | atoms += p.atoms 146 | 147 | atoms.sort(lambda a,b: cmp(b.sortOrder(), a.sortOrder())) 148 | content = "".join([ atom.content() for atom in atoms ]) 149 | if content: return content 150 | 151 | return "

thinking..

" 152 | 153 | # stop this clue from thinking soon. Tell all the providers to stop. 154 | def stop(self): 155 | NSObject.cancelPreviousPerformRequestsWithTarget_( self ) 156 | for current in self.providers: 157 | current.stop() 158 | 159 | 160 | # strip urls matching the passed regexp from the boring_urls list, and 161 | # return them. This lets providers 'take posession of' urls so the FeedProvider 162 | # doesn't see them 163 | def takeUrls(self,pattern): 164 | interesting = [] 165 | for u in self.urls(): 166 | if re.search(pattern, u): 167 | interesting.append(u) 168 | for u in interesting: 169 | if u in self.boring_urls: 170 | self.boring_urls.remove(u) 171 | return interesting 172 | 173 | 174 | ###################################### 175 | # These methods represent the properties of the underlying person 176 | 177 | # Used for == comparison 178 | def __eq__(self, other): 179 | if not other: return False 180 | return self.uniqueId() == other.uniqueId() 181 | 182 | # Stringify to something readable 183 | def __str__(self): 184 | return ""%self.displayName() 185 | 186 | # must be globally unique 187 | def uniqueId(self): 188 | return self.person.uniqueId() 189 | 190 | # returns an NSImage for this person. Falls back to a nice default if there's 191 | # nothing in the Address Book 192 | def image(self): 193 | if self.person.imageData(): 194 | return NSImage.alloc().initWithData_( self.person.imageData() ) 195 | if self.isCompany(): 196 | return NSImage.imageNamed_("NSUserGroup") 197 | else: 198 | return NSImage.imageNamed_("NSUser") 199 | 200 | def forename(self): 201 | return self.person.valueForProperty_(kABFirstNameProperty) 202 | 203 | def surname(self): 204 | return self.person.valueForProperty_(kABLastNameProperty) 205 | 206 | def isCompany(self): 207 | return ( self.person.valueForProperty_(kABPersonFlags) or 0 ) & kABShowAsCompany 208 | 209 | def displayName(self): 210 | if self.isCompany(): 211 | return self.person.valueForProperty_(kABOrganizationProperty) 212 | f = self.forename() 213 | s = self.surname() 214 | if s and f: return f + " " + s 215 | if s: return s 216 | if f: return f 217 | return "" 218 | 219 | def companyName(self): 220 | if self.isCompany(): return "" 221 | c = self.person.valueForProperty_(kABOrganizationProperty) 222 | if c: return c 223 | return "" 224 | 225 | def addresses(self): 226 | return self._multi_to_list( self.person.valueForProperty_(kABAddressProperty) ) 227 | 228 | def emails(self): 229 | return self._multi_to_list( self.person.valueForProperty_(kABEmailProperty) ) 230 | 231 | # Address Book urls. Rather than urls we have from all sources. 232 | def ab_urls(self): 233 | return self._multi_to_list( self.person.valueForProperty_(kABURLsProperty) ) 234 | 235 | def urls(self): 236 | return self.ab_urls() + self.extra_urls 237 | 238 | def email(self): 239 | return self.emails()[0] 240 | 241 | def birthday(self): 242 | try: 243 | if self.person.valueForProperty_( kABBirthdayProperty ): 244 | return gmtime( self.person.valueForProperty_( kABBirthdayProperty ).timeIntervalSince1970() ) 245 | except ValueError: # too old.. TODO - Um, I know people born <1970. Must fix. 246 | pass 247 | return None 248 | 249 | # utility method for dealing with the Cocoa Address Book interface. 250 | def _multi_to_list(self, multi): 251 | if not multi: return [] 252 | output = [] 253 | for i in range(0, multi.count() ): 254 | output.append( multi.valueAtIndex_( i ) ) 255 | return output 256 | -------------------------------------------------------------------------------- /PyShelfWindowController.py: -------------------------------------------------------------------------------- 1 | from Foundation import * 2 | from AppKit import * 3 | from WebKit import * 4 | from AddressBook import * 5 | from ScriptingBridge import * 6 | 7 | from Carbon.AppleEvents import kAEISGetURL, kAEInternetSuite 8 | import struct 9 | 10 | import objc 11 | import re 12 | import traceback 13 | import os 14 | from time import time as epoch_time 15 | 16 | from Utilities import * 17 | from Clue import * 18 | 19 | class ShelfController (NSWindowController): 20 | companyView = objc.IBOutlet() 21 | imageView = objc.IBOutlet() 22 | nameView = objc.IBOutlet() 23 | webView = objc.IBOutlet() 24 | prefsWindow = objc.IBOutlet() 25 | 26 | # first-cut init goes here - we've been woken up, and all the GUI 27 | # component objects exist. Don't spend too long here, though, I think 28 | # the app icon is still bouncing. 29 | def awakeFromNib(self): 30 | self.handlers = {} 31 | self.current_clue = None 32 | 33 | # get the RGB hex code of the system 'background' color. 34 | bg = self.window().backgroundColor().colorUsingColorSpaceName_( NSCalibratedRGBColorSpace ) 35 | rgb = "%x%x%x"%( 36 | bg.redComponent() * 255.999999, 37 | bg.greenComponent() * 255.999999, 38 | bg.blueComponent() * 255.999999 39 | ) 40 | # TODO - ok. Now do something with this information. Specifically, get it into the CSS. 41 | 42 | # evil. Alter the webkit view object so that it'll accept a clickthrough 43 | # - this is very handy, as the window is on top and full of context. 44 | # Alas, right now, the hover doesn't percolate through, so you don't 45 | # get mouseover effects. But clicks work. 46 | objc.classAddMethod( WebHTMLView, "acceptsFirstMouse:", lambda a,b: 1 ) 47 | # ps - when I say 'evil', I mean it. Really, _really_ evil. TODO - 48 | # subclass the thing and do it properly. 49 | 50 | # create application support folder. The cache goes here. I suppose 51 | # I should really keep it in a Cache folder somewhere 52 | folder = os.path.join( os.environ['HOME'], "Library", "Application Support", "Shelf" ) 53 | if not os.path.exists( folder ): 54 | os.mkdir( folder ) 55 | 56 | 57 | 58 | # Add a handler for the event GURL/GURL. One might think that 59 | # Carbon.AppleEvents.kEISInternetSuite/kAEISGetURL would work, 60 | # but the system headers (and hence the Python wrapper for those) 61 | # are wrong. 62 | manager = NSAppleEventManager.sharedAppleEventManager() 63 | 64 | manager.setEventHandler_andSelector_forEventClass_andEventID_( 65 | self, 'handleURLEvent:withReplyEvent:', fourCharToInt( "GURL" ), fourCharToInt( "GURL" )) 66 | 67 | # this is called once we're all launched. Bouncing all over now. 68 | def applicationDidFinishLaunching_(self, sender): 69 | # There's no initial context. 70 | self.fade() 71 | 72 | # start polling right away 73 | self.performSelector_withObject_afterDelay_( 'poll', None, 0 ) 74 | 75 | 76 | # we've been told to close 77 | def applicationWillTerminate_(self, sender): 78 | # if we're doing anything in the background, stop it. 79 | if self.current_clue: 80 | self.current_clue.stop() 81 | 82 | # kill the poller and any other long-running things 83 | NSObject.cancelPreviousPerformRequestsWithTarget_( self ) 84 | 85 | 86 | # This is the callback from the little right-pointing arrow on the main 87 | # window, to the right of the person icon. Means 'open in address book' 88 | def openRecord_(self, thing): 89 | if self.current_clue: 90 | NSWorkspace.sharedWorkspace().openURL_( 91 | NSURL.URLWithString_("addressbook://%s"%self.current_clue.uniqueId()) 92 | ) 93 | 94 | def hotKeyPressed(self): 95 | # TODO - this list here is a vage grab-bag of things I want to happen. 96 | # Should think about how we want to feedback on a deliberate poll, 97 | # and how to fade the window. 98 | self.current_clue = None 99 | self.fade() 100 | self.deferFade(3) # cause the window to vanish again if we don'e find anything 101 | self.window().setHidesOnDeactivate_( False ) 102 | self.showWindow_(self) 103 | self.window().display() 104 | self.window().orderFrontRegardless() 105 | self.poll() 106 | 107 | # return an Extractor class instance (confusingly called 'handler' for now. Must fix..) 108 | # for the app with the passed bundle name. 109 | def handler_for( self, bundle ): 110 | if not bundle: return None 111 | 112 | # haven't seen this application before? Look for a file on disk with 113 | # the right name and load it. 114 | if not bundle in self.handlers: 115 | # convert bundlename to classname like 'ComAppleMail' 116 | classname = re.sub(r'\.(\w)', lambda m: m.group(1).upper(), bundle ) 117 | classname = re.sub(r'^(\w)', lambda m: m.group(1).upper(), classname ) 118 | 119 | print_info("** importing file for class %s"%( classname )) 120 | try: 121 | # this imports the module with the name 'clasname'.py as the local variable mod 122 | mod = __import__(classname, globals(), locals(), ['']) 123 | # then get the single class attribute from that module object 124 | cls = getattr( mod, classname ) 125 | # instantiate the class, and remember it so we don't do this again 126 | self.handlers[ bundle ] = cls() 127 | except ImportError: 128 | import traceback 129 | print_info( "** Couldn't import file for %s"%( classname ) ) 130 | print_info( traceback.format_exc () ) 131 | self.handlers[ bundle ] = None 132 | 133 | return self.handlers[ bundle ] 134 | 135 | # the main poll loop. Called regularly. 136 | def poll(self): 137 | print_info( "\n---- poll start ----" ) 138 | 139 | # First thing I do, schedule the next poll event, so that I can just return with impunity from this function 140 | if not NSUserDefaults.standardUserDefaults().boolForKey_("useHotkey"): 141 | self.performSelector_withObject_afterDelay_( 'poll', None, 4 ) 142 | 143 | # get bundle name of active application 144 | try: 145 | bundle = NSWorkspace.sharedWorkspace().activeApplication()['NSApplicationBundleIdentifier'] 146 | except KeyError: 147 | # have seen this in the real world. Can't explain it. 148 | print( "Inexplicable lack of 'NSApplicationBundleIdentifier' for %s"%repr( NSWorkspace.sharedWorkspace().activeApplication() ) ) 149 | return 150 | 151 | print_info( "current app is %s"%bundle ) 152 | 153 | # this app has no effect on the current context, otherwise activating 154 | # the app drops the current context. TODO - don't hard-code bundle name 155 | if bundle.lower() in ["org.jerakeen.pyshelf"]: 156 | print_info("Ignoring myself") 157 | self.deferFade() 158 | return 159 | 160 | handler = self.handler_for( bundle ) 161 | if not handler: 162 | print_info("Don't know how to get clues from %s"%bundle) 163 | return 164 | 165 | self.performSelector_withObject_afterDelay_("lookForCluesWith:", handler, 0) 166 | 167 | def lookForCluesWith_( self, handler ): 168 | # tell the handler to look for clues. Pass it 'self' so that it 169 | # can call us back 170 | handler.getClue(self) 171 | 172 | 173 | # callback from getClue on the handler function 174 | def gotClue(self, clue): 175 | self.deferFade() # put off the context fade 176 | 177 | if self.current_clue and self.current_clue == clue: 178 | # the context hasn't changed. Don't do anything. 179 | return 180 | 181 | # clue has changed 182 | print_info("New context - %s"%clue) 183 | if self.current_clue: 184 | self.current_clue.stop() 185 | self.current_clue = clue 186 | self.performSelector_withObject_afterDelay_('updateInfo', None, 0 ) 187 | 188 | 189 | # fade the active context if we don't recieve any context for a while. 190 | # call this method every time something interesting happens, and it'll 191 | # stop the window going away for another few seconds. That way, I don't 192 | # have to explicitly watch for 'nothing happened'. 193 | def deferFade(self, count = 5): 194 | NSObject.cancelPreviousPerformRequestsWithTarget_selector_object_( self, "fade", None ) 195 | self.performSelector_withObject_afterDelay_('fade', None, count ) 196 | 197 | # Put window into 'no context, fall to background' state, clear current state 198 | def fade(self): 199 | if NSUserDefaults.standardUserDefaults().boolForKey_("useHotkey") and self.current_clue: 200 | # if we're hotkey driven, don't passively fade ever 201 | return 202 | 203 | print_info("fading...") 204 | if self.current_clue: 205 | self.current_clue.stop() 206 | self.current_clue = None 207 | 208 | self.window().setLevel_( NSNormalWindowLevel ) # unstuff from 'on top' 209 | self.window().setHidesOnDeactivate_( True ) # hide window if we have nothing 210 | 211 | self.nameView.setStringValue_( "" ) 212 | self.companyView.setStringValue_( "" ) 213 | self.imageView.setImage_( NSImage.imageNamed_("NSUser") ) 214 | base = NSURL.fileURLWithPath_( NSBundle.mainBundle().resourcePath() ) 215 | self.setWebContent_( "" ) 216 | 217 | # put the window into 'I have context' state, display the header, and 218 | # tell the Clue object to start fetching state about itself. 219 | def updateInfo(self): 220 | clue = self.current_clue 221 | if not clue: return 222 | clue.setDelegate_(self) # so the clue can send us 'I have updated' messages 223 | 224 | self.nameView.setStringValue_( clue.displayName() ) 225 | self.companyView.setStringValue_( clue.companyName() ) 226 | self.imageView.setImage_( clue.image() ) # does this leak? 227 | base = NSURL.fileURLWithPath_( NSBundle.mainBundle().resourcePath() ) 228 | self.setWebContent_( clue.content() ) # will initially be 'thinking..' 229 | 230 | # always safe 231 | self.window().setHidesOnDeactivate_( False ) 232 | 233 | if NSUserDefaults.standardUserDefaults().boolForKey_("bringAppForward"): 234 | # slightly voodoo, this. But otherwise it doesn't seem 100% reliable 235 | self.showWindow_(self) 236 | self.window().orderFrontRegardless() 237 | 238 | if NSUserDefaults.standardUserDefaults().boolForKey_("alwaysOnTop"): 239 | self.window().setLevel_( NSFloatingWindowLevel ) # 'on top' 240 | 241 | self.window().display() 242 | 243 | # do this so we can return to the main runloop ASAP, so the 244 | # webview has a chance to display something. 245 | self.performSelector_withObject_afterDelay_('kickClue', None, 0 ) 246 | 247 | 248 | def kickClue(self): 249 | if self.current_clue: 250 | self.current_clue.start() 251 | 252 | # called from the clue when it's updated itself and wants to add to the webview 253 | def updateWebContent_fromClue_(self, content, clue): 254 | # old clues may still expect to be able to update the data. 255 | if clue == self.current_clue: 256 | self.setWebContent_( content ) 257 | 258 | def setWebContent_(self, html): 259 | # the base path of the webview is the resource folder, so I can use 260 | # relative paths to refer to the CSS. 261 | base = NSURL.fileURLWithPath_( NSBundle.mainBundle().resourcePath() ) 262 | self.webView.mainFrame().loadHTMLString_baseURL_( """ 263 | 264 | 265 | 266 | 267 | 268 | %s 269 | 270 | 271 | """%( 272 | #"file:///Users/tomi/svn/Projects/Shelf/style.css?%s"%int(epoch_time()), # dev 273 | "style.css", # live 274 | html 275 | ), base ) 276 | 277 | # supress the 'reload' item from the right-click menu - it makes no sense 278 | def webView_contextMenuItemsForElement_defaultMenuItems_( self, webview, element, items ): 279 | return filter( lambda i: i.title() != "Reload", items ) 280 | 281 | # stolen from djangokit. When the webview wants to fetch a resource, 282 | # it means either 'I want a file off disk to serve this page' (ok, then) 283 | # or 'I want to follow this link' (no, I'll just get the system to do that). 284 | def webView_decidePolicyForNavigationAction_request_frame_decisionListener_( self, webview, action, request, frame, listener): 285 | url = request.URL() 286 | 287 | # serve files 288 | if url.scheme() == 'file' or url.scheme() == 'about': # local files 289 | listener.use() 290 | return 291 | 292 | # everything else can be ignored, and opened by the system 293 | listener.ignore() 294 | NSWorkspace.sharedWorkspace().openURL_( url ) 295 | 296 | def getDopplrToken_(self, sender): 297 | url = "https://www.dopplr.com/api/AuthSubRequest?scope=http://www.dopplr.com&next=shelf://shelf/&session=1" 298 | NSWorkspace.sharedWorkspace().openURL_( NSURL.URLWithString_(url) ) 299 | 300 | def handleURLEvent_withReplyEvent_(self, event, replyEvent): 301 | theURL = event.descriptorForKeyword_(fourCharToInt('----')) 302 | 303 | # hack this to check for file: URLs 304 | matches = re.search('^shelf:(file://.+)', theURL.stringValue()) 305 | if matches: 306 | filename = matches.group(1) 307 | print filename 308 | os.popen('open %s' % filename) 309 | return 310 | 311 | matches = re.search(r'token=(.*)', theURL.stringValue()) 312 | if matches: 313 | token = matches.group(1) 314 | url = "https://www.dopplr.com/api/AuthSubSessionToken?token=%s"%( token ) 315 | Cache.getContentOfUrlAndCallback( callback = self.gotDopplrToken, url = url, timeout = 3600, wantStale = False ) 316 | return 317 | else: 318 | alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_( 319 | "Shelf", "Continue", None, None, "Failed to get token - sorry.") 320 | 321 | self.prefsWindow.display() 322 | self.prefsWindow.makeKeyAndOrderFront_(self) 323 | alert.beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo_( 324 | self.prefsWindow, self, None, None) 325 | 326 | 327 | def gotDopplrToken(self, data, stale): 328 | matches = re.search(r'Token=(.*)', data) 329 | if matches: 330 | token = matches.group(1) 331 | NSUserDefaults.standardUserDefaults().setObject_forKey_(token, "dopplrToken") 332 | NSUserDefaults.standardUserDefaults().synchronize() 333 | alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_( 334 | "Shelf", "Continue", None, None, "Got a Dopplr token!") 335 | else: 336 | alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_( 337 | "Shelf", "Continue", None, None, "Failed to get token - sorry.") 338 | 339 | self.prefsWindow.display() 340 | self.prefsWindow.makeKeyAndOrderFront_(self) 341 | alert.beginSheetModalForWindow_modalDelegate_didEndSelector_contextInfo_( 342 | self.prefsWindow, self, None, None) 343 | 344 | 345 | def fourCharToInt(code): 346 | return struct.unpack('>l', code)[0] 347 | 348 | --------------------------------------------------------------------------------