├── star_full.png ├── star_empty.png ├── Vendor ├── .DS_Store └── STTwitter │ ├── STTwitter.h │ ├── NSDateFormatter+STTwitter.h │ ├── NSString+STTwitter.h │ ├── NSDateFormatter+STTwitter.m │ ├── STTwitterOS.h │ ├── STTwitterOSRequest.h │ ├── STHTTPRequest+STTwitter.h │ ├── STTwitterAppOnly.h │ ├── STTwitterHTML.h │ ├── Vendor │ ├── BAVPlistNode.h │ ├── BAVPlistNode.m │ ├── JSONSyntaxHighlight.h │ ├── STHTTPRequest.h │ └── JSONSyntaxHighlight.m │ ├── STTwitterProtocol.h │ ├── NSString+STTwitter.m │ ├── NSError+STTwitter.m │ ├── STTwitterOAuth.h │ ├── NSError+STTwitter.h │ ├── STTwitterHTML.m │ ├── STHTTPRequest+STTwitter.m │ ├── STTwitterOSRequest.m │ ├── STTwitterOS.m │ └── STTwitterAppOnly.m ├── art └── twithunter.png ├── English.lproj └── InfoPlist.strings ├── TwitHunter_Prefix.pch ├── TwitterXAuthTokens.plist ├── TwitHunter.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── nst.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── WorkspaceSettings.xcsettings └── xcuserdata │ └── nst.xcuserdatad │ ├── xcdebugger │ └── Breakpoints.xcbkptlist │ └── xcschemes │ ├── xcschememanagement.plist │ └── TwitHunter.xcscheme ├── THTextView.h ├── THCollectionView.h ├── THTextRule.m ├── main.m ├── Defaults.plist ├── NSString+TH.h ├── THTextRule.h ├── STJSONIP.h ├── version.plist ├── NSManagedObject+ST.h ├── THTweetView.h ├── THTweetLocation.h ├── TwitterClients.plist ├── THTweetCollectionViewItem.h ├── THAppDelegate.h ├── THTweetLocation.m ├── Info.plist ├── THLocationVC.h ├── THCumulativeChartView.h ├── THUser.h ├── THTweetView.m ├── STJSONIP.m ├── THPreferencesWC.h ├── THTweet.h ├── THUser.m ├── THController.h ├── NSString+TH.m ├── README.markdown ├── NSManagedObject+ST.m ├── THTextView.m ├── TwitHunter.xcdatamodel └── contents ├── THLocationVC.m ├── THCollectionView.m ├── THCumulativeChartView.m ├── THTweetCollectionViewItem.m ├── THAppDelegate.m ├── THPreferencesWC.m └── THTweet.m /star_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/TwitHunter/master/star_full.png -------------------------------------------------------------------------------- /star_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/TwitHunter/master/star_empty.png -------------------------------------------------------------------------------- /Vendor/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/TwitHunter/master/Vendor/.DS_Store -------------------------------------------------------------------------------- /art/twithunter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/TwitHunter/master/art/twithunter.png -------------------------------------------------------------------------------- /English.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/TwitHunter/master/English.lproj/InfoPlist.strings -------------------------------------------------------------------------------- /Vendor/STTwitter/STTwitter.h: -------------------------------------------------------------------------------- 1 | #import "STTwitterAPI.h" 2 | #import "STTwitterHTML.h" 3 | #import "NSDateFormatter+STTwitter.h" 4 | -------------------------------------------------------------------------------- /TwitHunter_Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'TwitHunter' target in the 'TwitHunter' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /TwitterXAuthTokens.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /TwitHunter.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TwitHunter.xcodeproj/project.xcworkspace/xcuserdata/nst.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/TwitHunter/master/TwitHunter.xcodeproj/project.xcworkspace/xcuserdata/nst.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /THTextView.h: -------------------------------------------------------------------------------- 1 | // 2 | // THTextView.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/13/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface THTextView : NSTextView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /THCollectionView.h: -------------------------------------------------------------------------------- 1 | // 2 | // THCollectionView.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/13/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface THCollectionView : NSCollectionView 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /THTextRule.m: -------------------------------------------------------------------------------- 1 | // 2 | // TextRule.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 21.04.09. 6 | // Copyright 2009 Sen:te. All rights reserved. 7 | // 8 | 9 | #import "THTextRule.h" 10 | 11 | 12 | @implementation THTextRule 13 | 14 | @dynamic score; 15 | @dynamic keyword; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 19.04.09. 6 | // Copyright Sen:te 2009. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | return NSApplicationMain(argc, (const char **) argv); 14 | } 15 | -------------------------------------------------------------------------------- /Defaults.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | score 6 | 40 7 | updateFrequency 8 | 10 9 | 10 | 11 | -------------------------------------------------------------------------------- /NSString+TH.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+TH.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 06.05.09. 6 | // Copyright 2009 Sen:te. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | @interface NSString (TH) 13 | 14 | //- (NSAttributedString *)attributedStringWithURLs; 15 | - (unsigned long long)unsignedLongLongValue; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Vendor/STTwitter/NSDateFormatter+STTwitter.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSDateFormatter+STTwitter.h 3 | // curtter 4 | // 5 | // Created by Nicolas Seriot on 16/11/13. 6 | // Copyright (c) 2013 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSDateFormatter (STTwitter) 12 | 13 | + (NSDateFormatter *)st_TwitterDateFormatter; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /THTextRule.h: -------------------------------------------------------------------------------- 1 | // 2 | // TextRule.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 21.04.09. 6 | // Copyright 2009 Sen:te. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | @interface THTextRule : NSManagedObject 13 | { 14 | } 15 | 16 | @property (nonatomic, strong) NSNumber * score; 17 | @property (nonatomic, strong) NSString * keyword; 18 | 19 | @end 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /STJSONIP.h: -------------------------------------------------------------------------------- 1 | // 2 | // STIPAddress.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/10/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface STJSONIP : NSObject 12 | 13 | + (void)getExternalIPAddressWithSuccessBlock:(void(^)(NSString *ipAddress))successBlock 14 | errorBlock:(void(^)(NSError *error))errorBlock; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /TwitHunter.xcodeproj/project.xcworkspace/xcuserdata/nst.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges 6 | 7 | SnapshotAutomaticallyBeforeSignificantChanges 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /version.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildVersion 6 | 18 7 | CFBundleVersion 8 | 1.0 9 | ProjectName 10 | NibPBTemplates 11 | SourceVersion 12 | 1200000 13 | 14 | 15 | -------------------------------------------------------------------------------- /TwitHunter.xcodeproj/xcuserdata/nst.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /NSManagedObject+ST.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSManagedObject+iLog.h 3 | // iLog 4 | // 5 | // Created by Nicolas Seriot on 22.03.09. 6 | // Copyright 2009 Sen:te. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | @interface NSManagedObject (ST) 13 | 14 | + (NSEntityDescription *)entityInContext:(NSManagedObjectContext *)context; 15 | + (id)createInContext:(NSManagedObjectContext *)context; 16 | + (void)deleteAllObjectsInContext:(NSManagedObjectContext *)context; 17 | 18 | - (BOOL)save; 19 | - (void)deleteObject; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /THTweetView.h: -------------------------------------------------------------------------------- 1 | // 2 | // THTweetView.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/15/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class THTweetView; 12 | 13 | @protocol THTweetViewProtocol 14 | - (void)tweetViewWasClicked:(THTweetView *)tweetView; 15 | @end 16 | 17 | @interface THTweetView : NSView 18 | 19 | @property (nonatomic, unsafe_unretained) id delegate; 20 | 21 | @property (nonatomic) BOOL selected; 22 | @property (nonatomic) BOOL isRead; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /THTweetLocation.h: -------------------------------------------------------------------------------- 1 | // 2 | // THTweetLocation.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/4/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface THTweetLocation : NSObject 12 | 13 | @property (nonatomic, strong) NSString *ipAddress; 14 | 15 | @property (nonatomic, strong) NSString *placeID; 16 | @property (nonatomic, strong) NSString *fullName; 17 | 18 | @property (nonatomic, strong) NSString *latitude; 19 | @property (nonatomic, strong) NSString *longitude; 20 | 21 | @property (nonatomic, strong) NSString *query; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /TwitHunter.xcodeproj/xcuserdata/nst.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | TwitHunter.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 8D1107260486CEB800E47090 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /TwitterClients.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ck 7 | CjulERsDeqhhjSme66ECg 8 | cs 9 | IQWdVyqFxghAtURHGeGiWAsmCAGmdW3WmbEx6Hck 10 | name 11 | Twitter for iPad 12 | 13 | 14 | ck 15 | IQKbtAYlXLripLGPWd0HUA 16 | cs 17 | GgDYlkSvaPxGxC4X8liwpUoqKwwr3lCADbz8A7ADU 18 | name 19 | Twitter for iPhone 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /THTweetCollectionViewItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // TweetCollectionViewItem.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 20.04.09. 6 | // Copyright 2009 Sen:te. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "THTweetView.h" 11 | 12 | @class THTextView; 13 | 14 | @interface THTweetCollectionViewItem : NSCollectionViewItem 15 | 16 | @property (nonatomic, strong) IBOutlet THTextView *tweetTextTextView; 17 | 18 | - (IBAction)openUserWebTimeline:(id)sender; 19 | - (IBAction)toggleReadState:(id)sender; 20 | - (IBAction)markAsRead:(id)sender; 21 | 22 | - (IBAction)retweet:(id)sender; 23 | - (IBAction)reply:(id)sender; 24 | - (IBAction)remoteDelete:(id)sender; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /Vendor/STTwitter/NSString+STTwitter.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+STTwitter.h 3 | // STTwitter 4 | // 5 | // Created by Nicolas Seriot on 11/2/12. 6 | // Copyright (c) 2012 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | extern NSUInteger kSTTwitterDefaultShortURLLength; 12 | extern NSUInteger kSTTwitterDefaultShortURLLengthHTTPS; 13 | 14 | @interface NSString (STTwitter) 15 | 16 | - (NSString *)st_firstMatchWithRegex:(NSString *)regex error:(NSError **)e; 17 | 18 | // use values from GET help/configuration 19 | - (NSInteger)st_numberOfCharactersInATweetWithShortURLLength:(NSUInteger)shortURLLength 20 | shortURLLengthHTTPS:(NSUInteger)shortURLLengthHTTS; 21 | 22 | // use default values for URL shortening 23 | - (NSInteger)st_numberOfCharactersInATweet; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /THAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // TwitHunter_AppDelegate.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 19.04.09. 6 | // Copyright Sen:te 2009 . All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface THAppDelegate : NSObject 12 | { 13 | NSWindow *window; 14 | 15 | NSPersistentStoreCoordinator *persistentStoreCoordinator; 16 | NSManagedObjectModel *managedObjectModel; 17 | NSManagedObjectContext *managedObjectContext; 18 | } 19 | 20 | @property (nonatomic, strong) IBOutlet NSWindow *window; 21 | 22 | @property (nonatomic, strong, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; 23 | @property (nonatomic, strong, readonly) NSManagedObjectModel *managedObjectModel; 24 | @property (nonatomic, strong, readonly) NSManagedObjectContext *managedObjectContext; 25 | 26 | - (IBAction)saveAction:sender; 27 | 28 | @end -------------------------------------------------------------------------------- /Vendor/STTwitter/NSDateFormatter+STTwitter.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDateFormatter+STTwitter.m 3 | // curtter 4 | // 5 | // Created by Nicolas Seriot on 16/11/13. 6 | // Copyright (c) 2013 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import "NSDateFormatter+STTwitter.h" 10 | 11 | static NSDateFormatter *stTwitterDateFormatter = nil; 12 | 13 | @implementation NSDateFormatter (STTwitter) 14 | 15 | + (NSDateFormatter *)st_TwitterDateFormatter { 16 | 17 | // parses the 'created_at' field, eg. "Sun Jun 28 20:33:01 +0000 2009" 18 | 19 | if(stTwitterDateFormatter == nil) { 20 | stTwitterDateFormatter = [[NSDateFormatter alloc] init]; 21 | [stTwitterDateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; 22 | [stTwitterDateFormatter setDateFormat:@"EEE MMM dd HH:mm:ss Z yyyy"]; 23 | } 24 | 25 | return stTwitterDateFormatter; 26 | } 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /THTweetLocation.m: -------------------------------------------------------------------------------- 1 | // 2 | // THTweetLocation.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/4/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import "THTweetLocation.h" 10 | 11 | @implementation THTweetLocation 12 | 13 | - (id)copyWithZone:(NSZone *)zone { 14 | THTweetLocation *tl = [[THTweetLocation alloc] init]; 15 | 16 | tl.ipAddress = [_ipAddress copy]; 17 | tl.placeID = [_placeID copy]; 18 | tl.latitude = [_latitude copy]; 19 | tl.longitude = [_longitude copy]; 20 | tl.fullName = [_fullName copy]; 21 | tl.query = [_query copy]; 22 | 23 | return tl; 24 | } 25 | 26 | - (NSString *)description { 27 | if(_placeID) { 28 | return _fullName; 29 | } else if(_latitude && _longitude) { 30 | return [NSString stringWithFormat:@"%@, %@", _latitude, _longitude]; 31 | } 32 | 33 | return @""; 34 | } 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /Vendor/STTwitter/STTwitterOS.h: -------------------------------------------------------------------------------- 1 | // 2 | // STTwitterOS.h 3 | // STTwitter 4 | // 5 | // Created by Nicolas Seriot on 5/1/10. 6 | // Copyright 2010 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "STTwitterProtocol.h" 11 | 12 | NS_ENUM(NSUInteger, STTwitterOSErrorCode) { 13 | STTwitterOSSystemCannotAccessTwitter, 14 | STTwitterOSCannotFindTwitterAccount, 15 | STTwitterOSUserDeniedAccessToTheirAccounts, 16 | STTwitterOSNoTwitterAccountIsAvailable 17 | }; 18 | 19 | @class ACAccount; 20 | 21 | @interface STTwitterOS : NSObject 22 | 23 | + (instancetype)twitterAPIOSWithAccount:(ACAccount *)account; 24 | + (instancetype)twitterAPIOSWithFirstAccount; 25 | 26 | - (BOOL)canVerifyCredentials; 27 | - (void)verifyCredentialsWithSuccessBlock:(void(^)(NSString *username))successBlock errorBlock:(void(^)(NSError *error))errorBlock; 28 | 29 | - (NSString *)username; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | ch.seriot.TwitHunter 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.2 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 22 25 | NSMainNibFile 26 | MainMenu 27 | NSPrincipalClass 28 | NSApplication 29 | 30 | 31 | -------------------------------------------------------------------------------- /Vendor/STTwitter/STTwitterOSRequest.h: -------------------------------------------------------------------------------- 1 | // 2 | // STTwitterOSRequest.h 3 | // STTwitterDemoOSX 4 | // 5 | // Created by Nicolas Seriot on 20/02/14. 6 | // Copyright (c) 2014 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class ACAccount; 12 | 13 | @interface STTwitterOSRequest : NSObject 14 | 15 | - (id)initWithAPIResource:(NSString *)resource 16 | baseURLString:(NSString *)baseURLString 17 | httpMethod:(NSInteger)httpMethod 18 | parameters:(NSDictionary *)params 19 | account:(ACAccount *)account 20 | uploadProgressBlock:(void(^)(NSInteger bytesWritten, NSInteger totalBytesWritten, NSInteger totalBytesExpectedToWrite))uploadProgressBlock 21 | completionBlock:(void(^)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, id response))completionBlock 22 | errorBlock:(void(^)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error))errorBlock; 23 | 24 | - (NSURLConnection *)startRequest; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /THLocationVC.h: -------------------------------------------------------------------------------- 1 | // 2 | // THLocationVC.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/9/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class STTwitterAPI; 12 | @class THTweetLocation; 13 | @class THLocationPanel; 14 | @class THLocationVC; 15 | 16 | @protocol THLocationVCProtocol 17 | - (void)locationVC:(THLocationVC *)locationVC didChooseLocation:(THTweetLocation *)location; 18 | - (void)locationVCDidCancel:(THLocationVC *)locationVC; 19 | @end 20 | 21 | @interface THLocationVC : NSViewController 22 | 23 | @property (nonatomic, unsafe_unretained) id locationDelegate; 24 | @property (nonatomic, strong) STTwitterAPI *twitter; 25 | @property (nonatomic, strong) THTweetLocation *tweetLocation; 26 | @property (nonatomic, strong) IBOutlet NSArrayController *twitterPlacesController; 27 | @property (nonatomic, strong) NSArray *twitterPlaces; 28 | 29 | - (IBAction)lookupIPAddress:(id)sender; 30 | - (IBAction)lookupCoordinates:(id)sender; 31 | - (IBAction)lookupQuery:(id)sender; 32 | 33 | - (IBAction)ok:(id)sender; 34 | - (IBAction)cancel:(id)sender; 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /THCumulativeChartView.h: -------------------------------------------------------------------------------- 1 | // 2 | // CumulativeChartView.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 5/1/10. 6 | // Copyright 2010 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #define MAX_COUNT 100 12 | 13 | @class THCumulativeChartView; 14 | 15 | @protocol CumulativeChartViewDelegate 16 | - (void)chartView:(THCumulativeChartView *)aChartView didSlideToScore:(NSUInteger)aScore; 17 | - (void)chartView:(THCumulativeChartView *)aChartView didStopSlidingOnScore:(NSUInteger)aScore; 18 | @end 19 | 20 | @protocol CumulativeChartViewDataSource 21 | - (NSUInteger)numberOfTweets; 22 | - (NSUInteger)cumulatedTweetsForScore:(NSUInteger)aScore; 23 | @end 24 | 25 | @interface THCumulativeChartView : NSView { 26 | NSUInteger score; 27 | 28 | IBOutlet NSObject *delegate; 29 | IBOutlet NSObject *dataSource; 30 | 31 | NSTrackingRectTag tag; 32 | } 33 | 34 | @property (nonatomic, strong) NSObject *delegate; 35 | @property (nonatomic, strong) NSObject *dataSource; 36 | 37 | - (void)setScore:(NSUInteger)aScore; 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /Vendor/STTwitter/STHTTPRequest+STTwitter.h: -------------------------------------------------------------------------------- 1 | // 2 | // STHTTPRequest+STTwitter.h 3 | // STTwitter 4 | // 5 | // Created by Nicolas Seriot on 8/6/13. 6 | // Copyright (c) 2013 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import "STHTTPRequest.h" 10 | 11 | @interface STHTTPRequest (STTwitter) 12 | 13 | + (STHTTPRequest *)twitterRequestWithURLString:(NSString *)urlString 14 | stTwitterUploadProgressBlock:(void(^)(NSInteger bytesWritten, NSInteger totalBytesWritten, NSInteger totalBytesExpectedToWrite))uploadProgressBlock 15 | stTwitterDownloadProgressBlock:(void(^)(id json))downloadProgressBlock 16 | stTwitterSuccessBlock:(void(^)(NSDictionary *requestHeaders, NSDictionary *responseHeaders, id json))successBlock 17 | stTwitterErrorBlock:(void(^)(NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error))errorBlock; 18 | 19 | + (void)expandedURLStringForShortenedURLString:(NSString *)urlString 20 | successBlock:(void(^)(NSString *expandedURLString))successBlock 21 | errorBlock:(void(^)(NSError *error))errorBlock; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /THUser.h: -------------------------------------------------------------------------------- 1 | // 2 | // User.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 19.04.09. 6 | // Copyright 2009 Sen:te. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @class THTweet; 13 | 14 | @interface THUser : NSManagedObject 15 | { 16 | } 17 | 18 | @property (nonatomic, strong) NSNumber * uid; 19 | @property (nonatomic, strong) NSNumber * score; 20 | @property (nonatomic, strong) NSString * name; 21 | @property (nonatomic, strong) NSString * screenName; 22 | @property (nonatomic, strong) NSString * imageURL; 23 | @property (nonatomic, strong) NSNumber * friendsCount; 24 | @property (nonatomic, strong) NSNumber * followersCount; 25 | @property (nonatomic, strong) NSSet* tweets; 26 | 27 | + (THUser *)getOrCreateUserWithDictionary:(NSDictionary *)d context:(NSManagedObjectContext *)context; 28 | + (THUser *)userWithName:(NSString *)aName context:(NSManagedObjectContext *)context; 29 | - (NSImage *)image; 30 | 31 | @end 32 | 33 | 34 | //@interface User (CoreDataGeneratedAccessors) 35 | //- (void)addTweetsObject:(Tweet *)value; 36 | //- (void)removeTweetsObject:(Tweet *)value; 37 | //- (void)addTweets:(NSSet *)value; 38 | //- (void)removeTweets:(NSSet *)value; 39 | // 40 | //@end 41 | 42 | -------------------------------------------------------------------------------- /THTweetView.m: -------------------------------------------------------------------------------- 1 | // 2 | // THTweetView.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/15/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import "THTweetView.h" 10 | 11 | @implementation THTweetView 12 | 13 | //- (id)initWithFrame:(NSRect)frameRect { 14 | // self = [super initWithFrame:frameRect]; 15 | // NSLog(@"-- initWithFrame"); 16 | // return self; 17 | //} 18 | 19 | - (void)drawRect:(NSRect)dirtyRect { 20 | if (self.selected) { 21 | [[NSColor orangeColor] set]; 22 | NSRectFill(dirtyRect); 23 | } else if (self.isRead) { 24 | [[NSColor lightGrayColor] set]; 25 | NSRectFill(dirtyRect); 26 | } 27 | } 28 | 29 | - (void)setIsRead:(BOOL)flag { 30 | if (_isRead == flag) { 31 | return; 32 | } 33 | 34 | _isRead = flag; 35 | [self setNeedsDisplay:YES]; 36 | } 37 | 38 | - (void)setSelected:(BOOL)flag { 39 | if (_selected == flag) { 40 | return; 41 | } 42 | 43 | _selected = flag; 44 | [self setNeedsDisplay:YES]; 45 | } 46 | 47 | - (void)mouseDown:(NSEvent *)theEvent { 48 | 49 | NSLog(@"-- mouse down: %@", theEvent); 50 | 51 | [super mouseDown:theEvent]; 52 | 53 | //[_delegate tweetViewWasClicked:self]; 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /STJSONIP.m: -------------------------------------------------------------------------------- 1 | // 2 | // STIPAddress.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/10/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import "STJSONIP.h" 10 | #import "STHTTPRequest.h" 11 | 12 | @implementation STJSONIP 13 | 14 | + (void)getExternalIPAddressWithSuccessBlock:(void(^)(NSString *ipAddress))successBlock 15 | errorBlock:(void(^)(NSError *error))errorBlock { 16 | 17 | __block STHTTPRequest *r = [STHTTPRequest requestWithURLString:@"http://jsonip.com/"]; 18 | __weak STHTTPRequest *wr = r; 19 | 20 | r.completionBlock = ^(NSDictionary *headers, NSString *body) { 21 | 22 | NSError *jsonError = nil; 23 | id json = [NSJSONSerialization JSONObjectWithData:wr.responseData options:NSJSONReadingMutableLeaves error:&jsonError]; 24 | 25 | if(json == nil) { 26 | errorBlock(jsonError); 27 | return; 28 | } 29 | 30 | NSString *ip = [json valueForKey:@"ip"]; 31 | 32 | if(ip) { 33 | successBlock(ip); 34 | } else { 35 | errorBlock(nil); 36 | } 37 | }; 38 | 39 | r.errorBlock = ^(NSError *error) { 40 | errorBlock(error); 41 | }; 42 | 43 | [r startAsynchronous]; 44 | } 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /Vendor/STTwitter/STTwitterAppOnly.h: -------------------------------------------------------------------------------- 1 | // 2 | // STTwitterAppOnly.h 3 | // STTwitter 4 | // 5 | // Created by Nicolas Seriot on 3/13/13. 6 | // Copyright (c) 2013 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "STTwitterProtocol.h" 11 | 12 | #if DEBUG 13 | # define STLog(...) NSLog(__VA_ARGS__) 14 | #else 15 | # define STLog(...) 16 | #endif 17 | 18 | NS_ENUM(NSUInteger, STTwitterAppOnlyErrorCode) { 19 | STTwitterAppOnlyCannotFindBearerTokenToBeInvalidated, 20 | STTwitterAppOnlyCannotFindJSONInResponse, 21 | STTwitterAppOnlyCannotFindBearerTokenInResponse 22 | }; 23 | 24 | @interface STTwitterAppOnly : NSObject { 25 | 26 | } 27 | 28 | @property (nonatomic, retain) NSString *consumerName; 29 | @property (nonatomic, retain) NSString *consumerKey; 30 | @property (nonatomic, retain) NSString *consumerSecret; 31 | @property (nonatomic, retain) NSString *bearerToken; 32 | 33 | + (instancetype)twitterAppOnlyWithConsumerName:(NSString *)conumerName consumerKey:(NSString *)consumerKey consumerSecret:(NSString *)consumerSecret; 34 | 35 | + (NSString *)base64EncodedBearerTokenCredentialsWithConsumerKey:(NSString *)consumerKey consumerSecret:(NSString *)consumerSecret; 36 | 37 | - (void)invalidateBearerTokenWithSuccessBlock:(void(^)())successBlock 38 | errorBlock:(void(^)(NSError *error))errorBlock; 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /THPreferencesWC.h: -------------------------------------------------------------------------------- 1 | // 2 | // THPreferencesWC.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/28/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef void (^UsernamePasswordBlock_t)(NSString *username, NSString *password); 12 | 13 | @class STTwitterAPI; 14 | @class THPreferencesWC; 15 | 16 | @protocol THPreferencesWCDelegate 17 | - (void)preferences:(THPreferencesWC *)preferences didChooseTwitter:(STTwitterAPI *)twitter; 18 | @end 19 | 20 | @interface THPreferencesWC : NSWindowController 21 | 22 | + (THPreferencesWC *)sharedPreferencesWC; 23 | 24 | @property (nonatomic, unsafe_unretained) id preferencesDelegate; 25 | 26 | @property (nonatomic, copy) UsernamePasswordBlock_t usernamePasswordBlock; 27 | 28 | @property (nonatomic, strong) NSString *connectionStatus; 29 | 30 | @property (nonatomic, strong) STTwitterAPI *twitter; 31 | 32 | @property (nonatomic, strong) NSArray *twitterClients; 33 | 34 | @property (nonatomic, strong) IBOutlet NSArrayController *twitterClientsController; 35 | @property (nonatomic, strong) IBOutlet NSPanel *usernameAndPasswordPanel; 36 | 37 | @property (nonatomic, strong) NSString *username; 38 | @property (nonatomic, strong) NSString *password; 39 | 40 | - (IBAction)usernamePasswordCancel:(id)sender; 41 | - (IBAction)usernamePasswordOK:(id)sender; 42 | 43 | - (IBAction)loginAction:(id)sender; 44 | 45 | - (STTwitterAPI *)twitterWrapper; 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /Vendor/STTwitter/STTwitterHTML.h: -------------------------------------------------------------------------------- 1 | // 2 | // STTwitterWeb.h 3 | // STTwitterRequests 4 | // 5 | // Created by Nicolas Seriot on 9/13/12. 6 | // Copyright (c) 2012 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ENUM(NSUInteger, STTwitterHTMLErrorCode) { 12 | STTwitterHTMLCannotPostWithoutCredentials 13 | }; 14 | 15 | @interface STTwitterHTML : NSObject 16 | 17 | - (void)getLoginForm:(void(^)(NSString *authenticityToken))successBlock 18 | errorBlock:(void(^)(NSError *error))errorBlock; 19 | 20 | - (void)postLoginFormWithUsername:(NSString *)username 21 | password:(NSString *)password 22 | authenticityToken:(NSString *)authenticityToken 23 | successBlock:(void(^)(NSString *body))successBlock 24 | errorBlock:(void(^)(NSError *error))errorBlock; 25 | 26 | 27 | /**/ 28 | 29 | - (void)getAuthorizeFormAtURL:(NSURL *)url 30 | successBlock:(void(^)(NSString *authenticityToken, NSString *oauthToken))successBlock 31 | errorBlock:(void(^)(NSError *error))errorBlock; 32 | 33 | - (void)postAuthorizeFormResultsAtURL:(NSURL *)url 34 | authenticityToken:(NSString *)authenticityToken 35 | oauthToken:(NSString *)oauthToken 36 | successBlock:(void(^)(NSString *PIN))successBlock 37 | errorBlock:(void(^)(NSError *error))errorBlock; 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /THTweet.h: -------------------------------------------------------------------------------- 1 | // 2 | // Tweet.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 19.04.09. 6 | // Copyright 2009 Sen:te. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class THUser; 12 | 13 | @interface THTweet : NSManagedObject 14 | { 15 | } 16 | 17 | @property (nonatomic, strong) NSString * text; 18 | @property (nonatomic, strong) NSNumber * uid; 19 | @property (nonatomic, strong) NSNumber * score; 20 | @property (nonatomic, strong) NSNumber * isRead; 21 | //@property (nonatomic, retain) NSNumber * containsURL; 22 | @property (nonatomic, strong) NSNumber * isFavorite; 23 | @property (nonatomic, strong) NSDate * date; 24 | @property (nonatomic, strong) THUser * user; 25 | 26 | + (THTweet *)tweetWithHighestUidInContext:(NSManagedObjectContext *)context; 27 | + (THTweet *)tweetWithUid:(NSString *)uid context:(NSManagedObjectContext *)context; 28 | + (void)unfavorFavoritesBetweenMinId:(NSNumber *)unfavorMinId maxId:(NSNumber *)unfavorMaxId context:(NSManagedObjectContext *)context; 29 | + (THTweet *)updateOrCreateTweetFromDictionary:(NSDictionary *)d context:(NSManagedObjectContext *)context; 30 | + (NSArray *)saveTweetsFromDictionariesArray:(NSArray *)a; 31 | + (NSArray *)tweetsContainingKeyword:(NSString *)keyword context:(NSManagedObjectContext *)context; 32 | + (NSUInteger)nbOfTweetsForScore:(NSNumber *)aScore andPredicates:(NSArray *)predicates context:(NSManagedObjectContext *)context; 33 | + (NSArray *)tweetsWithAndPredicates:(NSArray *)predicates context:(NSManagedObjectContext *)context; 34 | + (NSUInteger)tweetsCountWithAndPredicates:(NSArray *)predicates context:(NSManagedObjectContext *)context; 35 | + (NSArray *)tweetsWithIdGreaterOrEqualTo:(NSNumber *)anId context:(NSManagedObjectContext *)context; 36 | 37 | - (NSAttributedString *)attributedString; 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /Vendor/STTwitter/Vendor/BAVPlistNode.h: -------------------------------------------------------------------------------- 1 | // 2 | // BAVPlistNode.h 3 | // Plistorious 4 | // 5 | // Created by Bavarious on 01/10/2013. 6 | // Copyright (c) 2013 No Organisation. All rights reserved. 7 | // 8 | 9 | /* 10 | The MIT License (MIT) 11 | 12 | Copyright (c) 2013 Bavarious 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in 22 | all copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 30 | THE SOFTWARE. 31 | */ 32 | 33 | #import 34 | 35 | @interface BAVPlistNode : NSObject 36 | @property (nonatomic, copy) NSString *key; 37 | @property (nonatomic, copy) NSString *type; 38 | @property (nonatomic, copy) NSObject *value; 39 | @property (nonatomic, copy) NSArray *children; 40 | @property (nonatomic, assign, getter = isCollection) bool collection; 41 | 42 | + (instancetype)plistNodeFromObject:(id)object key:(NSString *)key; 43 | @end 44 | -------------------------------------------------------------------------------- /THUser.m: -------------------------------------------------------------------------------- 1 | // 2 | // User.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 19.04.09. 6 | // Copyright 2009 Sen:te. All rights reserved. 7 | // 8 | 9 | #import "THUser.h" 10 | #import "NSManagedObject+ST.h" 11 | 12 | #import "THTweet.h" 13 | 14 | @implementation THUser 15 | 16 | @dynamic uid; 17 | @dynamic score; 18 | @dynamic name; 19 | @dynamic screenName; 20 | @dynamic imageURL; 21 | @dynamic tweets; 22 | @dynamic friendsCount; 23 | @dynamic followersCount; 24 | 25 | + (THUser *)userWithName:(NSString *)aName context:(NSManagedObjectContext *)context { 26 | NSFetchRequest *request = [[NSFetchRequest alloc] init]; 27 | [request setEntity:[self entityInContext:context]]; 28 | NSPredicate *p = [NSPredicate predicateWithFormat:@"name == %@", aName, nil]; 29 | [request setPredicate:p]; 30 | [request setFetchLimit:1]; 31 | 32 | NSError *error = nil; 33 | NSArray *array = [context executeFetchRequest:request error:&error]; 34 | return [array lastObject]; 35 | } 36 | 37 | + (THUser *)getOrCreateUserWithDictionary:(NSDictionary *)d context:(NSManagedObjectContext *)context { 38 | //NSLog(@"-- %@", d); 39 | 40 | THUser *user = [THUser userWithName:[d objectForKey:@"name"] context:context]; 41 | 42 | if(!user) { 43 | user = [NSEntityDescription insertNewObjectForEntityForName:@"THUser" inManagedObjectContext:context]; 44 | user.uid = [NSNumber numberWithInt:[(NSString *)[d objectForKey:@"id"] intValue]]; 45 | user.name = [d objectForKey:@"name"]; 46 | } 47 | 48 | user.screenName = [d objectForKey:@"screen_name"]; 49 | user.imageURL = [d objectForKey:@"profile_image_url"]; 50 | user.friendsCount = [NSNumber numberWithInt:[(NSString *)[d objectForKey:@"friends_count"] intValue]]; 51 | user.followersCount = [NSNumber numberWithInt:[(NSString *)[d objectForKey:@"followers_count"] intValue] ]; 52 | 53 | return user; 54 | } 55 | 56 | - (NSImage *)image { 57 | return [[NSImage alloc] initByReferencingURL:[NSURL URLWithString:self.imageURL]]; 58 | } 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /THController.h: -------------------------------------------------------------------------------- 1 | // 2 | // THController.h 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 19.04.09. 6 | // Copyright 2009 Sen:te. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "THCumulativeChartView.h" 11 | #import "THLocationVC.h" 12 | #import "THPreferencesWC.h" 13 | 14 | #pragma mark FIXME: favorites syncronisation 15 | 16 | #define MAX_COUNT 100 17 | 18 | @class STTwitterAPI; 19 | @class THTweet; 20 | @class THTweetLocation; 21 | @class THLocationPanel; 22 | @class THLocationVC; 23 | @class THCumulativeChartView; 24 | @class THPreferencesWC; 25 | 26 | @interface THController : NSObject { 27 | NSUInteger tweetsCount; 28 | NSUInteger numberOfTweetsForScore[MAX_COUNT+1]; 29 | NSUInteger cumulatedTweetsForScore[MAX_COUNT+1]; 30 | } 31 | 32 | @property (nonatomic, strong) IBOutlet NSWindow *window; 33 | @property (nonatomic, strong) IBOutlet NSArrayController *tweetArrayController; 34 | @property (nonatomic, strong) IBOutlet NSArrayController *userArrayController; 35 | @property (nonatomic, strong) IBOutlet NSArrayController *keywordArrayController; 36 | @property (nonatomic, strong) IBOutlet NSPanel *locationPanel; 37 | @property (nonatomic, strong) IBOutlet NSCollectionView *collectionView; 38 | @property (nonatomic, strong) IBOutlet THCumulativeChartView *cumulativeChartView; 39 | @property (nonatomic, strong) IBOutlet NSTextField *expectedNbTweetsLabel; 40 | @property (nonatomic, strong) IBOutlet NSTextField *expectedScoreLabel; 41 | @property (nonatomic, strong) NSArray *twitterClients; 42 | @property (nonatomic, strong) THPreferencesWC *preferencesWC; 43 | 44 | - (IBAction)update:(id)sender; 45 | - (IBAction)synchronizeFavorites:(id)sender; 46 | - (IBAction)chooseMedia:(id)sender; 47 | - (IBAction)chooseLocation:(id)sender; 48 | - (IBAction)tweet:(id)sender; 49 | - (IBAction)updateTweetScores:(id)sender; 50 | 51 | - (IBAction)markAllAsRead:(id)sender; 52 | - (IBAction)markAllAsUnread:(id)sender; 53 | 54 | - (IBAction)openPreferences:(id)sender; 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /NSString+TH.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+TH.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 06.05.09. 6 | // Copyright 2009 Sen:te. All rights reserved. 7 | // 8 | 9 | #import "NSString+TH.h" 10 | #import 11 | 12 | @implementation NSString (TH) 13 | 14 | - (unsigned long long)unsignedLongLongValue { 15 | return strtoull([self UTF8String], NULL, 0); 16 | } 17 | 18 | //- (NSAttributedString *)attributedStringWithURLs { 19 | // 20 | // // TODO: use NSRegularExpression 21 | // 22 | // NSMutableAttributedString *as = [[NSMutableAttributedString alloc] initWithString:self]; 23 | // 24 | // NSRange searchRange = NSMakeRange(0, [self length]); 25 | // NSRange foundRange; 26 | // 27 | // [as beginEditing]; 28 | // do { 29 | // foundRange = [self rangeOfString:@"http://" options:0 range:searchRange]; 30 | // 31 | // if (foundRange.length > 0) { 32 | // searchRange.location = foundRange.location + foundRange.length; 33 | // searchRange.length = [self length] - searchRange.location; 34 | // 35 | // NSRange endOfURLRange = [self rangeOfCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet] options:0 range:searchRange]; 36 | // 37 | // if (endOfURLRange.length == 0) { 38 | // endOfURLRange.location = [self length]; 39 | // } 40 | // 41 | // foundRange.length = endOfURLRange.location - foundRange.location; 42 | // 43 | // NSURL *url = [NSURL URLWithString:[self substringWithRange:foundRange]]; 44 | // 45 | // NSDictionary *linkAttributes= [NSDictionary dictionaryWithObjectsAndKeys: 46 | // url, NSLinkAttributeName, 47 | // [NSNumber numberWithInt:NSSingleUnderlineStyle], NSUnderlineStyleAttributeName, 48 | // [NSColor blueColor], NSForegroundColorAttributeName, 49 | // [NSCursor pointingHandCursor], NSCursorAttributeName, NULL]; 50 | // 51 | // [as addAttributes:linkAttributes range:foundRange]; 52 | // } 53 | // 54 | // } while (foundRange.length!=0); 55 | // 56 | // [as addAttributes:@{NSFontAttributeName:[NSFont fontWithName:@"Helvetica" size:12]} range:NSMakeRange(0, [as length])]; 57 | // 58 | // [as endEditing]; 59 | // 60 | // return [as autorelease]; 61 | //} 62 | 63 | @end 64 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | TwitHunter is an attempt to see how scoring could be useful in a Twitter client. 2 | 3 | By scoring, I mean setting up rules that increase or descrease the score of individual tweets. 4 | 5 | You can then filter the tweets and display only the ones with the highest score. 6 | 7 | So far, it is just a quick hack, but it works :-) 8 | 9 | TwitHunter also demontrates how simple it is to write a Twitter client on OS X 10.8 without using any third party library. 10 | 11 | ![TwitHunter](art/twithunter.png "Screenshot") 12 | 13 | A signed "nightly build" can be found at [http://seriot.ch/temp/TwitHunter.app.zip](http://seriot.ch/temp/TwitHunter.app.zip) 14 | 15 | If the data model changes (and it will) delete ~/Library/Application Support/TwitHunter/TwitHunter.sqlite3 and relaunch. 16 | 17 | TwitHunter is released into public domain. Do not hesitate to fork and patch. 18 | 19 | Nicolas Seriot, 2009-04 - 2010-05 - 2012-08 20 | 21 | --- 22 | 23 | #### Motivation 24 | 25 | I follow more than one hundred persons on Twitter, and keeping up gets increasingly time consuming. 26 | 27 | I could just unfollow some of them but I don't want to because sometimes I actually have enough time to read them. 28 | 29 | Also, fun or interesting twits may be written by people posting tons of junk twits besides. 30 | 31 | #### Scoring 32 | 33 | I envisionned a scoring system, where you define rules that increase or decrease the score of individual tweets. 34 | 35 | That way, when you only have 5 minutes to spend on 5 hours of tweets, you just read the tweets with the highest scores. 36 | 37 | #### Implementation 38 | 39 | TwitHunter is an attempt to see how scoring could be useful in a Twitter client. (Using the ugliest Twitter client on earth is not a part of the experiment.) 40 | 41 | So far, it is just a quick hack (ca 8 hours work), but it works :-) 42 | 43 | Basically every tweet gets 50 points. The score is then changed according to simple rules, per user or per keyword. 44 | 45 | For instance, on the screenshot, Sebastien's tweet has 50+15 points for mentioning iPhone (keyword rule) and Fraser's one got 50+10 (user rule). 46 | 47 | The slider is set on 53, so only tweets with 53 points or more are displayed. 48 | 49 | Disclaimer: the data model is subject to change at anytime, so don't rely on it to store your data for now. 50 | 51 | #### Project 52 | 53 | I have neigher time nor interest to write a "real" Cocoa Twitter client. 54 | 55 | Instead, I would like the scoring approach, if considered useful, to be added to full featured clients. 56 | 57 | I would also like to try out baysian filtering when I have time. 58 | 59 | So, don't hesitate to fork the project, it is still very young, I only added 1000 lines of my code and some Cocoa bindings, so it is still very easy to change. 60 | 61 | By the way, my Twitter account is @nst021. 62 | -------------------------------------------------------------------------------- /NSManagedObject+ST.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSManagedObject+iLog.m 3 | // iLog 4 | // 5 | // Created by Nicolas Seriot on 22.03.09. 6 | // Copyright 2009 Sen:te. All rights reserved. 7 | // 8 | 9 | #import "NSManagedObject+ST.h" 10 | #import 11 | 12 | @implementation NSManagedObject (ST) 13 | 14 | + (NSEntityDescription *)entityInContext:(NSManagedObjectContext *)context { 15 | return [NSEntityDescription entityForName:NSStringFromClass(self) inManagedObjectContext:context]; 16 | } 17 | 18 | + (id)createInContext:(NSManagedObjectContext *)context { 19 | NSEntityDescription *entityDecription = [self entityInContext:context]; 20 | NSString *name = [entityDecription name]; 21 | return [NSEntityDescription insertNewObjectForEntityForName:name inManagedObjectContext:context] ; 22 | } 23 | 24 | - (BOOL)save { 25 | return [[self managedObjectContext] save:nil]; 26 | } 27 | 28 | - (void)deleteObject { 29 | [[self managedObjectContext] deleteObject:self]; 30 | } 31 | 32 | + (void)deleteAllObjectsInContext:(NSManagedObjectContext *)context { 33 | NSFetchRequest *fr = [[NSFetchRequest alloc] init]; 34 | [fr setEntity:[self entityInContext:context]]; 35 | [fr setIncludesPropertyValues:NO]; // only fetch the managedObjectID 36 | 37 | NSError *error = nil; 38 | NSArray *allObjects = [context executeFetchRequest:fr error:&error]; 39 | 40 | if(allObjects == nil) { 41 | NSLog(@"-- error: %@", [error localizedDescription]); 42 | } 43 | 44 | for (NSManagedObject *mo in allObjects) { 45 | [context deleteObject:mo]; 46 | } 47 | } 48 | 49 | //+ (id)create { 50 | // NSEntityDescription *entityDecription = [self entity]; 51 | // NSString *name = [entityDecription name]; 52 | // return [NSEntityDescription insertNewObjectForEntityForName:name inManagedObjectContext:[self moc]] ; 53 | //} 54 | // 55 | //+ (NSManagedObjectContext *)moc { 56 | // return [(id)[[NSApplication sharedApplication] delegate] managedObjectContext]; 57 | //} 58 | // 59 | //- (NSManagedObjectContext *)moc { 60 | // return [[self class] moc]; 61 | //} 62 | // 63 | //+ (NSManagedObjectModel *)mom { 64 | // return [(id)[[NSApplication sharedApplication] delegate] managedObjectModel]; 65 | //} 66 | // 67 | //+ (NSEntityDescription *)entity { 68 | // return [[[self mom] entitiesByName] objectForKey:NSStringFromClass([self class])]; 69 | //} 70 | // 71 | //+ (NSFetchRequest *)allFetchRequest { 72 | // NSFetchRequest *fr = [[NSFetchRequest alloc] init]; 73 | // [fr setEntity:[self entity]]; 74 | // return [fr autorelease]; 75 | //} 76 | // 77 | //+ (NSArray *)allObjects { 78 | // return [[self moc] executeFetchRequest:[self allFetchRequest] error:nil]; 79 | //} 80 | // 81 | //+ (NSUInteger)allObjectsCount { 82 | // return [[self moc] countForFetchRequest:[self allFetchRequest] error:nil]; 83 | //} 84 | // 85 | //+ (BOOL)save { 86 | // return [[self moc] save:nil]; 87 | //} 88 | 89 | @end 90 | -------------------------------------------------------------------------------- /Vendor/STTwitter/STTwitterProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // STOAuthProtocol.h 3 | // STTwitterRequests 4 | // 5 | // Created by Nicolas Seriot on 9/18/12. 6 | // Copyright (c) 2012 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class STHTTPRequest; 12 | 13 | @protocol STTwitterProtocol 14 | 15 | - (BOOL)canVerifyCredentials; 16 | - (void)verifyCredentialsWithSuccessBlock:(void(^)(NSString *username))successBlock 17 | errorBlock:(void(^)(NSError *error))errorBlock; 18 | 19 | - (id)fetchResource:(NSString *)resource 20 | HTTPMethod:(NSString *)HTTPMethod 21 | baseURLString:(NSString *)baseURLString 22 | parameters:(NSDictionary *)params 23 | uploadProgressBlock:(void(^)(NSInteger bytesWritten, NSInteger totalBytesWritten, NSInteger totalBytesExpectedToWrite))uploadProgressBlock 24 | downloadProgressBlock:(void(^)(id request, id response))progressBlock 25 | successBlock:(void(^)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, id response))successBlock 26 | errorBlock:(void(^)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error))errorBlock; 27 | 28 | - (NSString *)consumerName; 29 | - (NSString *)loginTypeDescription; 30 | 31 | @optional 32 | 33 | - (void)postTokenRequest:(void(^)(NSURL *url, NSString *oauthToken))successBlock 34 | forceLogin:(NSNumber *)forceLogin 35 | screenName:(NSString *)screenName 36 | oauthCallback:(NSString *)oauthCallback 37 | errorBlock:(void(^)(NSError *error))errorBlock; 38 | 39 | - (void)postAccessTokenRequestWithPIN:(NSString *)pin 40 | successBlock:(void(^)(NSString *oauthToken, NSString *oauthTokenSecret, NSString *userID, NSString *screenName))successBlock 41 | errorBlock:(void(^)(NSError *error))errorBlock; 42 | 43 | - (void)invalidateBearerTokenWithSuccessBlock:(void(^)(id response))successBlock 44 | errorBlock:(void(^)(NSError *error))errorBlock; 45 | 46 | // access tokens are available only with plain OAuth authentication 47 | - (NSString *)oauthAccessToken; 48 | - (NSString *)oauthAccessTokenSecret; 49 | 50 | - (NSString *)bearerToken; 51 | 52 | // reverse auth phase 1, implemented only in STTwitterOAuth 53 | - (void)postReverseOAuthTokenRequest:(void(^)(NSString *authenticationHeader))successBlock 54 | errorBlock:(void(^)(NSError *error))errorBlock; 55 | 56 | // reverse auth phase 2, implemented only in STTwitterOS 57 | - (void)postReverseAuthAccessTokenWithAuthenticationHeader:(NSString *)authenticationHeader 58 | successBlock:(void(^)(NSString *oAuthToken, NSString *oAuthTokenSecret, NSString *userID, NSString *screenName))successBlock 59 | errorBlock:(void(^)(NSError *error))errorBlock; 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /Vendor/STTwitter/NSString+STTwitter.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+STTwitter.m 3 | // STTwitter 4 | // 5 | // Created by Nicolas Seriot on 11/2/12. 6 | // Copyright (c) 2012 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import "NSString+STTwitter.h" 10 | 11 | NSUInteger kSTTwitterDefaultShortURLLength = 22; 12 | NSUInteger kSTTwitterDefaultShortURLLengthHTTPS = 23; 13 | 14 | @implementation NSString (STTwitter) 15 | 16 | - (NSString *)st_firstMatchWithRegex:(NSString *)regex error:(NSError **)e { 17 | NSError *error = nil; 18 | NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:regex options:0 error:&error]; 19 | 20 | if(re == nil) { 21 | if(e) *e = error; 22 | return nil; 23 | } 24 | 25 | NSArray *matches = [re matchesInString:self options:0 range:NSMakeRange(0, [self length])]; 26 | 27 | if([matches count] == 0) { 28 | NSString *errorDescription = [NSString stringWithFormat:@"Can't find a match for regex: %@", regex]; 29 | if(e) *e = [NSError errorWithDomain:NSStringFromClass([self class]) code:0 userInfo:@{NSLocalizedDescriptionKey : errorDescription}]; 30 | return nil; 31 | } 32 | 33 | NSTextCheckingResult *match = [matches lastObject]; 34 | NSRange matchRange = [match rangeAtIndex:1]; 35 | return [self substringWithRange:matchRange]; 36 | } 37 | 38 | // use values from GET help/configuration 39 | - (NSInteger)st_numberOfCharactersInATweetWithShortURLLength:(NSUInteger)shortURLLength shortURLLengthHTTPS:(NSUInteger)shortURLLengthHTTPS { 40 | 41 | // NFC normalized string https://dev.twitter.com/docs/counting-characters 42 | NSString *s = [self precomposedStringWithCanonicalMapping]; 43 | 44 | NSInteger count = [s length]; 45 | 46 | NSError *error = nil; 47 | NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(https?://[A-Za-z0-9_\\.\\-/]+)" 48 | options:0 49 | error:&error]; 50 | 51 | NSArray *matches = [regex matchesInString:s 52 | options:0 53 | range:NSMakeRange(0, [s length])]; 54 | 55 | for (NSTextCheckingResult *match in matches) { 56 | NSRange urlRange = [match rangeAtIndex:1]; 57 | NSString *urlString = [s substringWithRange:urlRange]; 58 | 59 | count -= urlRange.length; 60 | count += [urlString hasPrefix:@"https"] ? shortURLLengthHTTPS : shortURLLength; 61 | } 62 | 63 | return count; 64 | } 65 | 66 | // use default values for URL shortening 67 | - (NSInteger)st_numberOfCharactersInATweet { 68 | return [self st_numberOfCharactersInATweetWithShortURLLength:kSTTwitterDefaultShortURLLength 69 | shortURLLengthHTTPS:kSTTwitterDefaultShortURLLengthHTTPS]; 70 | } 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /THTextView.m: -------------------------------------------------------------------------------- 1 | // 2 | // THTextView.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/13/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import "THTextView.h" 10 | 11 | @implementation THTextView 12 | // 13 | //- (void)mouseEntered:(NSEvent *)theEvent { 14 | // 15 | // [[NSCursor arrowCursor] set]; 16 | // 17 | // [super mouseEntered:theEvent]; 18 | //} 19 | 20 | // TODO: prevent area selection 21 | 22 | - (void)mouseMoved:(NSEvent *)theEvent { 23 | 24 | [super mouseMoved:theEvent]; 25 | 26 | NSPoint point = [self convertPoint:[theEvent locationInWindow] fromView:nil]; 27 | 28 | NSInteger charIndex = [self characterIndexForInsertionAtPoint:point]; 29 | 30 | BOOL movedOnATextCharacter = NSLocationInRange(charIndex, NSMakeRange(0, [[self string] length])); 31 | 32 | if (movedOnATextCharacter == NO) { 33 | [[NSCursor arrowCursor] set]; 34 | } 35 | } 36 | 37 | - (void)mouseDown:(NSEvent *)theEvent { 38 | NSPoint point = [self convertPoint:[theEvent locationInWindow] fromView:nil]; 39 | 40 | NSInteger charIndex = [self characterIndexForInsertionAtPoint:point]; 41 | 42 | BOOL clickOnATextCharacter = NSLocationInRange(charIndex, NSMakeRange(0, [[self string] length])); 43 | 44 | if (clickOnATextCharacter) { 45 | 46 | NSDictionary *attributes = [[self attributedString] attributesAtIndex:charIndex effectiveRange:NULL]; 47 | 48 | if( [attributes objectForKey:@"LinkMatch"] != nil ) { 49 | NSLog( @"LinkMatch: %@", [attributes objectForKey:@"LinkMatch"]); 50 | NSString *urlString = [attributes objectForKey:@"LinkMatch"]; 51 | NSURL *url = [NSURL URLWithString:urlString]; 52 | [[NSWorkspace sharedWorkspace] openURL:url]; 53 | } 54 | 55 | if( [attributes objectForKey:@"UsernameMatch"] != nil ) { 56 | NSLog( @"UsernameMatch: %@", [attributes objectForKey:@"UsernameMatch"] ); 57 | NSString *username = [attributes objectForKey:@"UsernameMatch"]; 58 | NSString *urlString = [NSString stringWithFormat:@"https://www.twitter.com/%@", username]; 59 | NSURL *url = [NSURL URLWithString:urlString]; 60 | [[NSWorkspace sharedWorkspace] openURL:url]; 61 | } 62 | 63 | if( [attributes objectForKey:@"HashtagMatch"] != nil ) { 64 | NSLog( @"HashtagMatch: %@", [attributes objectForKey:@"HashtagMatch"] ); 65 | NSString *hashtag = [attributes objectForKey:@"HashtagMatch"]; 66 | hashtag = [hashtag stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; 67 | NSString *escapedHashtag = [hashtag stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; 68 | 69 | // https://twitter.com/search?q=%23free&src=hash 70 | 71 | NSString *urlString = [NSString stringWithFormat:@"https://twitter.com/search?q=%@&src=hash", escapedHashtag]; 72 | NSURL *url = [NSURL URLWithString:urlString]; 73 | [[NSWorkspace sharedWorkspace] openURL:url]; 74 | } 75 | 76 | } 77 | 78 | [[self nextResponder] mouseDown:theEvent]; 79 | } 80 | 81 | @end 82 | -------------------------------------------------------------------------------- /TwitHunter.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Vendor/STTwitter/NSError+STTwitter.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSError+STTwitter.m 3 | // STTwitterDemoOSX 4 | // 5 | // Created by Nicolas Seriot on 19/03/14. 6 | // Copyright (c) 2014 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import "NSError+STTwitter.h" 10 | 11 | @implementation NSError (STTwitter) 12 | 13 | + (NSError *)st_twitterErrorFromResponseData:(NSData *)responseData 14 | responseHeaders:(NSDictionary *)responseHeaders 15 | underlyingError:(NSError *)underlyingError { 16 | 17 | NSError *jsonError = nil; 18 | NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingMutableLeaves error:&jsonError]; 19 | 20 | NSString *message = nil; 21 | NSInteger code = 0; 22 | 23 | if([json isKindOfClass:[NSDictionary class]]) { 24 | id errors = [json valueForKey:@"errors"]; 25 | if([errors isKindOfClass:[NSArray class]] && [(NSArray *)errors count] > 0) { 26 | // assume format: {"errors":[{"message":"Sorry, that page does not exist","code":34}]} 27 | NSDictionary *errorDictionary = [errors lastObject]; 28 | if([errorDictionary isKindOfClass:[NSDictionary class]]) { 29 | message = errorDictionary[@"message"]; 30 | code = [[errorDictionary valueForKey:@"code"] integerValue]; 31 | } 32 | } else if ([json valueForKey:@"error"]) { 33 | /* 34 | eg. when requesting timeline from a protected account 35 | { 36 | error = "Not authorized."; 37 | request = "/1.1/statuses/user_timeline.json?count=20&screen_name=premfe"; 38 | } 39 | */ 40 | message = [json valueForKey:@"error"]; 41 | } else if([errors isKindOfClass:[NSString class]]) { 42 | // assume format {errors = "Screen name can't be blank";} 43 | message = errors; 44 | } 45 | } 46 | 47 | if(message) { 48 | NSString *rateLimitLimit = [responseHeaders valueForKey:@"x-rate-limit-limit"]; 49 | NSString *rateLimitRemaining = [responseHeaders valueForKey:@"x-rate-limit-remaining"]; 50 | NSString *rateLimitReset = [responseHeaders valueForKey:@"x-rate-limit-reset"]; 51 | 52 | NSDate *rateLimitResetDate = rateLimitReset ? [NSDate dateWithTimeIntervalSince1970:[rateLimitReset doubleValue]] : nil; 53 | 54 | NSMutableDictionary *md = [NSMutableDictionary dictionary]; 55 | md[NSLocalizedDescriptionKey] = message; 56 | if(underlyingError) md[NSUnderlyingErrorKey] = underlyingError; 57 | if(rateLimitLimit) md[kSTTwitterRateLimitLimit] = rateLimitLimit; 58 | if(rateLimitRemaining) md[kSTTwitterRateLimitRemaining] = rateLimitRemaining; 59 | if(rateLimitResetDate) md[kSTTwitterRateLimitResetDate] = rateLimitResetDate; 60 | 61 | NSDictionary *userInfo = [NSDictionary dictionaryWithDictionary:md]; 62 | 63 | return [NSError errorWithDomain:kSTTwitterTwitterErrorDomain code:code userInfo:userInfo]; 64 | } 65 | 66 | return nil; 67 | } 68 | 69 | @end 70 | -------------------------------------------------------------------------------- /Vendor/STTwitter/Vendor/BAVPlistNode.m: -------------------------------------------------------------------------------- 1 | // 2 | // BAVPlistNode.m 3 | // Plistorious 4 | // 5 | // Created by Bavarious on 01/10/2013. 6 | // Copyright (c) 2013 No Organisation. All rights reserved. 7 | // 8 | 9 | #import "BAVPlistNode.h" 10 | 11 | 12 | static NSString *typeForObject(id object) { 13 | return ([object isKindOfClass:[NSArray class]] ? @"Array" : 14 | [object isKindOfClass:[NSDictionary class]] ? @"Dictionary" : 15 | [object isKindOfClass:[NSString class]] ? @"String" : 16 | [object isKindOfClass:[NSData class]] ? @"Data" : 17 | [object isKindOfClass:[NSDate class]] ? @"Date" : 18 | object == (id)kCFBooleanTrue || object == (id)kCFBooleanFalse ? @"Boolean" : 19 | [object isKindOfClass:[NSNumber class]] ? @"Number" : 20 | [object isKindOfClass:[NSNull class]] ? @"Null" : 21 | @"Unknown"); 22 | } 23 | 24 | static NSString *formatItemCount(NSUInteger count) { 25 | return (count == 1 ? @"1 item" : [NSString stringWithFormat:@"%@ items", @(count)]); 26 | } 27 | 28 | 29 | @implementation BAVPlistNode 30 | 31 | + (instancetype)plistNodeFromObject:(id)object key:(NSString *)key 32 | { 33 | BAVPlistNode *newNode = [BAVPlistNode new]; 34 | newNode.key = key; 35 | newNode.type = typeForObject(object); 36 | 37 | if ([object isKindOfClass:[NSArray class]]) { 38 | NSArray *array = object; 39 | 40 | NSMutableArray *children = [NSMutableArray new]; 41 | NSUInteger elementIndex = 0; 42 | for (id element in array) { 43 | NSString *elementKey = [NSString stringWithFormat:@"Item %@", @(elementIndex)]; 44 | [children addObject:[self plistNodeFromObject:element key:elementKey]]; 45 | elementIndex++; 46 | } 47 | 48 | newNode.value = formatItemCount(array.count); 49 | newNode.children = children; 50 | } 51 | else if ([object isKindOfClass:[NSDictionary class]]) { 52 | NSDictionary *dictionary = object; 53 | NSArray *keys = [dictionary.allKeys sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; 54 | 55 | NSMutableArray *children = [NSMutableArray new]; 56 | for (NSString *elementKey in keys) 57 | [children addObject:[self plistNodeFromObject:dictionary[elementKey] key:elementKey]]; 58 | 59 | newNode.value = formatItemCount(keys.count); 60 | newNode.children = children; 61 | } 62 | else if ([object isKindOfClass:[NSNull class]]) { 63 | newNode.value = @"null"; 64 | } 65 | else if (object == (id)kCFBooleanTrue) { 66 | newNode.value = @"true"; 67 | } 68 | else if (object == (id)kCFBooleanFalse) { 69 | newNode.value = @"false"; 70 | } 71 | else { 72 | newNode.value = [NSString stringWithFormat:@"%@", object]; 73 | } 74 | 75 | return newNode; 76 | } 77 | 78 | - (bool)isCollection 79 | { 80 | return [self.type isEqualToString:@"Array"] || [self.type isEqualToString:@"Dictionary"]; 81 | } 82 | 83 | - (NSString *)description 84 | { 85 | return [NSString stringWithFormat:@"%@ node with key %@", self.type, self.key]; 86 | } 87 | 88 | @end 89 | -------------------------------------------------------------------------------- /THLocationVC.m: -------------------------------------------------------------------------------- 1 | // 2 | // THLocationVC.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/9/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import "THLocationVC.h" 10 | #import "THTweetLocation.h" 11 | #import "STTwitter.h" 12 | #import "STJSONIP.h" 13 | 14 | @interface THLocationVC () 15 | 16 | @end 17 | 18 | @implementation THLocationVC 19 | 20 | - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { 21 | self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 22 | if (self) { 23 | // Initialization code here. 24 | } 25 | 26 | return self; 27 | } 28 | 29 | - (void)awakeFromNib { 30 | [STJSONIP getExternalIPAddressWithSuccessBlock:^(NSString *ipAddress) { 31 | _tweetLocation.ipAddress = ipAddress; 32 | } errorBlock:^(NSError *error) { 33 | NSLog(@"-- %@", [error localizedDescription]); 34 | }]; 35 | } 36 | 37 | - (IBAction)ok:(id)sender { 38 | NSLog(@"-- ok"); 39 | 40 | NSString *selectedPlaceID = [[_twitterPlacesController selectedObjects] lastObject]; 41 | 42 | // 46.5199617 43 | // 6.6335971 44 | 45 | _tweetLocation.placeID = [selectedPlaceID valueForKey:@"id"]; 46 | _tweetLocation.fullName = [selectedPlaceID valueForKey:@"full_name"]; 47 | 48 | [_locationDelegate locationVC:self didChooseLocation:_tweetLocation]; 49 | } 50 | 51 | - (IBAction)cancel:(id)sender { 52 | NSLog(@"-- cancel"); 53 | 54 | [_locationDelegate locationVCDidCancel:self]; 55 | } 56 | 57 | - (IBAction)lookupIPAddress:(id)sender { 58 | 59 | [_twitter getGeoSearchWithIPAddress:_tweetLocation.ipAddress successBlock:^(NSArray *places) { 60 | 61 | self.twitterPlaces = places; 62 | 63 | NSLog(@"-- places: %@", places); 64 | 65 | // self.locationDescription = [firstPlace valueForKey:@"full_name"]; 66 | 67 | } errorBlock:^(NSError *error) { 68 | NSLog(@"-- %@", [error localizedDescription]); 69 | }]; 70 | } 71 | 72 | - (IBAction)lookupCoordinates:(id)sender { 73 | 74 | [_twitter getGeoSearchWithLatitude:_tweetLocation.latitude longitude:_tweetLocation.longitude successBlock:^(NSArray *places) { 75 | 76 | self.twitterPlaces = places; 77 | 78 | NSLog(@"-- places: %@", places); 79 | 80 | // self.locationDescription = [firstPlace valueForKey:@"full_name"]; 81 | 82 | } errorBlock:^(NSError *error) { 83 | NSLog(@"-- %@", [error localizedDescription]); 84 | }]; 85 | } 86 | 87 | - (IBAction)lookupQuery:(id)sender { 88 | 89 | [_twitter getGeoSearchWithQuery:_tweetLocation.query successBlock:^(NSArray *places) { 90 | 91 | self.twitterPlaces = places; 92 | 93 | NSLog(@"-- places: %@", places); 94 | 95 | // self.locationDescription = [firstPlace valueForKey:@"full_name"]; 96 | 97 | } errorBlock:^(NSError *error) { 98 | NSLog(@"-- %@", [error localizedDescription]); 99 | }]; 100 | } 101 | 102 | @end 103 | -------------------------------------------------------------------------------- /THCollectionView.m: -------------------------------------------------------------------------------- 1 | // 2 | // THCollectionView.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/13/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import "THCollectionView.h" 10 | #import "THTweetCollectionViewItem.h" 11 | #import "THTweet.h" 12 | #import "THTweetView.h" 13 | 14 | @interface THCollectionView () 15 | @property (nonatomic, strong) NSIndexSet *formerSelectionIndexSet; 16 | @end 17 | 18 | @implementation THCollectionView 19 | 20 | - (NSCollectionViewItem *)newItemForRepresentedObject:(id)object { 21 | 22 | NSCollectionViewItem *item = [super newItemForRepresentedObject:object]; 23 | // NSView *view = [item view]; 24 | // 25 | // [view bind:@"title" 26 | // toObject:object 27 | // withKeyPath:@"title" 28 | // options:nil]; 29 | // 30 | // [view bind:@"option" 31 | // toObject:object 32 | // withKeyPath:@"option" 33 | // options:nil]; 34 | 35 | return item; 36 | } 37 | 38 | - (void)setSelectionIndexes:(NSIndexSet *)indexes { 39 | [super setSelectionIndexes:indexes]; 40 | 41 | [_formerSelectionIndexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { 42 | NSCollectionViewItem *item = [self itemAtIndex:idx]; 43 | THTweetView *view = (THTweetView *)[item view]; 44 | [view setSelected:NO]; 45 | }]; 46 | 47 | self.formerSelectionIndexSet = indexes; 48 | 49 | [indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { 50 | NSCollectionViewItem *item = [self itemAtIndex:idx]; 51 | THTweetView *view = (THTweetView *)[item view]; 52 | [view setSelected:YES]; 53 | }]; 54 | } 55 | 56 | - (void)awakeFromNib { 57 | [super awakeFromNib]; 58 | [self setMinItemSize:NSMakeSize(0.0, 50)]; 59 | [self setMaxItemSize:NSMakeSize(0.0, 50)]; 60 | } 61 | // 62 | //// get the view for a tweet 63 | //- (NSCollectionViewItem *)newItemForRepresentedObject:(THTweet *)tweet { 64 | // 65 | //// if([track isFault]) { 66 | //// track.uti; // fetch the track 67 | //// } 68 | // NSAssert([tweet isFault] == NO, @"error: tweet is fault"); 69 | // 70 | // THTweetCollectionViewItem *item = (THTweetCollectionViewItem *)[super newItemForRepresentedObject:tweet]; 71 | // 72 | // THTweetView *tweetView = (THTweetView *)[item view]; 73 | // 74 | // [tweetView setStatus:@"asd"]; // tweet.text 75 | // 76 | //// [item setText:@"fghfg"]; 77 | // 78 | // //NSLog(@"-- %@", tweet.text); 79 | // 80 | //// [item.tweetTextTextView setEditable:YES]; 81 | //// [item.tweetTextTextView setAutomaticLinkDetectionEnabled:YES]; 82 | //// [item.tweetTextTextView setString:tweet.text]; 83 | //// [item.tweetTextTextView setEditable:NO]; 84 | // 85 | //// [item setRepresentedObject:track]; 86 | //// 87 | //// SLTrackView *trackView = (SLTrackView *)[item view]; 88 | //// 89 | //// track.trackView = trackView; 90 | // // trackView.track = track; 91 | // 92 | // // trackView.mdItems = track.queryResults; 93 | // // trackView.query = track.query; 94 | // // 95 | // // [[item view] setValue:track.queryResults forKey:@"controller"]; 96 | // // [[item view] setValue:track.query forKey:@"query"]; 97 | // // 98 | // // track.collectionView = self; 99 | // // track.mainView = [item view]; 100 | // 101 | // return item; 102 | //} 103 | 104 | @end 105 | -------------------------------------------------------------------------------- /Vendor/STTwitter/Vendor/JSONSyntaxHighlight.h: -------------------------------------------------------------------------------- 1 | /** 2 | * JSONSyntaxHighlight.h 3 | * JSONSyntaxHighlight 4 | * 5 | * Syntax highlight JSON 6 | * 7 | * Created by Dave Eddy on 8/3/13. 8 | * Copyright (c) 2013 Dave Eddy. All rights reserved. 9 | * 10 | * The MIT License (MIT) 11 | * 12 | * Permission is hereby granted, free of charge, to any person obtaining a copy 13 | * of this software and associated documentation files (the "Software"), to deal 14 | * in the Software without restriction, including without limitation the rights 15 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | * copies of the Software, and to permit persons to whom the Software is 17 | * furnished to do so, subject to the following conditions: 18 | * 19 | * The above copyright notice and this permission notice shall be included in 20 | * all copies or substantial portions of the Software. 21 | * 22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | * THE SOFTWARE. 29 | */ 30 | 31 | #import 32 | 33 | #if (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE) 34 | #import 35 | #else 36 | #import 37 | #endif 38 | 39 | @interface JSONSyntaxHighlight : NSObject 40 | 41 | // Create the object by giving a JSON object, nil will be returned 42 | // if the object can't be serialized 43 | - (JSONSyntaxHighlight *)init; 44 | - (JSONSyntaxHighlight *)initWithJSON:(id)JSON; 45 | 46 | // Return an NSAttributedString with the highlighted JSON in a pretty format 47 | - (NSAttributedString *)highlightJSON; 48 | 49 | // Return an NSAttributedString with the highlighted JSON optionally pretty formatted 50 | - (NSAttributedString *)highlightJSONWithPrettyPrint:(BOOL)prettyPrint; 51 | 52 | // Fire a callback for every key item found in the parsed JSON, each callback 53 | // is fired with the NSRange the substring appears in `self.parsedJSON`, as well 54 | // as the NSString at that location. 55 | - (void)enumerateMatchesWithIndentBlock:(void(^)(NSRange, NSString*))indentBlock 56 | keyBlock:(void(^)(NSRange, NSString*))keyBlock 57 | valueBlock:(void(^)(NSRange, NSString*))valueBlock 58 | endBlock:(void(^)(NSRange, NSString*))endBlock; 59 | 60 | // The JSON object, unmodified 61 | @property (readonly, nonatomic, strong) id JSON; 62 | 63 | // The serialized JSON string 64 | @property (readonly, nonatomic, strong) NSString *parsedJSON; 65 | 66 | // The attributes for Attributed Text 67 | @property (nonatomic, strong) NSDictionary *keyAttributes; 68 | @property (nonatomic, strong) NSDictionary *stringAttributes; 69 | @property (nonatomic, strong) NSDictionary *nonStringAttributes; 70 | 71 | // Platform dependent helper functions 72 | #if (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE) 73 | + (UIColor *)colorWithRGB:(NSInteger)rgbValue; 74 | + (UIColor *)colorWithRGB:(NSInteger)rgbValue alpha:(CGFloat)alpha; 75 | #else 76 | + (NSColor *)colorWithRGB:(NSInteger)rgbValue; 77 | + (NSColor *)colorWithRGB:(NSInteger)rgbValue alpha:(CGFloat)alpha; 78 | #endif 79 | 80 | @end -------------------------------------------------------------------------------- /TwitHunter.xcodeproj/xcuserdata/nst.xcuserdatad/xcschemes/TwitHunter.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 51 | 52 | 58 | 59 | 60 | 61 | 62 | 63 | 69 | 70 | 76 | 77 | 78 | 79 | 81 | 82 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /Vendor/STTwitter/STTwitterOAuth.h: -------------------------------------------------------------------------------- 1 | // 2 | // STTwitterRequest.h 3 | // STTwitterRequests 4 | // 5 | // Created by Nicolas Seriot on 9/5/12. 6 | // Copyright (c) 2012 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "STTwitterProtocol.h" 11 | 12 | extern NSString * const kSTPOSTDataKey; 13 | 14 | /* 15 | Based on the following documentation 16 | http://oauth.net/core/1.0/ 17 | https://dev.twitter.com/docs/auth/authorizing-request 18 | https://dev.twitter.com/docs/auth/implementing-sign-twitter 19 | https://dev.twitter.com/docs/auth/creating-signature 20 | https://dev.twitter.com/docs/api/1/post/oauth/request_token 21 | https://dev.twitter.com/docs/oauth/xauth 22 | ... 23 | */ 24 | 25 | NS_ENUM(NSUInteger, STTwitterOAuthErrorCode) { 26 | STTwitterOAuthCannotPostAccessTokenRequestWithoutPIN, 27 | STTwitterOAuthBadCredentialsOrConsumerTokensNotXAuthEnabled 28 | }; 29 | 30 | @interface STTwitterOAuth : NSObject 31 | 32 | + (instancetype)twitterOAuthWithConsumerName:(NSString *)consumerName 33 | consumerKey:(NSString *)consumerKey 34 | consumerSecret:(NSString *)consumerSecret; 35 | 36 | + (instancetype)twitterOAuthWithConsumerName:(NSString *)consumerName 37 | consumerKey:(NSString *)consumerKey 38 | consumerSecret:(NSString *)consumerSecret 39 | oauthToken:(NSString *)oauthToken 40 | oauthTokenSecret:(NSString *)oauthTokenSecret; 41 | 42 | + (instancetype)twitterOAuthWithConsumerName:(NSString *)consumerName 43 | consumerKey:(NSString *)consumerKey 44 | consumerSecret:(NSString *)consumerSecret 45 | username:(NSString *)username 46 | password:(NSString *)password; 47 | 48 | - (void)postTokenRequest:(void(^)(NSURL *url, NSString *oauthToken))successBlock 49 | forceLogin:(NSNumber *)forceLogin // optional, default @(NO) 50 | screenName:(NSString *)screenName // optional, default nil 51 | oauthCallback:(NSString *)oauthCallback 52 | errorBlock:(void(^)(NSError *error))errorBlock; 53 | 54 | // convenience 55 | - (void)postTokenRequest:(void(^)(NSURL *url, NSString *oauthToken))successBlock 56 | oauthCallback:(NSString *)oauthCallback 57 | errorBlock:(void(^)(NSError *error))errorBlock; 58 | 59 | 60 | - (void)postAccessTokenRequestWithPIN:(NSString *)pin 61 | successBlock:(void(^)(NSString *oauthToken, NSString *oauthTokenSecret, NSString *userID, NSString *screenName))successBlock 62 | errorBlock:(void(^)(NSError *error))errorBlock; 63 | 64 | - (void)postXAuthAccessTokenRequestWithUsername:(NSString *)username 65 | password:(NSString *)password 66 | successBlock:(void(^)(NSString *oauthToken, NSString *oauthTokenSecret, NSString *userID, NSString *screenName))successBlock 67 | errorBlock:(void(^)(NSError *error))errorBlock; 68 | 69 | // reverse auth phase 1 70 | - (void)postReverseOAuthTokenRequest:(void(^)(NSString *authenticationHeader))successBlock 71 | errorBlock:(void(^)(NSError *error))errorBlock; 72 | 73 | - (BOOL)canVerifyCredentials; 74 | 75 | - (void)verifyCredentialsWithSuccessBlock:(void(^)(NSString *username))successBlock errorBlock:(void(^)(NSError *error))errorBlock; 76 | 77 | @end 78 | 79 | @interface NSString (STTwitterOAuth) 80 | + (NSString *)st_random32Characters; 81 | - (NSString *)st_signHmacSHA1WithKey:(NSString *)key; 82 | - (NSDictionary *)st_parametersDictionary; 83 | - (NSString *)st_urlEncodedString; 84 | @end 85 | 86 | @interface NSURL (STTwitterOAuth) 87 | - (NSString *)st_normalizedForOauthSignatureString; 88 | - (NSArray *)st_rawGetParametersDictionaries; 89 | @end 90 | -------------------------------------------------------------------------------- /Vendor/STTwitter/NSError+STTwitter.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSError+STTwitter.h 3 | // STTwitterDemoOSX 4 | // 5 | // Created by Nicolas Seriot on 19/03/14. 6 | // Copyright (c) 2014 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | static NSString *kSTTwitterTwitterErrorDomain = @"STTwitterTwitterErrorDomain"; 12 | static NSString *kSTTwitterRateLimitLimit = @"STTwitterRateLimitLimit"; 13 | static NSString *kSTTwitterRateLimitRemaining = @"STTwitterRateLimitRemaining"; 14 | static NSString *kSTTwitterRateLimitResetDate = @"STTwitterRateLimitResetDate"; 15 | 16 | // https://dev.twitter.com/docs/error-codes-responses 17 | typedef NS_ENUM( NSInteger, STTwitterTwitterErrorCode ) { 18 | STTwitterTwitterErrorCouldNotAuthenticate = 32, // Your call could not be completed as dialed. 19 | STTwitterTwitterErrorPageDoesNotExist = 34, // Corresponds with an HTTP 404 - the specified resource was not found. 20 | STTwitterTwitterErrorAccountSuspended = 64, // Corresponds with an HTTP 403 — the access token being used belongs to a suspended user and they can't complete the action you're trying to take 21 | STTwitterTwitterErrorAPIv1Inactive = 68, // Corresponds to a HTTP request to a retired v1-era URL. 22 | STTwitterTwitterErrorRateLimitExceeded = 88, // The request limit for this resource has been reached for the current rate limit window. 23 | STTwitterTwitterErrorInvalidOrExpiredToken = 89, // The access token used in the request is incorrect or has expired. Used in API v1.1 24 | STTwitterTwitterErrorSSLRequired = 92, // Only SSL connections are allowed in the API, you should update your request to a secure connection. See how to connect using SSL 25 | STTwitterTwitterErrorOverCapacity = 130, // Corresponds with an HTTP 503 - Twitter is temporarily over capacity. 26 | STTwitterTwitterErrorInternalError = 131, // Corresponds with an HTTP 500 - An unknown internal error occurred. 27 | STTwitterTwitterErrorCouldNotAuthenticateYou = 135, // Corresponds with a HTTP 401 - it means that your oauth_timestamp is either ahead or behind our acceptable range 28 | STTwitterTwitterErrorUnableToFollow = 161, // Corresponds with HTTP 403 — thrown when a user cannot follow another user due to some kind of limit 29 | STTwitterTwitterErrorNotAuthorizedToSeeStatus = 179, // Corresponds with HTTP 403 — thrown when a Tweet cannot be viewed by the authenticating user, usually due to the tweet's author having protected their tweets. 30 | STTwitterTwitterErrorDailyStatuUpdateLimitExceeded = 185, // Corresponds with HTTP 403 — thrown when a tweet cannot be posted due to the user having no allowance remaining to post. Despite the text in the error message indicating that this error is only thrown when a daily limit is reached, this error will be thrown whenever a posting limitation has been reached. Posting allowances have roaming windows of time of unspecified duration. 31 | STTwitterTwitterErrorDuplicatedStatus = 187, // The status text has been Tweeted already by the authenticated account. 32 | STTwitterTwitterErrorBadAuthenticationData = 215, // Typically sent with 1.1 responses with HTTP code 400. The method requires authentication but it was not presented or was wholly invalid. 33 | STTwitterTwitterErrorUserMustVerifyLogin = 231, // Returned as a challenge in xAuth when the user has login verification enabled on their account and needs to be directed to twitter.com to generate a temporary password. 34 | STTwitterTwitterErrorRetiredEndpoint = 251, // Corresponds to a HTTP request to a retired URL. 35 | STTwitterTwitterErrorApplicationCannotWrite = 261 // Corresponds with HTTP 403 — thrown when the application is restricted from POST, PUT, or DELETE actions. See How to appeal application suspension and other disciplinary actions. 36 | }; 37 | 38 | @interface NSError (STTwitter) 39 | 40 | + (NSError *)st_twitterErrorFromResponseData:(NSData *)responseData 41 | responseHeaders:(NSDictionary *)responseHeaders 42 | underlyingError:(NSError *)underlyingError; 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /Vendor/STTwitter/STTwitterHTML.m: -------------------------------------------------------------------------------- 1 | // 2 | // STTwitterWeb.m 3 | // STTwitterRequests 4 | // 5 | // Created by Nicolas Seriot on 9/13/12. 6 | // Copyright (c) 2012 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import "STTwitterHTML.h" 10 | #import "STHTTPRequest.h" 11 | #import "NSString+STTwitter.h" 12 | 13 | @implementation STTwitterHTML 14 | 15 | - (void)getLoginForm:(void(^)(NSString *authenticityToken))successBlock errorBlock:(void(^)(NSError *error))errorBlock { 16 | 17 | __block STHTTPRequest *r = [STHTTPRequest requestWithURLString:@"https://twitter.com/login"]; 18 | 19 | r.completionBlock = ^(NSDictionary *headers, NSString *body) { 20 | 21 | NSError *error = nil; 22 | // NSString *token = [body firstMatchWithRegex:@"" error:&error]; 23 | NSString *token = [body st_firstMatchWithRegex:@"formAuthenticityToken":"(\\S+?)"" error:&error]; 24 | 25 | if(token == nil) { 26 | errorBlock(error); 27 | return; 28 | } 29 | 30 | successBlock(token); 31 | }; 32 | 33 | r.errorBlock = ^(NSError *error) { 34 | errorBlock(error); 35 | }; 36 | 37 | [r startAsynchronous]; 38 | } 39 | 40 | - (void)postLoginFormWithUsername:(NSString *)username 41 | password:(NSString *)password 42 | authenticityToken:(NSString *)authenticityToken 43 | successBlock:(void(^)(NSString *body))successBlock 44 | errorBlock:(void(^)(NSError *error))errorBlock { 45 | 46 | if([username length] == 0 || [password length] == 0) { 47 | NSString *errorDescription = [NSString stringWithFormat:@"Missing credentials"]; 48 | NSError *error = [NSError errorWithDomain:NSStringFromClass([self class]) code:STTwitterHTMLCannotPostWithoutCredentials userInfo:@{NSLocalizedDescriptionKey : errorDescription}]; 49 | errorBlock(error); 50 | return; 51 | } 52 | 53 | __block STHTTPRequest *r = [STHTTPRequest requestWithURLString:@"https://twitter.com/sessions"]; 54 | 55 | r.POSTDictionary = @{@"authenticity_token" : authenticityToken, 56 | @"session[username_or_email]" : username, 57 | @"session[password]" : password, 58 | @"remember_me" : @"1", 59 | @"commit" : @"Sign in"}; 60 | 61 | r.completionBlock = ^(NSDictionary *headers, NSString *body) { 62 | successBlock(body); 63 | }; 64 | 65 | r.errorBlock = ^(NSError *error) { 66 | errorBlock(error); 67 | }; 68 | 69 | [r startAsynchronous]; 70 | } 71 | 72 | - (void)getAuthorizeFormAtURL:(NSURL *)url successBlock:(void(^)(NSString *authenticityToken, NSString *oauthToken))successBlock errorBlock:(void(^)(NSError *error))errorBlock { 73 | 74 | STHTTPRequest *r = [STHTTPRequest requestWithURL:url]; 75 | 76 | r.completionBlock = ^(NSDictionary *headers, NSString *body) { 77 | /* 78 |
79 | 80 | 81 | */ 82 | 83 | NSError *error1 = nil; 84 | NSString *authenticityToken = [body st_firstMatchWithRegex:@"" error:&error1]; 85 | 86 | if(authenticityToken == nil) { 87 | errorBlock(error1); 88 | return; 89 | } 90 | 91 | /**/ 92 | 93 | NSError *error2 = nil; 94 | 95 | NSString *oauthToken = [body st_firstMatchWithRegex:@"" error:&error2]; 96 | 97 | if(oauthToken == nil) { 98 | errorBlock(error2); 99 | return; 100 | } 101 | 102 | /**/ 103 | 104 | successBlock(authenticityToken, oauthToken); 105 | }; 106 | 107 | r.errorBlock = ^(NSError *error) { 108 | errorBlock(error); 109 | }; 110 | 111 | [r startAsynchronous]; 112 | } 113 | 114 | - (void)postAuthorizeFormResultsAtURL:(NSURL *)url authenticityToken:(NSString *)authenticityToken oauthToken:(NSString *)oauthToken successBlock:(void(^)(NSString *PIN))successBlock errorBlock:(void(^)(NSError *error))errorBlock { 115 | 116 | STHTTPRequest *r = [STHTTPRequest requestWithURL:url]; 117 | 118 | r.POSTDictionary = @{@"authenticity_token" : authenticityToken, 119 | @"oauth_token" : oauthToken}; 120 | 121 | r.completionBlock = ^(NSDictionary *headers, NSString *body) { 122 | 123 | NSError *error = nil; 124 | NSString *pin = [body st_firstMatchWithRegex:@"(\\d+)" error:&error]; 125 | 126 | if(pin == nil) { 127 | errorBlock(error); 128 | return; 129 | } 130 | 131 | successBlock(pin); 132 | }; 133 | 134 | r.errorBlock = ^(NSError *error) { 135 | errorBlock(error); 136 | }; 137 | 138 | [r startAsynchronous]; 139 | } 140 | 141 | @end 142 | 143 | -------------------------------------------------------------------------------- /Vendor/STTwitter/STHTTPRequest+STTwitter.m: -------------------------------------------------------------------------------- 1 | // 2 | // STHTTPRequest+STTwitter.m 3 | // STTwitter 4 | // 5 | // Created by Nicolas Seriot on 8/6/13. 6 | // Copyright (c) 2013 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import "STHTTPRequest+STTwitter.h" 10 | #import "NSString+STTwitter.h" 11 | #import "NSError+STTwitter.h" 12 | 13 | #if DEBUG 14 | # define STLog(...) NSLog(__VA_ARGS__) 15 | #else 16 | # define STLog(...) 17 | #endif 18 | 19 | @implementation STHTTPRequest (STTwitter) 20 | 21 | + (STHTTPRequest *)twitterRequestWithURLString:(NSString *)urlString 22 | stTwitterUploadProgressBlock:(void(^)(NSInteger bytesWritten, NSInteger totalBytesWritten, NSInteger totalBytesExpectedToWrite))uploadProgressBlock 23 | stTwitterDownloadProgressBlock:(void(^)(id json))downloadProgressBlock 24 | stTwitterSuccessBlock:(void(^)(NSDictionary *requestHeaders, NSDictionary *responseHeaders, id json))successBlock 25 | stTwitterErrorBlock:(void(^)(NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error))errorBlock { 26 | 27 | __block STHTTPRequest *r = [self requestWithURLString:urlString]; 28 | __weak STHTTPRequest *wr = r; 29 | 30 | r.ignoreSharedCookiesStorage = YES; 31 | 32 | r.timeoutSeconds = DBL_MAX; 33 | 34 | r.uploadProgressBlock = uploadProgressBlock; 35 | 36 | r.downloadProgressBlock = ^(NSData *data, NSInteger totalBytesReceived, long long totalBytesExpectedToReceive) { 37 | 38 | if(downloadProgressBlock == nil) return; 39 | 40 | NSError *jsonError = nil; 41 | id json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError]; 42 | 43 | if(json) { 44 | downloadProgressBlock(json); 45 | return; 46 | } 47 | 48 | // we can receive several dictionaries in the same data chunk 49 | // such as '{..}\r\n{..}\r\n{..}' which is not valid JSON 50 | // so we split them up into a 'jsonChunks' array such as [{..},{..},{..}] 51 | 52 | NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 53 | 54 | NSArray *jsonChunks = [jsonString componentsSeparatedByString:@"\r\n"]; 55 | 56 | for(NSString *jsonChunk in jsonChunks) { 57 | if([jsonChunk length] == 0) continue; 58 | NSData *data = [jsonChunk dataUsingEncoding:NSUTF8StringEncoding]; 59 | NSError *jsonError = nil; 60 | id json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError]; 61 | if(json == nil) { 62 | // errorBlock(wr.responseHeaders, jsonError); 63 | return; // not enough information to say it's an error 64 | } 65 | downloadProgressBlock(json); 66 | } 67 | }; 68 | 69 | r.completionDataBlock = ^(NSDictionary *responseHeaders, NSData *responseData) { 70 | 71 | NSError *jsonError = nil; 72 | id json = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingMutableLeaves error:&jsonError]; 73 | 74 | if(json == nil) { 75 | successBlock(wr.requestHeaders, wr.responseHeaders, wr.responseString); // response is not necessarily json 76 | return; 77 | } 78 | 79 | successBlock(wr.requestHeaders, wr.responseHeaders, json); 80 | }; 81 | 82 | r.errorBlock = ^(NSError *error) { 83 | 84 | NSError *e = [NSError st_twitterErrorFromResponseData:wr.responseData responseHeaders:wr.responseHeaders underlyingError:error]; 85 | if(e) { 86 | errorBlock(wr.requestHeaders, wr.responseHeaders, e); 87 | return; 88 | } 89 | 90 | if(error) { 91 | errorBlock(wr.requestHeaders, wr.responseHeaders, error); 92 | return; 93 | } 94 | 95 | e = [NSError errorWithDomain:NSStringFromClass([self class]) code:0 userInfo:@{NSLocalizedDescriptionKey : wr.responseString}]; 96 | 97 | if (wr.responseString) STLog(@"-- body: %@", wr.responseString); 98 | 99 | // BOOL isCancellationError = [[error domain] isEqualToString:@"STHTTPRequest"] && ([error code] == kSTHTTPRequestCancellationError); 100 | // if(isCancellationError) return; 101 | 102 | errorBlock(wr.requestHeaders, wr.responseHeaders, error); 103 | }; 104 | 105 | return r; 106 | } 107 | 108 | + (void)expandedURLStringForShortenedURLString:(NSString *)urlString 109 | successBlock:(void(^)(NSString *expandedURLString))successBlock 110 | errorBlock:(void(^)(NSError *error))errorBlock { 111 | 112 | STHTTPRequest *r = [STHTTPRequest requestWithURLString:urlString]; 113 | 114 | r.ignoreSharedCookiesStorage = YES; 115 | r.preventRedirections = YES; 116 | 117 | r.completionBlock = ^(NSDictionary *responseHeaders, NSString *body) { 118 | 119 | NSString *location = [responseHeaders valueForKey:@"location"]; 120 | if(location == nil) [responseHeaders valueForKey:@"Location"]; 121 | 122 | successBlock(location); 123 | }; 124 | 125 | r.errorBlock = ^(NSError *error) { 126 | errorBlock(error); 127 | }; 128 | 129 | [r startAsynchronous]; 130 | } 131 | 132 | @end 133 | -------------------------------------------------------------------------------- /Vendor/STTwitter/Vendor/STHTTPRequest.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, Nicolas Seriot 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | * Neither the name of the Nicolas Seriot nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | */ 13 | 14 | #import 15 | 16 | extern NSUInteger const kSTHTTPRequestCancellationError; 17 | extern NSUInteger const kSTHTTPRequestDefaultTimeout; 18 | 19 | @class STHTTPRequest; 20 | 21 | typedef void (^uploadProgressBlock_t)(NSInteger bytesWritten, NSInteger totalBytesWritten, NSInteger totalBytesExpectedToWrite); 22 | typedef void (^downloadProgressBlock_t)(NSData *data, NSInteger totalBytesReceived, long long totalBytesExpectedToReceive); 23 | typedef void (^completionBlock_t)(NSDictionary *headers, NSString *body); 24 | typedef void (^completionDataBlock_t)(NSDictionary *headers, NSData *body); 25 | typedef void (^errorBlock_t)(NSError *error); 26 | 27 | @interface STHTTPRequest : NSObject 28 | 29 | @property (copy) uploadProgressBlock_t uploadProgressBlock; 30 | @property (copy) downloadProgressBlock_t downloadProgressBlock; 31 | @property (copy) completionBlock_t completionBlock; 32 | @property (copy) errorBlock_t errorBlock; 33 | @property (copy) completionDataBlock_t completionDataBlock; 34 | 35 | // request 36 | @property (nonatomic, retain) NSString *HTTPMethod; // default: GET, or POST if POSTDictionary or files to upload 37 | @property (nonatomic, retain) NSMutableDictionary *requestHeaders; 38 | @property (nonatomic, retain) NSDictionary *POSTDictionary; // keys and values are NSString instances 39 | @property (nonatomic, retain) NSData *rawPOSTData; // eg. to post JSON contents 40 | @property (nonatomic) NSStringEncoding POSTDataEncoding; 41 | @property (nonatomic, assign) NSUInteger timeoutSeconds; 42 | @property (nonatomic) BOOL addCredentialsToURL; // default NO 43 | @property (nonatomic) BOOL encodePOSTDictionary; // default YES 44 | @property (nonatomic, retain, readonly) NSURL *url; 45 | @property (nonatomic) BOOL ignoreSharedCookiesStorage; 46 | @property (nonatomic) BOOL preventRedirections; 47 | 48 | // response 49 | @property (nonatomic) NSStringEncoding forcedResponseEncoding; 50 | @property (nonatomic, readonly) NSInteger responseStatus; 51 | @property (nonatomic, retain, readonly) NSString *responseStringEncodingName; 52 | @property (nonatomic, retain, readonly) NSDictionary *responseHeaders; 53 | @property (nonatomic, retain) NSString *responseString; 54 | @property (nonatomic, retain, readonly) NSMutableData *responseData; 55 | @property (nonatomic, retain, readonly) NSError *error; 56 | 57 | + (STHTTPRequest *)requestWithURL:(NSURL *)url; 58 | + (STHTTPRequest *)requestWithURLString:(NSString *)urlString; 59 | 60 | - (NSString *)debugDescription; // logged when launched with -STHTTPRequestShowDebugDescription 1 61 | - (NSString *)curlDescription; // logged when launched with -STHTTPRequestShowCurlDescription 1 62 | 63 | - (NSString *)startSynchronousWithError:(NSError **)error; 64 | - (void)startAsynchronous; 65 | - (void)cancel; 66 | 67 | // Cookies 68 | + (void)addCookieToSharedCookiesStorage:(NSHTTPCookie *)cookie; 69 | + (void)addCookieToSharedCookiesStorageWithName:(NSString *)name value:(NSString *)value url:(NSURL *)url; 70 | - (void)addCookieWithName:(NSString *)name value:(NSString *)value url:(NSURL *)url; 71 | - (void)addCookieWithName:(NSString *)name value:(NSString *)value; 72 | - (NSArray *)requestCookies; 73 | - (NSArray *)sessionCookies; 74 | + (NSArray *)sessionCookiesInSharedCookiesStorage; 75 | + (void)deleteAllCookiesFromSharedCookieStorage; 76 | - (void)deleteSessionCookies; 77 | 78 | // Credentials 79 | + (NSURLCredential *)sessionAuthenticationCredentialsForURL:(NSURL *)requestURL; 80 | - (void)setUsername:(NSString *)username password:(NSString *)password; 81 | - (NSString *)username; 82 | - (NSString *)password; 83 | + (void)deleteAllCredentials; 84 | 85 | // Headers 86 | - (void)setHeaderWithName:(NSString *)name value:(NSString *)value; 87 | - (void)removeHeaderWithName:(NSString *)name; 88 | - (NSDictionary *)responseHeaders; 89 | 90 | // Upload 91 | - (void)addFileToUpload:(NSString *)path parameterName:(NSString *)param; 92 | - (void)addDataToUpload:(NSData *)data parameterName:(NSString *)param; 93 | - (void)addDataToUpload:(NSData *)data parameterName:(NSString *)param mimeType:(NSString *)mimeType fileName:(NSString *)fileName; 94 | 95 | // Session 96 | + (void)clearSession; // delete all credentials and cookies 97 | 98 | @end 99 | 100 | @interface NSError (STHTTPRequest) 101 | - (BOOL)st_isAuthenticationError; 102 | - (BOOL)st_isCancellationError; 103 | @end 104 | 105 | @interface NSString (RFC3986) 106 | - (NSString *)st_stringByAddingRFC3986PercentEscapesUsingEncoding:(NSStringEncoding)encoding; 107 | @end 108 | -------------------------------------------------------------------------------- /THCumulativeChartView.m: -------------------------------------------------------------------------------- 1 | // 2 | // CumulativeChartView.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 5/1/10. 6 | // Copyright 2010 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import "THCumulativeChartView.h" 10 | 11 | #define DRAW_TOP 12 | #define DRAW_BOTTOM 13 | 14 | @implementation THCumulativeChartView 15 | 16 | @synthesize delegate, dataSource; 17 | 18 | - (void)setScore:(NSUInteger)aScore { 19 | 20 | aScore = MIN(aScore, MAX_COUNT); 21 | aScore = MAX(0, aScore); 22 | 23 | score = aScore; 24 | [self setNeedsDisplay:YES]; 25 | } 26 | 27 | - (void)resizeWithOldSuperviewSize:(NSSize)oldSize { 28 | [self removeTrackingRect:tag]; 29 | tag = [self addTrackingRect:[self bounds] owner:self userData:nil assumeInside:NO]; 30 | 31 | [super resizeWithOldSuperviewSize:oldSize]; 32 | } 33 | 34 | - (void)resetCursorRects { 35 | [super resetCursorRects]; 36 | 37 | NSCursor *cursor = [NSCursor resizeUpDownCursor]; 38 | [self addCursorRect:[self bounds] cursor:cursor]; 39 | [cursor setOnMouseEntered:YES]; 40 | } 41 | 42 | - (id)initWithFrame:(NSRect)frame { 43 | self = [super initWithFrame:frame]; 44 | if (self) { 45 | [[self window] setAcceptsMouseMovedEvents:YES]; 46 | tag = [self addTrackingRect:[self bounds] owner:self userData:nil assumeInside:NO]; 47 | } 48 | return self; 49 | } 50 | 51 | - (void)drawRect:(NSRect)dirtyRect { 52 | 53 | if([dataSource numberOfTweets] == 0) return; 54 | 55 | CGFloat height = [self bounds].size.height; 56 | CGFloat width = [self bounds].size.width; 57 | 58 | CGFloat heightFactor = height / (float)MAX_COUNT; 59 | CGFloat widthFactor = width / [dataSource numberOfTweets]; 60 | 61 | NSGraphicsContext *gc = [NSGraphicsContext currentContext]; 62 | CGContextRef context = (CGContextRef)[gc graphicsPort]; 63 | 64 | /* set pen and colors */ 65 | 66 | CGContextSetAllowsAntialiasing(context, false); 67 | 68 | CGContextSetLineWidth(context, 1.0); 69 | 70 | CGColorRef strokeColor = [NSColor blackColor].CGColor; 71 | CGContextSetStrokeColorWithColor(context, strokeColor); 72 | 73 | /* draw top */ 74 | 75 | CGColorRef fillColorTop = [NSColor colorForControlTint:NSBlueControlTint].CGColor; 76 | CGContextSetFillColorWithColor(context, fillColorTop); 77 | 78 | CGContextBeginPath(context); 79 | 80 | CGContextMoveToPoint(context, width, floorf(MAX_COUNT*heightFactor)); 81 | 82 | NSUInteger totalFrom = 0; 83 | NSUInteger totalTo = 0; 84 | 85 | for(NSUInteger i = MAX_COUNT; i >= score; i--) { 86 | totalFrom = [dataSource cumulatedTweetsForScore:i]; 87 | totalTo = [dataSource cumulatedTweetsForScore:i-1]; 88 | 89 | CGContextAddLineToPoint(context, width - floorf(totalFrom*widthFactor), floorf(i*heightFactor)); 90 | CGContextAddLineToPoint(context, width - floorf(totalTo*widthFactor), floorf(i*heightFactor)); 91 | 92 | if(i == 0) break; 93 | } 94 | 95 | CGContextAddLineToPoint(context, width - floor(totalTo*widthFactor), score*heightFactor); 96 | CGContextAddLineToPoint(context, width-1, score*heightFactor); 97 | CGContextAddLineToPoint(context, width-1, MAX_COUNT*heightFactor-1); 98 | CGContextAddLineToPoint(context, width - floor([dataSource cumulatedTweetsForScore:MAX_COUNT]*widthFactor), MAX_COUNT*heightFactor-1); 99 | 100 | #ifdef DRAW_TOP 101 | CGContextDrawPath(context, kCGPathFillStroke); 102 | #else 103 | CGContextClosePath(context); 104 | #endif 105 | 106 | /* draw bottom */ 107 | 108 | CGColorRef fillColorBottom = [NSColor colorForControlTint:NSGraphiteControlTint].CGColor; 109 | 110 | CGContextSetFillColorWithColor(context, fillColorBottom); 111 | 112 | CGContextBeginPath(context); 113 | 114 | CGContextMoveToPoint(context, width, floorf(score*heightFactor)); 115 | CGContextAddLineToPoint(context, width - floorf(totalTo*widthFactor), floorf(score*heightFactor)); 116 | CGContextAddLineToPoint(context, width - floorf(totalTo*widthFactor), floorf(score*heightFactor)); 117 | 118 | if(score == 100) { 119 | // close the top line because top drawing didn't 120 | CGContextAddLineToPoint(context, width - floorf(totalTo*widthFactor), height-1); 121 | CGContextAddLineToPoint(context, width, height-1); 122 | } 123 | 124 | for(NSUInteger i = score; i > 0; i--) { 125 | if(score == 0) { 126 | break; 127 | } 128 | totalFrom = [dataSource cumulatedTweetsForScore:i]; 129 | totalTo = [dataSource cumulatedTweetsForScore:(i-1)]; 130 | CGContextAddLineToPoint(context, width - floorf(totalFrom*widthFactor), floorf(i*heightFactor)); 131 | CGContextAddLineToPoint(context, width - floorf(totalTo*widthFactor), floorf(i*heightFactor)); 132 | } 133 | 134 | CGContextAddLineToPoint(context, 0, 0); 135 | CGContextAddLineToPoint(context, width-1, 0); 136 | CGContextAddLineToPoint(context, width-1, height); 137 | 138 | #ifdef DRAW_BOTTOM 139 | CGContextDrawPath(context, kCGPathFillStroke); 140 | #else 141 | CGContextClosePath(context); 142 | #endif 143 | 144 | CGContextSetAllowsAntialiasing(context, true); 145 | } 146 | 147 | 148 | #pragma mark mouse events 149 | 150 | - (BOOL)acceptsFirstResponder { 151 | return YES; 152 | } 153 | 154 | - (void)setScoreFromPoint:(NSPoint)p { 155 | CGFloat height = [self bounds].size.height; 156 | CGFloat heightFactor = height / (float)MAX_COUNT; 157 | NSUInteger theScore = p.y / heightFactor; 158 | [self setScore:theScore]; 159 | } 160 | 161 | - (void)mouseDown:(NSEvent *)theEvent { 162 | 163 | NSUInteger formerScore = score; 164 | NSUInteger slidingScore; 165 | 166 | NSPoint p = [self convertPoint:[theEvent locationInWindow] fromView:nil]; 167 | [self setScoreFromPoint:p]; 168 | 169 | BOOL keepOn = YES; 170 | NSPoint mouseLoc; 171 | 172 | while (keepOn) { 173 | theEvent = [[self window] nextEventMatchingMask: NSLeftMouseUpMask | NSLeftMouseDraggedMask]; 174 | mouseLoc = [self convertPoint:[theEvent locationInWindow] fromView:nil]; 175 | 176 | switch ([theEvent type]) { 177 | case NSLeftMouseDragged: 178 | slidingScore = score; 179 | [self setScoreFromPoint:mouseLoc]; 180 | if(score != slidingScore) [delegate chartView:self didSlideToScore:score]; 181 | break; 182 | case NSLeftMouseUp: 183 | keepOn = NO; 184 | break; 185 | default: 186 | break; 187 | } 188 | } 189 | if(score != formerScore) [delegate chartView:self didStopSlidingOnScore:score]; 190 | } 191 | 192 | @end 193 | -------------------------------------------------------------------------------- /THTweetCollectionViewItem.m: -------------------------------------------------------------------------------- 1 | // 2 | // TweetCollectionViewItem.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 20.04.09. 6 | // Copyright 2009 Sen:te. All rights reserved. 7 | // 8 | 9 | #import "THTweetCollectionViewItem.h" 10 | #import "THTweet.h" 11 | #import "THUser.h" 12 | #import "NSManagedObject+ST.h" 13 | #import "THTextView.h" 14 | 15 | @implementation THTweetCollectionViewItem 16 | 17 | - (void)awakeFromNib { 18 | [super awakeFromNib]; 19 | 20 | [_tweetTextTextView setEditable:NO]; 21 | [_tweetTextTextView setSelectable:YES]; 22 | [_tweetTextTextView setDrawsBackground:NO]; 23 | // [_tweetTextTextView setRichText:YES]; 24 | 25 | // CALayer *layer = [CALayer layer]; 26 | // CGColorRef color = [NSColor redColor].CGColor; 27 | // layer.backgroundColor = color; 28 | // [[self view] setLayer:layer]; 29 | } 30 | 31 | - (IBAction)openUserWebTimeline:(id)sender { 32 | THTweet *tweet = [self representedObject]; 33 | 34 | NSString *urlString = [NSString stringWithFormat:@"http://twitter.com/%@", tweet.user.screenName]; 35 | NSURL *url = [NSURL URLWithString:urlString]; 36 | [[NSWorkspace sharedWorkspace] openURL:url]; 37 | } 38 | 39 | - (void)setReadState:(BOOL)isRead { 40 | THTweet *tweet = [self representedObject]; 41 | 42 | tweet.isRead = @(isRead); 43 | 44 | NSLog(@"-- %@ %@", tweet.uid, tweet.isRead); 45 | 46 | BOOL success = [tweet save]; 47 | if(!success) NSLog(@"-- can't save tweet %@", tweet); 48 | 49 | NSDictionary *userInfo = [NSDictionary dictionaryWithObject:tweet forKey:@"Tweet"]; 50 | 51 | NSNotification *notification = [NSNotification notificationWithName:@"DidChangeTweetReadStateNotification" object:self userInfo:userInfo]; 52 | [[NSNotificationCenter defaultCenter] postNotification:notification]; 53 | } 54 | 55 | - (IBAction)toggleReadState:(id)sender { 56 | THTweet *tweet = [self representedObject]; 57 | 58 | BOOL wasRead = [tweet.isRead boolValue]; 59 | 60 | [self setReadState:!wasRead]; 61 | } 62 | 63 | - (IBAction)markAsRead:(id)sender { 64 | [self setReadState:YES]; 65 | } 66 | 67 | - (IBAction)retweet:(id)sender { 68 | THTweet *tweet = [self representedObject]; 69 | 70 | NSDictionary *userInfo = @{@"Tweet" : tweet, @"Action" : @"Retweet"}; 71 | NSNotification *notification = [NSNotification notificationWithName:@"THTweetAction" object:self userInfo:userInfo]; 72 | [[NSNotificationCenter defaultCenter] postNotification:notification]; 73 | } 74 | 75 | - (IBAction)reply:(id)sender { 76 | THTweet *tweet = [self representedObject]; 77 | 78 | NSDictionary *userInfo = @{@"Tweet" : tweet, @"Action" : @"Reply"}; 79 | NSNotification *notification = [NSNotification notificationWithName:@"THTweetAction" object:self userInfo:userInfo]; 80 | [[NSNotificationCenter defaultCenter] postNotification:notification]; 81 | } 82 | 83 | - (IBAction)remoteDelete:(id)sender { 84 | THTweet *tweet = [self representedObject]; 85 | 86 | NSDictionary *userInfo = @{@"Tweet" : tweet, @"Action" : @"RemoteDelete"}; 87 | NSNotification *notification = [NSNotification notificationWithName:@"THTweetAction" object:self userInfo:userInfo]; 88 | [[NSNotificationCenter defaultCenter] postNotification:notification]; 89 | } 90 | 91 | //- (IBAction)changeFavoriteState:(id)sender { 92 | // THTweet *tweet = [self representedObject]; 93 | // 94 | // BOOL wasFavorite = [tweet.isFavorite boolValue]; 95 | // 96 | // tweet.isFavorite = [NSNumber numberWithBool:!wasFavorite]; 97 | // 98 | // NSLog(@"-- %@ %@", tweet.uid, tweet.isFavorite); 99 | // 100 | // BOOL success = [tweet save]; 101 | // if(!success) NSLog(@"-- can't save tweet %@", tweet); 102 | // 103 | // NSDictionary *userInfo = [NSDictionary dictionaryWithObject:tweet forKey:@"Tweet"]; 104 | // 105 | //#warning TODO: listen to favorite status change notification in controller and do the appropriate API request 106 | // 107 | // NSNotification *notification = [NSNotification notificationWithName:@"DidChangeTweetFavoriteStateNotification" object:self userInfo:userInfo]; 108 | // [[NSNotificationCenter defaultCenter] postNotification:notification]; 109 | //} 110 | 111 | //- (IBAction)showContextMenu:(id)sender { 112 | // NSLog(@"-- show context menu, %@ %@", sender, NSStringFromRect([sender frame])); 113 | // 114 | // NSRect frame = [(NSButton *)sender frame]; 115 | // NSPoint menuOrigin = [[(NSButton *)sender superview] convertPoint:NSMakePoint(frame.origin.x, frame.origin.y+frame.size.height) 116 | // toView:nil]; 117 | // 118 | // NSEvent *event = [NSEvent mouseEventWithType:NSLeftMouseDown 119 | // location:menuOrigin 120 | // modifierFlags:NSLeftMouseDownMask // 0x100 121 | // timestamp:[[NSDate date] timeIntervalSince1970] 122 | // windowNumber:[[(NSButton *)sender window] windowNumber] 123 | // context:[[(NSButton *)sender window] graphicsContext] 124 | // eventNumber:0 125 | // clickCount:1 126 | // pressure:1]; 127 | // 128 | // NSMenu *menu = [[NSMenu alloc] init]; 129 | // [menu insertItemWithTitle:@"add" 130 | // action:@selector(add:) 131 | // keyEquivalent:@"" 132 | // atIndex:0]; 133 | // 134 | // [NSMenu popUpContextMenu:menu withEvent:event forView:(NSButton *)sender]; 135 | //} 136 | 137 | - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { 138 | self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 139 | return self; 140 | } 141 | 142 | - (void)loadView { 143 | [super loadView]; 144 | 145 | if(self.representedObject == nil) return; 146 | 147 | NSAttributedString *as = [self.representedObject attributedString]; 148 | 149 | if(as == nil) return; 150 | 151 | [[_tweetTextTextView textStorage] setAttributedString:as]; 152 | 153 | // [_tweetTextTextView bind:@"attributedString" toObject:self.representedObject withKeyPath:@"attributedString" options:nil]; 154 | } 155 | 156 | - (void)setRepresentedObject:(id)representedObject { 157 | [super setRepresentedObject:representedObject]; 158 | 159 | // NSLog(@"-- setRepresentedObject"); 160 | 161 | // THTweetView *tweetView = (THTweetView *)(self.view); 162 | // 163 | // NSLog(@"-- %@", tweetView); 164 | // 165 | // tweetView.delegate = self; 166 | // 167 | // if(representedObject == nil) return; 168 | // 169 | // [_tweetTextTextView bind:@"attributedString" toObject:representedObject withKeyPath:@"attributedString" options:nil]; 170 | 171 | // NSAttributedString *as = [representedObject attributedString]; 172 | // 173 | // if(as == nil) return; 174 | // 175 | // [[_tweetTextTextView textStorage] setAttributedString:as]; 176 | } 177 | 178 | #pragma mark THTweetViewProtocol 179 | 180 | - (void)tweetViewWasClicked:(THTweetView *)tweetView { 181 | // [self toggleSelection:self]; 182 | 183 | [self setReadState:YES]; 184 | } 185 | 186 | @end 187 | -------------------------------------------------------------------------------- /Vendor/STTwitter/Vendor/JSONSyntaxHighlight.m: -------------------------------------------------------------------------------- 1 | /** 2 | * JSONSyntaxHighlight.h 3 | * JSONSyntaxHighlight 4 | * 5 | * Syntax highlight JSON 6 | * 7 | * Created by Dave Eddy on 8/3/13. 8 | * Copyright (c) 2013 Dave Eddy. All rights reserved. 9 | * 10 | * The MIT License (MIT) 11 | * 12 | * Permission is hereby granted, free of charge, to any person obtaining a copy 13 | * of this software and associated documentation files (the "Software"), to deal 14 | * in the Software without restriction, including without limitation the rights 15 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | * copies of the Software, and to permit persons to whom the Software is 17 | * furnished to do so, subject to the following conditions: 18 | * 19 | * The above copyright notice and this permission notice shall be included in 20 | * all copies or substantial portions of the Software. 21 | * 22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 28 | * THE SOFTWARE. 29 | */ 30 | 31 | #import "JSONSyntaxHighlight.h" 32 | 33 | @implementation JSONSyntaxHighlight { 34 | NSRegularExpression *regex; 35 | } 36 | 37 | #pragma mark Object Initializer 38 | // Must init with a JSON object 39 | - (JSONSyntaxHighlight *)init 40 | { 41 | return nil; 42 | } 43 | 44 | - (JSONSyntaxHighlight *)initWithJSON:(id)JSON 45 | { 46 | self = [super init]; 47 | if (self) { 48 | // save the origin JSON 49 | _JSON = JSON; 50 | 51 | // create the object local regex 52 | regex = [NSRegularExpression regularExpressionWithPattern:@"^( *)(\".+\" : )?(\"[^\"]*\"|[\\w.+-]*)?([,\\[\\]{}]?,?$)" 53 | options:NSRegularExpressionAnchorsMatchLines 54 | error:nil]; 55 | 56 | // parse the JSON if possible 57 | if ([NSJSONSerialization isValidJSONObject:self.JSON]) { 58 | NSJSONWritingOptions options = NSJSONWritingPrettyPrinted; 59 | NSData *data = [NSJSONSerialization dataWithJSONObject:self.JSON options:options error:nil]; 60 | NSString *o = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 61 | _parsedJSON = o; 62 | } else { 63 | _parsedJSON = [NSString stringWithFormat:@"%@", self.JSON]; 64 | } 65 | 66 | // set the default attributes 67 | self.nonStringAttributes = @{NSForegroundColorAttributeName: [self.class colorWithRGB:0x000080]}; 68 | self.stringAttributes = @{NSForegroundColorAttributeName: [self.class colorWithRGB:0x808000]}; 69 | self.keyAttributes = @{NSForegroundColorAttributeName: [self.class colorWithRGB:0xa52a2a]}; 70 | } 71 | return self; 72 | } 73 | 74 | #pragma mark - 75 | #pragma mark JSON Highlighting 76 | - (NSAttributedString *)highlightJSON 77 | { 78 | return [self highlightJSONWithPrettyPrint:YES]; 79 | } 80 | 81 | - (NSAttributedString *)highlightJSONWithPrettyPrint:(BOOL)prettyPrint 82 | { 83 | NSMutableAttributedString *line = [[NSMutableAttributedString alloc] initWithString:@""]; 84 | [self enumerateMatchesWithIndentBlock: 85 | // The indent 86 | ^(NSRange range, NSString *s) { 87 | NSAttributedString *as = [[NSAttributedString alloc] initWithString:s attributes:@{}]; 88 | if (prettyPrint) [line appendAttributedString:as]; 89 | } 90 | keyBlock: 91 | // The key (with quotes and colon) 92 | ^(NSRange range, NSString *s) { 93 | // I hate this: this changes `"key" : ` to `"key"` 94 | NSString *key = [s substringToIndex:s.length - 3]; 95 | [line appendAttributedString:[[NSAttributedString alloc] initWithString:key attributes:self.keyAttributes]]; 96 | NSString *colon = prettyPrint ? @" : " : @":"; 97 | [line appendAttributedString:[[NSAttributedString alloc] initWithString:colon attributes:@{}]]; 98 | } 99 | valueBlock: 100 | // The value 101 | ^(NSRange range, NSString *s) { 102 | NSAttributedString *as; 103 | if ([s rangeOfString:@"\""].location == NSNotFound) // literal or number 104 | as = [[NSAttributedString alloc] initWithString:s attributes:self.nonStringAttributes]; 105 | else // string 106 | as = [[NSAttributedString alloc] initWithString:s attributes:self.stringAttributes]; 107 | 108 | [line appendAttributedString:as]; 109 | } 110 | endBlock: 111 | // The final comma, or ending character 112 | ^(NSRange range, NSString *s) { 113 | NSAttributedString *as = [[NSAttributedString alloc] initWithString:s attributes:@{}]; 114 | [line appendAttributedString:as]; 115 | if (prettyPrint) [line appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n"]]; 116 | }]; 117 | 118 | if ([line isEqualToAttributedString:[[NSAttributedString alloc] initWithString:@""]]) 119 | line = [[NSMutableAttributedString alloc] initWithString:self.parsedJSON]; 120 | return line; 121 | } 122 | 123 | #pragma mark JSON Parser 124 | - (void)enumerateMatchesWithIndentBlock:(void(^)(NSRange, NSString*))indentBlock 125 | keyBlock:(void(^)(NSRange, NSString*))keyBlock 126 | valueBlock:(void(^)(NSRange, NSString*))valueBlock 127 | endBlock:(void(^)(NSRange, NSString*))endBlock 128 | { 129 | [regex enumerateMatchesInString:self.parsedJSON 130 | options:0 131 | range:NSMakeRange(0, self.parsedJSON.length) 132 | usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop) { 133 | 134 | NSRange indentRange = [match rangeAtIndex:1]; 135 | NSRange keyRange = [match rangeAtIndex:2]; 136 | NSRange valueRange = [match rangeAtIndex:3]; 137 | NSRange endRange = [match rangeAtIndex:4]; 138 | 139 | if (indentRange.location != NSNotFound) 140 | indentBlock(indentRange, [self.parsedJSON substringWithRange:indentRange]); 141 | if (keyRange.location != NSNotFound) 142 | keyBlock(keyRange, [self.parsedJSON substringWithRange:keyRange]); 143 | if (valueRange.location != NSNotFound) 144 | valueBlock(valueRange, [self.parsedJSON substringWithRange:valueRange]); 145 | if (endRange.location != NSNotFound) 146 | endBlock(endRange, [self.parsedJSON substringWithRange:endRange]); 147 | }]; 148 | } 149 | 150 | #pragma mark - 151 | #pragma mark Color Helper Functions 152 | #if (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE) 153 | + (UIColor *)colorWithRGB:(NSInteger)rgbValue 154 | { 155 | return [self.class colorWithRGB:rgbValue alpha:1.0]; 156 | } 157 | 158 | + (UIColor *)colorWithRGB:(NSInteger)rgbValue alpha:(CGFloat)alpha 159 | { 160 | return [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16)) / 255.0 161 | green:((float)((rgbValue & 0x00FF00) >> 8 )) / 255.0 162 | blue:((float)((rgbValue & 0x0000FF) >> 0 )) / 255.0 163 | alpha:alpha]; 164 | } 165 | #else 166 | + (NSColor *)colorWithRGB:(NSInteger)rgbValue 167 | { 168 | return [self.class colorWithRGB:rgbValue alpha:1.0]; 169 | } 170 | 171 | + (NSColor *)colorWithRGB:(NSInteger)rgbValue alpha:(CGFloat)alpha 172 | { 173 | return [NSColor colorWithCalibratedRed:((float)((rgbValue & 0xFF0000) >> 16)) / 255.0 174 | green:((float)((rgbValue & 0x00FF00) >> 8 )) / 255.0 175 | blue:((float)((rgbValue & 0x0000FF) >> 0 )) / 255.0 176 | alpha:alpha]; 177 | } 178 | #endif 179 | 180 | @end 181 | -------------------------------------------------------------------------------- /THAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // TwitHunter_AppDelegate.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 19.04.09. 6 | // Copyright Sen:te 2009 . All rights reserved. 7 | // 8 | 9 | #import "THAppDelegate.h" 10 | 11 | @implementation THAppDelegate 12 | 13 | @synthesize window; 14 | 15 | /** 16 | Returns the support directory for the application, used to store the Core Data 17 | store file. This code uses a directory named "Untitled" for 18 | the content, either in the NSApplicationSupportDirectory location or (if the 19 | former cannot be found), the system's temporary directory. 20 | */ 21 | 22 | - (NSString *)applicationSupportDirectory { 23 | 24 | NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); 25 | NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : NSTemporaryDirectory(); 26 | return [basePath stringByAppendingPathComponent:@"TwitHunter"]; 27 | } 28 | 29 | /** 30 | Creates, retains, and returns the managed object model for the application 31 | by merging all of the models found in the application bundle. 32 | */ 33 | 34 | - (NSManagedObjectModel *)managedObjectModel { 35 | 36 | if (managedObjectModel) return managedObjectModel; 37 | 38 | managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil]; 39 | return managedObjectModel; 40 | } 41 | 42 | /** 43 | Returns the persistent store coordinator for the application. This 44 | implementation will create and return a coordinator, having added the 45 | store for the application to it. (The directory for the store is created, 46 | if necessary.) 47 | */ 48 | 49 | - (NSPersistentStoreCoordinator *) persistentStoreCoordinator { 50 | 51 | if (persistentStoreCoordinator) return persistentStoreCoordinator; 52 | 53 | NSManagedObjectModel *mom = [self managedObjectModel]; 54 | if (!mom) { 55 | NSAssert(NO, @"Managed object model is nil"); 56 | NSLog(@"%@:%@ No model to generate a store from", [self class], NSStringFromSelector(_cmd)); 57 | return nil; 58 | } 59 | 60 | NSFileManager *fileManager = [NSFileManager defaultManager]; 61 | NSString *applicationSupportDirectory = [self applicationSupportDirectory]; 62 | NSError *error = nil; 63 | 64 | if ( ![fileManager fileExistsAtPath:applicationSupportDirectory isDirectory:NULL] ) { 65 | if (![fileManager createDirectoryAtPath:applicationSupportDirectory withIntermediateDirectories:NO attributes:nil error:&error]) { 66 | NSAssert(NO, ([NSString stringWithFormat:@"Failed to create App Support directory %@ : %@", applicationSupportDirectory,error])); 67 | NSLog(@"Error creating application support directory at %@ : %@",applicationSupportDirectory,error); 68 | return nil; 69 | } 70 | } 71 | 72 | NSURL *url = [NSURL fileURLWithPath: [applicationSupportDirectory stringByAppendingPathComponent: @"TwitHunter.sqlite3"]]; 73 | persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: mom]; 74 | if (![persistentStoreCoordinator addPersistentStoreWithType:NSXMLStoreType 75 | configuration:nil 76 | URL:url 77 | options:nil 78 | error:&error]){ 79 | [[NSApplication sharedApplication] presentError:error]; 80 | persistentStoreCoordinator = nil; 81 | return nil; 82 | } 83 | 84 | return persistentStoreCoordinator; 85 | } 86 | 87 | /** 88 | Returns the managed object context for the application (which is already 89 | bound to the persistent store coordinator for the application.) 90 | */ 91 | 92 | - (NSManagedObjectContext *) managedObjectContext { 93 | 94 | if (managedObjectContext) return managedObjectContext; 95 | 96 | NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; 97 | if (!coordinator) { 98 | NSMutableDictionary *dict = [NSMutableDictionary dictionary]; 99 | [dict setValue:@"Failed to initialize the store" forKey:NSLocalizedDescriptionKey]; 100 | [dict setValue:@"There was an error building up the data file." forKey:NSLocalizedFailureReasonErrorKey]; 101 | NSError *error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict]; 102 | [[NSApplication sharedApplication] presentError:error]; 103 | return nil; 104 | } 105 | 106 | managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; 107 | [managedObjectContext setPersistentStoreCoordinator: coordinator]; 108 | 109 | return managedObjectContext; 110 | } 111 | 112 | /** 113 | Returns the NSUndoManager for the application. In this case, the manager 114 | returned is that of the managed object context for the application. 115 | */ 116 | 117 | - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window { 118 | return [[self managedObjectContext] undoManager]; 119 | } 120 | 121 | /** 122 | Performs the save action for the application, which is to send the save: 123 | message to the application's managed object context. Any encountered errors 124 | are presented to the user. 125 | */ 126 | 127 | - (IBAction) saveAction:(id)sender { 128 | 129 | NSError *error = nil; 130 | 131 | if (![[self managedObjectContext] commitEditing]) { 132 | NSLog(@"%@:%@ unable to commit editing before saving", [self class], NSStringFromSelector(_cmd)); 133 | } 134 | 135 | if (![[self managedObjectContext] save:&error]) { 136 | [[NSApplication sharedApplication] presentError:error]; 137 | } 138 | } 139 | 140 | /** 141 | Implementation of the applicationShouldTerminate: method, used here to 142 | handle the saving of changes in the application managed object context 143 | before the application terminates. 144 | */ 145 | 146 | - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { 147 | 148 | if (!managedObjectContext) return NSTerminateNow; 149 | 150 | if (![managedObjectContext commitEditing]) { 151 | NSLog(@"%@:%@ unable to commit editing to terminate", [self class], NSStringFromSelector(_cmd)); 152 | return NSTerminateCancel; 153 | } 154 | 155 | if (![managedObjectContext hasChanges]) return NSTerminateNow; 156 | 157 | NSError *error = nil; 158 | if (![managedObjectContext save:&error]) { 159 | 160 | // This error handling simply presents error information in a panel with an 161 | // "Ok" button, which does not include any attempt at error recovery (meaning, 162 | // attempting to fix the error.) As a result, this implementation will 163 | // present the information to the user and then follow up with a panel asking 164 | // if the user wishes to "Quit Anyway", without saving the changes. 165 | 166 | // Typically, this process should be altered to include application-specific 167 | // recovery steps. 168 | 169 | BOOL result = [sender presentError:error]; 170 | if (result) return NSTerminateCancel; 171 | 172 | NSString *question = NSLocalizedString(@"Could not save changes while quitting. Quit anyway?", @"Quit without saves error question message"); 173 | NSString *info = NSLocalizedString(@"Quitting now will lose any changes you have made since the last successful save", @"Quit without saves error question info"); 174 | NSString *quitButton = NSLocalizedString(@"Quit anyway", @"Quit anyway button title"); 175 | NSString *cancelButton = NSLocalizedString(@"Cancel", @"Cancel button title"); 176 | NSAlert *alert = [[NSAlert alloc] init]; 177 | [alert setMessageText:question]; 178 | [alert setInformativeText:info]; 179 | [alert addButtonWithTitle:quitButton]; 180 | [alert addButtonWithTitle:cancelButton]; 181 | 182 | NSInteger answer = [alert runModal]; 183 | alert = nil; 184 | 185 | if (answer == NSAlertAlternateReturn) return NSTerminateCancel; 186 | } 187 | 188 | return NSTerminateNow; 189 | } 190 | 191 | - (BOOL)windowShouldClose:(id)sender { 192 | [[NSApplication sharedApplication] terminate:self]; 193 | return YES; 194 | } 195 | 196 | 197 | @end 198 | -------------------------------------------------------------------------------- /THPreferencesWC.m: -------------------------------------------------------------------------------- 1 | // 2 | // THPreferencesWC.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 10/28/12. 6 | // Copyright (c) 2012 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import "THPreferencesWC.h" 10 | #import "STTwitter.h" 11 | 12 | static NSString *kTHOSXTwitterIntegrationName = @"OSX Twitter Integration"; 13 | 14 | @interface THPreferencesWC () 15 | 16 | @end 17 | 18 | static THPreferencesWC *sharedPreferencesWC = nil; 19 | 20 | @implementation THPreferencesWC 21 | 22 | @synthesize twitterClients = _twitterClients; 23 | @synthesize twitterClientsController = _twitterClientsController; 24 | @synthesize twitter = _twitter; 25 | @synthesize username = _username; 26 | @synthesize password = _password; 27 | @synthesize preferencesDelegate = _preferencesDelegate; 28 | @synthesize usernameAndPasswordPanel = _usernameAndPasswordPanel; 29 | @synthesize connectionStatus = _connectionStatus; 30 | 31 | + (THPreferencesWC *)sharedPreferencesWC { 32 | if (!sharedPreferencesWC) { 33 | sharedPreferencesWC = [[THPreferencesWC alloc] initWithWindowNibName:@"THPreferencesWC"]; 34 | } 35 | return sharedPreferencesWC; 36 | } 37 | 38 | - (id)initWithWindow:(NSWindow *)window { 39 | self = [super initWithWindow:window]; 40 | if (self) { 41 | // Initialization code here. 42 | } 43 | 44 | return self; 45 | } 46 | 47 | - (void)windowDidLoad { 48 | [super windowDidLoad]; 49 | 50 | NSString *path = [[NSBundle mainBundle] pathForResource:@"TwitterXAuthTokens" ofType:@"plist"]; 51 | NSArray *xAuthClients = [NSArray arrayWithContentsOfFile:path]; 52 | 53 | NSDictionary *defaultClient = @{@"name" : kTHOSXTwitterIntegrationName}; 54 | 55 | self.twitterClients = [@[defaultClient] arrayByAddingObjectsFromArray:xAuthClients]; 56 | 57 | /**/ 58 | 59 | NSString *clientName = [[NSUserDefaults standardUserDefaults] valueForKey:@"clientName"]; 60 | 61 | __block NSUInteger selectionIndex = 0; 62 | 63 | [_twitterClients enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 64 | NSDictionary *d = (NSDictionary *)obj; 65 | 66 | if([[d valueForKey:@"name"] isEqualToString:clientName]) { 67 | *stop = YES; 68 | selectionIndex = idx; 69 | 70 | // NSLog(@"---------- %d %@", selectionIndex, d); 71 | } 72 | }]; 73 | 74 | [_twitterClientsController setSelectionIndex:selectionIndex]; 75 | 76 | // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file. 77 | } 78 | 79 | - (void)askForUsernameAndPasswordWithCompletionBlock:(UsernamePasswordBlock_t)completionBlock { 80 | self.usernamePasswordBlock = completionBlock; 81 | 82 | [NSApp beginSheet:_usernameAndPasswordPanel 83 | modalForWindow:self.window 84 | modalDelegate:self 85 | didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) 86 | contextInfo:nil]; 87 | } 88 | 89 | - (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(NSDictionary *)contextInfo { 90 | 91 | if(returnCode != 1) { 92 | self.username = nil; 93 | self.password = nil; 94 | } 95 | 96 | _usernamePasswordBlock(_username, _password); 97 | 98 | self.username = nil; 99 | self.password = nil; 100 | } 101 | 102 | - (IBAction)usernamePasswordCancel:(id)sender { 103 | [NSApp endSheet:_usernameAndPasswordPanel returnCode:0]; 104 | [_usernameAndPasswordPanel orderOut:self]; 105 | } 106 | 107 | - (IBAction)usernamePasswordOK:(id)sender { 108 | [NSApp endSheet:_usernameAndPasswordPanel returnCode:1]; 109 | [_usernameAndPasswordPanel orderOut:self]; 110 | } 111 | 112 | - (NSUInteger)indexOfUsedKnownClientIdentity { 113 | 114 | NSString *name = [[NSUserDefaults standardUserDefaults] valueForKey:@"clientName"]; 115 | 116 | if(name == nil) return NSNotFound; 117 | 118 | __block NSUInteger index = NSNotFound; 119 | 120 | NSArray *twitterClients = [_twitterClientsController arrangedObjects]; 121 | 122 | [twitterClients enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 123 | NSDictionary *d = (NSDictionary *)obj; 124 | 125 | if([d[@"name"] isEqualToString:name]) { 126 | *stop = YES; 127 | index = idx; 128 | } 129 | }]; 130 | 131 | return index; 132 | } 133 | 134 | - (NSDictionary *)preferedClientIdentityDictionary { 135 | 136 | NSString *ak = [[NSUserDefaults standardUserDefaults] valueForKey:@"tokensAK"]; 137 | NSString *as = [[NSUserDefaults standardUserDefaults] valueForKey:@"tokensAS"]; 138 | NSString *name = [[NSUserDefaults standardUserDefaults] valueForKey:@"clientName"]; 139 | 140 | __block NSDictionary *d = nil; 141 | 142 | [_twitterClients enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 143 | d = (NSDictionary *)obj; 144 | 145 | if([d[@"name"] isEqualToString:name]) { 146 | *stop = YES; 147 | } 148 | }]; 149 | 150 | NSString *ck = d[@"ck"]; 151 | NSString *cs = d[@"cs"]; 152 | 153 | if(ck && cs && ak && as && name) { 154 | return @{@"ck":ck, @"cs":cs, @"ak":ak, @"as":as, @"name":name}; 155 | } 156 | 157 | return nil; 158 | } 159 | 160 | - (STTwitterAPI *)twitterWrapper { 161 | 162 | NSDictionary *d = [self preferedClientIdentityDictionary]; 163 | 164 | if (d == nil) { 165 | NSLog(@"-- USING OSX"); 166 | return [STTwitterAPI twitterAPIOSWithFirstAccount]; 167 | } 168 | 169 | NSLog(@"-- USING %@", d[@"name"]); 170 | return [STTwitterAPI twitterAPIWithOAuthConsumerName:d[@"name"] 171 | consumerKey:d[@"ck"] 172 | consumerSecret:d[@"cs"] 173 | oauthToken:d[@"ak"] 174 | oauthTokenSecret:d[@"as"]]; 175 | } 176 | 177 | - (IBAction)loginAction:(id)sender { 178 | 179 | self.connectionStatus = @"Trying to login..."; 180 | 181 | NSDictionary *selectedClient = [[_twitterClientsController selectedObjects] lastObject]; 182 | 183 | NSLog(@"-- %@", _twitterClientsController); 184 | NSLog(@"-- %@", [_twitterClientsController selectedObjects]); 185 | NSLog(@"-- %@", [[_twitterClientsController selectedObjects] lastObject]); 186 | 187 | if(selectedClient == nil || [[selectedClient valueForKey:@"name"] isEqualToString:kTHOSXTwitterIntegrationName]) { 188 | 189 | self.twitter = [STTwitterAPI twitterAPIOSWithFirstAccount]; 190 | 191 | [_twitter verifyCredentialsWithSuccessBlock:^(NSString *username) { 192 | 193 | self.connectionStatus = [NSString stringWithFormat:@"Access granted for @%@", username]; 194 | 195 | [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"tokensAK"]; 196 | [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"tokensAS"]; 197 | [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"clientName"]; 198 | 199 | [[NSUserDefaults standardUserDefaults] setValue:username forKey:@"userName"]; 200 | 201 | [_preferencesDelegate preferences:self didChooseTwitter:_twitter]; 202 | 203 | } errorBlock:^(NSError *error) { 204 | 205 | self.connectionStatus = [error localizedDescription]; 206 | }]; 207 | 208 | } else { 209 | 210 | NSString *consumerName = selectedClient[@"name"]; 211 | NSString *consumerKey = selectedClient[@"ck"]; 212 | NSString *consumerSecret = selectedClient[@"cs"]; 213 | 214 | NSLog(@"-- %@", consumerName); 215 | NSLog(@"-- %@", consumerKey); 216 | NSLog(@"-- %@", consumerSecret); 217 | 218 | if(consumerName == nil || consumerKey == nil || consumerSecret == nil) { 219 | self.connectionStatus = [NSString stringWithFormat:@"error: no name, consumer key or secret in %@", selectedClient]; 220 | return; 221 | } 222 | 223 | [self askForUsernameAndPasswordWithCompletionBlock:^(NSString *username, NSString *password) { 224 | 225 | if(username == nil || password == nil) { 226 | self.connectionStatus = @"no username or password"; 227 | return; 228 | }; 229 | 230 | self.twitter = [STTwitterAPI twitterAPIWithOAuthConsumerName:consumerName 231 | consumerKey:consumerKey 232 | consumerSecret:consumerSecret 233 | username:username 234 | password:password]; 235 | 236 | [_twitter verifyCredentialsWithSuccessBlock:^(NSString *username) { 237 | 238 | self.connectionStatus = [NSString stringWithFormat:@"Access granted for @%@ on %@", username, selectedClient[@"name"]]; 239 | 240 | [[NSUserDefaults standardUserDefaults] setValue:_twitter.oauthAccessToken forKey:@"tokensAK"]; 241 | [[NSUserDefaults standardUserDefaults] setValue:_twitter.oauthAccessTokenSecret forKey:@"tokensAS"]; 242 | 243 | [[NSUserDefaults standardUserDefaults] setValue:username forKey:@"userName"]; 244 | [[NSUserDefaults standardUserDefaults] setValue:selectedClient[@"name"] forKey:@"clientName"]; 245 | 246 | [_preferencesDelegate preferences:self didChooseTwitter:_twitter]; 247 | 248 | } errorBlock:^(NSError *error) { 249 | 250 | self.connectionStatus = [error localizedDescription]; 251 | }]; 252 | 253 | }]; 254 | } 255 | 256 | } 257 | 258 | 259 | @end 260 | -------------------------------------------------------------------------------- /Vendor/STTwitter/STTwitterOSRequest.m: -------------------------------------------------------------------------------- 1 | // 2 | // STTwitterOSRequest.m 3 | // STTwitterDemoOSX 4 | // 5 | // Created by Nicolas Seriot on 20/02/14. 6 | // Copyright (c) 2014 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import "STTwitterOSRequest.h" 10 | #import 11 | #import 12 | #if TARGET_OS_IPHONE 13 | #import // iOS 5 14 | #endif 15 | #import "NSString+STTwitter.h" 16 | #import "NSError+STTwitter.h" 17 | 18 | typedef void (^completion_block_t)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, id response); 19 | typedef void (^error_block_t)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error); 20 | typedef void (^upload_progress_block_t)(NSInteger bytesWritten, NSInteger totalBytesWritten, NSInteger totalBytesExpectedToWrite); 21 | 22 | @interface STTwitterOSRequest () 23 | @property (nonatomic, copy) completion_block_t completionBlock; 24 | @property (nonatomic, copy) error_block_t errorBlock; 25 | @property (nonatomic, copy) upload_progress_block_t uploadProgressBlock; 26 | @property (nonatomic, retain) NSHTTPURLResponse *httpURLResponse; // only used with streaming API 27 | @property (nonatomic, retain) NSMutableData *data; // only used with non-streaming API 28 | @property (nonatomic, retain) ACAccount *account; 29 | @property (nonatomic) NSInteger httpMethod; 30 | @property (nonatomic, retain) NSDictionary *params; 31 | @property (nonatomic, retain) NSString *baseURLString; 32 | @property (nonatomic, retain) NSString *resource; 33 | @end 34 | 35 | 36 | @implementation STTwitterOSRequest 37 | 38 | - (id)initWithAPIResource:(NSString *)resource 39 | baseURLString:(NSString *)baseURLString 40 | httpMethod:(NSInteger)httpMethod 41 | parameters:(NSDictionary *)params 42 | account:(ACAccount *)account 43 | uploadProgressBlock:(void(^)(NSInteger bytesWritten, NSInteger totalBytesWritten, NSInteger totalBytesExpectedToWrite))uploadProgressBlock 44 | completionBlock:(void(^)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, id response))completionBlock 45 | errorBlock:(void(^)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error))errorBlock { 46 | 47 | NSAssert(completionBlock, @"completionBlock is missing"); 48 | NSAssert(errorBlock, @"errorBlock is missing"); 49 | 50 | self = [super init]; 51 | 52 | self.resource = resource; 53 | self.baseURLString = baseURLString; 54 | self.httpMethod = httpMethod; 55 | self.params = params; 56 | self.account = account; 57 | self.completionBlock = completionBlock; 58 | self.errorBlock = errorBlock; 59 | self.uploadProgressBlock = uploadProgressBlock; 60 | 61 | return self; 62 | } 63 | 64 | - (NSURLConnection *)startRequest { 65 | 66 | NSData *mediaData = [_params valueForKey:@"media[]"]; 67 | 68 | NSMutableDictionary *paramsWithoutMedia = [_params mutableCopy]; 69 | [paramsWithoutMedia removeObjectForKey:@"media[]"]; 70 | 71 | NSString *urlString = [_baseURLString stringByAppendingString:_resource]; 72 | NSURL *url = [NSURL URLWithString:urlString]; 73 | 74 | id request = nil; 75 | 76 | #if TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_6_0) 77 | 78 | if (floor(NSFoundationVersionNumber) < NSFoundationVersionNumber_iOS_6_0) { 79 | TWRequestMethod method = (_httpMethod == 0) ? TWRequestMethodGET : TWRequestMethodPOST; 80 | request = [[TWRequest alloc] initWithURL:url parameters:paramsWithoutMedia requestMethod:method]; 81 | } else { 82 | request = [SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:_httpMethod URL:url parameters:paramsWithoutMedia]; 83 | } 84 | 85 | #else 86 | request = [SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:_httpMethod URL:url parameters:paramsWithoutMedia]; 87 | #endif 88 | 89 | [request setAccount:_account]; 90 | 91 | if(mediaData) { 92 | [request addMultipartData:mediaData withName:@"media[]" type:@"application/octet-stream" filename:@"media.jpg"]; 93 | } 94 | 95 | // we use NSURLConnection because SLRequest doesn't play well with the streaming API 96 | 97 | NSURLRequest *preparedURLRequest = nil; 98 | #if TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_6_0) 99 | if (floor(NSFoundationVersionNumber) < NSFoundationVersionNumber_iOS_6_0) { 100 | preparedURLRequest = [request signedURLRequest]; 101 | } else { 102 | preparedURLRequest = [request preparedURLRequest]; 103 | } 104 | #else 105 | preparedURLRequest = [request preparedURLRequest]; 106 | #endif 107 | 108 | NSURLConnection *connection = [NSURLConnection connectionWithRequest:preparedURLRequest delegate:self]; 109 | [connection start]; 110 | return connection; 111 | } 112 | 113 | - (NSDictionary *)requestHeadersForRequest:(id)request { 114 | 115 | if([request isKindOfClass:[NSURLRequest class]]) { 116 | return [request allHTTPHeaderFields]; 117 | } 118 | 119 | #if TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_6_0) 120 | if (floor(NSFoundationVersionNumber) < NSFoundationVersionNumber_iOS_6_0) { 121 | return [[request signedURLRequest] allHTTPHeaderFields]; 122 | } else { 123 | return [[request preparedURLRequest] allHTTPHeaderFields]; 124 | } 125 | #else 126 | return [[request preparedURLRequest] allHTTPHeaderFields]; 127 | #endif 128 | } 129 | 130 | - (void)handleStreamingResponse:(NSHTTPURLResponse *)urlResponse request:(id)request data:(NSData *)responseData { 131 | 132 | if(responseData == nil) { 133 | self.errorBlock(request, [self requestHeadersForRequest:request], [urlResponse allHeaderFields], nil); 134 | return; 135 | } 136 | 137 | NSError *jsonError = nil; 138 | NSJSONSerialization *json = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:&jsonError]; 139 | 140 | if([json valueForKey:@"error"]) { 141 | 142 | NSString *message = [json valueForKey:@"error"]; 143 | NSDictionary *userInfo = [NSDictionary dictionaryWithObject:message forKey:NSLocalizedDescriptionKey]; 144 | NSError *jsonErrorFromResponse = [NSError errorWithDomain:NSStringFromClass([self class]) code:0 userInfo:userInfo]; 145 | 146 | self.errorBlock(request, [self requestHeadersForRequest:request], [urlResponse allHeaderFields], jsonErrorFromResponse); 147 | 148 | return; 149 | } 150 | 151 | // we can receive several dictionaries in the same data chunk 152 | // such as '{..}\r\n{..}\r\n{..}' which is not valid JSON 153 | // so we split them up into a 'jsonChunks' array such as [{..},{..},{..}] 154 | 155 | NSString *jsonString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; 156 | 157 | NSArray *jsonChunks = [jsonString componentsSeparatedByString:@"\r\n"]; 158 | 159 | for(NSString *jsonChunk in jsonChunks) { 160 | if([jsonChunk length] == 0) continue; 161 | NSData *data = [jsonChunk dataUsingEncoding:NSUTF8StringEncoding]; 162 | NSError *jsonError = nil; 163 | id json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError]; 164 | if(json) { 165 | self.completionBlock(request, [self requestHeadersForRequest:request], [urlResponse allHeaderFields], json); 166 | } 167 | } 168 | 169 | } 170 | 171 | #pragma mark NSURLConnectionDataDelegate 172 | 173 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { 174 | 175 | if([response isKindOfClass:[NSHTTPURLResponse class]] == NO) return; 176 | 177 | self.httpURLResponse = (NSHTTPURLResponse *)response; 178 | 179 | self.data = [NSMutableData data]; 180 | } 181 | 182 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { 183 | 184 | BOOL isStreaming = [[[[connection originalRequest] URL] host] rangeOfString:@"stream"].location != NSNotFound; 185 | 186 | if(isStreaming) { 187 | [self handleStreamingResponse:_httpURLResponse request:[connection currentRequest] data:data]; 188 | } else { 189 | [self.data appendData:data]; 190 | } 191 | } 192 | 193 | - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { 194 | 195 | NSURLRequest *request = [connection currentRequest]; 196 | NSDictionary *requestHeaders = [request allHTTPHeaderFields]; 197 | NSDictionary *responseHeaders = [_httpURLResponse allHeaderFields]; 198 | 199 | self.errorBlock(request, requestHeaders, responseHeaders, error); 200 | } 201 | 202 | - (void)connectionDidFinishLoading:(NSURLConnection *)connection { 203 | 204 | NSURLRequest *request = [connection currentRequest]; 205 | 206 | if(_data == nil) { 207 | self.errorBlock(request, [self requestHeadersForRequest:request], [_httpURLResponse allHeaderFields], nil); 208 | return; 209 | } 210 | 211 | NSError *error = [NSError st_twitterErrorFromResponseData:_data responseHeaders:[_httpURLResponse allHeaderFields] underlyingError:nil]; 212 | 213 | if(error) { 214 | self.errorBlock(request, [self requestHeadersForRequest:request], [_httpURLResponse allHeaderFields], error); 215 | return; 216 | } 217 | 218 | NSError *jsonError = nil; 219 | id response = [NSJSONSerialization JSONObjectWithData:_data options:NSJSONReadingAllowFragments error:&jsonError]; 220 | 221 | if(response == nil) { 222 | // eg. reverse auth response 223 | // oauth_token=xxx&oauth_token_secret=xxx&user_id=xxx&screen_name=xxx 224 | response = [[NSString alloc] initWithData:_data encoding:NSUTF8StringEncoding]; 225 | } 226 | 227 | if(response) { 228 | self.completionBlock(request, [self requestHeadersForRequest:request], [_httpURLResponse allHeaderFields], response); 229 | } else { 230 | self.errorBlock(request, [self requestHeadersForRequest:request], [_httpURLResponse allHeaderFields], jsonError); 231 | } 232 | 233 | } 234 | 235 | - (void)connection:(NSURLConnection *)connection 236 | didSendBodyData:(NSInteger)bytesWritten 237 | totalBytesWritten:(NSInteger)totalBytesWritten 238 | totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite { 239 | if(self.uploadProgressBlock == nil) return; 240 | self.uploadProgressBlock(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite); 241 | } 242 | 243 | @end 244 | -------------------------------------------------------------------------------- /Vendor/STTwitter/STTwitterOS.m: -------------------------------------------------------------------------------- 1 | // 2 | // STTwitterOS.m 3 | // STTwitter 4 | // 5 | // Created by Nicolas Seriot on 5/1/10. 6 | // Copyright 2010 seriot.ch. All rights reserved. 7 | // 8 | 9 | #import "STTwitterOS.h" 10 | #import "NSString+STTwitter.h" 11 | #import "STTwitterOSRequest.h" 12 | #import 13 | #import 14 | #if TARGET_OS_IPHONE 15 | #import // iOS 5 16 | #endif 17 | 18 | @interface STTwitterOS () 19 | @property (nonatomic, retain) ACAccountStore *accountStore; // the ACAccountStore must be kept alive for as long as we need an ACAccount instance, see WWDC 2011 Session 124 for more info 20 | @property (nonatomic, retain) ACAccount *account; // if nil, will be set to first account available 21 | @end 22 | 23 | @implementation STTwitterOS 24 | 25 | - (id)init { 26 | self = [super init]; 27 | 28 | self.accountStore = [[ACAccountStore alloc] init]; 29 | 30 | return self; 31 | } 32 | 33 | - (instancetype)initWithAccount:(ACAccount *) account { 34 | self = [super init]; 35 | self.accountStore = [[ACAccountStore alloc] init]; 36 | self.account = account; 37 | return self; 38 | } 39 | 40 | + (instancetype)twitterAPIOSWithAccount:(ACAccount *)account { 41 | return [[self alloc] initWithAccount:account]; 42 | } 43 | 44 | + (instancetype)twitterAPIOSWithFirstAccount { 45 | return [self twitterAPIOSWithAccount:nil]; 46 | } 47 | 48 | - (NSString *)username { 49 | return self.account.username; 50 | } 51 | 52 | - (NSString *)consumerName { 53 | #if TARGET_OS_IPHONE 54 | return @"iOS"; 55 | #else 56 | return @"OS X"; 57 | #endif 58 | } 59 | 60 | - (NSString *)loginTypeDescription { 61 | return @"System"; 62 | } 63 | 64 | - (BOOL)canVerifyCredentials { 65 | return YES; 66 | } 67 | 68 | - (BOOL)hasAccessToTwitter { 69 | 70 | #if !TARGET_OS_IPHONE 71 | return YES; 72 | #else 73 | 74 | #if (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_6_0) 75 | if (floor(NSFoundationVersionNumber) < NSFoundationVersionNumber_iOS_6_0) { 76 | return [TWTweetComposeViewController canSendTweet]; // iOS 5 77 | } else { 78 | return [SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter]; 79 | } 80 | #else 81 | return [SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter]; 82 | #endif 83 | 84 | #endif 85 | } 86 | 87 | - (void)verifyCredentialsWithSuccessBlock:(void(^)(NSString *username))successBlock errorBlock:(void(^)(NSError *error))errorBlock { 88 | if([self hasAccessToTwitter] == NO) { 89 | NSString *message = @"This system cannot access Twitter."; 90 | NSError *error = [NSError errorWithDomain:NSStringFromClass([self class]) code:STTwitterOSSystemCannotAccessTwitter userInfo:@{NSLocalizedDescriptionKey : message}]; 91 | errorBlock(error); 92 | return; 93 | } 94 | 95 | ACAccountType *accountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter]; 96 | 97 | if(accountType == nil) { 98 | NSString *message = @"Cannot find Twitter account."; 99 | NSError *error = [NSError errorWithDomain:NSStringFromClass([self class]) code:STTwitterOSCannotFindTwitterAccount userInfo:@{NSLocalizedDescriptionKey : message}]; 100 | errorBlock(error); 101 | return; 102 | } 103 | 104 | ACAccountStoreRequestAccessCompletionHandler accountStoreRequestCompletionHandler = ^(BOOL granted, NSError *error) { 105 | [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 106 | 107 | if(granted == NO) { 108 | 109 | if(error) { 110 | errorBlock(error); 111 | return; 112 | } 113 | 114 | NSString *message = @"User denied access to their account(s)."; 115 | NSError *grantError = [NSError errorWithDomain:NSStringFromClass([self class]) code:STTwitterOSUserDeniedAccessToTheirAccounts userInfo:@{NSLocalizedDescriptionKey : message}]; 116 | errorBlock(grantError); 117 | return; 118 | } 119 | 120 | if(self.account == nil) { 121 | NSArray *accounts = [self.accountStore accountsWithAccountType:accountType]; 122 | 123 | if([accounts count] == 0) { 124 | NSString *message = @"No Twitter account available."; 125 | NSError *error = [NSError errorWithDomain:NSStringFromClass([self class]) code:STTwitterOSNoTwitterAccountIsAvailable userInfo:@{NSLocalizedDescriptionKey : message}]; 126 | errorBlock(error); 127 | return; 128 | } 129 | 130 | self.account = [accounts objectAtIndex:0]; 131 | } 132 | 133 | successBlock(self.account.username); 134 | }]; 135 | }; 136 | 137 | #if TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_6_0) 138 | if (floor(NSFoundationVersionNumber) < NSFoundationVersionNumber_iOS_6_0) { 139 | [self.accountStore requestAccessToAccountsWithType:accountType 140 | withCompletionHandler:accountStoreRequestCompletionHandler]; 141 | } else { 142 | [self.accountStore requestAccessToAccountsWithType:accountType 143 | options:NULL 144 | completion:accountStoreRequestCompletionHandler]; 145 | } 146 | #else 147 | [self.accountStore requestAccessToAccountsWithType:accountType 148 | options:NULL 149 | completion:accountStoreRequestCompletionHandler]; 150 | #endif 151 | } 152 | 153 | - (id)fetchAPIResource:(NSString *)resource 154 | baseURLString:(NSString *)baseURLString 155 | httpMethod:(NSInteger)httpMethod 156 | parameters:(NSDictionary *)params 157 | uploadProgressBlock:(void(^)(NSInteger bytesWritten, NSInteger totalBytesWritten, NSInteger totalBytesExpectedToWrite))uploadProgressBlock 158 | completionBlock:(void (^)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, id response))completionBlock 159 | errorBlock:(void (^)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error))errorBlock { 160 | 161 | STTwitterOSRequest *r = [[STTwitterOSRequest alloc] initWithAPIResource:resource 162 | baseURLString:baseURLString 163 | httpMethod:httpMethod 164 | parameters:params 165 | account:self.account 166 | uploadProgressBlock:uploadProgressBlock 167 | completionBlock:completionBlock 168 | errorBlock:errorBlock]; 169 | 170 | return [r startRequest]; // NSURLConnection 171 | } 172 | 173 | - (id)fetchResource:(NSString *)resource 174 | HTTPMethod:(NSString *)HTTPMethod 175 | baseURLString:(NSString *)baseURLString 176 | parameters:(NSDictionary *)params 177 | uploadProgressBlock:(void(^)(NSInteger bytesWritten, NSInteger totalBytesWritten, NSInteger totalBytesExpectedToWrite))uploadProgressBlock 178 | downloadProgressBlock:(void (^)(id request, id response))progressBlock // FIXME: how to handle progressBlock? 179 | successBlock:(void (^)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, id response))successBlock 180 | errorBlock:(void (^)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error))errorBlock { 181 | 182 | NSAssert(([ @[@"GET", @"POST"] containsObject:HTTPMethod]), @"unsupported HTTP method"); 183 | 184 | NSInteger slRequestMethod = SLRequestMethodGET; 185 | 186 | NSDictionary *d = params; 187 | 188 | if([HTTPMethod isEqualToString:@"POST"]) { 189 | if (d == nil) d = @{}; 190 | slRequestMethod = SLRequestMethodPOST; 191 | } 192 | 193 | NSString *baseURLStringWithTrailingSlash = baseURLString; 194 | if([baseURLString hasSuffix:@"/"] == NO) { 195 | baseURLStringWithTrailingSlash = [baseURLString stringByAppendingString:@"/"]; 196 | } 197 | 198 | return [self fetchAPIResource:resource 199 | baseURLString:baseURLStringWithTrailingSlash 200 | httpMethod:slRequestMethod 201 | parameters:d 202 | uploadProgressBlock:uploadProgressBlock 203 | completionBlock:successBlock 204 | errorBlock:errorBlock]; 205 | } 206 | 207 | + (NSDictionary *)parametersDictionaryFromCommaSeparatedParametersString:(NSString *)s { 208 | 209 | NSArray *parameters = [s componentsSeparatedByString:@", "]; 210 | 211 | NSMutableDictionary *md = [NSMutableDictionary dictionary]; 212 | 213 | for(NSString *parameter in parameters) { 214 | // transform k="v" into {'k':'v'} 215 | 216 | NSArray *keyValue = [parameter componentsSeparatedByString:@"="]; 217 | if([keyValue count] != 2) { 218 | continue; 219 | } 220 | 221 | NSString *value = [keyValue[1] stringByReplacingOccurrencesOfString:@"\"" withString:@""]; 222 | 223 | [md setObject:value forKey:keyValue[0]]; 224 | } 225 | 226 | return md; 227 | } 228 | 229 | // TODO: this code is duplicated from STTwitterOAuth 230 | + (NSDictionary *)parametersDictionaryFromAmpersandSeparatedParameterString:(NSString *)s { 231 | 232 | NSArray *parameters = [s componentsSeparatedByString:@"&"]; 233 | 234 | NSMutableDictionary *md = [NSMutableDictionary dictionary]; 235 | 236 | for(NSString *parameter in parameters) { 237 | NSArray *keyValue = [parameter componentsSeparatedByString:@"="]; 238 | if([keyValue count] != 2) { 239 | continue; 240 | } 241 | 242 | [md setObject:keyValue[1] forKey:keyValue[0]]; 243 | } 244 | 245 | return md; 246 | } 247 | 248 | // reverse auth phase 2 249 | - (void)postReverseAuthAccessTokenWithAuthenticationHeader:(NSString *)authenticationHeader 250 | successBlock:(void(^)(NSString *oAuthToken, NSString *oAuthTokenSecret, NSString *userID, NSString *screenName))successBlock 251 | errorBlock:(void(^)(NSError *error))errorBlock { 252 | 253 | NSAssert(self.account, @"no account is set, try to call -verifyCredentialsWithSuccessBlock:errorBlock: first"); 254 | 255 | NSParameterAssert(authenticationHeader); 256 | 257 | NSString *shortHeader = [authenticationHeader stringByReplacingOccurrencesOfString:@"OAuth " withString:@""]; 258 | 259 | NSDictionary *authenticationHeaderDictionary = [[self class] parametersDictionaryFromCommaSeparatedParametersString:shortHeader]; 260 | 261 | NSString *consumerKey = [authenticationHeaderDictionary valueForKey:@"oauth_consumer_key"]; 262 | 263 | NSAssert((consumerKey != nil), @"cannot find out consumerKey"); 264 | 265 | NSDictionary *d = @{@"x_reverse_auth_target" : consumerKey, 266 | @"x_reverse_auth_parameters" : authenticationHeader}; 267 | 268 | [self fetchResource:@"oauth/access_token" 269 | HTTPMethod:@"POST" 270 | baseURLString:@"https://api.twitter.com" 271 | parameters:d 272 | uploadProgressBlock:nil 273 | downloadProgressBlock:nil 274 | successBlock:^(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, id response) { 275 | 276 | NSDictionary *d = [[self class] parametersDictionaryFromAmpersandSeparatedParameterString:response]; 277 | 278 | NSString *oAuthToken = [d valueForKey:@"oauth_token"]; 279 | NSString *oAuthTokenSecret = [d valueForKey:@"oauth_token_secret"]; 280 | NSString *userID = [d valueForKey:@"user_id"]; 281 | NSString *screenName = [d valueForKey:@"screen_name"]; 282 | 283 | successBlock(oAuthToken, oAuthTokenSecret, userID, screenName); 284 | } errorBlock:^(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error) { 285 | errorBlock(error); 286 | }]; 287 | } 288 | 289 | @end -------------------------------------------------------------------------------- /Vendor/STTwitter/STTwitterAppOnly.m: -------------------------------------------------------------------------------- 1 | // 2 | // STTwitterAppOnly.m 3 | // STTwitter 4 | // 5 | // Created by Nicolas Seriot on 3/13/13. 6 | // Copyright (c) 2013 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | #import "STTwitterAppOnly.h" 10 | #import "STHTTPRequest.h" 11 | #import "NSString+STTwitter.h" 12 | #import "STHTTPRequest+STTwitter.h" 13 | 14 | @interface NSData (Base64) 15 | - (NSString *)base64Encoding; // private API 16 | @end 17 | 18 | @implementation STTwitterAppOnly 19 | 20 | - (id)init { 21 | self = [super init]; 22 | 23 | // TODO: remove cookies from Twitter if needed 24 | 25 | return self; 26 | } 27 | 28 | + (instancetype)twitterAppOnlyWithConsumerName:(NSString *)consumerName consumerKey:(NSString *)consumerKey consumerSecret:(NSString *)consumerSecret { 29 | STTwitterAppOnly *twitterAppOnly = [[[self class] alloc] init]; 30 | twitterAppOnly.consumerName = consumerName; 31 | twitterAppOnly.consumerKey = consumerKey; 32 | twitterAppOnly.consumerSecret = consumerSecret; 33 | return twitterAppOnly; 34 | } 35 | 36 | #pragma mark STTwitterOAuthProtocol 37 | 38 | - (BOOL)canVerifyCredentials { 39 | return YES; 40 | } 41 | 42 | - (NSString *)oauthAccessToken { 43 | return nil; 44 | } 45 | 46 | - (NSString *)oauthAccessTokenSecret { 47 | return nil; 48 | } 49 | 50 | - (void)invalidateBearerTokenWithSuccessBlock:(void(^)())successBlock 51 | errorBlock:(void(^)(NSError *error))errorBlock { 52 | 53 | if(_bearerToken == nil) { 54 | NSError *error = [NSError errorWithDomain:NSStringFromClass([self class]) code:STTwitterAppOnlyCannotFindBearerTokenToBeInvalidated userInfo:@{NSLocalizedDescriptionKey : @"Cannot invalidate missing bearer token"}]; 55 | errorBlock(error); 56 | return; 57 | } 58 | 59 | [self postResource:@"oauth2/invalidate_token" 60 | baseURLString:@"https://api.twitter.com" 61 | parameters:@{ @"access_token" : _bearerToken } 62 | useBasicAuth:YES 63 | uploadProgressBlock:nil 64 | downloadProgressBlock:nil 65 | successBlock:^(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, id json) { 66 | 67 | if([json isKindOfClass:[NSDictionary class]] == NO) { 68 | successBlock(json); 69 | return; 70 | } 71 | 72 | self.bearerToken = [json valueForKey:@"access_token"]; 73 | 74 | NSString *oldToken = self.bearerToken; 75 | 76 | self.bearerToken = nil; 77 | 78 | successBlock(oldToken); 79 | 80 | } errorBlock:^(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error) { 81 | errorBlock(error); 82 | }]; 83 | 84 | // POST /oauth2/invalidate_token HTTP/1.1 85 | // Authorization: Basic eHZ6MWV2RlM0d0VFUFRHRUZQSEJvZzpMOHFxOVBaeVJn 86 | // NmllS0dFS2hab2xHQzB2SldMdzhpRUo4OERSZHlPZw== 87 | // User-Agent: My Twitter App v1.0.23 88 | // Host: api.twitter.com 89 | // Accept: */* 90 | // 91 | // Content-Length: 119 92 | // Content-Type: application/x-www-form-urlencoded 93 | // 94 | // access_token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2FAAAAAAAAAAAAAAAAAAAA%3DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 95 | 96 | // HTTP/1.1 200 OK 97 | // Content-Type: application/json; charset=utf-8 98 | // Content-Length: 127 99 | // ... 100 | // 101 | // {"access_token":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2FAAAAAAAAAAAAAAAAAAAA%3DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"} 102 | } 103 | 104 | + (NSString *)base64EncodedBearerTokenCredentialsWithConsumerKey:(NSString *)consumerKey consumerSecret:(NSString *)consumerSecret { 105 | NSString *encodedConsumerToken = [consumerKey st_stringByAddingRFC3986PercentEscapesUsingEncoding:NSUTF8StringEncoding]; 106 | NSString *encodedConsumerSecret = [consumerSecret st_stringByAddingRFC3986PercentEscapesUsingEncoding:NSUTF8StringEncoding]; 107 | NSString *bearerTokenCredentials = [NSString stringWithFormat:@"%@:%@", encodedConsumerToken, encodedConsumerSecret]; 108 | NSData *data = [bearerTokenCredentials dataUsingEncoding:NSUTF8StringEncoding]; 109 | return [data base64Encoding]; 110 | } 111 | 112 | - (void)verifyCredentialsWithSuccessBlock:(void(^)(NSString *username))successBlock 113 | errorBlock:(void(^)(NSError *error))errorBlock { 114 | 115 | [self postResource:@"oauth2/token" 116 | baseURLString:@"https://api.twitter.com" 117 | parameters:@{ @"grant_type" : @"client_credentials" } 118 | useBasicAuth:YES 119 | uploadProgressBlock:nil 120 | downloadProgressBlock:nil 121 | successBlock:^(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, id json) { 122 | 123 | if([json isKindOfClass:[NSDictionary class]] == NO) { 124 | NSError *error = [NSError errorWithDomain:NSStringFromClass([self class]) code:STTwitterAppOnlyCannotFindJSONInResponse userInfo:@{NSLocalizedDescriptionKey : @"Cannot find JSON dictionary in response"}]; 125 | errorBlock(error); 126 | return; 127 | } 128 | 129 | NSString *tokenType = [json valueForKey:@"token_type"]; 130 | if([tokenType isEqualToString:@"bearer"] == NO) { 131 | NSError *error = [NSError errorWithDomain:NSStringFromClass([self class]) code:STTwitterAppOnlyCannotFindBearerTokenInResponse userInfo:@{NSLocalizedDescriptionKey : @"Cannot find bearer token in server response"}]; 132 | errorBlock(error); 133 | return; 134 | } 135 | 136 | self.bearerToken = [json valueForKey:@"access_token"]; 137 | 138 | successBlock(_bearerToken); 139 | 140 | } errorBlock:^(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error) { 141 | errorBlock(error); 142 | }]; 143 | } 144 | 145 | - (STHTTPRequest *)getResource:(NSString *)resource 146 | baseURLString:(NSString *)baseURLString // no trailing slash 147 | parameters:(NSDictionary *)params 148 | progressBlock:(void(^)(STHTTPRequest *r, id json))progressBlock 149 | successBlock:(void (^)(STHTTPRequest *r, NSDictionary *requestHeaders, NSDictionary *responseHeaders, id json))successBlock 150 | errorBlock:(void (^)(STHTTPRequest *r, NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error))errorBlock { 151 | 152 | /* 153 | GET /1.1/statuses/user_timeline.json?count=100&screen_name=twitterapi HTTP/1.1 154 | Host: api.twitter.com 155 | User-Agent: My Twitter App v1.0.23 156 | Authorization: Bearer AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2FAAAAAAAAAAAA 157 | AAAAAAAA%3DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 158 | Accept-Encoding: gzip 159 | */ 160 | 161 | NSMutableString *urlString = [NSMutableString stringWithFormat:@"%@/%@", baseURLString, resource]; 162 | 163 | NSMutableArray *parameters = [NSMutableArray array]; 164 | 165 | [params enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { 166 | NSString *s = [NSString stringWithFormat:@"%@=%@", key, obj]; 167 | [parameters addObject:s]; 168 | }]; 169 | 170 | if([parameters count]) { 171 | NSString *parameterString = [parameters componentsJoinedByString:@"&"]; 172 | 173 | [urlString appendFormat:@"?%@", parameterString]; 174 | } 175 | 176 | // NSString *requestID = [[NSUUID UUID] UUIDString]; 177 | 178 | __block STHTTPRequest *r = [STHTTPRequest twitterRequestWithURLString:urlString 179 | stTwitterUploadProgressBlock:nil 180 | stTwitterDownloadProgressBlock:^(id json) { 181 | if(progressBlock) progressBlock(r, json); 182 | } stTwitterSuccessBlock:^(NSDictionary *requestHeaders, NSDictionary *responseHeaders, id json) { 183 | successBlock(r, requestHeaders, responseHeaders, json); 184 | } stTwitterErrorBlock:^(NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error) { 185 | errorBlock(r, requestHeaders, responseHeaders, error); 186 | }]; 187 | if(_bearerToken) { 188 | [r setHeaderWithName:@"Authorization" value:[NSString stringWithFormat:@"Bearer %@", _bearerToken]]; 189 | } 190 | 191 | [r startAsynchronous]; 192 | 193 | return r; 194 | } 195 | 196 | - (id)fetchResource:(NSString *)resource 197 | HTTPMethod:(NSString *)HTTPMethod 198 | baseURLString:(NSString *)baseURLString 199 | parameters:(NSDictionary *)params 200 | uploadProgressBlock:(void(^)(NSInteger bytesWritten, NSInteger totalBytesWritten, NSInteger totalBytesExpectedToWrite))uploadProgressBlock 201 | downloadProgressBlock:(void(^)(id r, id json))downloadProgressBlock 202 | successBlock:(void(^)(id r, NSDictionary *requestHeaders, NSDictionary *responseHeaders, id json))successBlock 203 | errorBlock:(void(^)(id r, NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error))errorBlock { 204 | 205 | if([baseURLString hasSuffix:@"/"]) { 206 | baseURLString = [baseURLString substringToIndex:[baseURLString length]-1]; 207 | } 208 | 209 | if([HTTPMethod isEqualToString:@"GET"]) { 210 | 211 | return [self getResource:resource 212 | baseURLString:baseURLString 213 | parameters:params 214 | progressBlock:downloadProgressBlock 215 | successBlock:successBlock 216 | errorBlock:errorBlock]; 217 | 218 | } else if ([HTTPMethod isEqualToString:@"POST"]) { 219 | 220 | return [self postResource:resource 221 | baseURLString:baseURLString 222 | parameters:params 223 | uploadProgressBlock:uploadProgressBlock 224 | downloadProgressBlock:downloadProgressBlock 225 | successBlock:successBlock 226 | errorBlock:errorBlock]; 227 | 228 | } else { 229 | NSAssert(NO, @"unsupported HTTP method"); 230 | return nil; 231 | } 232 | } 233 | 234 | - (id)postResource:(NSString *)resource 235 | baseURLString:(NSString *)baseURLString // no trailing slash 236 | parameters:(NSDictionary *)params 237 | useBasicAuth:(BOOL)useBasicAuth 238 | uploadProgressBlock:(void(^)(NSInteger bytesWritten, NSInteger totalBytesWritten, NSInteger totalBytesExpectedToWrite))uploadProgressBlock 239 | downloadProgressBlock:(void(^)(id request, id json))downloadProgressBlock 240 | successBlock:(void(^)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, id json))successBlock 241 | errorBlock:(void(^)(id request, NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error))errorBlock { 242 | 243 | NSString *urlString = [NSString stringWithFormat:@"%@/%@", baseURLString, resource]; 244 | 245 | __block STHTTPRequest *r = [STHTTPRequest twitterRequestWithURLString:urlString 246 | stTwitterUploadProgressBlock:uploadProgressBlock 247 | stTwitterDownloadProgressBlock:^(id json) { 248 | if(downloadProgressBlock) downloadProgressBlock(r, json); 249 | } stTwitterSuccessBlock:^(NSDictionary *requestHeaders, NSDictionary *responseHeaders, id json) { 250 | successBlock(r, requestHeaders, responseHeaders, json); 251 | } stTwitterErrorBlock:^(NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error) { 252 | errorBlock(r, requestHeaders, responseHeaders, error); 253 | }]; 254 | 255 | r.POSTDictionary = params; 256 | 257 | NSMutableDictionary *mutableParams = [params mutableCopy]; 258 | 259 | r.encodePOSTDictionary = NO; 260 | 261 | r.POSTDictionary = mutableParams ? mutableParams : @{}; 262 | 263 | if(useBasicAuth) { 264 | NSString *base64EncodedTokens = [[self class] base64EncodedBearerTokenCredentialsWithConsumerKey:_consumerKey consumerSecret:_consumerSecret]; 265 | 266 | [r setHeaderWithName:@"Authorization" value:[NSString stringWithFormat:@"Basic %@", base64EncodedTokens]]; 267 | } else if(_bearerToken) { 268 | [r setHeaderWithName:@"Authorization" value:[NSString stringWithFormat:@"Bearer %@", _bearerToken]]; 269 | r.encodePOSTDictionary = YES; 270 | } 271 | 272 | [r startAsynchronous]; 273 | 274 | return r; 275 | } 276 | 277 | - (STHTTPRequest *)postResource:(NSString *)resource 278 | baseURLString:(NSString *)baseURLString 279 | parameters:(NSDictionary *)params 280 | uploadProgressBlock:(void(^)(NSInteger bytesWritten, NSInteger totalBytesWritten, NSInteger totalBytesExpectedToWrite))uploadProgressBlock 281 | downloadProgressBlock:(void(^)(id r, id json))downloadProgressBlock 282 | successBlock:(void(^)(id r, NSDictionary *requestHeaders, NSDictionary *responseHeaders, id json))successBlock 283 | errorBlock:(void(^)(id r, NSDictionary *requestHeaders, NSDictionary *responseHeaders, NSError *error))errorBlock { 284 | 285 | return [self postResource:resource 286 | baseURLString:baseURLString 287 | parameters:params 288 | useBasicAuth:NO 289 | uploadProgressBlock:uploadProgressBlock 290 | downloadProgressBlock:downloadProgressBlock 291 | successBlock:successBlock 292 | errorBlock:errorBlock]; 293 | } 294 | 295 | - (NSString *)loginTypeDescription { 296 | return @"App Only"; 297 | } 298 | 299 | @end 300 | -------------------------------------------------------------------------------- /THTweet.m: -------------------------------------------------------------------------------- 1 | // 2 | // Tweet.m 3 | // TwitHunter 4 | // 5 | // Created by Nicolas Seriot on 19.04.09. 6 | // Copyright 2009 Sen:te. All rights reserved. 7 | // 8 | 9 | #import "NSManagedObject+ST.h" 10 | #import "NSString+TH.h" 11 | #import "THTweet.h" 12 | #import "THUser.h" 13 | 14 | static NSRegularExpression *linksRegex = nil; 15 | static NSRegularExpression *usernamesRegex = nil; 16 | static NSRegularExpression *hashtagsRegex = nil; 17 | 18 | @implementation THTweet 19 | 20 | @dynamic text; 21 | @dynamic uid; 22 | @dynamic score; 23 | @dynamic date; 24 | @dynamic user; 25 | @dynamic isRead; 26 | @dynamic isFavorite; 27 | //@dynamic containsURL; 28 | 29 | static NSDateFormatter *createdAtDateFormatter = nil; 30 | 31 | - (NSDateFormatter *)createdAtDateFormatter { 32 | 33 | if (createdAtDateFormatter == nil) { 34 | createdAtDateFormatter = [[NSDateFormatter alloc] init]; 35 | 36 | NSLocale *usLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; 37 | [createdAtDateFormatter setLocale:usLocale]; 38 | [createdAtDateFormatter setDateStyle:NSDateFormatterLongStyle]; 39 | [createdAtDateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; 40 | [createdAtDateFormatter setDateFormat: @"EEE MMM dd HH:mm:ss Z yyyy"]; 41 | } 42 | 43 | return createdAtDateFormatter; 44 | } 45 | 46 | //- (NSAttributedString *)attributedString { 47 | // 48 | // if(self.text == nil) return nil; 49 | // 50 | // NSAttributedString *as = [[NSAttributedString alloc] initWithString:self.text]; 51 | // 52 | // return [as autorelease]; 53 | //} 54 | 55 | - (NSNumber *)isFavoriteWrapper { 56 | return self.isFavorite; 57 | } 58 | 59 | - (void)setIsFavoriteWrapper:(NSNumber *)n { 60 | BOOL flag = [n boolValue]; 61 | NSLog(@"-- set %d", flag); 62 | 63 | NSDictionary *userInfo = [NSDictionary dictionaryWithObject:n forKey:@"value"]; 64 | [[NSNotificationCenter defaultCenter] postNotificationName:@"SetFavoriteFlagForTweet" object:self userInfo:userInfo]; 65 | 66 | self.isFavorite = n; 67 | BOOL success = [self save]; 68 | if(!success) NSLog(@"-- can't save"); 69 | } 70 | 71 | + (NSArray *)tweetsWithAndPredicates:(NSArray *)predicates context:(NSManagedObjectContext *)context { 72 | NSFetchRequest *request = [[NSFetchRequest alloc] init]; 73 | [request setEntity:[THTweet entityInContext:context]]; 74 | 75 | NSPredicate *p = [NSCompoundPredicate andPredicateWithSubpredicates:predicates]; 76 | [request setPredicate:p]; 77 | 78 | NSError *error = nil; 79 | 80 | NSArray *tweets = [context executeFetchRequest:request error:&error]; 81 | 82 | if(error) { 83 | NSLog(@"-- error:%@", error); 84 | } 85 | return tweets; 86 | } 87 | 88 | + (NSUInteger)tweetsCountWithAndPredicates:(NSArray *)predicates context:(NSManagedObjectContext *)context { 89 | NSFetchRequest *request = [[NSFetchRequest alloc] init]; 90 | [request setEntity:[THTweet entityInContext:context]]; 91 | 92 | NSPredicate *p = [NSCompoundPredicate andPredicateWithSubpredicates:predicates]; 93 | [request setPredicate:p]; 94 | 95 | NSError *error = nil; 96 | 97 | NSUInteger count = [context countForFetchRequest:request error:&error]; 98 | 99 | if(error) { 100 | NSLog(@"-- error:%@", error); 101 | } 102 | return count; 103 | } 104 | 105 | + (NSUInteger)nbOfTweetsForScore:(NSNumber *)aScore andPredicates:(NSArray *)predicates context:(NSManagedObjectContext *)context { 106 | NSPredicate *p = [NSPredicate predicateWithFormat:@"score == %@", aScore]; 107 | NSArray *ps = [predicates arrayByAddingObject:p]; 108 | 109 | NSUInteger count = [self tweetsCountWithAndPredicates:ps context:context]; 110 | 111 | NSLog(@"-- score %@ -> %ld", aScore, count); 112 | 113 | return count; 114 | } 115 | 116 | + (NSArray *)tweetsContainingKeyword:(NSString *)keyword context:(NSManagedObjectContext *)context { 117 | 118 | NSAssert(keyword != nil, @"keyword should not be nil"); 119 | 120 | NSFetchRequest *request = [[NSFetchRequest alloc] init]; 121 | [request setEntity:[self entityInContext:context]]; 122 | NSPredicate *p = [NSPredicate predicateWithFormat:@"text contains[c] %@" argumentArray:[NSArray arrayWithObject:keyword]]; 123 | [request setPredicate:p]; 124 | 125 | NSError *error = nil; 126 | NSArray *array = [context executeFetchRequest:request error:&error]; 127 | if(error) { 128 | NSLog(@"-- error:%@", error); 129 | } 130 | return array; 131 | } 132 | 133 | + (THTweet *)tweetWithUid:(NSString *)uid context:(NSManagedObjectContext *)context { 134 | if(uid == nil) return nil; 135 | 136 | NSFetchRequest *request = [[NSFetchRequest alloc] init]; 137 | [request setEntity:[self entityInContext:context]]; 138 | NSNumber *uidNumber = [NSNumber numberWithUnsignedLongLong:[uid unsignedLongLongValue]]; 139 | 140 | //NSLog(@"--> %@", uidNumber); 141 | 142 | NSPredicate *p = [NSPredicate predicateWithFormat:@"uid == %@", uidNumber, nil]; 143 | [request setPredicate:p]; 144 | [request setFetchLimit:1]; 145 | 146 | NSError *error = nil; 147 | 148 | NSLog(@"-- fetching tweet with uid: %@", uid); 149 | 150 | NSArray *array = [context executeFetchRequest:request error:&error]; 151 | if(array == nil) { 152 | NSLog(@"-- error:%@", error); 153 | } 154 | 155 | return [array lastObject]; 156 | } 157 | 158 | + (THTweet *)tweetWithHighestUidInContext:(NSManagedObjectContext *)context { 159 | 160 | NSFetchRequest *request = [[NSFetchRequest alloc] init]; 161 | [request setEntity:[self entityInContext:context]]; 162 | 163 | NSSortDescriptor *sd = [NSSortDescriptor sortDescriptorWithKey:@"uid" ascending:NO]; 164 | [request setSortDescriptors:[NSArray arrayWithObject:sd]]; 165 | [request setFetchLimit:1]; 166 | 167 | NSError *error = nil; 168 | 169 | NSArray *array = [context executeFetchRequest:request error:&error]; 170 | if(array == nil) { 171 | NSLog(@"-- error:%@", error); 172 | } 173 | 174 | return [array lastObject]; 175 | } 176 | 177 | + (NSArray *)tweetsWithIdGreaterOrEqualTo:(NSNumber *)anId context:(NSManagedObjectContext *)context { 178 | NSFetchRequest *request = [[NSFetchRequest alloc] init]; 179 | [request setEntity:[self entityInContext:context]]; 180 | NSPredicate *p = [NSPredicate predicateWithFormat:@"uid >= %@", anId, nil]; 181 | [request setPredicate:p]; 182 | 183 | NSError *error = nil; 184 | NSArray *array = [context executeFetchRequest:request error:&error]; 185 | if(error) { 186 | NSLog(@"-- error:%@", error); 187 | } 188 | 189 | return array; 190 | } 191 | 192 | + (void)unfavorFavoritesBetweenMinId:(NSNumber *)unfavorMinId maxId:(NSNumber *)unfavorMaxId context:(NSManagedObjectContext *)context { 193 | if([unfavorMinId isGreaterThanOrEqualTo:unfavorMaxId]) { 194 | NSLog(@"-- can't unfavor ids, given maxId is smaller than minId"); 195 | return; 196 | } 197 | 198 | NSFetchRequest *request = [[NSFetchRequest alloc] init]; 199 | [request setEntity:[THTweet entityInContext:context]]; 200 | NSPredicate *p = [NSPredicate predicateWithFormat:@"isFavorite == YES AND uid <= %@ AND uid >= %@", unfavorMaxId, unfavorMinId, nil]; 201 | [request setPredicate:p]; 202 | 203 | NSError *error = nil; 204 | NSArray *array = [context executeFetchRequest:request error:&error]; 205 | if(error) { 206 | NSLog(@"-- error:%@", error); 207 | } 208 | 209 | for(THTweet *t in array) { 210 | t.isFavorite = [NSNumber numberWithBool:NO]; 211 | NSLog(@"** unfavor %@", t.user.screenName); 212 | } 213 | } 214 | 215 | + (THTweet *)updateOrCreateTweetFromDictionary:(NSDictionary *)d context:(NSManagedObjectContext *)context { 216 | 217 | NSString *uid = [d objectForKey:@"id"]; 218 | 219 | BOOL wasCreated = NO; 220 | THTweet *tweet = [self tweetWithUid:uid context:context]; 221 | if(!tweet) { 222 | tweet = [THTweet createInContext:context]; 223 | wasCreated = YES; 224 | tweet.uid = [NSNumber numberWithUnsignedLongLong:[[d objectForKey:@"id"] unsignedLongLongValue]]; 225 | 226 | NSDictionary *userDictionary = [d objectForKey:@"user"]; 227 | THUser *user = [THUser getOrCreateUserWithDictionary:userDictionary context:context]; 228 | 229 | NSMutableString *s = [NSMutableString stringWithString:[d objectForKey:@"text"]]; 230 | [s replaceOccurrencesOfString:@"<" withString:@"<" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [s length])]; 231 | [s replaceOccurrencesOfString:@">" withString:@">" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [s length])]; 232 | tweet.text = s; 233 | 234 | // if needed, use entities.urls to detect URLs 235 | /* 236 | "entities": 237 | { 238 | "hashtags":[], 239 | "urls":[], 240 | "user_mentions":[] 241 | } 242 | */ 243 | // BOOL doesContainURL = [tweet.text rangeOfString:@"http"].location != NSNotFound; 244 | // tweet.containsURL = [NSNumber numberWithBool:doesContainURL]; 245 | 246 | tweet.date = [[tweet createdAtDateFormatter] dateFromString:[d objectForKey:@"created_at"]]; 247 | tweet.user = user; 248 | } 249 | tweet.isFavorite = [d objectForKey:@"favorited"]; 250 | 251 | if(tweet.isFavorite) { 252 | NSLog(@"-- %@", tweet.text); 253 | NSLog(@"-- %@", d); 254 | } 255 | 256 | NSLog(@"** created %d favorite %@ %@ %@ %@", wasCreated, tweet.isFavorite, tweet.uid, tweet.user.screenName, tweet.text); 257 | 258 | return tweet; 259 | } 260 | 261 | + (NSArray *)saveTweetsFromDictionariesArray:(NSArray *)a { 262 | // TODO: remove non-favorites between new favorites bounds 263 | 264 | NSManagedObjectContext *parentContext = [(id)[[NSApplication sharedApplication] delegate] managedObjectContext]; 265 | NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; 266 | privateContext.parentContext = parentContext; 267 | 268 | __block BOOL success = NO; 269 | __block NSError *error = nil; 270 | 271 | __weak NSMutableArray *tweets = [NSMutableArray array]; 272 | 273 | [parentContext performBlockAndWait:^{ 274 | for(NSDictionary *d in a) { 275 | THTweet *t = [THTweet updateOrCreateTweetFromDictionary:d context:privateContext]; 276 | if(t) [tweets addObject:t]; 277 | } 278 | 279 | success = [privateContext save:&error]; 280 | }]; 281 | 282 | if(success == NO) { 283 | NSLog(@"-- save error: %@", [error localizedDescription]); 284 | return nil; 285 | } 286 | 287 | return tweets; 288 | } 289 | 290 | - (NSAttributedString *)attributedString { 291 | 292 | // NSLog(@"-- attributedString"); 293 | 294 | NSString *statusString = self.text; 295 | 296 | NSMutableAttributedString *attributedStatusString = [[NSMutableAttributedString alloc] initWithString:statusString]; 297 | 298 | // Defining our paragraph style for the tweet text. Starting with the shadow to make the text 299 | // appear inset against the gray background. 300 | NSShadow *textShadow = [[NSShadow alloc] init]; 301 | [textShadow setShadowColor:[NSColor colorWithDeviceWhite:1 alpha:.8]]; 302 | [textShadow setShadowBlurRadius:0]; 303 | [textShadow setShadowOffset:NSMakeSize(0, -1)]; 304 | 305 | NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; 306 | // [paragraphStyle setMinimumLineHeight:22]; 307 | // [paragraphStyle setMaximumLineHeight:22]; 308 | // [paragraphStyle setParagraphSpacing:0]; 309 | // [paragraphStyle setParagraphSpacingBefore:0]; 310 | // [paragraphStyle setTighteningFactorForTruncation:4]; 311 | [paragraphStyle setAlignment:NSNaturalTextAlignment]; 312 | [paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping]; 313 | 314 | // Our initial set of attributes that are applied to the full string length 315 | NSDictionary *fullAttributes = [NSDictionary dictionaryWithObjectsAndKeys: 316 | [NSColor colorWithDeviceHue:.53 saturation:.13 brightness:.26 alpha:1], NSForegroundColorAttributeName, 317 | textShadow, NSShadowAttributeName, 318 | //[NSCursor arrowCursor], NSCursorAttributeName, 319 | [NSNumber numberWithFloat:0.0], NSKernAttributeName, 320 | [NSNumber numberWithInt:0], NSLigatureAttributeName, 321 | paragraphStyle, NSParagraphStyleAttributeName, 322 | [NSFont systemFontOfSize:11.0], NSFontAttributeName, nil]; 323 | [attributedStatusString addAttributes:fullAttributes range:NSMakeRange(0, [statusString length])]; 324 | 325 | // Generate arrays of our interesting items. Links, usernames, hashtags. 326 | 327 | NSArray *linkMatches = [[self linksRegex] matchesInString:self.text options:0 range:NSMakeRange(0, [self.text length])]; 328 | NSArray *usernameMatches = [[self usernamesRegex] matchesInString:self.text options:0 range:NSMakeRange(0, [self.text length])]; 329 | NSArray *hashtagMatches = [[self hashtagsRegex] matchesInString:self.text options:0 range:NSMakeRange(0, [self.text length])]; 330 | 331 | // Iterate across the string matches from our regular expressions, find the range 332 | // of each match, add new attributes to that range 333 | for (NSTextCheckingResult *linkMatch in linkMatches) { 334 | NSRange range = [linkMatch range]; 335 | NSString *s = [statusString substringWithRange:range]; 336 | if( range.location != NSNotFound ) { 337 | // Add custom attribute of LinkMatch to indicate where our URLs are found. Could be blue 338 | // or any other color. 339 | NSDictionary *linkAttr = [[NSDictionary alloc] initWithObjectsAndKeys: 340 | [NSCursor pointingHandCursor], NSCursorAttributeName, 341 | [NSColor blueColor], NSForegroundColorAttributeName, 342 | [NSFont boldSystemFontOfSize:11.0], NSFontAttributeName, 343 | s, @"LinkMatch", 344 | nil]; 345 | [attributedStatusString addAttributes:linkAttr range:range]; 346 | } 347 | } 348 | 349 | for (NSTextCheckingResult *usernameMatch in usernameMatches) { 350 | NSRange range = [usernameMatch range]; 351 | NSString *s = [statusString substringWithRange:range]; 352 | if( range.location != NSNotFound ) { 353 | // Add custom attribute of UsernameMatch to indicate where our usernames are found 354 | NSDictionary *linkAttr2 = [[NSDictionary alloc] initWithObjectsAndKeys: 355 | [NSColor blackColor], NSForegroundColorAttributeName, 356 | [NSCursor pointingHandCursor], NSCursorAttributeName, 357 | [NSFont boldSystemFontOfSize:11.0], NSFontAttributeName, 358 | s, @"UsernameMatch", 359 | nil]; 360 | [attributedStatusString addAttributes:linkAttr2 range:range]; 361 | } 362 | } 363 | 364 | for (NSTextCheckingResult *hashtagMatch in hashtagMatches) { 365 | NSRange range = [hashtagMatch range]; 366 | NSString *s = [statusString substringWithRange:range]; 367 | if( range.location != NSNotFound ) { 368 | // Add custom attribute of HashtagMatch to indicate where our hashtags are found 369 | NSDictionary *linkAttr3 = [[NSDictionary alloc] initWithObjectsAndKeys: 370 | [NSColor grayColor], NSForegroundColorAttributeName, 371 | [NSCursor pointingHandCursor], NSCursorAttributeName, 372 | [NSFont systemFontOfSize:11.0], NSFontAttributeName, 373 | s, @"HashtagMatch", 374 | nil]; 375 | [attributedStatusString addAttributes:linkAttr3 range:range]; 376 | } 377 | } 378 | 379 | return attributedStatusString; 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | // [_tweetTextTextView setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable]; 390 | // [_tweetTextTextView setBackgroundColor:[NSColor clearColor]]; 391 | // [_tweetTextTextView setTextContainerInset:NSZeroSize]; 392 | // [[_tweetTextTextView textStorage] setAttributedString:attributedStatusString]; 393 | // [_tweetTextTextView setEditable:NO]; 394 | // [_tweetTextTextView setSelectable:YES]; 395 | // 396 | // [attributedStatusString release]; 397 | } 398 | 399 | #pragma mark - 400 | #pragma mark regex 401 | 402 | - (NSRegularExpression *)linksRegex { 403 | if(linksRegex == nil) { 404 | NSString *pattern = @"\\b(([\\w-]+://?|www[.])[^\\s()<>]+(?:\\([\\w\\d]+\\)|([^[:punct:]\\s]|/)))"; 405 | linksRegex = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil]; 406 | } 407 | return linksRegex; 408 | } 409 | 410 | - (NSRegularExpression *)usernamesRegex { 411 | if(usernamesRegex == nil) { 412 | NSString *pattern = @"@{1}([-A-Za-z0-9_]{2,})"; 413 | usernamesRegex = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil]; 414 | } 415 | return usernamesRegex; 416 | } 417 | 418 | - (NSRegularExpression *)hashtagsRegex { 419 | if(hashtagsRegex == nil) { 420 | NSString *pattern = @"[\\s]{1,}#{1}([^\\s]{2,})"; 421 | hashtagsRegex = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil]; 422 | } 423 | return hashtagsRegex; 424 | } 425 | 426 | @end 427 | 428 | 429 | --------------------------------------------------------------------------------