--- a/Adium.xcodeproj/project.pbxproj Wed Mar 20 01:55:36 2013 +0100
+++ b/Adium.xcodeproj/project.pbxproj Wed Mar 20 12:02:05 2013 -0400
@@ -141,9 +141,6 @@
1172FBD10CDAA8D400B8E233 /* libpurple.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1172FBC90CDAA8D400B8E233 /* libpurple.framework */; };
117D6DC00BC5F0C40080D02B /* msg-request-attention.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 117D6DBF0BC5F0C40080D02B /* msg-request-attention.tiff */; };
11819A1B10D0B95D003E8ECA /* AIMediaControllerProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 11819A0910D0B90E003E8ECA /* AIMediaControllerProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; };
- 11879C0B0F6FF4C400CACFB1 /* AITwitterAccountOAuthSetup.m in Sources */ = {isa = PBXBuildFile; fileRef = 11879C0A0F6FF4C400CACFB1 /* AITwitterAccountOAuthSetup.m */; };
- 11879DF80F6FFC0B00CACFB1 /* OAuthConsumer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 11879DF70F6FFC0B00CACFB1 /* OAuthConsumer.framework */; };
- 11879E0A0F6FFC1000CACFB1 /* OAuthConsumer.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 11879DF70F6FFC0B00CACFB1 /* OAuthConsumer.framework */; };
1192E6D90FD3056F003CAEF5 /* AIAnnoyingIRCMessagesHiderPlugin.h in Headers */ = {isa = PBXBuildFile; fileRef = 1192E6B10FD30307003CAEF5 /* AIAnnoyingIRCMessagesHiderPlugin.h */; };
1192E6DA0FD30578003CAEF5 /* AIAnnoyingIRCMessagesHiderPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 1192E6B20FD30307003CAEF5 /* AIAnnoyingIRCMessagesHiderPlugin.m */; };
1197F6710FCF8D180032F19B /* AITwitterStatusFollowup.m in Sources */ = {isa = PBXBuildFile; fileRef = 1197F6700FCF8D180032F19B /* AITwitterStatusFollowup.m */; };
@@ -187,15 +184,6 @@
11F738F90F58D18700B3285B /* AITwitterService.m in Sources */ = {isa = PBXBuildFile; fileRef = 11F738F80F58D18700B3285B /* AITwitterService.m */; };
11F738FC0F58D19B00B3285B /* AITwitterPlugin.m in Sources */ = {isa = PBXBuildFile; fileRef = 11F738FB0F58D19B00B3285B /* AITwitterPlugin.m */; };
11F739020F58D1C400B3285B /* AITwitterAccountViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 11F739010F58D1C400B3285B /* AITwitterAccountViewController.m */; };
- 11F7397A0F58D4DC00B3285B /* MGTwitterEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = 11F7395B0F58D4DC00B3285B /* MGTwitterEngine.m */; };
- 11F7397B0F58D4DC00B3285B /* MGTwitterHTTPURLConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 11F7395F0F58D4DC00B3285B /* MGTwitterHTTPURLConnection.m */; };
- 11F7397E0F58D4DD00B3285B /* MGTwitterMessagesParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 11F739650F58D4DC00B3285B /* MGTwitterMessagesParser.m */; };
- 11F739800F58D4DD00B3285B /* MGTwitterMiscParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 11F739690F58D4DC00B3285B /* MGTwitterMiscParser.m */; };
- 11F739820F58D4DD00B3285B /* MGTwitterStatusesParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 11F7396F0F58D4DC00B3285B /* MGTwitterStatusesParser.m */; };
- 11F739840F58D4DD00B3285B /* MGTwitterUsersParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 11F739730F58D4DC00B3285B /* MGTwitterUsersParser.m */; };
- 11F739850F58D4DD00B3285B /* MGTwitterXMLParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 11F739750F58D4DC00B3285B /* MGTwitterXMLParser.m */; };
- 11F739860F58D4DD00B3285B /* NSData+Base64.m in Sources */ = {isa = PBXBuildFile; fileRef = 11F739770F58D4DC00B3285B /* NSData+Base64.m */; };
- 11F739870F58D4DD00B3285B /* NSString+UUID.m in Sources */ = {isa = PBXBuildFile; fileRef = 11F739790F58D4DC00B3285B /* NSString+UUID.m */; };
11FC23C20F768C1600C1C906 /* AIXMLElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 11FC23BF0F768C0900C1C906 /* AIXMLElement.h */; settings = {ATTRIBUTES = (Public, ); }; };
11FC23C30F768C2900C1C906 /* AIXMLElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 11FC23C00F768C0900C1C906 /* AIXMLElement.m */; };
31034EFF0C8142680003F5AA /* TestStringAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 31034EFE0C8142680003F5AA /* TestStringAdditions.m */; };
@@ -1034,11 +1022,16 @@
4F1CB6400D640DA40073A1E6 /* get-info-advanced.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 4F1CB63C0D640DA40073A1E6 /* get-info-advanced.tiff */; };
4F1CB6410D640DA40073A1E6 /* get-info-events.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 4F1CB63D0D640DA40073A1E6 /* get-info-events.tiff */; };
4F1CB64C0D640F4F0073A1E6 /* ContactInfoInspector.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4F1CB64B0D640F4F0073A1E6 /* ContactInfoInspector.xib */; };
+ 5A0D236A16F4C7BC005DF211 /* STTwitterAppOnly.m in Sources */ = {isa = PBXBuildFile; fileRef = 5A0D236816F4C7BC005DF211 /* STTwitterAppOnly.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 5A1781860EC1215D00BA1E04 /* AIAutoScrollTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5A1781850EC1215D00BA1E04 /* AIAutoScrollTextView.m */; };
5A17D65D130F76B4002C852F /* AIGradientView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5A17D65C130F76B4002C852F /* AIGradientView.m */; };
5A1E3A1214DCE60400724574 /* Preferences-Xtras.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5A1E3A1114DCE60400724574 /* Preferences-Xtras.xib */; };
5A22D6E214834F44004E15F7 /* AIFacebookXMPPAccountView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5A22D6E014834F44004E15F7 /* AIFacebookXMPPAccountView.xib */; };
5A27FA7E14A272330063489D /* pref-messagestyle.png in Resources */ = {isa = PBXBuildFile; fileRef = 5A27FA7A14A272330063489D /* pref-messagestyle.png */; };
+ 5A3B4D7916D878AC00903E40 /* NSString+STTwitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 5A3B4D6C16D878AB00903E40 /* NSString+STTwitter.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 5A3B4D7A16D878AC00903E40 /* STTwitterAPIWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 5A3B4D6E16D878AB00903E40 /* STTwitterAPIWrapper.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 5A3B4D7C16D878AC00903E40 /* STTwitterOAuth.m in Sources */ = {isa = PBXBuildFile; fileRef = 5A3B4D7216D878AB00903E40 /* STTwitterOAuth.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 5A3B4D7E16D878AC00903E40 /* STHTTPRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5A3B4D7816D878AC00903E40 /* STHTTPRequest.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 5A44595E169143130078AB0A /* AIPreferenceCVPrototypeView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5A445957169118C60078AB0A /* AIPreferenceCVPrototypeView.m */; };
5A4BD41D13F855B000A4D3F7 /* SearchTerms.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5A4BD41B13F855B000A4D3F7 /* SearchTerms.plist */; };
5A4BD41E13F8568100A4D3F7 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5ACF27331392C585004B6AEF /* Preferences.xib */; };
@@ -1679,7 +1672,6 @@
116E369A10B72934002EDB0F /* Growl.framework in Copy Frameworks */,
633404710F9C18EF003C77A9 /* AIUtilities.framework in Copy Frameworks */,
639DF9D80F97E678003C9A32 /* AdiumLibpurple.framework in Copy Frameworks */,
- 11879E0A0F6FFC1000CACFB1 /* OAuthConsumer.framework in Copy Frameworks */,
11EE1CCF0CDD01120097F246 /* libglib.framework in Copy Frameworks */,
11EE1CD00CDD01120097F246 /* libgmodule.framework in Copy Frameworks */,
11EE1CD10CDD01120097F246 /* libgobject.framework in Copy Frameworks */,
@@ -1915,9 +1907,6 @@
11819A0210D0B8BE003E8ECA /* AIMediaController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AIMediaController.h; path = Source/AIMediaController.h; sourceTree = "<group>"; };
11819A0310D0B8BE003E8ECA /* AIMediaController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AIMediaController.m; path = Source/AIMediaController.m; sourceTree = "<group>"; };
11819A0910D0B90E003E8ECA /* AIMediaControllerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AIMediaControllerProtocol.h; path = "Frameworks/Adium Framework/Source/AIMediaControllerProtocol.h"; sourceTree = "<group>"; };
- 11879C090F6FF4C400CACFB1 /* AITwitterAccountOAuthSetup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AITwitterAccountOAuthSetup.h; path = "Plugins/Twitter Plugin/AITwitterAccountOAuthSetup.h"; sourceTree = "<group>"; };
- 11879C0A0F6FF4C400CACFB1 /* AITwitterAccountOAuthSetup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AITwitterAccountOAuthSetup.m; path = "Plugins/Twitter Plugin/AITwitterAccountOAuthSetup.m"; sourceTree = "<group>"; };
- 11879DF70F6FFC0B00CACFB1 /* OAuthConsumer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OAuthConsumer.framework; path = Frameworks/OAuthConsumer.framework; sourceTree = "<group>"; };
118A444F0FEEA828008153C0 /* libjson-glib.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = "libjson-glib.framework"; path = "Frameworks/libjson-glib.framework"; sourceTree = "<group>"; };
1192E6B10FD30307003CAEF5 /* AIAnnoyingIRCMessagesHiderPlugin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AIAnnoyingIRCMessagesHiderPlugin.h; path = "Plugins/Purple Service/AIAnnoyingIRCMessagesHiderPlugin.h"; sourceTree = "<group>"; };
1192E6B20FD30307003CAEF5 /* AIAnnoyingIRCMessagesHiderPlugin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AIAnnoyingIRCMessagesHiderPlugin.m; path = "Plugins/Purple Service/AIAnnoyingIRCMessagesHiderPlugin.m"; sourceTree = "<group>"; };
@@ -1994,28 +1983,6 @@
11F738FB0F58D19B00B3285B /* AITwitterPlugin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AITwitterPlugin.m; path = "Plugins/Twitter Plugin/AITwitterPlugin.m"; sourceTree = "<group>"; };
11F739000F58D1C300B3285B /* AITwitterAccountViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AITwitterAccountViewController.h; path = "Plugins/Twitter Plugin/AITwitterAccountViewController.h"; sourceTree = "<group>"; };
11F739010F58D1C400B3285B /* AITwitterAccountViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AITwitterAccountViewController.m; path = "Plugins/Twitter Plugin/AITwitterAccountViewController.m"; sourceTree = "<group>"; };
- 11F7395A0F58D4DC00B3285B /* MGTwitterEngine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTwitterEngine.h; sourceTree = "<group>"; };
- 11F7395B0F58D4DC00B3285B /* MGTwitterEngine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGTwitterEngine.m; sourceTree = "<group>"; };
- 11F7395C0F58D4DC00B3285B /* MGTwitterEngineDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTwitterEngineDelegate.h; sourceTree = "<group>"; };
- 11F7395D0F58D4DC00B3285B /* MGTwitterEngineGlobalHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTwitterEngineGlobalHeader.h; sourceTree = "<group>"; };
- 11F7395E0F58D4DC00B3285B /* MGTwitterHTTPURLConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTwitterHTTPURLConnection.h; sourceTree = "<group>"; };
- 11F7395F0F58D4DC00B3285B /* MGTwitterHTTPURLConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGTwitterHTTPURLConnection.m; sourceTree = "<group>"; };
- 11F739640F58D4DC00B3285B /* MGTwitterMessagesParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTwitterMessagesParser.h; sourceTree = "<group>"; };
- 11F739650F58D4DC00B3285B /* MGTwitterMessagesParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGTwitterMessagesParser.m; sourceTree = "<group>"; };
- 11F739680F58D4DC00B3285B /* MGTwitterMiscParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTwitterMiscParser.h; sourceTree = "<group>"; };
- 11F739690F58D4DC00B3285B /* MGTwitterMiscParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGTwitterMiscParser.m; sourceTree = "<group>"; };
- 11F7396A0F58D4DC00B3285B /* MGTwitterParserDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTwitterParserDelegate.h; sourceTree = "<group>"; };
- 11F7396B0F58D4DC00B3285B /* MGTwitterRequestTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTwitterRequestTypes.h; sourceTree = "<group>"; };
- 11F7396E0F58D4DC00B3285B /* MGTwitterStatusesParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTwitterStatusesParser.h; sourceTree = "<group>"; };
- 11F7396F0F58D4DC00B3285B /* MGTwitterStatusesParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGTwitterStatusesParser.m; sourceTree = "<group>"; };
- 11F739720F58D4DC00B3285B /* MGTwitterUsersParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTwitterUsersParser.h; sourceTree = "<group>"; };
- 11F739730F58D4DC00B3285B /* MGTwitterUsersParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGTwitterUsersParser.m; sourceTree = "<group>"; };
- 11F739740F58D4DC00B3285B /* MGTwitterXMLParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGTwitterXMLParser.h; sourceTree = "<group>"; };
- 11F739750F58D4DC00B3285B /* MGTwitterXMLParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGTwitterXMLParser.m; sourceTree = "<group>"; };
- 11F739760F58D4DC00B3285B /* NSData+Base64.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+Base64.h"; sourceTree = "<group>"; };
- 11F739770F58D4DC00B3285B /* NSData+Base64.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Base64.m"; sourceTree = "<group>"; };
- 11F739780F58D4DC00B3285B /* NSString+UUID.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+UUID.h"; sourceTree = "<group>"; };
- 11F739790F58D4DC00B3285B /* NSString+UUID.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+UUID.m"; sourceTree = "<group>"; };
11FC23BF0F768C0900C1C906 /* AIXMLElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AIXMLElement.h; path = "Frameworks/Adium Framework/Source/AIXMLElement.h"; sourceTree = "<group>"; };
11FC23C00F768C0900C1C906 /* AIXMLElement.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AIXMLElement.m; path = "Frameworks/Adium Framework/Source/AIXMLElement.m"; sourceTree = "<group>"; };
31034EFD0C8142680003F5AA /* TestStringAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TestStringAdditions.h; path = UnitTests/TestStringAdditions.h; sourceTree = "<group>"; };
@@ -3908,6 +3875,8 @@
4F1CB63C0D640DA40073A1E6 /* get-info-advanced.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = "get-info-advanced.tiff"; path = "Resources/get-info-advanced.tiff"; sourceTree = "<group>"; };
4F1CB63D0D640DA40073A1E6 /* get-info-events.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = "get-info-events.tiff"; path = "Resources/get-info-events.tiff"; sourceTree = "<group>"; };
4F1CB64B0D640F4F0073A1E6 /* ContactInfoInspector.xib */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xib; name = ContactInfoInspector.xib; path = Resources/ContactInfoInspector.xib; sourceTree = "<group>"; };
+ 5A0D236816F4C7BC005DF211 /* STTwitterAppOnly.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = STTwitterAppOnly.m; path = "Plugins/Twitter Plugin/STTwitter/STTwitterAppOnly.m"; sourceTree = "<group>"; }; + 5A0D236916F4C7BC005DF211 /* STTwitterAppOnly.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = STTwitterAppOnly.h; path = "Plugins/Twitter Plugin/STTwitter/STTwitterAppOnly.h"; sourceTree = "<group>"; }; 5A1781840EC1215D00BA1E04 /* AIAutoScrollTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AIAutoScrollTextView.h; path = Source/AIAutoScrollTextView.h; sourceTree = "<group>"; };
5A1781850EC1215D00BA1E04 /* AIAutoScrollTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AIAutoScrollTextView.m; path = Source/AIAutoScrollTextView.m; sourceTree = "<group>"; };
5A17D65B130F76B4002C852F /* AIGradientView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AIGradientView.h; path = Source/AIGradientView.h; sourceTree = "<group>"; };
@@ -3918,6 +3887,15 @@
5A1FEA601334549300C14951 /* MessageView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = MessageView.xib; path = "Plugins/Dual Window Interface/MessageView.xib"; sourceTree = "<group>"; };
5A22D6E114834F44004E15F7 /* en */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xib; name = en; path = "Plugins/Purple Service/Resources/en.lproj/AIFacebookXMPPAccountView.xib"; sourceTree = "<group>"; };
5A27FA7A14A272330063489D /* pref-messagestyle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "pref-messagestyle.png"; path = "Resources/pref-messagestyle.png"; sourceTree = "<group>"; };
+ 5A3B4D6B16D878AB00903E40 /* NSString+STTwitter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSString+STTwitter.h"; path = "Plugins/Twitter Plugin/STTwitter/NSString+STTwitter.h"; sourceTree = "<group>"; }; + 5A3B4D6C16D878AB00903E40 /* NSString+STTwitter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSString+STTwitter.m"; path = "Plugins/Twitter Plugin/STTwitter/NSString+STTwitter.m"; sourceTree = "<group>"; }; + 5A3B4D6D16D878AB00903E40 /* STTwitterAPIWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = STTwitterAPIWrapper.h; path = "Plugins/Twitter Plugin/STTwitter/STTwitterAPIWrapper.h"; sourceTree = "<group>"; }; + 5A3B4D6E16D878AB00903E40 /* STTwitterAPIWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = STTwitterAPIWrapper.m; path = "Plugins/Twitter Plugin/STTwitter/STTwitterAPIWrapper.m"; sourceTree = "<group>"; }; + 5A3B4D7116D878AB00903E40 /* STTwitterOAuth.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = STTwitterOAuth.h; path = "Plugins/Twitter Plugin/STTwitter/STTwitterOAuth.h"; sourceTree = "<group>"; }; + 5A3B4D7216D878AB00903E40 /* STTwitterOAuth.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = STTwitterOAuth.m; path = "Plugins/Twitter Plugin/STTwitter/STTwitterOAuth.m"; sourceTree = "<group>"; }; + 5A3B4D7516D878AB00903E40 /* STTwitterOAuthProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = STTwitterOAuthProtocol.h; path = "Plugins/Twitter Plugin/STTwitter/STTwitterOAuthProtocol.h"; sourceTree = "<group>"; }; + 5A3B4D7716D878AC00903E40 /* STHTTPRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = STHTTPRequest.h; path = "Plugins/Twitter Plugin/STTwitter/Vendor/STHTTPRequest.h"; sourceTree = "<group>"; }; + 5A3B4D7816D878AC00903E40 /* STHTTPRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = STHTTPRequest.m; path = "Plugins/Twitter Plugin/STTwitter/Vendor/STHTTPRequest.m"; sourceTree = "<group>"; }; 5A445956169118C60078AB0A /* AIPreferenceCVPrototypeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AIPreferenceCVPrototypeView.h; path = Source/AIPreferenceCVPrototypeView.h; sourceTree = "<group>"; };
5A445957169118C60078AB0A /* AIPreferenceCVPrototypeView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AIPreferenceCVPrototypeView.m; path = Source/AIPreferenceCVPrototypeView.m; sourceTree = "<group>"; };
5A4BD41C13F855B000A4D3F7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = Resources/en.lproj/SearchTerms.plist; sourceTree = "<group>"; };
@@ -4841,7 +4819,6 @@
11AA1EFA0BCAE9C3003DDA66 /* Quartz.framework in Frameworks */,
340C1ABF0BD58FAB00D09235 /* Security.framework in Frameworks */,
31E0CD810C5EEF5200271DB1 /* CoreAudio.framework in Frameworks */,
- 11879DF80F6FFC0B00CACFB1 /* OAuthConsumer.framework in Frameworks */,
34C846AF101E515900140B4B /* QTKit.framework in Frameworks */,
9719C92C1530EDF700217FBE /* FriBidi.framework in Frameworks */,
9719C92E1530EE0C00217FBE /* ShortcutRecorder.framework in Frameworks */,
@@ -5064,7 +5041,7 @@
11F738F40F58D15500B3285B /* Twitter */ = {
- 11F739590F58D4DC00B3285B /* MGTwitterEngine */,
+ 5A3B4D6A16D878AB00903E40 /* STTwitter */, 113891850F6B6B0300A7D7DC /* Laconica */,
1197F66F0FCF8D180032F19B /* AITwitterStatusFollowup.h */,
1197F6700FCF8D180032F19B /* AITwitterStatusFollowup.m */,
@@ -5073,9 +5050,7 @@
110966190F61D3E70064CA0E /* AITwitterReplyWindowController.m */,
11F739000F58D1C300B3285B /* AITwitterAccountViewController.h */,
11F739010F58D1C400B3285B /* AITwitterAccountViewController.m */,
- 11879C090F6FF4C400CACFB1 /* AITwitterAccountOAuthSetup.h */,
11D135D80FBE4C65000B0A5E /* AITwitterAccountView.xib */,
- 11879C0A0F6FF4C400CACFB1 /* AITwitterAccountOAuthSetup.m */,
11BD73D10F5A54BB007D438A /* twitter-small.png */,
11BD73D20F5A54BB007D438A /* twitter.png */,
EFB1C3120DDBDA3100B3973D /* AITwitterIMPlugin.h */,
@@ -5096,36 +5071,6 @@
- 11F739590F58D4DC00B3285B /* MGTwitterEngine */ = {
- 11F7395A0F58D4DC00B3285B /* MGTwitterEngine.h */,
- 11F7395B0F58D4DC00B3285B /* MGTwitterEngine.m */,
- 11F7395C0F58D4DC00B3285B /* MGTwitterEngineDelegate.h */,
- 11F7395D0F58D4DC00B3285B /* MGTwitterEngineGlobalHeader.h */,
- 11F7395E0F58D4DC00B3285B /* MGTwitterHTTPURLConnection.h */,
- 11F7395F0F58D4DC00B3285B /* MGTwitterHTTPURLConnection.m */,
- 11F739640F58D4DC00B3285B /* MGTwitterMessagesParser.h */,
- 11F739650F58D4DC00B3285B /* MGTwitterMessagesParser.m */,
- 11F739680F58D4DC00B3285B /* MGTwitterMiscParser.h */,
- 11F739690F58D4DC00B3285B /* MGTwitterMiscParser.m */,
- 11F7396A0F58D4DC00B3285B /* MGTwitterParserDelegate.h */,
- 11F7396B0F58D4DC00B3285B /* MGTwitterRequestTypes.h */,
- 11F7396E0F58D4DC00B3285B /* MGTwitterStatusesParser.h */,
- 11F7396F0F58D4DC00B3285B /* MGTwitterStatusesParser.m */,
- 11F739720F58D4DC00B3285B /* MGTwitterUsersParser.h */,
- 11F739730F58D4DC00B3285B /* MGTwitterUsersParser.m */,
- 11F739740F58D4DC00B3285B /* MGTwitterXMLParser.h */,
- 11F739750F58D4DC00B3285B /* MGTwitterXMLParser.m */,
- 11F739760F58D4DC00B3285B /* NSData+Base64.h */,
- 11F739770F58D4DC00B3285B /* NSData+Base64.m */,
- 11F739780F58D4DC00B3285B /* NSString+UUID.h */,
- 11F739790F58D4DC00B3285B /* NSString+UUID.m */,
- name = MGTwitterEngine;
- path = "Plugins/Twitter Plugin/MGTwitterEngine";
- sourceTree = "<group>";
19C28FACFE9D520D11CA2CBB /* Products */ = {
@@ -7371,6 +7316,32 @@
+ 5A3B4D6A16D878AB00903E40 /* STTwitter */ = { + 5A0D236816F4C7BC005DF211 /* STTwitterAppOnly.m */, + 5A0D236916F4C7BC005DF211 /* STTwitterAppOnly.h */, + 5A3B4D6B16D878AB00903E40 /* NSString+STTwitter.h */, + 5A3B4D6C16D878AB00903E40 /* NSString+STTwitter.m */, + 5A3B4D6D16D878AB00903E40 /* STTwitterAPIWrapper.h */, + 5A3B4D6E16D878AB00903E40 /* STTwitterAPIWrapper.m */, + 5A3B4D7116D878AB00903E40 /* STTwitterOAuth.h */, + 5A3B4D7216D878AB00903E40 /* STTwitterOAuth.m */, + 5A3B4D7516D878AB00903E40 /* STTwitterOAuthProtocol.h */, + 5A3B4D7616D878AC00903E40 /* Vendor */, + sourceTree = "<group>"; + 5A3B4D7616D878AC00903E40 /* Vendor */ = { + 5A3B4D7716D878AC00903E40 /* STHTTPRequest.h */, + 5A3B4D7816D878AC00903E40 /* STHTTPRequest.m */, + sourceTree = "<group>"; 5A8A6A46124456B1004965A8 /* Segmented control with menu popup */ = {
@@ -8307,7 +8278,6 @@
3496A8E707CE6CA30055BBAB /* AutoHyperlinks.framework.xcodeproj */,
9719C92B1530EDF700217FBE /* FriBidi.framework */,
7E9A8CB2104DEBC400F210CC /* Growl.framework */,
- 11879DF70F6FFC0B00CACFB1 /* OAuthConsumer.framework */,
377EC8930AE9525B00CB7BDF /* PSMTabBarControl.framework */,
9719C92D1530EE0C00217FBE /* ShortcutRecorder.framework */,
9E1E1DFC0A96741500E16DFC /* LMX.framework */,
@@ -10550,15 +10520,6 @@
11F738F90F58D18700B3285B /* AITwitterService.m in Sources */,
11F738FC0F58D19B00B3285B /* AITwitterPlugin.m in Sources */,
11F739020F58D1C400B3285B /* AITwitterAccountViewController.m in Sources */,
- 11F7397A0F58D4DC00B3285B /* MGTwitterEngine.m in Sources */,
- 11F7397B0F58D4DC00B3285B /* MGTwitterHTTPURLConnection.m in Sources */,
- 11F7397E0F58D4DD00B3285B /* MGTwitterMessagesParser.m in Sources */,
- 11F739800F58D4DD00B3285B /* MGTwitterMiscParser.m in Sources */,
- 11F739820F58D4DD00B3285B /* MGTwitterStatusesParser.m in Sources */,
- 11F739840F58D4DD00B3285B /* MGTwitterUsersParser.m in Sources */,
- 11F739850F58D4DD00B3285B /* MGTwitterXMLParser.m in Sources */,
- 11F739860F58D4DD00B3285B /* NSData+Base64.m in Sources */,
- 11F739870F58D4DD00B3285B /* NSString+UUID.m in Sources */,
113F26A00F5CC03F00954772 /* AITwitterURLParser.m in Sources */,
112523190F5F7F86003FC58A /* AITwitterURLHandler.m in Sources */,
1109661A0F61D3E70064CA0E /* AITwitterReplyWindowController.m in Sources */,
@@ -10567,7 +10528,6 @@
1138918D0F6B6B3F00A7D7DC /* AILaconicaPlugin.m in Sources */,
113891950F6B6B9C00A7D7DC /* AILaconicaAccountViewController.m in Sources */,
1163F0EC0F6C7A8300F12F5D /* AIURLShortenerPlugin.m in Sources */,
- 11879C0B0F6FF4C400CACFB1 /* AITwitterAccountOAuthSetup.m in Sources */,
11700A350F7E8BE80078D6AB /* AISpecialPasswordPromptController.m in Sources */,
112B490A0F82FB1700690E84 /* AIGroupChatStatusTooltipPlugin.m in Sources */,
112B4A250F83194700690E84 /* AIMentionAdvancedPreferences.m in Sources */,
@@ -10606,6 +10566,11 @@
761D58831636EDE100210B12 /* AINewMessageTextFieldCell.m in Sources */,
761D58861636F94300210B12 /* AINewMessageSearchField.m in Sources */,
5A44595E169143130078AB0A /* AIPreferenceCVPrototypeView.m in Sources */,
+ 5A3B4D7916D878AC00903E40 /* NSString+STTwitter.m in Sources */, + 5A3B4D7A16D878AC00903E40 /* STTwitterAPIWrapper.m in Sources */, + 5A3B4D7C16D878AC00903E40 /* STTwitterOAuth.m in Sources */, + 5A3B4D7E16D878AC00903E40 /* STHTTPRequest.m in Sources */, + 5A0D236A16F4C7BC005DF211 /* STTwitterAppOnly.m in Sources */, runOnlyForDeploymentPostprocessing = 0;
--- a/Plugins/Twitter Plugin/AITwitterAccount.m Wed Mar 20 01:55:36 2013 +0100
+++ b/Plugins/Twitter Plugin/AITwitterAccount.m Wed Mar 20 12:02:05 2013 -0400
@@ -36,6 +36,7 @@
#import <Adium/AIHTMLDecoder.h>
#import <Adium/AIContentEvent.h>
#import <AIUtilities/AIApplicationAdditions.h>
+#import "STTwitterOAuth.h" @interface AITwitterAccount()
- (void)updateUserIcon:(NSString *)url forContact:(AIListContact *)listContact;
@@ -54,11 +55,6 @@
linkDestination:(NSString *)destination
linkClass:(NSString *)attributeName;
-- (void)setRequestType:(AITwitterRequestType)type forRequestID:(NSString *)requestID withDictionary:(NSDictionary *)info;
-- (AITwitterRequestType)requestTypeForRequestID:(NSString *)requestID;
-- (NSDictionary *)dictionaryForRequestID:(NSString *)requestID;
-- (void)clearRequestTypeForRequestID:(NSString *)requestID;
- (void)displayQueuedUpdatesForRequestType:(AITwitterRequestType)requestType;
@@ -142,58 +138,67 @@
- twitterEngine = [[MGTwitterEngine alloc] initWithDelegate:self];
- [twitterEngine setClientName:@"Adium"
- version:[NSApp applicationVersion]
- URL:@"http://www.adium.im"
- token:self.sourceToken];
- [twitterEngine setAPIDomain:[self.host stringByAppendingPathComponent:self.apiPath]];
- [twitterEngine setUsesSecureConnection:self.useSSL];
- if (!self.passwordWhileConnected.length) {
- /* If we weren't able to retrieve the 'password', we can't proceed with oauth - we stored the oauth
- * http response body in the keychain as the password.
- * Note that this can happen not only if Adium isn't authorized but also if it *is* authorized but the
- * keychain was inaccessible - e.g. keychain access wasn't allowed after an upgrade. Hm.
- [self setLastDisconnectionError:TWITTER_OAUTH_NOT_AUTHORIZED];
- [[NSNotificationCenter defaultCenter] postNotificationName:@"AIEditAccount"
- // Don't try and connect.
- twitterEngine.useOAuth = YES;
- OAToken *token = [[OAToken alloc] initWithHTTPResponseBody:self.passwordWhileConnected];
- OAConsumer *consumer = [[OAConsumer alloc] initWithKey:self.consumerKey secret:self.secretKey];
- twitterEngine.accessToken = token;
- twitterEngine.consumer = consumer;
- [twitterEngine setUsername:self.UID password:self.passwordWhileConnected];
+ if (!self.passwordWhileConnected.length) { + /* If we weren't able to retrieve the 'password', we can't proceed with oauth - we stored the oauth + * http response body in the keychain as the password. + * Note that this can happen not only if Adium isn't authorized but also if it *is* authorized but the + * keychain was inaccessible - e.g. keychain access wasn't allowed after an upgrade. Hm. + [self setLastDisconnectionError:TWITTER_OAUTH_NOT_AUTHORIZED]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"AIEditAccount" + // Don't try and connect. - AILogWithSignature(@"%@ connecting to %@", self, twitterEngine.APIDomain);
+ NSDictionary *oauth = [self.passwordWhileConnected parametersDictionary]; + NSString *oauthToken = [oauth objectForKey:@"oauth_token"]; + NSString *oauthSecret = [oauth objectForKey:@"oauth_token_secret"]; - NSString *requestID = [twitterEngine checkUserCredentials];
+ twitterEngine = [STTwitterAPIWrapper twitterAPIWithOAuthConsumerName:@"Adium" + consumerKey:self.consumerKey + consumerSecret:self.secretKey + oauthTokenSecret:oauthSecret]; + AILogWithSignature(@"%@ connecting to %@", self, twitterEngine.userName);
- [self setRequestType:AITwitterValidateCredentials forRequestID:requestID withDictionary:nil];
- [self setLastDisconnectionError:AILocalizedString(@"Unable to connect to server", nil)];
+ [twitterEngine verifyCredentialsWithSuccessBlock:^(id response) { + if ([response isKindOfClass:[NSDictionary class]]) + [self userInfoReceived:(NSDictionary *)response forRequest:AITwitterValidateCredentials]; + if ([[self preferenceForKey:TWITTER_PREFERENCE_LOAD_CONTACTS group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue]) { + // If we load our follows as contacts, do so now. + // Delay updates on initial login. + [self silenceAllContactUpdatesForInterval:18.0]; + [twitterEngine getFriendsForScreenName:self.UID + successBlock:^(NSArray *friends) { + [self userInfoReceived:@{ @"friends" : friends } forRequest:AITwitterInitialUserInfo]; + if ([self boolValueForProperty:@"isConnecting"]) { + // Trigger our normal update routine. + } errorBlock:^(NSError *error) { + [self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list [fail]", "Message when a (vital) twitter request to retrieve the follow list fails")]; + // If we don't load follows as contacts, we've finished connecting (fast, wasn't it?) + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterValidateCredentials withError:error userInfo:nil]; @@ -333,7 +338,7 @@
- return (self.online && [twitterEngine usesSecureConnection]);
@@ -464,13 +469,50 @@
- (void)setSocialNetworkingStatusMessage:(NSAttributedString *)inStatusMessage
- NSString *requestID = [twitterEngine sendUpdate:[inStatusMessage string]];
- [self setRequestType:AITwitterSendUpdate
+ [self sendUpdate:inStatusMessage.string forChat:nil]; + AILogWithSignature(@"%@ Sending social networking update %@", self, inStatusMessage); +- (void)sendUpdate:(NSString *)inStatusMessage forChat:(AIChat *)chat { + [twitterEngine postStatusUpdate:inStatusMessage + inReplyToStatusID:[chat valueForProperty:@"TweetInReplyToStatusID"] + successBlock:^(NSDictionary *status) { + [adium.contentController displayEvent:AILocalizedString(@"Tweet successfully sent.", nil) + inChat:self.timelineChat]; + [[NSNotificationCenter defaultCenter] postNotificationName:AITwitterNotificationPostedStatus + userInfo:[NSDictionary dictionaryWithObjectsAndKeys:self.timelineChat, @"AIChat", nil]]; + NSDictionary *retweet = [status valueForKey:TWITTER_STATUS_RETWEET]; + NSString *text = [[status objectForKey:TWITTER_STATUS_TEXT] stringByEscapingForXMLWithEntities:nil]; + if (retweet && [retweet isKindOfClass:[NSDictionary class]]) { + text = [[NSString stringWithFormat:@"RT @%@: %@", + [[retweet objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID], + [retweet objectForKey:TWITTER_STATUS_TEXT]] stringByEscapingForXMLWithEntities:nil]; + if ([[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_GLOBAL group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue] && + (![text hasPrefix:@"@"] || [[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_GLOBAL_REPLIES group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue])) { + AIStatus *availableStatus = [AIStatus statusOfType:AIAvailableStatusType]; + availableStatus.statusMessage = [NSAttributedString stringWithString:text]; + [adium.statusController setActiveStatusState:availableStatus]; + [self performSelector:@selector(periodicUpdate) withObject:nil afterDelay:0.0]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterSendUpdate withError:error userInfo:@{ @"Chat" : chat }]; - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forListObject:(AIListObject *)inListObject
@@ -487,44 +529,27 @@
- (BOOL)sendMessageObject:(AIContentMessage *)inContentMessage
- if(inContentMessage.chat.isGroupChat) {
- requestID = [twitterEngine sendUpdate:inContentMessage.encodedMessage
- inReplyTo:[inContentMessage.chat valueForProperty:@"TweetInReplyToStatusID"]];
- [self setRequestType:AITwitterSendUpdate
- withDictionary:[NSDictionary dictionaryWithObject:inContentMessage.chat
- inContentMessage.displayContent = NO;
+ if (inContentMessage.chat.isGroupChat) { + [self sendUpdate:inContentMessage.encodedMessage forChat:inContentMessage.chat]; + inContentMessage.displayContent = NO; + AILogWithSignature(@"%@ Sending update [in reply to %@]: %@", self, [inContentMessage.chat valueForProperty:@"TweetInReplyToStatusID"], inContentMessage.encodedMessage); + [twitterEngine postDirectMessage:inContentMessage.encodedMessage + to:inContentMessage.destination.UID + successBlock:^(NSDictionary *dm) { + [queuedOutgoingDM addObject:dm]; + [self displayQueuedUpdatesForRequestType:AITwitterDirectMessageSend]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterDirectMessageSend withError:error userInfo:@{ @"Chat" : inContentMessage.chat }]; - AILogWithSignature(@"%@ Sending update [in reply to %@]: %@", self, [inContentMessage.chat valueForProperty:@"TweetInReplyToStatusID"], inContentMessage.encodedMessage);
- requestID = [twitterEngine sendDirectMessage:inContentMessage.encodedMessage
- to:inContentMessage.destination.UID];
+ inContentMessage.displayContent = NO;
- [self setRequestType:AITwitterDirectMessageSend
- withDictionary:[NSDictionary dictionaryWithObject:inContentMessage.chat
- inContentMessage.displayContent = NO;
- AILogWithSignature(@"%@ Sending DM to %@: %@", self, inContentMessage.destination.UID, inContentMessage.encodedMessage);
+ AILogWithSignature(@"%@ Sending DM to %@: %@", self, inContentMessage.destination.UID, inContentMessage.encodedMessage);
- AILogWithSignature(@"%@ Message immediate fail.", self);
- return (requestID != nil);
@@ -539,13 +564,82 @@
- NSString *requestID = [twitterEngine getUserInformationFor:inContact.UID];
- [self setRequestType:AITwitterProfileUserInfo
- withDictionary:[NSDictionary dictionaryWithObject:inContact forKey:@"ListContact"]];
+ [twitterEngine getUserInformationFor:inContact.UID + successBlock:^(NSDictionary *thisUserInfo) { + NSArray *keyNames = [NSArray arrayWithObjects:@"name", @"location", @"description", @"url", @"friends_count", @"followers_count", @"statuses_count", nil]; + NSArray *readableNames = [NSArray arrayWithObjects:AILocalizedString(@"Name", nil), AILocalizedString(@"Location", nil), + AILocalizedString(@"Biography", nil), AILocalizedString(@"Website", nil), AILocalizedString(@"Following", nil), + AILocalizedString(@"Followers", nil), AILocalizedString(@"Updates", nil), nil]; + __block NSMutableArray *profileArray = [NSMutableArray array]; + for (NSUInteger idx = 0; idx < keyNames.count; idx++) { + NSString *keyName = [keyNames objectAtIndex:idx]; + id unattributedValue = [thisUserInfo objectForKey:keyName]; + NSString *stringValue = nil; + if ([unattributedValue isKindOfClass:[NSNumber class]]) + stringValue = [(NSNumber *)unattributedValue stringValue]; + else if ([unattributedValue isKindOfClass:[NSNumber class]]) + stringValue = unattributedValue; + NSString *readableName = [readableNames objectAtIndex:idx]; + NSAttributedString *value; + if([keyName isEqualToString:@"friends_count"]) { + value = [NSAttributedString attributedStringWithLinkLabel:stringValue + linkDestination:[self addressForLinkType:AITwitterLinkFriends userID:inContact.UID statusID:nil context:nil]]; + } else if ([keyName isEqualToString:@"followers_count"]) { + value = [NSAttributedString attributedStringWithLinkLabel:stringValue + linkDestination:[self addressForLinkType:AITwitterLinkFollowers userID:inContact.UID statusID:nil context:nil]]; + } else if ([keyName isEqualToString:@"statuses_count"]) { + value = [NSAttributedString attributedStringWithLinkLabel:stringValue + linkDestination:[self addressForLinkType:AITwitterLinkUserPage userID:inContact.UID statusID:nil context:nil]]; + value = [NSAttributedString stringWithString:stringValue]; + [profileArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:readableName, KEY_KEY, value, KEY_VALUE, nil]]; + AILogWithSignature(@"%@ Updating profileArray for user %@", self, inContact); + // Grab their statuses. + [twitterEngine getUserTimelineWithScreenName:inContact.UID + count:TWITTER_UPDATE_USER_INFO_COUNT + successBlock:^(NSArray *statuses) { + AILogWithSignature(@"%@ Updating statuses for profile, user %@", self, inContact); + for (NSDictionary *update in statuses) { + NSAttributedString *message; + NSDictionary *retweet = [update valueForKey:TWITTER_STATUS_RETWEET]; + NSString *text = [update objectForKey:TWITTER_STATUS_TEXT]; + if (retweet && [retweet isKindOfClass:[NSDictionary class]]) { + text = [NSString stringWithFormat:@"RT @%@: %@", + [[retweet objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID], + [retweet objectForKey:TWITTER_STATUS_TEXT]]; + message = [self parseMessage:text + tweetID:[update objectForKey:TWITTER_STATUS_ID] + inReplyToUser:[update objectForKey:TWITTER_STATUS_REPLY_UID] + inReplyToTweetID:[update objectForKey:TWITTER_STATUS_REPLY_ID]]; + [profileArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:message, KEY_VALUE, nil]]; + [inContact setProfileArray:profileArray notify:NotifyNow]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterProfileUserInfo withError:error userInfo:@{ @"ListContact" : inContact }]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterProfileUserInfo withError:error userInfo:@{ @"ListContact" : inContact }]; @@ -560,21 +654,16 @@
* @brief Update the Twitter profile
- (void)setProfileName:(NSString *)name
location:(NSString *)location
description:(NSString *)description
- NSString *requestID = [twitterEngine updateProfileName:name
- description:description];
- [self setRequestType:AITwitterProfileSelf
+ [twitterEngine postUpdateProfile:@{ @"name" : name, @"url" : url, @"location" : location, @"description" : description } + successBlock:^(NSDictionary *status) { + [self userInfoReceived:status forRequest:AITwitterProfileSelf]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterProfileSelf withError:error userInfo:nil]; @@ -663,7 +752,7 @@
[menuItem setImage:serviceIcon];
[menuItem setRepresentedObject:inContact];
- [menuItemArray addObject:menuItem];
+ [menuItemArray addObject:menuItem]; menuItem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:AILocalizedString(@"Enable device notifications for %@", "Enable sending Twitter notifications to your phone (device)"), inContact.UID]
@@ -711,39 +800,21 @@
BOOL enableNotification = menuItem.tag;
AIListContact *contact = menuItem.representedObject;
- NSString *requestID = nil;
- BOOL initialFailure = NO;
- if (enableNotification) {
- requestID = [twitterEngine enableNotificationsFor:contact.UID];
- [self setRequestType:AITwitterNotificationEnable
- withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:contact, @"ListContact", nil]];
- requestID = [twitterEngine disableNotificationsFor:contact.UID];
- [self setRequestType:AITwitterNotificationDisable
- withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:contact, @"ListContact", nil]];
- [adium.interfaceController handleErrorMessage:(enableNotification ?
- AILocalizedString(@"Unable to Enable Notifications", nil) :
- AILocalizedString(@"Unable to Disable Notifications", nil))
- withDescription:AILocalizedString(@"Unable to connect to the Twitter server.", nil)];
+ [twitterEngine postUpdateNotifications:enableNotification + forScreenName:contact.UID + successBlock:^(NSDictionary *relationship) { + NSString *status = (enableNotification ? + AILocalizedString(@"Notifications Enabled", nil) : + AILocalizedString(@"Notifications Disabled", nil)); + [adium.interfaceController handleMessage:status + withDescription:[NSString stringWithFormat:(enableNotification ? + AILocalizedString(@"You will now receive device notifications for %@.", nil) : + AILocalizedString(@"You will no longer receive device notifications for %@.", nil)), + withWindowTitle:status]; + } errorBlock:^(NSError *error) { + [self requestFailed:(enableNotification ? AITwitterNotificationEnable : AITwitterNotificationDisable) withError:error userInfo:@{ @"ListContact" : contact }]; @@ -830,14 +901,38 @@
- (void)getRateLimitAmount
- NSString *requestID = [twitterEngine getRateLimitStatus];
- [self setRequestType:AITwitterRateLimitStatus
+ [twitterEngine getRateLimitsForResources:@[ @"users", @"statuses", @"friendships", @"direct_messages" ] + successBlock:^(NSDictionary *rateLimits) { + NSMutableString *formattedString = [NSMutableString string]; + [rateLimits[@"resources"] enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if ([obj isKindOfClass:[NSDictionary class]]) { + __block BOOL displayedHeader = NO; + [obj enumerateKeysAndObjectsUsingBlock:^(id subKey, id subObj, BOOL *subStop) { + NSDate *resetDate = [NSDate dateWithTimeIntervalSince1970:[[subObj objectForKey:TWITTER_RATE_LIMIT_RESET_SECONDS] intValue]]; + int limit = [[subObj objectForKey:TWITTER_RATE_LIMIT] intValue]; + int remaining = [[subObj objectForKey:TWITTER_RATE_LIMIT_REMAINING] intValue]; + NSString *resource = [subKey stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"/%@/", key] + if (remaining < limit) { + if (!displayedHeader) { + [formattedString appendFormat:@"%@:\n", key]; + [formattedString appendFormat:@"\t%@: %d/%d for %@\n", resource, + [NSDateFormatter stringForTimeInterval:[resetDate timeIntervalSinceNow] + [[NSAlert alertWithMessageText:AILocalizedString(@"Current Twitter rate limits", "Message in the rate limits status window") defaultButton:nil alternateButton:nil otherButton:nil informativeTextWithFormat:@"%@", formattedString] beginSheetModalForWindow:nil modalDelegate:nil didEndSelector:nil contextInfo:nil]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterRateLimitStatus withError:error userInfo:nil]; #pragma mark Contact handling
@@ -929,20 +1024,26 @@
// If we don't already have an icon for the user...
if(![listContact boolValueForProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON]) {
- NSString *fileName = [[url lastPathComponent] stringByReplacingOccurrencesOfString:@"_normal." withString:@"_bigger."];
- url = [[url stringByDeletingLastPathComponent] stringByAppendingPathComponent:fileName];
+ [listContact setValue:[NSNumber numberWithBool:YES] forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever]; // Grab the user icon and set it as their serverside icon.
- NSString *requestID = [twitterEngine getImageAtURL:url];
- [self setRequestType:AITwitterUserIconPull
- withDictionary:[NSDictionary dictionaryWithObject:listContact forKey:@"ListContact"]];
- [listContact setValue:[NSNumber numberWithBool:YES] forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
+ NSString *imageURL = [url stringByReplacingOccurrencesOfString:@"_normal." withString:@"_bigger."]; + NSURLRequest *imageRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:imageURL]]; + [NSURLConnection sendAsynchronousRequest:imageRequest + queue:[NSOperationQueue currentQueue] + completionHandler:^(NSURLResponse *resp, NSData *data, NSError *error) { + NSImage *image = [[NSImage alloc] initWithData:data]; + AILogWithSignature(@"%@ Updated user icon for %@", self, listContact); + [listContact setServersideIconData:[image TIFFRepresentation] + [listContact setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever]; + [self requestFailed:AITwitterUserIconPull withError:error userInfo:@{ @"ListContact" : listContact }]; @@ -950,17 +1051,17 @@
* @brief Unfollow the requested contacts.
- (void)removeContacts:(NSArray *)objects fromGroups:(NSArray *)groups
for (AIListContact *object in objects) {
- NSString *requestID = [twitterEngine disableUpdatesFor:object.UID];
AILogWithSignature(@"%@ Requesting unfollow for: %@", self, object.UID);
- [self setRequestType:AITwitterRemoveFollow
- withDictionary:[NSDictionary dictionaryWithObject:object forKey:@"ListContact"]];
+ [twitterEngine postUnfollow:object.UID + successBlock:^(NSDictionary *user) { + for (NSString *groupName in object.remoteGroupNames) { + [object removeRemoteGroupName:groupName]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterRemoveFollow withError:error userInfo:@{ @"ListContact" : UID }]; @@ -997,58 +1098,14 @@
AILogWithSignature(@"Not adding contact %@ to group %@, it's me!", contact.UID, group.UID);
- NSString *requestID = [twitterEngine enableUpdatesFor:contact.UID];
AILogWithSignature(@"%@ Requesting follow for: %@", self, contact.UID);
- NSString *updateRequestID = [twitterEngine getUserInformationFor:contact.UID];
- [self setRequestType:AITwitterAddFollow
- forRequestID:updateRequestID
- withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:contact.UID, @"UID", nil]];
-#pragma mark Request cataloguing
- * @brief Set the type and optional dictionary for a request ID
- * Sets the AITwitterRequestType for a particular request ID, so when the request finishes we can identify what it is for.
- * Optionally sets a dictionary which can be retrieved in association with the request type.
-- (void)setRequestType:(AITwitterRequestType)type forRequestID:(NSString *)requestID withDictionary:(NSDictionary *)info
- [pendingRequests setObject:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:type], @"Type",
- * @brief Get the request type for a request ID
-- (AITwitterRequestType)requestTypeForRequestID:(NSString *)requestID
- return [(NSNumber *)[[pendingRequests objectForKey:requestID] objectForKey:@"Type"] intValue];
- * @brief Get the dictionary associated with a request ID
-- (NSDictionary *)dictionaryForRequestID:(NSString *)requestID
- return (NSDictionary *)[[pendingRequests objectForKey:requestID] objectForKey:@"Info"];
- * @brief Remove a request ID's saved information.
-- (void)clearRequestTypeForRequestID:(NSString *)requestID
- [pendingRequests removeObjectForKey:requestID];
+ [twitterEngine postFollow:contact.UID + successBlock:^(NSDictionary *friend) { + [self userInfoReceived:@{ @"friends" : friend } forRequest:AITwitterAddFollow]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterAddFollow withError:error userInfo:@{ @"UID" : contact.UID }]; #pragma mark Preference updating
@@ -1056,7 +1113,7 @@
preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
[super preferencesChangedForGroup:group key:key object:object preferenceDict:prefDict firstTime:firstTime];
// We only care about our changes.
@@ -1066,18 +1123,15 @@
if([key isEqualToString:KEY_USER_ICON]) {
// Avoid pushing an icon update which we just downloaded.
if(![self boolValueForProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON]) {
- NSString *requestID = [twitterEngine updateProfileImage:[prefDict objectForKey:KEY_USER_ICON]];
- AILogWithSignature(@"%@ Pushing self icon update", self);
- [self setRequestType:AITwitterProfileSelf
+ AILogWithSignature(@"%@ Pushing self icon update", self); + [twitterEngine postUpdateProfileImage:[prefDict objectForKey:KEY_USER_ICON] + successBlock:^(NSDictionary *myInfo) { + [self userInfoReceived:myInfo forRequest:AITwitterProfileSelf]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterProfileSelf withError:error userInfo:nil]; + [self setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
- [self setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
@@ -1106,27 +1160,18 @@
// Delay updates when loading our contacts list.
[self silenceAllContactUpdatesForInterval:18.0];
- NSString *friendRequestType;
- if (self.supportsCursors) {
- requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtCursor:-1];
- friendRequestType = @"Cursor";
- requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtPage:1];
- friendRequestType = @"Page";
- [self setRequestType:AITwitterInitialUserInfo
- withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:1] forKey:friendRequestType]];
+ [twitterEngine getFriendsForScreenName:self.UID + successBlock:^(NSArray *friends) { + [self userInfoReceived:@{ @"friends" : friends } forRequest:AITwitterInitialUserInfo]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterInitialUserInfo withError:error userInfo:nil]; [[self timelineChat] removeAllParticipatingContactsSilently];
[self removeAllContacts];
#pragma mark Periodic update scheduler
@@ -1140,68 +1185,58 @@
- // We haven't completed the timeline nor replies. This lets us know if we should display statuses.
- followedTimelineCompleted = repliesCompleted = NO;
- futureTimelineLastID = futureRepliesLastID = nil;
// Prevent triggering this update routine multiple times.
// We haven't printed error messages for this set.
timelineErrorMessagePrinted = NO;
[queuedUpdates removeAllObjects];
[queuedDM removeAllObjects];
AILogWithSignature(@"%@ Periodic update fire", self);
- // Pull direct messages
+ // Pull direct messages lastID = [self preferenceForKey:TWITTER_PREFERENCE_DM_LAST_ID
group:TWITTER_PREFERENCE_GROUP_UPDATES];
- requestID = [twitterEngine getDirectMessagesSinceID:lastID startingAtPage:1];
+ [twitterEngine getDirectMessagesSinceID:lastID + count:TWITTER_UPDATE_DM_COUNT + successBlock:^(NSArray *statuses) { + [self directMessagesReceived:statuses forRequest:AITwitterUpdateDirectMessage]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterUpdateDirectMessage withError:error userInfo:nil];
- [self setRequestType:AITwitterUpdateDirectMessage
- withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1], @"Page", nil]];
+ // We haven't completed the timeline nor replies. This lets us know if we should display statuses. + followedTimelineCompleted = repliesCompleted = NO; + futureTimelineLastID = futureRepliesLastID = nil; // Pull followed timeline
lastID = [self preferenceForKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
group:TWITTER_PREFERENCE_GROUP_UPDATES];
- requestID = [twitterEngine getFollowedTimelineFor:nil
- count:(lastID ? TWITTER_UPDATE_TIMELINE_COUNT : TWITTER_UPDATE_TIMELINE_COUNT_FIRST_RUN)];
- [self setRequestType:AITwitterUpdateFollowedTimeline
- withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1], @"Page", nil]];
+ [twitterEngine getHomeTimelineSinceID:lastID + count:TWITTER_UPDATE_TIMELINE_COUNT + successBlock:^(NSArray *statuses) { + [self statusesReceived:statuses forRequest:AITwitterUpdateFollowedTimeline]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterUpdateFollowedTimeline withError:error userInfo:nil]; lastID = [self preferenceForKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
group:TWITTER_PREFERENCE_GROUP_UPDATES];
- requestID = [twitterEngine getRepliesSinceID:lastID startingAtPage:1];
- [self setRequestType:AITwitterUpdateReplies
- withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1], @"Page", nil]];
+ [twitterEngine getMentionsTimelineSinceID:lastID + count:TWITTER_UPDATE_REPLIES_COUNT + successBlock:^(NSArray *statuses) { + [self statusesReceived:statuses forRequest:AITwitterUpdateReplies]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterUpdateReplies withError:error userInfo:nil]; #pragma mark Message Display
@@ -1213,8 +1248,7 @@
// Bad Request: your request is invalid, and we'll return an error message that tells you why.
- // This is the status code returned if you've exceeded the rate limit.
- return AILocalizedString(@"You've exceeded the rate limit.", nil);
+ return AILocalizedString(@"The request is invalid.", nil); @@ -1232,6 +1266,11 @@
return AILocalizedString(@"Requested resource not found.", nil);
+ // This is the status code returned if you've exceeded the rate limit. + return AILocalizedString(@"You've exceeded the rate limit.", nil); // Internal Server Error: we did something wrong. Please post to the group about it and the Twitter team will investigate.
return AILocalizedString(@"The server reported an internal error.", nil);
@@ -1301,15 +1340,12 @@
- (BOOL)retweetTweet:(NSString *)tweetID
- NSString *requestID = [twitterEngine retweetUpdate:tweetID];
- [self setRequestType:AITwitterSendUpdate
- withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:tweetID, @"tweetID", nil]];
- [self.timelineChat receivedError:[NSNumber numberWithInt:AIChatMessageSendingConnectionError]];
+ [twitterEngine postStatusRetweetWithID:tweetID + successBlock:^(NSDictionary *status) { + [self statusesReceived:@[status] forRequest:AITwitterSendUpdate]; + } errorBlock:^(NSError *error) { + [self requestFailed:AITwitterSendUpdate withError:error userInfo:@{ @"Chat" : self.timelineChat }]; @@ -1322,19 +1358,59 @@
- (void)toggleFavoriteTweet:(NSString *)tweetID
- NSString *requestID = [twitterEngine markUpdate:tweetID asFavorite:YES];
- [self setRequestType:AITwitterFavoriteYes
- withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:tweetID, @"tweetID", nil]];
- AIChat *timelineChat = self.timelineChat;
- [adium.contentController displayEvent:AILocalizedString(@"Attempt to favorite tweet failed to connect.", nil)
+ [self markTweet:tweetID asFavorite:YES]; +- (void)markTweet:(NSString *)tweetID asFavorite:(BOOL)favorite + [twitterEngine postFavoriteState:favorite + successBlock:^(NSDictionary *status) { + AIChat *timelineChat = self.timelineChat; + // Use HTML for the status message since it's just easier to localize that way. + message = AILocalizedString(@"The <a href=\"%@\">requested tweet</a> by <a href=\"%@\">%@</a> is now a favorite.", nil); + message = AILocalizedString(@"The <a href=\"%@\">requested tweet</a> by <a href=\"%@\">%@</a> is no longer a favorite.", nil); + NSString *userID = [[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID]; + message = [NSString stringWithFormat:message, + [self addressForLinkType:AITwitterLinkStatus + statusID:[status objectForKey:TWITTER_STATUS_ID] + [self addressForLinkType:AITwitterLinkUserPage + NSAttributedString *attributedMessage = [[AIHTMLDecoder decoder] decodeHTML:message withDefaultAttributes:nil]; + AIContentEvent *content = [AIContentEvent eventInChat:timelineChat + message:attributedMessage + content.postProcessContent = NO; + content.coalescingKey = @"favorite"; + [adium.contentController receiveContentObject:content]; + } errorBlock:^(NSError *error) { + if (error.code == 403) { + // We've attempted to add or remove when we already have it marked as such. Try the opposite. + [self markTweet:tweetID asFavorite:!favorite]; + [self requestFailed:(favorite ? AITwitterFavoriteYes : AITwitterFavoriteNo) withError:error userInfo:nil]; @@ -1344,19 +1420,16 @@
- (void)destroyTweet:(NSString *)tweetID
- NSString *requestID = [twitterEngine deleteUpdate:tweetID];
- [self setRequestType:AITwitterDestroyStatus
- AIChat *timelineChat = self.timelineChat;
- [adium.contentController displayEvent:AILocalizedString(@"Attempt to delete tweet failed to connect.", nil)
+ [twitterEngine postDestroyStatusWithID:tweetID + successBlock:^(NSDictionary *status) { + [adium.contentController displayEvent:AILocalizedString(@"Your tweet has been successfully deleted.", nil) + inChat:self.timelineChat]; + } errorBlock:^(NSError *error) { + [adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"Your tweet failed to delete. %@", nil), [self errorMessageForError:error]] + inChat:self.timelineChat]; @@ -1367,20 +1440,22 @@
- (void)destroyDirectMessage:(NSString *)messageID
forUser:(NSString *)userID
- NSString *requestID = [twitterEngine deleteDirectMessage:messageID];
- AIListContact *contact = [self contactWithUID:userID];
- [self setRequestType:AITwitterDestroyDM
- withDictionary:[NSDictionary dictionaryWithObject:contact forKey:@"ListContact"]];
- AIChat *chat = [adium.chatController chatWithContact:contact];
- [adium.contentController displayEvent:AILocalizedString(@"Attempt to delete tweet failed to connect.", nil)
+ [twitterEngine postDestroyDirectMessageWithID:messageID + successBlock:^(NSDictionary *dm) { + AIListContact *contact = [self contactWithUID:userID]; + AIChat *chat = [adium.chatController chatWithContact:contact]; + [adium.contentController displayEvent:AILocalizedString(@"The direct message has been successfully deleted.", nil) + } errorBlock:^(NSError *error) { + AIListContact *contact = [self contactWithUID:userID]; + AIChat *chat = [adium.chatController chatWithContact:contact]; + [adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"The direct message failed to delete. %@", nil), [self errorMessageForError:error]] @@ -1455,7 +1530,7 @@
message = [self linkifiedAttributedStringFromString:message];
- BOOL replyTweet = (replyTweetID.length);
+ BOOL replyTweet = (replyTweetID.length > 0); BOOL tweetLink = (tweetID.length && userID.length);
if (replyTweet || tweetLink) {
@@ -1724,7 +1799,7 @@
- if(![self.UID isCaseInsensitivelyEqualToString:contactUID]) {
+ if (![self.UID isCaseInsensitivelyEqualToString:contactUID]) { AIListContact *listContact = [self contactWithUID:contactUID];
// Update the user's status message
@@ -1773,7 +1848,7 @@
NSArray *sortedQueuedDM = [*unsortedArray sortedArrayUsingFunction:queuedDMSort context:nil];
for (NSDictionary *message in sortedQueuedDM) {
- NSDate *date = [message objectForKey:TWITTER_DM_CREATED];
+ NSDate *date = [NSDate dateWithNaturalLanguageString:[message objectForKey:TWITTER_DM_CREATED]]; NSString *text = [message objectForKey:TWITTER_DM_TEXT];
NSString *fromUID = [message objectForKey:TWITTER_DM_SENDER_UID];
NSString *toUID = [message objectForKey:TWITTER_DM_RECIPIENT_UID];
@@ -1810,52 +1885,20 @@
-#pragma mark MGTwitterEngine Delegate Methods
- * @brief A request was successful
- * We only care about requests succeeding if they aren't specifically handled in another location.
-- (void)requestSucceeded:(NSString *)identifier
- // If a request succeeds and we think we're offline, call ourselves online.
- if ([self requestTypeForRequestID:identifier] == AITwitterDisconnect) {
- } else if ([self requestTypeForRequestID:identifier] == AITwitterRemoveFollow) {
- AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
- for (NSString *groupName in listContact.remoteGroupNames) {
- [listContact removeRemoteGroupName:groupName];
- } else if ([self requestTypeForRequestID:identifier] == AITwitterDestroyStatus) {
- AIChat *timelineChat = self.timelineChat;
- [adium.contentController displayEvent:AILocalizedString(@"Your tweet has been successfully deleted.", nil)
- } else if ([self requestTypeForRequestID:identifier] == AITwitterDestroyDM) {
- AIListContact *contact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
- AIChat *chat = [adium.chatController chatWithContact:contact];
- [adium.contentController displayEvent:AILocalizedString(@"The direct message has been successfully deleted.", nil)
+#pragma mark Response Handling * @brief A request failed
* If it's a fatal error, we need to kill the session and retry. Otherwise, twitter's reliability is
* pretty terrible, so let's ignore errors for the most part.
-- (void)requestFailed:(NSString *)identifier withError:(NSError *)error
- switch ([self requestTypeForRequestID:identifier]) {
+- (void)requestFailed:(AITwitterRequestType)identifier withError:(NSError *)error userInfo:(NSDictionary *)userInfo case AITwitterDirectMessageSend:
case AITwitterSendUpdate:
- AIChat *chat = [[self dictionaryForRequestID:identifier] objectForKey:@"Chat"];
+ AIChat *chat = [userInfo objectForKey:@"Chat"]; [chat receivedError:[NSNumber numberWithInt:AIChatMessageSendingConnectionError]];
@@ -1869,14 +1912,9 @@
- case AITwitterInitialUserInfo:
- [self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list [fail]", "Message when a (vital) twitter request to retrieve the follow list fails")];
case AITwitterUserIconPull:
- AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
+ AIListContact *listContact = [userInfo objectForKey:@"ListContact"]; // Image pull failed, flag ourselves as needing to try again.
[listContact setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
@@ -1920,12 +1958,12 @@
[adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Add Contact", nil)
withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to add %@ to account %@, the user does not exist.", nil),
- [[self dictionaryForRequestID:identifier] objectForKey:@"UID"],
+ [userInfo objectForKey:@"UID"], self.explicitFormattedUID]];
[adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Add Contact", nil)
withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to add %@ to account %@. %@",nil),
- [[self dictionaryForRequestID:identifier] objectForKey:@"UID"],
+ [userInfo objectForKey:@"UID"], self.explicitFormattedUID,
[self errorMessageForError:error]]];
@@ -1934,13 +1972,13 @@
case AITwitterRemoveFollow:
[adium.interfaceController handleErrorMessage:AILocalizedString(@"Unable to Remove Contact", nil)
withDescription:[NSString stringWithFormat:AILocalizedString(@"Unable to remove %@ on account %@. %@", nil),
- ((AIListContact *)[[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"]).UID,
+ ((AIListContact *)[userInfo objectForKey:@"ListContact"]).UID, self.explicitFormattedUID,
[self errorMessageForError:error]]];
case AITwitterValidateCredentials:
- if(error.code == 401) {
+ if(error.code == 401) { [self setPasswordTemporarily:nil];
[self setLastDisconnectionError:TWITTER_OAUTH_NOT_AUTHORIZED];
@@ -1964,29 +2002,9 @@
case AITwitterFavoriteNo:
AIChat *timelineChat = self.timelineChat;
- if (error.code == 403) {
- // We've attempted to add or remove when we already have it marked as such. Try the opposite.
- BOOL addAsFavorite = ([self requestTypeForRequestID:identifier] == AITwitterFavoriteNo);
- NSString *tweetID = [[self dictionaryForRequestID:identifier] objectForKey:@"tweetID"];
- NSString *requestID = [twitterEngine markUpdate:tweetID
- asFavorite:addAsFavorite];
- [self setRequestType:(addAsFavorite ? AITwitterFavoriteYes : AITwitterFavoriteNo)
- withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:tweetID, @"tweetID", nil]];
- [adium.contentController displayEvent:AILocalizedString(@"Attempt to favorite tweet failed to connect.", nil)
[adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"Attempt to favorite tweet failed. %@", nil), [self errorMessageForError:error]]
@@ -1994,8 +2012,8 @@
case AITwitterNotificationEnable:
case AITwitterNotificationDisable:
- BOOL enableNotification = ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable);
- AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
+ BOOL enableNotification = (identifier == AITwitterNotificationEnable); + AIListContact *listContact = [userInfo objectForKey:@"ListContact"]; [adium.interfaceController handleErrorMessage:(enableNotification ?
AILocalizedString(@"Unable to Enable Notifications", nil) :
@@ -2003,636 +2021,241 @@
withDescription:[NSString stringWithFormat:AILocalizedString(@"Cannot change notification setting for %@. %@", nil), listContact.UID, [self errorMessageForError:error]]];
case AITwitterDestroyStatus:
- AIChat *timelineChat = self.timelineChat;
- [adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"Your tweet failed to delete. %@", nil), [self errorMessageForError:error]]
- AIListContact *contact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
- AIChat *chat = [adium.chatController chatWithContact:contact];
- [adium.contentController displayEvent:[NSString stringWithFormat:AILocalizedString(@"The direct message failed to delete. %@", nil), [self errorMessageForError:error]]
case AITwitterUnknownType:
case AITwitterRateLimitStatus:
case AITwitterProfileSelf:
case AITwitterSelfUserIconPull:
case AITwitterProfileUserInfo:
case AITwitterProfileStatusUpdates:
+ case AITwitterInitialUserInfo: // While we don't handle the errors, it's a good idea to not have a "default" just to prevent accidentally letting something
// we should really handle slip through.
- AILogWithSignature(@"%@ Request failed (%@ - %u) - %@", self, identifier, [self requestTypeForRequestID:identifier], error);
- [self clearRequestTypeForRequestID:identifier];
+ AILogWithSignature(@"%@ Request failed (%u) - %@", self, identifier, error); * @brief Status updates received
-- (void)statusesReceived:(NSArray *)statuses forRequest:(NSString *)identifier
+- (void)statusesReceived:(NSArray *)statuses forRequest:(AITwitterRequestType)identifier - if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline ||
- [self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
+ if(identifier == AITwitterUpdateFollowedTimeline || + identifier == AITwitterUpdateReplies) { - BOOL nextPageNecessary = NO;
- if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
+ if(identifier == AITwitterUpdateFollowedTimeline) { lastID = [self preferenceForKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
group:TWITTER_PREFERENCE_GROUP_UPDATES];
- nextPageNecessary = (lastID && statuses.count >= TWITTER_UPDATE_TIMELINE_COUNT - 5);
lastID = [self preferenceForKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
group:TWITTER_PREFERENCE_GROUP_UPDATES];
- nextPageNecessary = (lastID && statuses.count >= TWITTER_UPDATE_REPLIES_COUNT - 5);
// Store the largest tweet ID we find; this will be our "last ID" the next time we run.
- NSString *largestTweet = [[self dictionaryForRequestID:identifier] objectForKey:@"LargestTweet"];
+ NSString *largestTweet = nil; + largestTweet = [[statuses objectAtIndex:0] objectForKey:TWITTER_STATUS_ID]; - // The largest ID is first, compare.
- NSString *tweetID = [[statuses objectAtIndex:0] objectForKey:TWITTER_STATUS_ID];
- if (!largestTweet || [largestTweet compare:tweetID options:NSNumericSearch] == NSOrderedAscending) {
- largestTweet = tweetID;
+ //Convert the TWITTER_STATUS_CREATED datestrings to NSDates + NSMutableArray *ms = (__bridge_transfer NSMutableArray *)CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (__bridge CFArrayRef)statuses, kCFPropertyListMutableContainers); + [ms enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [obj setObject:[NSDate dateWithNaturalLanguageString:[obj objectForKey:TWITTER_STATUS_CREATED]] + forKey:TWITTER_STATUS_CREATED]; + [queuedUpdates addObjectsFromArray:ms]; + AILogWithSignature(@"%@ Last ID: %@ Largest Tweet: %@", self, lastID, largestTweet); + if (identifier == AITwitterUpdateFollowedTimeline) { + followedTimelineCompleted = YES; + futureTimelineLastID = largestTweet; + } else if (identifier == AITwitterUpdateReplies) { + repliesCompleted = YES; + futureRepliesLastID = largestTweet; - [queuedUpdates addObjectsFromArray:statuses];
- AILogWithSignature(@"%@ Last ID: %@ Largest Tweet: %@ Next Page Necessary: %d", self, lastID, largestTweet, nextPageNecessary);
+ AILogWithSignature(@"%@ Followed completed: %d Replies completed: %d", self, followedTimelineCompleted, repliesCompleted); - // See if we need to pull more updates.
- if (nextPageNecessary) {
- int nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
- if ([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
- requestID = [twitterEngine getFollowedTimelineFor:nil
- startingAtPage:nextPage
- count:TWITTER_UPDATE_TIMELINE_COUNT];
- AILogWithSignature(@"%@ Pulling additional timeline page %d", self, nextPage);
- [self setRequestType:AITwitterUpdateFollowedTimeline
- withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page",
- largestTweet, @"LargestTweet", nil]];
- // Gracefully fail: remove all stored objects.
- AILogWithSignature(@"%@ Immediate timeline fail", self);
- [queuedUpdates removeAllObjects];
+ if (followedTimelineCompleted && repliesCompleted) { + if (queuedUpdates.count) { + // Set the "last pulled" for the timeline and replies, since we've completed both. + if(futureRepliesLastID) { + AILogWithSignature(@"%@ futureRepliesLastID = %@", self, futureRepliesLastID); + [self setPreference:futureRepliesLastID + forKey:TWITTER_PREFERENCE_REPLIES_LAST_ID + group:TWITTER_PREFERENCE_GROUP_UPDATES]; + futureRepliesLastID = nil; - } else if ([self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
- requestID = [twitterEngine getRepliesSinceID:lastID startingAtPage:nextPage];
- AILogWithSignature(@"%@ Pulling additional replies page %d", self, nextPage);
+ if(futureTimelineLastID) { + AILogWithSignature(@"%@ futureTimelineLastID = %@", self, futureTimelineLastID); + [self setPreference:futureTimelineLastID + forKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID + group:TWITTER_PREFERENCE_GROUP_UPDATES]; + futureTimelineLastID = nil;
- [self setRequestType:AITwitterUpdateReplies
- withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page",
- largestTweet, @"LargestTweet", nil]];
- // Gracefully fail: remove all stored objects.
- AILogWithSignature(@"%@ Immediate reply fail", self);
- [queuedUpdates removeAllObjects];
- if([self requestTypeForRequestID:identifier] == AITwitterUpdateFollowedTimeline) {
- followedTimelineCompleted = YES;
- futureTimelineLastID = largestTweet;
- } else if ([self requestTypeForRequestID:identifier] == AITwitterUpdateReplies) {
- repliesCompleted = YES;
- futureRepliesLastID = largestTweet;
+ [self displayQueuedUpdatesForRequestType:identifier];
- AILogWithSignature(@"%@ Followed completed: %d Replies completed: %d", self, followedTimelineCompleted, repliesCompleted);
- if (followedTimelineCompleted && repliesCompleted) {
- if (queuedUpdates.count) {
- // Set the "last pulled" for the timeline and replies, since we've completed both.
- if(futureRepliesLastID) {
- AILogWithSignature(@"%@ futureRepliesLastID = %@", self, futureRepliesLastID);
- [self setPreference:futureRepliesLastID
- forKey:TWITTER_PREFERENCE_REPLIES_LAST_ID
- group:TWITTER_PREFERENCE_GROUP_UPDATES];
- futureRepliesLastID = nil;
- if(futureTimelineLastID) {
- AILogWithSignature(@"%@ futureTimelineLastID = %@", self, futureTimelineLastID);
- [self setPreference:futureTimelineLastID
- forKey:TWITTER_PREFERENCE_TIMELINE_LAST_ID
- group:TWITTER_PREFERENCE_GROUP_UPDATES];
- futureTimelineLastID = nil;
- [self displayQueuedUpdatesForRequestType:[self requestTypeForRequestID:identifier]];
- if (![self preferenceForKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE group:TWITTER_PREFERENCE_GROUP_UPDATES]) {
- [self setPreference:[NSNumber numberWithBool:YES]
- forKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE
- group:TWITTER_PREFERENCE_GROUP_UPDATES];
+ if (![self preferenceForKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE group:TWITTER_PREFERENCE_GROUP_UPDATES]) { + [self setPreference:[NSNumber numberWithBool:YES] + forKey:TWITTER_PREFERENCE_EVER_LOADED_TIMELINE + group:TWITTER_PREFERENCE_GROUP_UPDATES]; - } else if ([self requestTypeForRequestID:identifier] == AITwitterProfileStatusUpdates) {
- AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
- NSMutableArray *profileArray = [[listContact profileArray] mutableCopy];
- AILogWithSignature(@"%@ Updating statuses for profile, user %@", self, listContact);
- for (NSDictionary *update in statuses) {
- NSAttributedString *message;
- NSDictionary *retweet = [update valueForKey:TWITTER_STATUS_RETWEET];
- NSString *text = [update objectForKey:TWITTER_STATUS_TEXT];
- if (retweet && [retweet isKindOfClass:[NSDictionary class]]) {
- text = [NSString stringWithFormat:@"RT @%@: %@",
- [[retweet objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID],
- [retweet objectForKey:TWITTER_STATUS_TEXT]];
- message = [self parseMessage:text
- tweetID:[update objectForKey:TWITTER_STATUS_ID]
- inReplyToUser:[update objectForKey:TWITTER_STATUS_REPLY_UID]
- inReplyToTweetID:[update objectForKey:TWITTER_STATUS_REPLY_ID]];
- [profileArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:message, KEY_VALUE, nil]];
- [listContact setProfileArray:profileArray notify:NotifyNow];
- } else if ([self requestTypeForRequestID:identifier] == AITwitterSendUpdate) {
- [adium.contentController displayEvent:AILocalizedString(@"Tweet successfully sent.", nil)
- inChat:self.timelineChat];
- for(NSDictionary *update in statuses) {
- [[NSNotificationCenter defaultCenter] postNotificationName:AITwitterNotificationPostedStatus
- userInfo:[NSDictionary dictionaryWithObjectsAndKeys:self.timelineChat, @"AIChat", nil]];
- NSDictionary *retweet = [update valueForKey:TWITTER_STATUS_RETWEET];
- NSString *text = [[update objectForKey:TWITTER_STATUS_TEXT] stringByEscapingForXMLWithEntities:nil];
- if (retweet && [retweet isKindOfClass:[NSDictionary class]]) {
- text = [[NSString stringWithFormat:@"RT @%@: %@",
- [[retweet objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID],
- [retweet objectForKey:TWITTER_STATUS_TEXT]] stringByEscapingForXMLWithEntities:nil];
- if([[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_GLOBAL group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue] &&
- (![text hasPrefix:@"@"] || [[self preferenceForKey:TWITTER_PREFERENCE_UPDATE_GLOBAL_REPLIES group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue])) {
- AIStatus *availableStatus = [AIStatus statusOfType:AIAvailableStatusType];
- availableStatus.statusMessage = [NSAttributedString stringWithString:text];
- [adium.statusController setActiveStatusState:availableStatus];
- } else if ([self requestTypeForRequestID:identifier] == AITwitterFavoriteYes ||
- [self requestTypeForRequestID:identifier] == AITwitterFavoriteNo) {
- AIChat *timelineChat = self.timelineChat;
- for (NSDictionary *status in statuses) {
- // Use HTML for the status message since it's just easier to localize that way.
- if ([self requestTypeForRequestID:identifier] == AITwitterFavoriteYes) {
- message = AILocalizedString(@"The <a href=\"%@\">requested tweet</a> by <a href=\"%@\">%@</a> is now a favorite.", nil);
- message = AILocalizedString(@"The <a href=\"%@\">requested tweet</a> by <a href=\"%@\">%@</a> is no longer a favorite.", nil);
- NSString *userID = [[status objectForKey:TWITTER_STATUS_USER] objectForKey:TWITTER_STATUS_UID];
- message = [NSString stringWithFormat:message,
- [self addressForLinkType:AITwitterLinkStatus
- statusID:[status objectForKey:TWITTER_STATUS_ID]
- [self addressForLinkType:AITwitterLinkUserPage
- NSAttributedString *attributedMessage = [[AIHTMLDecoder decoder] decodeHTML:message withDefaultAttributes:nil];
- AIContentEvent *content = [AIContentEvent eventInChat:timelineChat
- message:attributedMessage
- content.postProcessContent = NO;
- content.coalescingKey = @"favorite";
- [adium.contentController receiveContentObject:content];
- [self clearRequestTypeForRequestID:identifier];
* @brief Direct messages received
-- (void)directMessagesReceived:(NSArray *)messages forRequest:(NSString *)identifier
- if ([self requestTypeForRequestID:identifier] == AITwitterUpdateDirectMessage) {
+- (void)directMessagesReceived:(NSArray *)messages forRequest:(AITwitterRequestType)identifier + if (identifier == AITwitterUpdateDirectMessage) { NSString *lastID = [self preferenceForKey:TWITTER_PREFERENCE_DM_LAST_ID
group:TWITTER_PREFERENCE_GROUP_UPDATES];
BOOL nextPageNecessary = (lastID && messages.count >= TWITTER_UPDATE_DM_COUNT);
// Store the largest tweet ID we find; this will be our "last ID" the next time we run.
- NSString *largestTweet = [[self dictionaryForRequestID:identifier] objectForKey:@"LargestTweet"];
+ NSString *largestTweet = nil; - // The largest ID is first, compare.
- NSString *tweetID = [[messages objectAtIndex:0] objectForKey:TWITTER_DM_ID];
- if (!largestTweet || [largestTweet compare:tweetID] == NSOrderedAscending) {
- largestTweet = tweetID;
+ largestTweet = [[messages objectAtIndex:0] objectForKey:TWITTER_DM_ID]; [queuedDM addObjectsFromArray:messages];
AILogWithSignature(@"%@ Last ID: %@ Largest Tweet: %@ Next Page Necessary: %d", self, lastID, largestTweet, nextPageNecessary);
- if(nextPageNecessary) {
- int nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
- NSString *requestID = [twitterEngine getDirectMessagesSinceID:lastID
- startingAtPage:nextPage];
- AILogWithSignature(@"%@ Pulling additional DM page %d", self, nextPage);
- [self setRequestType:AITwitterUpdateDirectMessage
- withDictionary:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:nextPage], @"Page",
- largestTweet, @"LargestTweet", nil]];
- // Gracefully fail: remove all stored objects.
- AILogWithSignature(@"%@ Immediate DM pull fail", self);
- [queuedDM removeAllObjects];
+ AILogWithSignature(@"%@ Largest DM pulled = %@", self, largestTweet);
- AILogWithSignature(@"%@ Largest DM pulled = %@", self, largestTweet);
- [self setPreference:largestTweet
- forKey:TWITTER_PREFERENCE_DM_LAST_ID
- group:TWITTER_PREFERENCE_GROUP_UPDATES];
- // On first load, don't display any direct messages. Just ge the largest ID.
- if (queuedDM.count && lastID) {
- [self displayQueuedUpdatesForRequestType:[self requestTypeForRequestID:identifier]];
- [queuedDM removeAllObjects];
+ [self setPreference:largestTweet + forKey:TWITTER_PREFERENCE_DM_LAST_ID + group:TWITTER_PREFERENCE_GROUP_UPDATES]; - } else if ([self requestTypeForRequestID:identifier] == AITwitterDirectMessageSend) {
- [queuedOutgoingDM addObjectsFromArray:messages];
- [self displayQueuedUpdatesForRequestType:AITwitterDirectMessageSend];
+ // On first load, don't display any direct messages. Just ge the largest ID. + if (queuedDM.count && lastID) { + [self displayQueuedUpdatesForRequestType:identifier]; + [queuedDM removeAllObjects];
- [self clearRequestTypeForRequestID:identifier];
* @brief User information received
-- (void)userInfoReceived:(NSArray *)userInfo forRequest:(NSString *)identifier
- if (([self requestTypeForRequestID:identifier] == AITwitterInitialUserInfo ||
- [self requestTypeForRequestID:identifier] == AITwitterAddFollow) &&
- [[self preferenceForKey:TWITTER_PREFERENCE_LOAD_CONTACTS group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue]) {
+- (void)userInfoReceived:(NSDictionary *)userInfo forRequest:(AITwitterRequestType)identifier + if (identifier == AITwitterInitialUserInfo || + identifier == AITwitterAddFollow) { [[AIContactObserverManager sharedManager] delayListObjectNotifications];
- BOOL nextPageNecessary = NO;
- long long nextCursor = 0;
- for (NSDictionary *info in userInfo) {
- // Iterate users and next_cursor (previous_cursor is not used yet)
+ NSArray *users = [userInfo objectForKey:@"friends"]; + AILogWithSignature(@"%@ User info pull, Users count: %ld", self, users.count); + for (NSDictionary *user in users) { + NSString *twitterInfoUID = [user objectForKey:TWITTER_INFO_UID]; - NSArray *users = [info objectForKey:@"users"];
+ AIListContact *listContact = [self contactWithUID:twitterInfoUID]; - AILogWithSignature(@"%@ User info pull, Users count: %ld", self, users.count);
+ // If the user isn't in a group, set them in the Twitter group. + if (listContact.countOfRemoteGroupNames == 0) { + [listContact addRemoteGroupName:self.timelineGroupName]; - for (NSDictionary *user in users) {
- NSString *twitterInfoUID = [user objectForKey:TWITTER_INFO_UID];
- AIListContact *listContact = [self contactWithUID:twitterInfoUID];
- // If the user isn't in a group, set them in the Twitter group.
- if (listContact.countOfRemoteGroupNames == 0) {
- [listContact addRemoteGroupName:self.timelineGroupName];
- // Grab the Twitter display name and set it as the remote alias.
- if (![[listContact valueForProperty:@"serverDisplayName"] isEqualToString:[user objectForKey:TWITTER_INFO_DISPLAY_NAME]]) {
- [listContact setServersideAlias:[user objectForKey:TWITTER_INFO_DISPLAY_NAME]
- silently:silentAndDelayed];
- // Grab the user icon and set it as their serverside icon.
- [self updateUserIcon:[user objectForKey:TWITTER_INFO_ICON] forContact:listContact];
- // Set the user as available.
- [listContact setStatusWithName:nil
- statusType:AIAvailableStatusType
- // Set the user's status message to their current twitter status text
- NSString *statusText = [[user objectForKey:TWITTER_INFO_STATUS] objectForKey:TWITTER_INFO_STATUS_TEXT];
- if (!statusText) //nil if they've never tweeted
- [listContact setStatusMessage:[NSAttributedString stringWithString:[statusText stringByUnescapingFromXMLWithEntities:nil]] notify:NotifyLater];
- // Set the user as online.
- [listContact setOnline:YES notify:NotifyLater silently:silentAndDelayed];
- [listContact notifyOfChangedPropertiesSilently:silentAndDelayed];
- AILogWithSignature(@"%@ User info pull, Next page necessary: %d", self, nextPageNecessary);
- NSString *nextCursorString = [info objectForKey:TWITTER_INFO_NEXT_CURSOR];
- if (([nextCursorString length] > 0) && self.supportsCursors) {
- nextCursor = [nextCursorString longLongValue];
- nextPageNecessary = (nextCursor > 0) ? YES:NO;
- } else if (!self.supportsCursors) {
- nextPageNecessary = (userInfo.count >= 100);
+ // Grab the Twitter display name and set it as the remote alias. + if (![[listContact valueForProperty:@"serverDisplayName"] isEqualToString:[user objectForKey:TWITTER_INFO_DISPLAY_NAME]]) { + [listContact setServersideAlias:[user objectForKey:TWITTER_INFO_DISPLAY_NAME] + silently:silentAndDelayed]; + // Grab the user icon and set it as their serverside icon. + [self updateUserIcon:[user objectForKey:TWITTER_INFO_ICON] forContact:listContact]; + // Set the user as available. + [listContact setStatusWithName:nil + statusType:AIAvailableStatusType + // Set the user's status message to their current twitter status text + NSString *statusText = [[user objectForKey:TWITTER_INFO_STATUS] objectForKey:TWITTER_INFO_STATUS_TEXT]; + if (!statusText) //nil if they've never tweeted + [listContact setStatusMessage:[NSAttributedString stringWithString:[statusText stringByUnescapingFromXMLWithEntities:nil]] notify:NotifyLater]; + // Set the user as online. + [listContact setOnline:YES notify:NotifyLater silently:silentAndDelayed]; + [listContact notifyOfChangedPropertiesSilently:silentAndDelayed]; [[AIContactObserverManager sharedManager] endListObjectNotificationsDelay];
- if (nextPageNecessary) {
- if (self.supportsCursors) {
- NSString *requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtCursor:nextCursor];
- [self setRequestType:AITwitterInitialUserInfo
- withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithUnsignedLongLong:nextCursor]
- [self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list [additional fail]", "Message when a (vital) twitter request to retrieve the follow list fails")];
- int nextPage = [[[self dictionaryForRequestID:identifier] objectForKey:@"Page"] intValue] + 1;
- NSString *requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtPage:nextPage];
- [self setRequestType:AITwitterInitialUserInfo
- withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:nextPage]
- [self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list [additional fail]", "Message when a (vital) twitter request to retrieve the follow list fails")];
- AILogWithSignature(@"%@ Pulling additional user info page", self);
- } else if ([self boolValueForProperty:@"isConnecting"]) {
- // Trigger our normal update routine.
- } else if ([self requestTypeForRequestID:identifier] == AITwitterProfileUserInfo) {
- NSDictionary *thisUserInfo = [userInfo objectAtIndex:0];
+ } else if (identifier == AITwitterValidateCredentials || + identifier == AITwitterProfileSelf) { + [self filterAndSetUID:[userInfo objectForKey:TWITTER_INFO_UID]];
- AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
- NSArray *keyNames = [NSArray arrayWithObjects:@"name", @"location", @"description", @"url", @"friends_count", @"followers_count", @"statuses_count", nil];
- NSArray *readableNames = [NSArray arrayWithObjects:AILocalizedString(@"Name", nil), AILocalizedString(@"Location", nil),
- AILocalizedString(@"Biography", nil), AILocalizedString(@"Website", nil), AILocalizedString(@"Following", nil),
- AILocalizedString(@"Followers", nil), AILocalizedString(@"Updates", nil), nil];
- NSMutableArray *profileArray = [NSMutableArray array];
- for (NSUInteger idx = 0; idx < keyNames.count; idx++) {
- NSString *keyName = [keyNames objectAtIndex:idx];
- NSString *unattributedValue = [thisUserInfo objectForKey:keyName];
- if(![unattributedValue isEqualToString:@""]) {
- NSString *readableName = [readableNames objectAtIndex:idx];
- NSAttributedString *value;
- if([keyName isEqualToString:@"friends_count"]) {
- value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
- linkDestination:[self addressForLinkType:AITwitterLinkFriends userID:listContact.UID statusID:nil context:nil]];
- } else if ([keyName isEqualToString:@"followers_count"]) {
- value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
- linkDestination:[self addressForLinkType:AITwitterLinkFollowers userID:listContact.UID statusID:nil context:nil]];
- } else if ([keyName isEqualToString:@"statuses_count"]) {
- value = [NSAttributedString attributedStringWithLinkLabel:unattributedValue
- linkDestination:[self addressForLinkType:AITwitterLinkUserPage userID:listContact.UID statusID:nil context:nil]];
- value = [NSAttributedString stringWithString:unattributedValue];
- [profileArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:readableName, KEY_KEY, value, KEY_VALUE, nil]];
- AILogWithSignature(@"%@ Updating profileArray for user %@", self, listContact);
- [listContact setProfileArray:profileArray notify:NotifyNow];
- // Grab their statuses.
- NSString *requestID = [twitterEngine getUserTimelineFor:listContact.UID since:nil startingAtPage:0 count:TWITTER_UPDATE_USER_INFO_COUNT];
- [self setRequestType:AITwitterProfileStatusUpdates
- withDictionary:[NSDictionary dictionaryWithObject:listContact forKey:@"ListContact"]];
- } else if ([self requestTypeForRequestID:identifier] == AITwitterValidateCredentials ||
- [self requestTypeForRequestID:identifier] == AITwitterProfileSelf) {
- for (NSDictionary *info in userInfo) {
- NSString *requestID = [twitterEngine getImageAtURL:[info objectForKey:TWITTER_INFO_ICON]];
- [self setRequestType:AITwitterSelfUserIconPull
- [self filterAndSetUID:[info objectForKey:TWITTER_INFO_UID]];
- if ([info objectForKey:@"name"]) {
- [self setPreference:[[NSAttributedString stringWithString:[info objectForKey:@"name"]] dataRepresentation]
- forKey:KEY_ACCOUNT_DISPLAY_NAME
- group:GROUP_ACCOUNT_STATUS];
- [self setValue:[info objectForKey:@"name"] forProperty:@"Profile Name" notify:NotifyLater];
- [self setValue:[info objectForKey:@"url"] forProperty:@"Profile URL" notify:NotifyLater];
- [self setValue:[info objectForKey:@"location"] forProperty:@"Profile Location" notify:NotifyLater];
- [self setValue:[info objectForKey:@"description"] forProperty:@"Profile Description" notify:NotifyLater];
- [self notifyOfChangedPropertiesSilently:NO];
+ if ([userInfo objectForKey:@"name"]) { + [self setPreference:[[NSAttributedString stringWithString:[userInfo objectForKey:@"name"]] dataRepresentation] + forKey:KEY_ACCOUNT_DISPLAY_NAME + group:GROUP_ACCOUNT_STATUS];
- if([self requestTypeForRequestID:identifier] == AITwitterValidateCredentials) {
- // Our UID is definitely set; grab our friends.
- if ([[self preferenceForKey:TWITTER_PREFERENCE_LOAD_CONTACTS group:TWITTER_PREFERENCE_GROUP_UPDATES] boolValue]) {
- // If we load our follows as contacts, do so now.
- // Delay updates on initial login.
- [self silenceAllContactUpdatesForInterval:18.0];
- NSString *requestID = [twitterEngine getRecentlyUpdatedFriendsFor:self.UID startingAtCursor:-1];
- [self setRequestType:AITwitterInitialUserInfo
- withDictionary:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:-1] forKey:@"Cursor"]];
- [self setLastDisconnectionError:AILocalizedString(@"Unable to retrieve user list", nil)];
- // If we don't load follows as contacts, we've finished connecting (fast, wasn't it?)
- } else if ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable ||
- [self requestTypeForRequestID:identifier] == AITwitterNotificationDisable) {
- BOOL enableNotification = ([self requestTypeForRequestID:identifier] == AITwitterNotificationEnable);
- AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
- for (NSDictionary *info in userInfo) {
- [adium.interfaceController handleMessage:(enableNotification ?
- AILocalizedString(@"Notifications Enabled", nil) :
- AILocalizedString(@"Notifications Disabled", nil))
- withDescription:[NSString stringWithFormat:(enableNotification ?
- AILocalizedString(@"You will now receive device notifications for %@.", nil) :
- AILocalizedString(@"You will no longer receive device notifications for %@.", nil)),
- withWindowTitle:(enableNotification ?
- AILocalizedString(@"Notifications Enabled", nil) :
- AILocalizedString(@"Notifications Disabled", nil))];
- [self clearRequestTypeForRequestID:identifier];
- * @brief Miscellaneous information received
-- (void)miscInfoReceived:(NSArray *)miscInfo forRequest:(NSString *)identifier
- if([self requestTypeForRequestID:identifier] == AITwitterRateLimitStatus) {
- NSDictionary *rateLimit = [miscInfo objectAtIndex:0];
- NSDate *resetDate = [NSDate dateWithTimeIntervalSince1970:[[rateLimit objectForKey:TWITTER_RATE_LIMIT_RESET_SECONDS] intValue]];
- [adium.interfaceController handleMessage:AILocalizedString(@"Current Twitter rate limit", "Message in the rate limit status window")
- withDescription:[NSString stringWithFormat:AILocalizedString(@"You have %d/%d more requests for %@.", "The first %d is the number of requests, the second is the total number of requests per hour. The %@ is the duration of time until the count resets."),
- [[rateLimit objectForKey:TWITTER_RATE_LIMIT_REMAINING] intValue],
- [[rateLimit objectForKey:TWITTER_RATE_LIMIT_HOURLY_LIMIT] intValue],
- [NSDateFormatter stringForTimeInterval:[resetDate timeIntervalSinceNow]
- withWindowTitle:AILocalizedString(@"Rate Limit Status", nil)];
- [self clearRequestTypeForRequestID:identifier];
- * @brief Requested image received
-- (void)imageReceived:(NSImage *)image forRequest:(NSString *)identifier
- if([self requestTypeForRequestID:identifier] == AITwitterUserIconPull) {
- AIListContact *listContact = [[self dictionaryForRequestID:identifier] objectForKey:@"ListContact"];
- AILogWithSignature(@"%@ Updated user icon for %@", self, listContact);
- [listContact setServersideIconData:[image TIFFRepresentation]
- [listContact setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
- } else if([self requestTypeForRequestID:identifier] == AITwitterSelfUserIconPull) {
- AILogWithSignature(@"Updated self icon for %@", self);
- // Set a property so we don't re-send thie image we're just now downloading.
- [self setValue:[NSNumber numberWithBool:YES] forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever];
- [self setPreference:[NSNumber numberWithBool:YES]
- forKey:KEY_USE_USER_ICON
- group:GROUP_ACCOUNT_STATUS];
+ [self setValue:[userInfo objectForKey:@"name"] forProperty:@"Profile Name" notify:NotifyLater]; + [self setValue:[userInfo objectForKey:@"url"] forProperty:@"Profile URL" notify:NotifyLater]; + [self setValue:[userInfo objectForKey:@"location"] forProperty:@"Profile Location" notify:NotifyLater]; + [self setValue:[userInfo objectForKey:@"description"] forProperty:@"Profile Description" notify:NotifyLater]; - [self setPreference:[image TIFFRepresentation]
- group:GROUP_ACCOUNT_STATUS];
+ NSString *imageURL = [userInfo objectForKey:TWITTER_INFO_ICON]; + NSURLRequest *imageRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:imageURL]]; + [NSURLConnection sendAsynchronousRequest:imageRequest + queue:[NSOperationQueue currentQueue] + completionHandler:^(NSURLResponse *resp, NSData *data, NSError *error) { + NSImage *image = [[NSImage alloc] initWithData:data]; + AILogWithSignature(@"Updated self icon for %@", self); + // Set a property so that we don't re-send the image we're just now downloading. + [self setValue:[NSNumber numberWithBool:YES] forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever]; + [self setPreference:[NSNumber numberWithBool:YES] + forKey:KEY_USE_USER_ICON + group:GROUP_ACCOUNT_STATUS]; + [self setPreference:[image TIFFRepresentation] + group:GROUP_ACCOUNT_STATUS]; + [self notifyOfChangedPropertiesSilently:NO]; + [self setValue:nil forProperty:TWITTER_PROPERTY_REQUESTED_USER_ICON notify:NotifyNever]; + [self requestFailed:AITwitterSelfUserIconPull withError:error userInfo:nil];
- [self clearRequestTypeForRequestID:identifier];