Index: Witty/Witty/Witty.csproj =================================================================== --- Witty/Witty/Witty.csproj (revision 349) +++ Witty/Witty/Witty.csproj (working copy) @@ -225,6 +225,10 @@ {EA9BEE27-11D2-4346-83D9-9A056282B7B9} Common + + {580176B8-7199-406C-AFFB-C8B81450AEDC} + IdenticaLib + {6CB55919-6C11-43A1-8769-57461BE5B0AE} TwitterLib Index: Witty/Witty/MainWindow.xaml.cs =================================================================== --- Witty/Witty/MainWindow.xaml.cs (revision 349) +++ Witty/Witty/MainWindow.xaml.cs (working copy) @@ -14,8 +14,10 @@ using TwitterLib.Utilities; using Witty.ClickOnce; using Witty.Properties; +using IdenticaLib; using System.Windows.Documents; + namespace Witty { public partial class MainWindow @@ -96,9 +98,10 @@ System.Security.SecureString password = TwitterNet.DecryptString(AppSettings.Password); + // Jason Follas: Reworked Web Proxy - don't need to explicitly pass into TwitterNet ctor //twitter = new TwitterNet(AppSettings.Username, password, WebProxyHelper.GetConfiguredWebProxy()); - twitter = new TwitterNet(AppSettings.Username, password); + twitter = new IdenticaAPI(AppSettings.Username, password); // Jason Follas: Twitter proxy servers, anyone? (Network Nazis who block twitter.com annoy me) twitter.TwitterServerUrl = AppSettings.TwitterHost; @@ -178,10 +181,10 @@ private delegate void NoArgDelegate(); private delegate void OneArgDelegate(TweetCollection arg); private delegate void OneStringArgDelegate(string arg); - private delegate void AddTweetUpdateDelegate(Tweet arg); + private delegate void AddTweetUpdateDelegate(ITweet arg); private delegate void MessagesDelegate(DirectMessageCollection arg); private delegate void SendMessageDelegate(string user, string text); - private delegate void LoginDelegate(User arg); + private delegate void LoginDelegate(IUser arg); private delegate void DeleteTweetDelegate(double id); // Settings used by the application @@ -224,14 +227,14 @@ private int popupCount = 0; - internal Tweet SelectedTweet + internal ITweet SelectedTweet { get { - Tweet selectedTweet = null; + ITweet selectedTweet = null; if (this.currentView == CurrentView.Replies) { - if (null != RepliesListBox.SelectedItem) selectedTweet = (Tweet)RepliesListBox.SelectedItem; + if (null != RepliesListBox.SelectedItem) selectedTweet = (ITweet)RepliesListBox.SelectedItem; } else if (this.currentView == CurrentView.Messages) { @@ -239,7 +242,7 @@ } else { - if (null != TweetsListBox.SelectedItem) selectedTweet = (Tweet)TweetsListBox.SelectedItem; + if (null != TweetsListBox.SelectedItem) selectedTweet = (ITweet)TweetsListBox.SelectedItem; } return selectedTweet; } @@ -317,7 +320,7 @@ // Add the new tweets for (int i = newTweets.Count - 1; i >= 0; i--) { - Tweet tweet = newTweets[i]; + ITweet tweet = newTweets[i]; if (!tweets.Contains(tweet)) { tweets.Insert(0, tweet); @@ -392,7 +395,7 @@ else { int index = 0; - foreach (Tweet tweet in newTweets) + foreach (ITweet tweet in newTweets) { Popup p = new Popup(tweet, index++); p.FadeOutFinished += new FadeOutFinishedDelegate(RemovePopup); @@ -408,7 +411,7 @@ private static string BuiltNewTweetMessage(TweetCollection newTweets) { string message = string.Format("You have {0} new tweets!\n", newTweets.Count); - foreach (Tweet tweet in newTweets) + foreach (ITweet tweet in newTweets) { message += " " + tweet.User.ScreenName; } @@ -440,7 +443,7 @@ } else { - foreach (Tweet tweet in newTweets) + foreach (ITweet tweet in newTweets) { SnarlInterface.SendMessage(string.Format("New Tweet from {0}", tweet.User.ScreenName), string.Format("{0}\n\n{1}", tweet.Text, tweet.RelativeTime), "", 4); } @@ -455,7 +458,7 @@ private static void UpdateExistingTweets(TweetCollection oldTweets) { // Update existing tweets - foreach (Tweet tweet in oldTweets) + foreach (ITweet tweet in oldTweets) { tweet.IsNew = false; tweet.UpdateRelativeTime(); @@ -489,7 +492,7 @@ TinyUrlHelper tinyUrls = new TinyUrlHelper(); tweetText = tinyUrls.ConvertUrlsToTinyUrls(tweetText); } - Tweet tweet = twitter.AddTweet(tweetText); ; + ITweet tweet = twitter.AddTweet(tweetText); ; // Schedule the update function in the UI thread. LayoutRoot.Dispatcher.BeginInvoke( @@ -514,7 +517,7 @@ } - private void UpdatePostUserInterface(Tweet newlyAdded) + private void UpdatePostUserInterface(ITweet newlyAdded) { if (newlyAdded != null) { @@ -607,7 +610,7 @@ for (int i = newReplies.Count - 1; i >= 0; i--) { - Tweet reply = newReplies[i]; + ITweet reply = newReplies[i]; if (!replies.Contains(reply)) { replies.Insert(0, reply); @@ -807,7 +810,7 @@ fakeTweets[0].Id = -1; fakeTweets[0].Text = userNotFoundEx.Message; fakeTweets[0].Source = "Witty Error Handler"; - fakeTweets[0].User = new User(); + fakeTweets[0].User = new TwitterLib.User(); fakeTweets[0].User.ScreenName = "@" + userNotFoundEx.UserId; fakeTweets[0].User.Description = userNotFoundEx.Message; @@ -842,7 +845,7 @@ for (int i = newTweets.Count - 1; i >= 0; i--) { - Tweet tweet = newTweets[i]; + ITweet tweet = newTweets[i]; if (!userTweets.Contains(tweet)) { userTweets.Insert(0, tweet); @@ -892,7 +895,7 @@ } } - private void UpdatePostLoginInterface(User user) + private void UpdatePostLoginInterface(IUser user) { App.LoggedInUser = user; if (App.LoggedInUser != null) @@ -928,9 +931,10 @@ private void LoginControl_Login(object sender, RoutedEventArgs e) { + // Jason Follas: Reworked Web Proxy - don't need to explicitly pass into TwitterNet ctor //twitter = new TwitterNet(AppSettings.Username, TwitterNet.DecryptString(AppSettings.Password), WebProxyHelper.GetConfiguredWebProxy()); - twitter = new TwitterNet(AppSettings.Username, TwitterNet.DecryptString(AppSettings.Password)); + twitter = new IdenticaAPI(AppSettings.Username, TwitterNet.DecryptString(AppSettings.Password)); // Jason Follas: Twitter proxy servers, anyone? (Network Nazis who block twitter.com annoy me) twitter.TwitterServerUrl = AppSettings.TwitterHost; @@ -1353,7 +1357,7 @@ { if (listbox.SelectedItem != null && currentView != CurrentView.User) { - Tweet tweet = (Tweet)listbox.SelectedItem; + ITweet tweet = (ITweet)listbox.SelectedItem; //System.Diagnostics.Process.Start(tweet.User.TwitterUrl); DelegateUserTimelineFetch(tweet.User.ScreenName); } @@ -1578,7 +1582,7 @@ createDirectMessage(screenName); } - void PopupClicked(Tweet tweet) + void PopupClicked(ITweet tweet) { if (this.WindowState == WindowState.Minimized) { @@ -1788,7 +1792,7 @@ // TODO: this should be displayed somewhere else instead of the main tweets listbox. for (int i = searchResults.Count - 1; i >= 0; i--) { - Tweet tweet = searchResults[i]; + ITweet tweet = searchResults[i]; if (!tweets.Contains(tweet)) { tweets.Insert(0, tweet); Index: Witty/Witty/App.xaml.cs =================================================================== --- Witty/Witty/App.xaml.cs (revision 349) +++ Witty/Witty/App.xaml.cs (working copy) @@ -16,7 +16,7 @@ public static readonly ILog Logger = LogManager.GetLogger("Witty.Logging"); // Global variable for the user - public static User LoggedInUser = null; + public static IUser LoggedInUser = null; protected override void OnStartup(StartupEventArgs e) { Index: Witty/Witty/Popup.xaml.cs =================================================================== --- Witty/Witty/Popup.xaml.cs (revision 349) +++ Witty/Witty/Popup.xaml.cs (working copy) @@ -11,14 +11,14 @@ public delegate void PopupReplyClickedDelegate(string screenName); public delegate void PopupDirectMessageClickedDelegate(string screenName); public delegate void PopupCloseButtonClickedDelegate(Popup p); - public delegate void PopupClickedDelegate(Tweet tweet); + public delegate void PopupClickedDelegate(ITweet tweet); /// /// Interaction logic for Popup.xaml /// public partial class Popup : Window { - private Tweet _tweet; + private ITweet _tweet; private Storyboard sbFadeOut; private Storyboard ShowPopup; private TimeSpan ts = new TimeSpan(); @@ -28,7 +28,7 @@ public event PopupCloseButtonClickedDelegate CloseButtonClicked; public event PopupClickedDelegate Clicked; - public Popup(Tweet tweet, Int32 numPopups) : this(tweet.User.ScreenName, tweet.Text, tweet.User.ImageUrl, numPopups) + public Popup(ITweet tweet, Int32 numPopups) : this(tweet.User.ScreenName, tweet.Text, tweet.User.ImageUrl, numPopups) { _tweet = tweet; } @@ -43,9 +43,10 @@ tweetText.Text = body; userName.Text = heading; - ImageSourceConverter conv = new ImageSourceConverter(); - avatarImage.Source = (ImageSource)conv.ConvertFromString(imageSource); - + if (imageSource != null) { + ImageSourceConverter conv = new ImageSourceConverter(); + avatarImage.Source = (ImageSource)conv.ConvertFromString(imageSource); + } this.Topmost = true; sbFadeOut = (Storyboard)FindResource("sbFadeOut"); Index: Witty/Witty/LoginControl.xaml.cs =================================================================== --- Witty/Witty/LoginControl.xaml.cs (revision 349) +++ Witty/Witty/LoginControl.xaml.cs (working copy) @@ -2,6 +2,7 @@ using System.Windows; using log4net; using TwitterLib; +using IdenticaLib; namespace Witty { @@ -11,8 +12,8 @@ private readonly Properties.Settings AppSettings = Properties.Settings.Default; - private delegate void LoginDelegate(TwitterNet arg); - private delegate void PostLoginDelegate(User arg); + private delegate void LoginDelegate(IServiceApi arg); + private delegate void PostLoginDelegate(IUser arg); public LoginControl() { @@ -23,7 +24,7 @@ { // Jason Follas: Reworked Web Proxy - don't need to explicitly pass into TwitterNet ctor //TwitterNet twitter = new TwitterNet(UsernameTextBox.Text, TwitterNet.ToSecureString(PasswordTextBox.Password), WebProxyHelper.GetConfiguredWebProxy()); - TwitterNet twitter = new TwitterNet(UsernameTextBox.Text, TwitterNet.ToSecureString(PasswordTextBox.Password)); //, WebProxyHelper.GetConfiguredWebProxy()); + IServiceApi twitter = new IdenticaAPI(UsernameTextBox.Text, TwitterNet.ToSecureString(PasswordTextBox.Password)); //, WebProxyHelper.GetConfiguredWebProxy()); // Jason Follas: Twitter proxy servers, anyone? (Network Nazis who block twitter.com annoy me) twitter.TwitterServerUrl = AppSettings.TwitterHost; @@ -34,7 +35,7 @@ new LoginDelegate(TryLogin), twitter); } - private void TryLogin(TwitterNet twitter) + private void TryLogin(IServiceApi twitter) { try { @@ -60,7 +61,7 @@ } } - private void UpdatePostLoginInterface(User user) + private void UpdatePostLoginInterface(IUser user) { App.LoggedInUser = user; if (App.LoggedInUser != null) Index: Witty/WittyUnitTests/TwitterNetTest.cs =================================================================== --- Witty/WittyUnitTests/TwitterNetTest.cs (revision 349) +++ Witty/WittyUnitTests/TwitterNetTest.cs (working copy) @@ -14,7 +14,7 @@ protected int alanUserId; protected int alanFriendsCount; - protected User testUser; + protected IUser testUser; [SetUp] public void Init() @@ -31,7 +31,7 @@ public void Login() { TwitterNet twitter = new TwitterNet(username, password); - User user = twitter.Login(); + IUser user = twitter.Login(); Assert.AreEqual(userId, user.Id); } @@ -75,7 +75,7 @@ public void GetUser() { TwitterNet twitter = new TwitterNet(username,password); - User user = twitter.GetUser(userId); + IUser user = twitter.GetUser(userId); Assert.AreEqual(username, user.ScreenName); } } Index: Witty/IdenticaLib/User.cs =================================================================== --- Witty/IdenticaLib/User.cs (revision 0) +++ Witty/IdenticaLib/User.cs (revision 0) @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.Text; +using TwitterLib; +using System.ComponentModel; + +namespace IdenticaLib { + + [Serializable] + public class User : INotifyPropertyChanged, TwitterLib.IUser { + private int id; + private string name; + private string screenName; + private string imageUrl; + private string siteUrl; + private string location; + private string description; + + public int Id { + get { return id; } + set { + if (value != id) { + id = value; + OnPropertyChanged("Id"); + } + } + } + + public string Name { + get { return name; } + set { + if (value != name) { + name = value; + OnPropertyChanged("Name"); + } + } + } + + public string ScreenName { + get { return screenName; } + set { + if (value != screenName) { + screenName = value; + OnPropertyChanged("ScreenName"); + } + } + } + + public string ImageUrl { + get { + return imageUrl; + } + set { + if (value != imageUrl) { + imageUrl = value; + OnPropertyChanged("ImageUrl"); + } + } + } + + public string SiteUrl { + get { return siteUrl; } + set { + if (value != siteUrl) { + siteUrl = value; + OnPropertyChanged("SiteUrl"); + } + } + } + + public string TwitterUrl { + get { + return "http://identica.ca/" + screenName; + } + } + + public string Location { + get { return location; } + set { + if (value != location) { + location = value; + OnPropertyChanged("location"); + } + } + } + + public string Description { + get { return description; } + set { + if (value != description) { + description = value; + OnPropertyChanged("Description"); + } + } + } + + /// + /// The user name along with the screenname. This makes binding a little easier. + /// + public string FullName { + get { return Name + " (" + screenName + ")"; } + } + + private string backgroundColor; + + public string BackgroundColor { + get { return backgroundColor; } + set { + if (value != backgroundColor) { + backgroundColor = value; + OnPropertyChanged("BackgroundColor"); + } + } + } + + private string textColor; + + public string TextColor { + get { return textColor; } + set { + if (value != textColor) { + textColor = value; + OnPropertyChanged("TextColor"); + } + } + } + private string linkColor; + + public string LinkColor { + get { return linkColor; } + set { + if (value != linkColor) { + linkColor = value; + OnPropertyChanged("LinkColor"); + } + } + } + + private string sidebarFillColor; + + public string SidebarFillColor { + get { return sidebarFillColor; } + set { + if (value != sidebarFillColor) { + sidebarFillColor = value; + OnPropertyChanged("SidebarFillColor"); + } + } + } + + private string sidebarBorderColor; + + public string SidebarBorderColor { + get { return sidebarBorderColor; } + set { + if (value != sidebarBorderColor) { + sidebarBorderColor = value; + OnPropertyChanged("SidebarBorderColor"); + } + } + } + + private ITweet tweet; + + public ITweet Tweet { + get { return tweet; } + set { + if (value != tweet) { + tweet = value; + OnPropertyChanged("Tweet"); + } + } + } + + private int followingCount; + + public int FollowingCount { + get { return followingCount; } + set { + if (value != followingCount) { + followingCount = value; + OnPropertyChanged("FollowingCount"); + } + } + } + + private int followersCount; + + public int FollowersCount { + get { return followersCount; } + set { + if (value != followersCount) { + followersCount = value; + OnPropertyChanged("FollowersCount"); + } + } + } + + private int statusesCount; + + public int StatusesCount { + get { return statusesCount; } + set { + if (value != statusesCount) { + statusesCount = value; + OnPropertyChanged("StatusesCount"); + } + } + } + + private int favoritesCount; + + public int FavoritesCount { + get { return favoritesCount; } + set { + if (value != favoritesCount) { + favoritesCount = value; + OnPropertyChanged("FavoritesCount"); + } + } + } + + #region INotifyPropertyChanged Members + + /// + /// INotifyPropertyChanged requires a property called PropertyChanged. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Fires the event for the property when it changes. + /// + protected virtual void OnPropertyChanged(string propertyName) { + if (PropertyChanged != null) + PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion + } +} Index: Witty/IdenticaLib/Notice.cs =================================================================== --- Witty/IdenticaLib/Notice.cs (revision 0) +++ Witty/IdenticaLib/Notice.cs (revision 0) @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Text; +using TwitterLib; +using System.ComponentModel; + +namespace IdenticaLib { + [Serializable] + public class Notice : INotifyPropertyChanged, TwitterLib.ITweet { + #region Private fields + + private double id; + private DateTime? dateCreated; + private string relativeTime; + private string text; + private string source; + private IUser user; + private bool isNew; + private int index; + private bool isSearchResult; + + #endregion + + #region Public Properties + + /// + /// The Tweet id + /// + public double Id { + get { return id; } + set { + if (value != id) { + id = value; + OnPropertyChanged("Id"); + } + } + } + + /// + /// Date and time the tweet was added + /// + public DateTime? DateCreated { + get { return dateCreated; } + set { + if (value != dateCreated) { + dateCreated = value; + OnPropertyChanged("DateCreated"); + UpdateRelativeTime(); + } + } + } + + /// + /// The Tweet text + /// + public string Text { + get { return text; } + set { + if (value != text) { + text = value; + OnPropertyChanged("Text"); + } + } + } + + /// + /// The Tweet source + /// + public string Source { + get { return "from " + source; } + set { + if (value != source) { + source = value; + OnPropertyChanged("Source"); + } + } + } + + /// + /// Twitter User associated with the Tweet + /// + public IUser User { + get { return user; } + set { + if (value != user) { + user = value; + OnPropertyChanged("User"); + } + } + } + + /// + /// How long ago the tweet was added based on DatedCreated and DateTime.Now + /// + public string RelativeTime { + get { + return relativeTime; + } + set { + relativeTime = value; + OnPropertyChanged("Relativetime"); + } + } + + public bool IsNew { + get { return isNew; } + set { + if (value != isNew) { + isNew = value; + OnPropertyChanged("IsNew"); + } + } + } + + public int Index { + get { return index; } + set { + if (value != index) { + index = value; + OnPropertyChanged("Index"); + } + } + } + + public bool IsSearchResult { + get { return isSearchResult; } + set { + if (value != isSearchResult) { + isSearchResult = value; + OnPropertyChanged("IsSearchResult"); + } + } + } + #endregion + + #region INotifyPropertyChanged Members + + /// + /// INotifyPropertyChanged requires a property called PropertyChanged. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Fires the event for the property when it changes. + /// + protected virtual void OnPropertyChanged(string propertyName) { + if (PropertyChanged != null) + PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion + + #region IEquatable Members + + /// + /// Tweets are the same if they have the same Id. + /// Collection.Contains needs IEquatable implemented to be effective. + /// + public bool Equals(ITweet other) { + if (other == null) + throw new ArgumentNullException("other"); + + return (Id == other.Id); + } + + #endregion + + /// + /// Updates the relativeTime based on the DateCreated and DateTime.Now + /// + public void UpdateRelativeTime() { + if (!dateCreated.HasValue) + RelativeTime = string.Empty; + + DateTime StatusCreatedDate = (DateTime)dateCreated; + + TimeSpan ts = new TimeSpan(DateTime.Now.Ticks - StatusCreatedDate.Ticks); + double delta = ts.TotalSeconds; + + if (delta <= 1) { + RelativeTime = "a second ago"; + } else if (delta < 60) { + RelativeTime = ts.Seconds + " seconds ago"; + } else if (delta < 120) { + RelativeTime = "about a minute ago"; + } else if (delta < (45 * 60)) { + RelativeTime = ts.Minutes + " minutes ago"; + } else if (delta < (90 * 60)) { + RelativeTime = "about an hour ago"; + } else if (delta < (24 * 60 * 60)) { + RelativeTime = "about " + ts.Hours + " hours ago"; + } else if (delta < (48 * 60 * 60)) { + RelativeTime = "1 day ago"; + } else { + RelativeTime = ts.Days + " days ago"; + } + } + } +} Index: Witty/IdenticaLib/IdenticaLib.csproj =================================================================== --- Witty/IdenticaLib/IdenticaLib.csproj (revision 0) +++ Witty/IdenticaLib/IdenticaLib.csproj (revision 0) @@ -0,0 +1,69 @@ + + + + Debug + AnyCPU + 9.0.21022 + 2.0 + {580176B8-7199-406C-AFFB-C8B81450AEDC} + Library + Properties + IdenticaLib + IdenticaLib + v3.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + 3.5 + + + 3.5 + + + + + 3.0 + + + + + + + + + + + {6CB55919-6C11-43A1-8769-57461BE5B0AE} + TwitterLib + + + + + \ No newline at end of file Index: Witty/IdenticaLib/Properties/AssemblyInfo.cs =================================================================== --- Witty/IdenticaLib/Properties/AssemblyInfo.cs (revision 0) +++ Witty/IdenticaLib/Properties/AssemblyInfo.cs (revision 0) @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("IdenticaLib")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("IdenticaLib")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2008")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4646c9a5-75c1-4860-87af-9104ac6877b7")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] Index: Witty/IdenticaLib/IdenticaAPI.cs =================================================================== --- Witty/IdenticaLib/IdenticaAPI.cs (revision 0) +++ Witty/IdenticaLib/IdenticaAPI.cs (revision 0) @@ -0,0 +1,1255 @@ +using System; +using System.Collections.Generic; +using System.Text; +using TwitterLib; +using System.Xml; +using System.IO; +using System.Net; +using System.Security; +using System.Web; +using System.Text.RegularExpressions; +using System.Globalization; + +namespace IdenticaLib { + public class IdenticaAPI : IServiceApi + { + #region Private Fields + private string username; + private SecureString password; + private string loginUrl; + private string publicTimelineUrl; + private string friendsTimelineUrl; + private string userTimelineUrl; + private string repliesTimelineUrl; + private string directMessagesUrl; + private string updateUrl; + private string friendsUrl; + private string followersUrl; + private string userShowUrl; + private string sendMessageUrl; + private string destroyUrl; + private string destroyDirectMessageUrl; + private string createFriendshipUrl; + private string twitterServerUrl; + private string format; + private IWebProxy webProxy; + private CookieContainer cookieJar = new CookieContainer(); + + private IUser currentLoggedInUser; + + private static int characterLimit; + private string clientName; + + #endregion + + #region Public Properties + + public IUser CurrentlyLoggedInUser + { + get + { + if (null == currentLoggedInUser) + { + currentLoggedInUser = Login(); + } + return currentLoggedInUser; + } + set + { + currentLoggedInUser = value; + } + } + + /// + /// Identi.ca username + /// + public string UserName + { + get { return (null == currentLoggedInUser ? currentLoggedInUser.Name: String.Empty) ; } + } + + /// + /// Identi.ca password + /// + public SecureString Password + { + get { return password; } + set { password = value; } + } + + /// + /// Web proxy to use when communicating with the Identi.ca service + /// + public IWebProxy WebProxy + { + get { return webProxy; } + set { webProxy = value; } + } + + public string LoginUrl { + get { + if (string.IsNullOrEmpty(loginUrl)) + return "http://identi.ca/main/login"; + else + return loginUrl; + } + set { loginUrl = value; } + } + + + + /// + /// Url to the Identi.ca Public Timeline. + /// + /// + /// This value should only be changed if Identi.ca API urls have been changed on http://laconi.ca/Main/Version0API + /// + public string PublicTimelineUrl + { + get + { + if (string.IsNullOrEmpty(publicTimelineUrl)) + return "http://identi.ca/rss"; + else + return publicTimelineUrl; + } + set { publicTimelineUrl = value; } + } + + /// + /// Url to the Identi.ca Friends Timeline. Defaults to http://Identi.ca.com/statuses/friends_timeline + /// + /// + /// This value should only be changed if Identi.ca API urls have been changed on http://laconi.ca/Main/Version0API + /// + public string FriendsTimelineUrl + { + get + { + if (string.IsNullOrEmpty(friendsTimelineUrl)) + return "http://identi.ca/{username}/all/rss"; + else + return friendsTimelineUrl; + } + set { friendsTimelineUrl = value; } + } + + /// + /// Url to the user's timeline. Defaults to http://Identi.ca.com/statuses/user_timeline + /// + /// + /// This value should only be changed if Identi.ca API urls have been changed on http://laconi.ca/Main/Version0API + /// + public string UserTimelineUrl + { + get + { + if (string.IsNullOrEmpty(userTimelineUrl)) + return "http://identi.ca/{username}/rss"; + else + return userTimelineUrl; + } + set { userTimelineUrl = value; } + } + + /// + /// Url to the 20 most recent replies (status updates prefixed with @username posted by users who are friends with the user being replied to) to the authenticating user. + /// Defaults to http://Identi.ca.com/statuses/user_timeline + /// + /// + /// This value should only be changed if Identi.ca API urls have been changed on http://laconi.ca/Main/Version0API + /// + public string RepliesTimelineUrl + { + get + { + if (string.IsNullOrEmpty(repliesTimelineUrl)) + return "http://identi.ca/{username}/replies/rss"; + else + return repliesTimelineUrl; + } + set { repliesTimelineUrl = value; } + } + + /// + /// Url to the list of the 20 most recent direct messages sent to the authenticating user. + /// + /// + /// Identi.ca does not yet support direct messages + /// + public string DirectMessagesUrl + { + get + { + throw new NotImplementedException(); + } + set { directMessagesUrl = value; } + } + + /// + /// Url to the Identi.ca HTTP Post. + /// + /// + /// This value should only be changed if Identi.ca API urls have been changed on http://laconi.ca/Main/Version0API + /// + public string UpdateUrl + { + get + { + if (string.IsNullOrEmpty(updateUrl)) + return "http://identi.ca/notice/new"; + else + return updateUrl; + } + set { updateUrl = value; } + } + + /// + /// Url to the user's friends. Defaults to http://Identi.ca.com/statuses/friends + /// + /// + /// This value should only be changed if Identi.ca API urls have been changed on http://laconi.ca/Main/Version0API + /// + public string FriendsUrl + { + get + { + if (string.IsNullOrEmpty(friendsUrl)) + return "http://Identi.ca.com/statuses/friends"; + else + return friendsUrl; + } + set { friendsUrl = value; } + } + + /// + /// Url to the user's followers. Defaults to http://Identi.ca.com/statuses/followers + /// + /// + /// This value should only be changed if Identi.ca API urls have been changed on http://laconi.ca/Main/Version0API + /// + public string FollowersUrl + { + get + { + if (string.IsNullOrEmpty(followersUrl)) + return "http://Identi.ca.com/statuses/followers"; + else + return followersUrl; + } + set { followersUrl = value; } + } + + /// + /// Returns extended information of a given user, specified by ID or screen name as per the required id parameter below. + /// This information includes design settings, so third party developers can theme their widgets according to a given user's preferences. + /// Defaults to http://Identi.ca.com/users/show/ + /// + /// + /// This value should only be changed if Identi.ca API urls have been changed on http://laconi.ca/Main/Version0API + /// + public string UserShowUrl + { + get + { + if (string.IsNullOrEmpty(userShowUrl)) + return "http://identi.ca/"; + else + return userShowUrl; + } + set { userShowUrl = value; } + } + + /// + /// Url to sends a new direct message to the specified user from the authenticating user. Defaults to http://Identi.ca.com/direct_messages/new + /// + /// + /// This value should only be changed if Identi.ca API urls have been changed on http://laconi.ca/Main/Version0API + /// + public string SendMessageUrl + { + get + { + if (string.IsNullOrEmpty(sendMessageUrl)) + return "http://identi.ca/notice/new"; + else + return sendMessageUrl; + } + set { sendMessageUrl = value; } + } + + /// + /// Url to destroy a status from the authenticating user. Defaults to http://Identi.ca.com/statuses/destroy + /// + /// + /// This value should only be changed if Identi.ca API urls have been changed on http://laconi.ca/Main/Version0API + /// + public string DestroyUrl + { + get + { + if (string.IsNullOrEmpty(destroyUrl)) + return "http://Identi.ca.com/statuses/destroy/"; + else + return destroyUrl; + } + set { destroyUrl = value; } + } + + /// + /// Url to destroy a direct message from the authenticating user. Defaults to http://Identi.ca.com/direct_messages/destroy/ + /// + /// + /// This value should only be changed if Identi.ca API urls have been changed on http://laconi.ca/Main/Version0API + /// + public string DestroyDirectMessageUrl + { + get + { + if (string.IsNullOrEmpty(destroyDirectMessageUrl)) + return "http://Identi.ca.com/direct_messages/destroy/"; + else + return destroyDirectMessageUrl; + } + set { destroyDirectMessageUrl = value; } + } + + + + public string CreateFriendshipUrl + { + get { + if (string.IsNullOrEmpty(createFriendshipUrl)) + { + return "http://Identi.ca.com/friendships/create/"; + } + else + { + return createFriendshipUrl; + } + } + set { createFriendshipUrl = value; } + } + + + + /// + /// URL of the Twitter host. Defaults to http://twitter.com/. Why would + /// you need to override this? Think: Alternate endpoints, like other + /// services with identical API's, or maybe a Twitter Proxy. + /// + public string TwitterServerUrl { + get { + if (String.IsNullOrEmpty(twitterServerUrl)) + return "http://identi.ca/"; + else + return twitterServerUrl; + } + set { + twitterServerUrl = value; + + if (!twitterServerUrl.EndsWith("/")) + twitterServerUrl += "/"; + } + } + + /// + /// The format of the results from the Identi.ca API. Ex: .xml, .json, .rss, .atom. Defaults to ".xml" + /// + /// + /// This value should only be changed if Identi.ca API urls have been changed on http://laconi.ca/Main/Version0API + /// + public string Format + { + get + { + if (string.IsNullOrEmpty(format)) + return ".xml"; + else + return format; + } + set { format = value; } + } + + /// + /// The number of characters available for the Tweet text. Defaults to 140. + /// + /// + /// This value should only be changed if the character limit on Identi.ca.com has been changed. + /// + public static int CharacterLimit + { + get + { + if (characterLimit == 0) + return 140; + else + return characterLimit; + } + set { characterLimit = value; } + } + + /// + /// The name of the current client. Defaults to "Witty" + /// + /// + /// This value can be changed if you're using this Library for your own Identi.ca client + /// + public string ClientName + { + get + { + if (string.IsNullOrEmpty(clientName)) + return "witty"; + else + return clientName; + } + set { clientName = value; } + } + + #endregion + + #region Constructors + + /// + /// Unauthenticated constructor + /// + public IdenticaAPI() + { + this.Format = String.Empty; + } + + /// + /// Authenticated constructor + /// + public IdenticaAPI(string username, SecureString password) + { + this.username = username; + this.password = password; + this.Format = String.Empty; + } + + /// + /// Authenticated constructor with Proxy + /// + public IdenticaAPI(string username, SecureString password, IWebProxy webProxy) + { + this.username = username; + this.password = password; + this.webProxy = webProxy; + this.Format = String.Empty; + } + + #endregion + + #region Public Methods + + #region Timeline Methods + + /// + /// Retrieves the public timeline + /// + public TweetCollection GetPublicTimeline() + { + return RetrieveTimeline(Timeline.Public); + } + + /// + /// Retrieves the public timeline. Narrows the result to after the since date. + /// + public TweetCollection GetPublicTimeline(string since) + { + return RetrieveTimeline(Timeline.Public, since); + } + + /// + /// Retrieves the friends timeline + /// + public TweetCollection GetFriendsTimeline() + { + if (!string.IsNullOrEmpty(username)) + return RetrieveTimeline(Timeline.Friends); + else + return RetrieveTimeline(Timeline.Public); + } + + /// + /// Retrieves the friends timeline. Narrows the result to after the since date. + /// + public TweetCollection GetFriendsTimeline(string since) + { + if (!string.IsNullOrEmpty(username)) + return RetrieveTimeline(Timeline.Friends, since); + else + return RetrieveTimeline(Timeline.Public, since); + } + + /// + /// Retrieves the friends timeline. Narrows the result to after the since date. + /// + public TweetCollection GetFriendsTimeline(string since, string userId) + { + return RetrieveTimeline(Timeline.Friends, since, userId); + } + + public TweetCollection GetUserTimeline(string userId) + { + return RetrieveTimeline(Timeline.User, "", userId); + } + + public TweetCollection GetReplies() + { + return RetrieveTimeline(Timeline.Replies); + } + + #endregion + + /// + /// Returns the authenticated user's friends who have most recently updated, each with current status inline. + /// + public UserCollection GetFriends() + { + return GetFriends(CurrentlyLoggedInUser.Id); + } + + /// + /// Returns the user's friends who have most recently updated, each with current status inline. + /// + public UserCollection GetFriends(int userId) + { + UserCollection users = new UserCollection(); + + // Identi.ca expects http://Identi.ca.com/statuses/friends/12345.xml + string requestURL = FriendsUrl + "/" + userId + Format; + + int friendsCount = 0; + + // Since the API docs state "Returns up to 100 of the authenticating user's friends", we need + // to use the page param and to fetch ALL of the users friends. We can find out how many pages + // we need by dividing the # of friends by 100 and rounding any remainder up. + // merging the responses from each request may be tricky. + if (currentLoggedInUser != null && currentLoggedInUser.Id == userId) + { + friendsCount = CurrentlyLoggedInUser.FollowingCount; + } + else + { + // need to make an extra call to Identi.ca + IUser user = GetUser(userId); + friendsCount = user.FollowingCount; + } + + int numberOfPagesToFetch = (friendsCount / 100) + 1; + + string pageRequestUrl = requestURL; + + for (int count = 1; count <= numberOfPagesToFetch; count++) + { + pageRequestUrl = requestURL + "?page=" + count; + + // Create the web request + HttpWebRequest request = WebRequest.Create(pageRequestUrl) as HttpWebRequest; + + // Add credendtials to request + request.Credentials = new NetworkCredential(username, TwitterNet.ToInsecureString(password)); + + try + { + // Get the Response + using (HttpWebResponse response = request.GetResponse() as HttpWebResponse) + { + // Get the response stream + StreamReader reader = new StreamReader(response.GetResponseStream()); + + // Create a new XmlDocument + XmlDocument doc = new XmlDocument(); + + // Load data + doc.Load(reader); + + // Get statuses with XPath + XmlNodeList nodes = doc.SelectNodes("/users/user"); + + foreach (XmlNode node in nodes) + { + IUser user = new User(); + user.Id = int.Parse(node.SelectSingleNode("id").InnerText); + user.Name = node.SelectSingleNode("name").InnerText; + user.ScreenName = node.SelectSingleNode("screen_name").InnerText; + user.ImageUrl = node.SelectSingleNode("profile_image_url").InnerText; + user.SiteUrl = node.SelectSingleNode("url").InnerText; + user.Location = node.SelectSingleNode("location").InnerText; + user.Description = node.SelectSingleNode("description").InnerText; + + users.Add(user); + } + + } + } + catch (WebException webExcp) + { + // Get the WebException status code. + WebExceptionStatus status = webExcp.Status; + // If status is WebExceptionStatus.ProtocolError, + // there has been a protocol error and a WebResponse + // should exist. Display the protocol error. + if (status == WebExceptionStatus.ProtocolError) + { + // Get HttpWebResponse so that you can check the HTTP status code. + HttpWebResponse httpResponse = (HttpWebResponse)webExcp.Response; + + switch ((int)httpResponse.StatusCode) + { + case 304: // 304 Not modified = no new tweets so ignore error. + break; + case 400: // rate limit exceeded + throw new RateLimitException("Rate limit exceeded. Clients may not make more than 70 requests per hour. Please try again in a few minutes."); + case 401: // unauthorized + throw new SecurityException("Not Authorized."); + default: + throw; + } + } + } + } + return users; + } + + public IUser GetUser(int userId) + { + IUser user = new User(); + + // Identi.ca expects http://Identi.ca.com/users/show/12345.xml + string requestURL = UserShowUrl + userId + Format; + + // Create the web request + HttpWebRequest request = WebRequest.Create(requestURL) as HttpWebRequest; + + // Add credendtials to request + request.Credentials = new NetworkCredential(username, TwitterNet.ToInsecureString(password)); + + // Add configured web proxy + request.Proxy = webProxy; + + using (HttpWebResponse response = request.GetResponse() as HttpWebResponse) + { + // Get the response stream + StreamReader reader = new StreamReader(response.GetResponseStream()); + + // Load the response data into a XmlDocument + XmlDocument doc = new XmlDocument(); + doc.Load(reader); + + // Get statuses with XPath + XmlNode userNode = doc.SelectSingleNode("user"); + + if (userNode != null) + { + user.Id = int.Parse(userNode.SelectSingleNode("id").InnerText); + user.Name = userNode.SelectSingleNode("name").InnerText; + user.ScreenName = userNode.SelectSingleNode("screen_name").InnerText; + user.ImageUrl = userNode.SelectSingleNode("profile_image_url").InnerText; + user.SiteUrl = userNode.SelectSingleNode("url").InnerText; + user.Location = userNode.SelectSingleNode("location").InnerText; + user.Description = userNode.SelectSingleNode("description").InnerText; + user.BackgroundColor = userNode.SelectSingleNode("profile_background_color").InnerText; + user.TextColor = userNode.SelectSingleNode("profile_text_color").InnerText; + user.LinkColor = userNode.SelectSingleNode("profile_link_color").InnerText; + user.SidebarBorderColor = userNode.SelectSingleNode("profile_sidebar_border_color").InnerText; + user.SidebarFillColor = userNode.SelectSingleNode("profile_sidebar_fill_color").InnerText; + user.FollowingCount = int.Parse(userNode.SelectSingleNode("friends_count").InnerText); + user.FavoritesCount = int.Parse(userNode.SelectSingleNode("favourites_count").InnerText); + user.StatusesCount = int.Parse(userNode.SelectSingleNode("statuses_count").InnerText); + user.FollowersCount = int.Parse(userNode.SelectSingleNode("followers_count").InnerText); + } + } + + return user; + } + + /// + /// Delete a tweet from a users timeline + /// + /// id of the Tweet to delete + public void DestroyTweet(double id) + { + string urlToCall = DestroyUrl + id + Format; + MakeDestroyRequestCall(urlToCall); + } + + /// + /// Destroy a direct message sent by a user + /// + /// id of the direct message to delete + public void DestroyDirectMessage(double id) + { + string urlToCall = DestroyDirectMessageUrl + id + Format; + MakeDestroyRequestCall(urlToCall); + } + + /// + /// Post new tweet to Identi.ca + /// + /// newly added tweet + public ITweet AddTweet(string text) + { + bool isDirectMessage = (text.StartsWith("d ", StringComparison.CurrentCultureIgnoreCase)); + + if (string.IsNullOrEmpty(text)) + return null; + + text = HttpUtility.UrlEncode(text); + + // Create the web request + HttpWebRequest request = WebRequest.Create(UpdateUrl) as HttpWebRequest; + + // Add authentication to request + request.Credentials = new NetworkCredential(username, TwitterNet.ToInsecureString(password)); + + // Add configured web proxy + request.Proxy = webProxy; + request.CookieContainer = cookieJar; + + request.Method = "POST"; + + // Set values for the request back + request.ContentType = "application/x-www-form-urlencoded"; + string param = "status_textarea=" + text; + request.ContentLength = param.Length; + + // Write the request paramater + StreamWriter stOut = new StreamWriter(request.GetRequestStream(), System.Text.Encoding.ASCII); + stOut.Write(param); + stOut.Close(); + + ITweet tweet; + + // Do the request to get the response + using (HttpWebResponse response = request.GetResponse() as HttpWebResponse) + { + // Get the response stream + StreamReader reader = new StreamReader(response.GetResponseStream()); + + // Load the response data into a XmlDocument + XmlDocument doc = new XmlDocument(); + doc.Load(reader); + XmlNamespaceManager mgr = new XmlNamespaceManager(doc.NameTable); + mgr.AddNamespace("xhtml", "http://www.w3.org/1999/xhtml"); + tweet = new Notice(); // Hack until I know what laconica returns from the post + tweet.Id = int.Parse(doc.SelectSingleNode("//xhtml:li[@class='notice_single']/@id",mgr).InnerText.Replace("notice-", "")); + tweet.Text = HttpUtility.UrlDecode(text); + tweet.User = CurrentlyLoggedInUser; + tweet.DateCreated = DateTime.Now; + tweet.IsNew = true; + + //XmlNode node = doc.SelectSingleNode("status"); + + //tweet = new Notice(); + //tweet.Id = double.Parse(node.SelectSingleNode("id").InnerText); + ////Defect 43 - Identi.ca incorrectly returns last tweet sent when you direct message someone. + //if (isDirectMessage) + // tweet.Text = HttpUtility.UrlDecode(text); + //else + // tweet.Text = HttpUtility.HtmlDecode(node.SelectSingleNode("text").InnerText); + + //string source = HttpUtility.HtmlDecode(node.SelectSingleNode("source").InnerText); + //// Remove html from the source string + //if (!string.IsNullOrEmpty(source)) + // tweet.Source = Regex.Replace(source, @"<(.|\n)*?>", string.Empty); + + //string dateString = node.SelectSingleNode("created_at").InnerText; + //if (!string.IsNullOrEmpty(dateString)) + //{ + // tweet.DateCreated = DateTime.ParseExact( + // dateString, + // Identi.caCreatedAtDateFormat, + // CultureInfo.GetCultureInfoByIetfLanguageTag("en-us"), DateTimeStyles.AllowWhiteSpaces); + //} + //tweet.IsNew = true; + + //User user = new User(); + //XmlNode userNode = node.SelectSingleNode("user"); + //user.Name = userNode.SelectSingleNode("name").InnerText; + //user.ScreenName = userNode.SelectSingleNode("screen_name").InnerText; + //user.ImageUrl = userNode.SelectSingleNode("profile_image_url").InnerText; + //user.SiteUrl = userNode.SelectSingleNode("url").InnerText; + //user.Location = userNode.SelectSingleNode("location").InnerText; + //user.Description = userNode.SelectSingleNode("description").InnerText; + //tweet.User = user; + } + + return tweet; + } + + /// + /// Authenticating with the provided credentials and retrieve the user's settings + /// + /// + public IUser Login() + { + + + IUser user = new User(); + + // Create the web request + HttpWebRequest request = WebRequest.Create(LoginUrl) as HttpWebRequest; + request.CookieContainer = cookieJar; + request.Method = "POST"; + request.AllowAutoRedirect = false; + // Add credendtials to request + request.Credentials = new NetworkCredential(username, TwitterNet.ToInsecureString(password)); + + // Add configured web proxy + request.Proxy = webProxy; + + try + { + + // Set values for the request back + request.ContentType = "application/x-www-form-urlencoded"; + string userParam = "nickname=" + username; + string passParam = "&password=" + TwitterNet.ToInsecureString(password); + + request.ContentLength = userParam.Length + passParam.Length; + + // Write the request paramater + StreamWriter stOut = new StreamWriter(request.GetRequestStream(), System.Text.Encoding.ASCII); + stOut.Write(userParam); + stOut.Write(passParam); + stOut.Close(); + + + using (HttpWebResponse response = request.GetResponse() as HttpWebResponse) + { + // Get the response stream + StreamReader reader = new StreamReader(response.GetResponseStream()); + + // Load the response data into a XmlDocument + XmlDocument doc = new XmlDocument(); + doc.Load(reader); + + // Get statuses with XPath + XmlNode userNode = doc.SelectSingleNode("user"); + + if (userNode != null) + { + user.Id = int.Parse(userNode.SelectSingleNode("id").InnerText); + user.Name = userNode.SelectSingleNode("name").InnerText; + user.ScreenName = userNode.SelectSingleNode("screen_name").InnerText; + user.ImageUrl = userNode.SelectSingleNode("profile_image_url").InnerText; + user.SiteUrl = userNode.SelectSingleNode("url").InnerText; + user.Location = userNode.SelectSingleNode("location").InnerText; + user.Description = userNode.SelectSingleNode("description").InnerText; + //user.BackgroundColor = userNode.SelectSingleNode("profile_background_color").InnerText; + //user.TextColor = userNode.SelectSingleNode("profile_text_color").InnerText; + //user.LinkColor = userNode.SelectSingleNode("profile_link_color").InnerText; + //user.SidebarBorderColor = userNode.SelectSingleNode("profile_sidebar_border_color").InnerText; + //user.SidebarFillColor = userNode.SelectSingleNode("profile_sidebar_fill_color").InnerText; + //user.FollowingCount = int.Parse(userNode.SelectSingleNode("friends_count").InnerText); + //user.FavoritesCount = int.Parse(userNode.SelectSingleNode("favourites_count").InnerText); + //user.StatusesCount = int.Parse(userNode.SelectSingleNode("statuses_count").InnerText); + user.FollowersCount = int.Parse(userNode.SelectSingleNode("followers_count").InnerText); + } + } + } + catch (WebException webExcp) + { + // Get the WebException status code. + WebExceptionStatus status = webExcp.Status; + // If status is WebExceptionStatus.ProtocolError, + // there has been a protocol error and a WebResponse + // should exist. Display the protocol error. + if (status == WebExceptionStatus.ProtocolError) + { + // Get HttpWebResponse so that you can check the HTTP status code. + HttpWebResponse httpResponse = (HttpWebResponse)webExcp.Response; + + switch ((int)httpResponse.StatusCode) + { + case 400: // rate limit exceeded + throw new RateLimitException("Rate limit exceeded. Clients may not make more than 70 requests per hour. Please try again in a few minutes."); + case 401: // unauthorized + return null; + case 407: // proxy authentication required + throw new ProxyAuthenticationRequiredException("Proxy authentication required."); + case 502 : //Bad Gateway, Identi.ca is freaking out. + throw new BadGatewayException("There was a problem calling the Identi.ca API"); + default: + throw; + } + } + else + throw; + } + currentLoggedInUser = user; + return user; + } + + static byte[] entropy = System.Text.Encoding.Unicode.GetBytes("WittyPasswordSalt"); + + /// + /// + /// + /// + /// + public static string EncryptString(System.Security.SecureString input) + { + byte[] encryptedData = System.Security.Cryptography.ProtectedData.Protect( + System.Text.Encoding.Unicode.GetBytes(ToInsecureString(input)), + entropy, + System.Security.Cryptography.DataProtectionScope.CurrentUser); + return Convert.ToBase64String(encryptedData); + } + + public static SecureString ToSecureString(string input) + { + SecureString secure = new SecureString(); + foreach (char c in input) + { + secure.AppendChar(c); + } + secure.MakeReadOnly(); + return secure; + } + + public static string ToInsecureString(SecureString input) + { + string returnValue = string.Empty; + IntPtr ptr = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(input); + try + { + returnValue = System.Runtime.InteropServices.Marshal.PtrToStringBSTR(ptr); + } + finally + { + System.Runtime.InteropServices.Marshal.ZeroFreeBSTR(ptr); + } + return returnValue; + } + + /// + /// + /// + /// + /// + public static SecureString DecryptString(string encryptedData) + { + try + { + byte[] decryptedData = System.Security.Cryptography.ProtectedData.Unprotect( + Convert.FromBase64String(encryptedData), + entropy, + System.Security.Cryptography.DataProtectionScope.CurrentUser); + return ToSecureString(System.Text.Encoding.Unicode.GetString(decryptedData)); + } + catch + { + return new SecureString(); + } + } + + /// + /// Gets direct messages for the user + /// + /// Collection of direct messages + public DirectMessageCollection RetrieveMessages() + { + throw new NotImplementedException(); + } + + public void SendMessage(string user, string text) + { + if (string.IsNullOrEmpty(text)) + return; + + text = HttpUtility.UrlEncode(text); + + // Create the web request + HttpWebRequest request = WebRequest.Create(SendMessageUrl + Format) as HttpWebRequest; + + // Add authentication to request + request.Credentials = new NetworkCredential(username, TwitterNet.ToInsecureString(password)); + + // Add configured web proxy + request.Proxy = webProxy; + request.CookieContainer = cookieJar; + + request.Method = "POST"; + + // Set values for the request back + request.ContentType = "application/x-www-form-urlencoded"; + string postbody = "&status_textarea="+text; + + request.ContentLength = postbody.Length; + + // Write the request paramater + StreamWriter stOut = new StreamWriter(request.GetRequestStream(), System.Text.Encoding.ASCII); + stOut.Write(postbody); + stOut.Close(); + + try + { + // Perform the web request + request.GetResponse(); + } + catch (WebException webExcp) + { + // Get the WebException status code. + WebExceptionStatus status = webExcp.Status; + // If status is WebExceptionStatus.ProtocolError, + // there has been a protocol error and a WebResponse + // should exist. Display the protocol error. + if (status == WebExceptionStatus.ProtocolError) + { + // Get HttpWebResponse so that you can check the HTTP status code. + HttpWebResponse httpResponse = (HttpWebResponse)webExcp.Response; + + switch ((int)httpResponse.StatusCode) + { + case 401: // unauthorized + throw new SecurityException("Not Authorized."); + default: + throw; + } + } + } + } + + /// + /// Follow the user specified by userId + /// + /// + public void FollowUser(string userName) + { + string followUrl = CreateFriendshipUrl + userName + Format; + MakeTwitterApiCall(followUrl); + } + + #endregion + + #region Private Methods + + /// + /// Retrieves the specified timeline from Identi.ca + /// + /// Collection of Tweets + private TweetCollection RetrieveTimeline(Timeline timeline) + { + return RetrieveTimeline(timeline, string.Empty); + } + + /// + /// Retrieves the specified timeline from Identi.ca + /// + /// Collection of Tweets + private TweetCollection RetrieveTimeline(Timeline timeline, string since) + { + return RetrieveTimeline(timeline, since, string.Empty); + } + + /// + /// The Main function for interfacing with the Identi.ca API + /// + /// Collection of Tweets. Identi.ca limits the max to 20. + private TweetCollection RetrieveTimeline(Timeline timeline, string since, string userId) + { + TweetCollection tweets = new TweetCollection(); + + string timelineUrl; + + switch (timeline) + { + case Timeline.Public: + timelineUrl = PublicTimelineUrl; + break; + case Timeline.Friends: + timelineUrl = FriendsTimelineUrl; + break; + case Timeline.User: + timelineUrl = UserTimelineUrl; + + break; + case Timeline.Replies: + timelineUrl = RepliesTimelineUrl; + break; + default: + timelineUrl = PublicTimelineUrl; + break; + } + + //if (!string.IsNullOrEmpty(userId)) + // timelineUrl += "/" + userId; + if (String.IsNullOrEmpty(userId)) { + userId = username; + } + timelineUrl = timelineUrl.Replace("{username}", userId); + + + if (!string.IsNullOrEmpty(since)) + { + throw new NotImplementedException("Identi.ca does not implement the since parameter"); + } + + // Create the web request + HttpWebRequest request = WebRequest.Create(timelineUrl) as HttpWebRequest; + + // Add configured web proxy + request.Proxy = webProxy; + request.CookieContainer = cookieJar; + + // Friends and Replies timeline requests need to be authenticated + if (timeline == Timeline.Friends || timeline == Timeline.Replies) + { + // Add credendtials to request + request.Credentials = new NetworkCredential(username, TwitterNet.ToInsecureString(password)); + } + + // moved this out of the try catch to use it later on in the XMLException + // trying to fix a bug someone report + XmlDocument doc = new XmlDocument(); + try + { + // Get the Web Response + using (HttpWebResponse response = request.GetResponse() as HttpWebResponse) + { + // Get the response stream + StreamReader reader = new StreamReader(response.GetResponseStream()); + + // Load the response data into a XmlDocument + doc.Load(reader); + XmlNamespaceManager mgr = new XmlNamespaceManager(doc.NameTable); + string rdfns = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; + mgr.AddNamespace("rdf", rdfns); + mgr.AddNamespace("dc", "http://purl.org/dc/elements/1.1/"); + mgr.AddNamespace("rss", "http://purl.org/rss/1.0/"); + + // Get statuses with XPath + XmlNodeList nodes = doc.SelectNodes("//rss:item",mgr); + + foreach (XmlNode node in nodes) + { + ITweet tweet = new Notice(); + string about = node.Attributes["about", rdfns].Value; + tweet.Id = double.Parse(about.Replace("http://identi.ca/notice/", string.Empty)); + tweet.Text = HttpUtility.HtmlDecode(node.SelectSingleNode("rss:title",mgr).InnerText); + string source = string.Empty; +// if (!string.IsNullOrEmpty(source)) + // tweet.Source = Regex.Replace(source, @"<(.|\n)*?>", string.Empty); + + string dateString = node.SelectSingleNode("dc:date",mgr).InnerText; + if (!string.IsNullOrEmpty(dateString)) + { + tweet.DateCreated = XmlConvert.ToDateTime(dateString); + } + + User user = new User(); + //XmlNode userNode = node.SelectSingleNode("user"); + string description = node.SelectSingleNode("rss:description", mgr).InnerText; + + user.Name = node.SelectSingleNode("dc:creator", mgr).InnerText; + user.ScreenName = description.Substring(0, description.IndexOf("'")); + user.ImageUrl = "http://identi.ca/" + user.ScreenName + "/avatar/48"; + //user.SiteUrl = userNode.SelectSingleNode("url").InnerText; + //user.Location = userNode.SelectSingleNode("location").InnerText; + //user.Description = userNode.SelectSingleNode("description").InnerText; + + tweet.User = user; + + tweets.Add(tweet); + } + + tweets.SaveToDisk(); + } + } + catch (XmlException exXML) + { + // adding the XML document data to the exception so it will get logged + // so we can debug the issue + exXML.Data.Add("XMLDoc", doc); + throw; + } + catch (WebException webExcp) + { + // Get the WebException status code. + WebExceptionStatus status = webExcp.Status; + // If status is WebExceptionStatus.ProtocolError, + // there has been a protocol error and a WebResponse + // should exist. Display the protocol error. + if (status == WebExceptionStatus.ProtocolError) + { + // Get HttpWebResponse so that you can check the HTTP status code. + HttpWebResponse httpResponse = (HttpWebResponse)webExcp.Response; + + switch ((int)httpResponse.StatusCode) + { + case 304: // 304 Not modified = no new tweets so ignore error. + break; + case 400: // rate limit exceeded + throw new RateLimitException("Rate limit exceeded. Clients may not make more than 70 requests per hour. Please try again in a few minutes."); + default: + throw; + } + } + } + return tweets; + } + + /// + /// Generic call to destroy a status ** still in progress ** + /// + /// + private void MakeDestroyRequestCall(string urlToCall) + { + MakeTwitterApiCall(urlToCall); + } + + /// + /// Generic Identi.ca API call. Use this when you don't need/want to parse the return message + /// and just want to make a succeed/fail call to the API. + /// + /// + private void MakeTwitterApiCall(string urlToCall) + { + //REMARK: We may want to refactor this to return the message returned by the API. + // Create the web request + HttpWebRequest request = WebRequest.Create(urlToCall) as HttpWebRequest; + + // Add authentication to request + request.Credentials = new NetworkCredential(username, TwitterNet.ToInsecureString(password)); + + // Add configured web proxy + request.Proxy = webProxy; + + //request.Method = "GET"; + + try + { + // perform the destroy web request + request.GetResponse(); + } + catch (WebException webExcp) + { + // Get the WebException status code. + WebExceptionStatus status = webExcp.Status; + // If status is WebExceptionStatus.ProtocolError, + // there has been a protocol error and a WebResponse + // should exist. Display the protocol error. + if (status == WebExceptionStatus.ProtocolError) + { + // Get HttpWebResponse so that you can check the HTTP status code. + HttpWebResponse httpResponse = (HttpWebResponse)webExcp.Response; + + switch ((int)httpResponse.StatusCode) + { + case 401: // unauthorized + throw new SecurityException("Not Authorized.", webExcp); + default: + throw; + } + } + } + } + + #endregion + } +} Index: Witty/TwitterLib/User.cs =================================================================== --- Witty/TwitterLib/User.cs (revision 349) +++ Witty/TwitterLib/User.cs (working copy) @@ -202,9 +202,9 @@ } } - private Tweet tweet; + private ITweet tweet; - public Tweet Tweet + public ITweet Tweet { get { return tweet; } set @@ -297,7 +297,7 @@ } [Serializable] - public class UserCollection : ObservableCollection + public class UserCollection : ObservableCollection { } } Index: Witty/TwitterLib/DirectMessage.cs =================================================================== --- Witty/TwitterLib/DirectMessage.cs (revision 349) +++ Witty/TwitterLib/DirectMessage.cs (working copy) @@ -55,9 +55,9 @@ } } - private User sender; + private IUser sender; - public User Sender + public IUser Sender { get { return sender; } set @@ -70,9 +70,9 @@ } } - private User recipient; + private IUser recipient; - public User Recipient + public IUser Recipient { get { return recipient; } set @@ -192,9 +192,9 @@ #endregion - public Tweet ToTweet() + public ITweet ToTweet() { - Tweet tweet = new Tweet(); + ITweet tweet = new Tweet(); tweet.DateCreated = this.DateCreated; tweet.User = this.Sender; tweet.Text = this.Text; Index: Witty/TwitterLib/ITweet.cs =================================================================== --- Witty/TwitterLib/ITweet.cs (revision 349) +++ Witty/TwitterLib/ITweet.cs (working copy) @@ -1,10 +1,10 @@ using System; namespace TwitterLib { - interface ITweet + public interface ITweet : IEquatable { DateTime? DateCreated { get; set; } - bool Equals(Tweet other); + bool Equals(ITweet other); double Id { get; set; } int Index { get; set; } bool IsNew { get; set; } @@ -14,6 +14,6 @@ string Source { get; set; } string Text { get; set; } void UpdateRelativeTime(); - User User { get; set; } + IUser User { get; set; } } } Index: Witty/TwitterLib/ITwitterNet.cs =================================================================== --- Witty/TwitterLib/ITwitterNet.cs (revision 349) +++ Witty/TwitterLib/ITwitterNet.cs (working copy) @@ -2,7 +2,7 @@ { public interface IServiceApi { - Tweet AddTweet(string text); + ITweet AddTweet(string text); string ClientName { get; set; } string CreateFriendshipUrl { get; set; } @@ -28,9 +28,9 @@ UserCollection GetFriends(int userId); UserCollection GetFriends(); - User CurrentlyLoggedInUser { get; set; } - User GetUser(int userId); - User Login(); + IUser CurrentlyLoggedInUser { get; set; } + IUser GetUser(int userId); + IUser Login(); TweetCollection GetFriendsTimeline(); TweetCollection GetFriendsTimeline(string since, string userId); Index: Witty/TwitterLib/IUser.cs =================================================================== --- Witty/TwitterLib/IUser.cs (revision 349) +++ Witty/TwitterLib/IUser.cs (working copy) @@ -1,6 +1,6 @@ namespace TwitterLib { - interface IUser + public interface IUser { string BackgroundColor { get; set; } string Description { get; set; } @@ -20,7 +20,7 @@ string SiteUrl { get; set; } int StatusesCount { get; set; } string TextColor { get; set; } - Tweet Tweet { get; set; } + ITweet Tweet { get; set; } string TwitterUrl { get; } } } Index: Witty/TwitterLib/Tweet.cs =================================================================== --- Witty/TwitterLib/Tweet.cs (revision 349) +++ Witty/TwitterLib/Tweet.cs (working copy) @@ -11,7 +11,7 @@ /// Represents the status post for a Twitter User. /// [Serializable] - public class Tweet : INotifyPropertyChanged, IEquatable, TwitterLib.ITweet + public class Tweet : INotifyPropertyChanged, TwitterLib.ITweet { #region Private fields @@ -20,7 +20,7 @@ private string relativeTime; private string text; private string source; - private User user; + private IUser user; private bool isNew; private int index; private bool isSearchResult; @@ -97,7 +97,7 @@ /// /// Twitter User associated with the Tweet /// - public User User + public IUser User { get { return user; } set @@ -190,15 +190,15 @@ /// Tweets are the same if they have the same Id. /// Collection.Contains needs IEquatable implemented to be effective. /// - public bool Equals(Tweet other) + public bool Equals(ITweet other) { if (other == null) throw new ArgumentNullException("other"); - if (id != other.id) + if (id != other.Id) return false; if (Id == -1) // special type of tweet, so compare the user and text - return ((user.Id == other.user.Id) && (text == other.text)); + return ((user.Id == other.User.Id) && (text == other.Text)); return true; } @@ -257,7 +257,7 @@ /// Collection of Tweets /// [Serializable] - public class TweetCollection : ObservableCollection + public class TweetCollection : ObservableCollection { #region Class Constants Index: Witty/TwitterLib/TwitterNet.cs =================================================================== --- Witty/TwitterLib/TwitterNet.cs (revision 349) +++ Witty/TwitterLib/TwitterNet.cs (working copy) @@ -34,7 +34,7 @@ private IWebProxy webProxy = HttpWebRequest.DefaultWebProxy; // Jason Follas: Added initialization private string twitterServerUrl; // Jason Follas - private User currentLoggedInUser; + private IUser currentLoggedInUser; // REMARK: might need to fix this for globalization private readonly string twitterCreatedAtDateFormat = "ddd MMM dd HH:mm:ss zzzz yyyy"; // Thu Apr 26 01:36:08 +0000 2007 @@ -47,7 +47,7 @@ #region Public Properties - public User CurrentlyLoggedInUser + public IUser CurrentlyLoggedInUser { get { @@ -349,6 +349,7 @@ set { createFriendshipUrl = value; } } + /// /// The format of the results from the twitter API. Ex: .xml, .json, .rss, .atom. Defaults to ".xml" /// @@ -534,7 +535,7 @@ else { // need to make an extra call to twitter - User user = GetUser(userId); + IUser user = GetUser(userId); friendsCount = user.FollowingCount; } @@ -613,9 +614,9 @@ return users; } - public User GetUser(int userId) + public IUser GetUser(int userId) { - User user = new User(); + IUser user = new User(); // Twitter expects http://twitter.com/users/show/12345.xml string requestURL = UserShowUrl + userId + Format; @@ -674,7 +675,7 @@ /// Post new tweet to Twitter /// /// newly added tweet - public Tweet AddTweet(string text) + public ITweet AddTweet(string text) { bool isDirectMessage = (text.StartsWith("d ", StringComparison.CurrentCultureIgnoreCase)); @@ -811,7 +812,7 @@ /// Authenticating with the provided credentials and retrieve the user's settings /// /// - public User Login() + public IUser Login() { string timelineUrl = UserShowUrl + username + Format; Index: Witty/Witty.sln =================================================================== --- Witty/Witty.sln (revision 349) +++ Witty/Witty.sln (working copy) @@ -13,6 +13,8 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{EA9BEE27-11D2-4346-83D9-9A056282B7B9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdenticaLib", "IdenticaLib\IdenticaLib.csproj", "{580176B8-7199-406C-AFFB-C8B81450AEDC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +45,10 @@ {EA9BEE27-11D2-4346-83D9-9A056282B7B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA9BEE27-11D2-4346-83D9-9A056282B7B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA9BEE27-11D2-4346-83D9-9A056282B7B9}.Release|Any CPU.Build.0 = Release|Any CPU + {580176B8-7199-406C-AFFB-C8B81450AEDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {580176B8-7199-406C-AFFB-C8B81450AEDC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {580176B8-7199-406C-AFFB-C8B81450AEDC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {580176B8-7199-406C-AFFB-C8B81450AEDC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE