Engine-Twitter: fixed InvalidOperationException thrown by /unfollow command (closes...
[smuxi:smuxi.git] / src / Engine-Twitter / Protocols / Twitter / TwitterProtocolManager.cs
1 // Smuxi - Smart MUltipleXed Irc
2 // 
3 // Copyright (c) 2009-2013 Mirco Bauer <meebey@meebey.net>
4 // 
5 // Full GPL License: <http://www.gnu.org/licenses/gpl.txt>
6 // 
7 // This program is free software; you can redistribute it and/or modify
8 // it under the terms of the GNU General Public License as published by
9 // the Free Software Foundation; either version 2 of the License, or
10 // (at your option) any later version.
11 // 
12 // This program is distributed in the hope that it will be useful,
13 // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 // GNU General Public License for more details.
16 // 
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
20
21 using System;
22 using System.Net;
23 using System.Net.Security;
24 using System.Web;
25 using System.Linq;
26 using System.Security.Cryptography.X509Certificates;
27 using System.Threading;
28 using System.Collections.Generic;
29 using Twitterizer;
30 using Twitterizer.Core;
31 using Smuxi.Common;
32
33 namespace Smuxi.Engine
34 {
35     public enum TwitterChatType {
36         FriendsTimeline,
37         Replies,
38         DirectMessages
39     }
40
41     [ProtocolManagerInfo(Name = "Twitter", Description = "Twitter Micro-Blogging", Alias = "twitter")]
42     public class TwitterProtocolManager : ProtocolManagerBase
43     {
44 #if LOG4NET
45         private static readonly log4net.ILog f_Logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
46 #endif
47         static readonly string f_LibraryTextDomain = "smuxi-engine-twitter";
48         static readonly TextColor f_BlueTextColor = new TextColor(0x0000FF);
49         
50         OAuthTokens             f_OAuthTokens;
51         string                  f_RequestToken;
52         OptionalProperties      f_OptionalProperties;
53         TwitterUser             f_TwitterUser;
54         WebProxy                f_WebProxy;
55         string                  f_Username;
56         ProtocolChatModel       f_ProtocolChat;
57         Dictionary<string, PersonModel> f_Friends;
58         List<GroupChatModel>    f_GroupChats = new List<GroupChatModel>();
59
60         GroupChatModel          f_FriendsTimelineChat;
61         AutoResetEvent          f_FriendsTimelineEvent = new AutoResetEvent(false);
62         Thread                  f_UpdateFriendsTimelineThread;
63         int                     f_UpdateFriendsTimelineInterval = 120;
64         decimal                 f_LastFriendsTimelineStatusID;
65         DateTime                f_LastFriendsUpdate;
66
67         GroupChatModel          f_RepliesChat;
68         Thread                  f_UpdateRepliesThread;
69         int                     f_UpdateRepliesInterval = 120;
70         decimal                 f_LastReplyStatusID;
71
72         GroupChatModel          f_DirectMessagesChat;
73         AutoResetEvent          f_DirectMessageEvent = new AutoResetEvent(false);
74         Thread                  f_UpdateDirectMessagesThread;
75         int                     f_UpdateDirectMessagesInterval = 120;
76         decimal                 f_LastDirectMessageReceivedStatusID;
77         decimal                 f_LastDirectMessageSentStatusID;
78
79         bool                    f_Listening;
80         bool                    f_IsConnected;
81
82         int                     ErrorResponseCount { get; set; }
83         const int               MaxErrorResponseCount = 3;
84
85         TwitterStatus[]         StatusIndex { get; set; }
86         int                     StatusIndexOffset { get; set; }
87
88         public override string NetworkID {
89             get {
90                 if (f_TwitterUser == null) {
91                     return "Twitter";
92                 }
93
94                 return String.Format("Twitter/{0}", f_TwitterUser.ScreenName);
95             }
96         }
97
98         public override string Protocol {
99             get {
100                 return "Twitter";
101             }
102         }
103
104         public override ChatModel Chat {
105             get {
106                 return f_ProtocolChat;
107             }
108         }
109         
110         protected bool HasTokens {
111             get {
112                 return f_OAuthTokens != null &&
113                        f_OAuthTokens.HasConsumerToken &&
114                        f_OAuthTokens.HasAccessToken;
115             }
116         }
117
118         public TwitterProtocolManager(Session session) : base(session)
119         {
120             Trace.Call(session);
121
122             f_FriendsTimelineChat = new GroupChatModel(
123                 TwitterChatType.FriendsTimeline.ToString(),
124                 _("Home Timeline"),
125                 this
126             );
127             f_FriendsTimelineChat.InitMessageBuffer(
128                 MessageBufferPersistencyType.Volatile
129             );
130             f_FriendsTimelineChat.ApplyConfig(Session.UserConfig);
131             f_GroupChats.Add(f_FriendsTimelineChat);
132
133             f_RepliesChat = new GroupChatModel(
134                 TwitterChatType.Replies.ToString(),
135                 _("Replies"),
136                 this
137             );
138             f_RepliesChat.InitMessageBuffer(
139                 MessageBufferPersistencyType.Volatile
140             );
141             f_RepliesChat.ApplyConfig(Session.UserConfig);
142             f_GroupChats.Add(f_RepliesChat);
143
144             f_DirectMessagesChat = new GroupChatModel(
145                 TwitterChatType.DirectMessages.ToString(),
146                 _("Direct Messages"),
147                 this
148             );
149             f_DirectMessagesChat.InitMessageBuffer(
150                 MessageBufferPersistencyType.Volatile
151             );
152             f_DirectMessagesChat.ApplyConfig(Session.UserConfig);
153             f_GroupChats.Add(f_DirectMessagesChat);
154
155             StatusIndex = new TwitterStatus[99];
156         }
157
158         public override void Connect(FrontendManager fm, ServerModel server)
159         {
160             Trace.Call(fm, server);
161
162             if (server == null) {
163                 throw new ArgumentNullException("server");
164             }
165
166             f_Username = server.Username;
167
168             var proxySettings = new ProxySettings();
169             proxySettings.ApplyConfig(Session.UserConfig);
170             var twitterUrl = new OptionalProperties().APIBaseAddress;
171             var proxy = proxySettings.GetWebProxy(twitterUrl);
172             // HACK: Twitterizer will always use the system proxy if set to null
173             // so explicitely override this by setting an empty proxy
174             if (proxy == null) {
175                 f_WebProxy = new WebProxy();
176             } else {
177                 f_WebProxy = proxy;
178             }
179
180             f_OptionalProperties = CreateOptions<OptionalProperties>();
181             f_ProtocolChat = new ProtocolChatModel(NetworkID, "Twitter " + f_Username, this);
182             f_ProtocolChat.InitMessageBuffer(
183                 MessageBufferPersistencyType.Volatile
184             );
185             f_ProtocolChat.ApplyConfig(Session.UserConfig);
186             Session.AddChat(f_ProtocolChat);
187             Session.SyncChat(f_ProtocolChat);
188
189             MessageBuilder builder;
190             if (proxy != null && proxy.Address != null) {
191                 builder = CreateMessageBuilder();
192                 builder.AppendEventPrefix();
193                 builder.AppendText(_("Using proxy: {0}:{1}"),
194                                    proxy.Address.Host,
195                                    proxy.Address.Port);
196                 Session.AddMessageToChat(Chat, builder.ToMessage());
197             }
198
199             if (!server.ValidateServerCertificate) {
200                 var whitelist = Session.CertificateValidator.HostnameWhitelist;
201                 lock (whitelist) {
202                     // needed for favicon
203                     if (!whitelist.Contains("www.twitter.com")) {
204                         whitelist.Add("www.twitter.com");
205                     }
206                     if (!whitelist.Contains("api.twitter.com")) {
207                         whitelist.Add("api.twitter.com");
208                     }
209                 }
210             }
211
212             string msgStr = _("Connecting to Twitter...");
213             if (fm != null) {
214                 fm.SetStatus(msgStr);
215             }
216             var msg = CreateMessageBuilder().
217                 AppendEventPrefix().AppendText(msgStr).ToMessage();
218             Session.AddMessageToChat(Chat, msg);
219             try {
220                 var key = GetApiKey();
221                 f_OAuthTokens = new OAuthTokens();
222                 f_OAuthTokens.ConsumerKey = key[0];
223                 f_OAuthTokens.ConsumerSecret = key[1];
224
225                 var password = server.Password ?? String.Empty;
226                 var access = password.Split('|');
227                 if (access.Length == 2) {
228                     f_OAuthTokens.AccessToken = access[0];
229                     f_OAuthTokens.AccessTokenSecret = access[1];
230
231                     // verify access token
232                     var options = CreateOptions<VerifyCredentialsOptions>();
233                     var response = TwitterAccount.VerifyCredentials(
234                         f_OAuthTokens, options
235                     );
236                     if (response.Result == RequestResult.Unauthorized) {
237 #if LOG4NET
238                         f_Logger.Warn("Connect(): Invalid access token, " +
239                                       "re-authorization required");
240 #endif
241                         f_OAuthTokens.AccessToken = null;
242                         f_OAuthTokens.AccessTokenSecret = null;
243                     }
244                 }
245
246                 if (!f_OAuthTokens.HasAccessToken) {
247                     // new account or basic auth user that needs to be migrated
248                     var reqToken = OAuthUtility.GetRequestToken(key[0], key[1],
249                                                             "oob", f_WebProxy);
250                     f_RequestToken = reqToken.Token;
251                     var authUri = OAuthUtility.BuildAuthorizationUri(f_RequestToken);
252                     builder = CreateMessageBuilder();
253                     builder.AppendEventPrefix();
254                     builder.AppendText(_("Twitter authorization required."));
255                     Session.AddMessageToChat(f_ProtocolChat, builder.ToMessage());
256
257                     builder = CreateMessageBuilder();
258                     builder.AppendEventPrefix();
259                     // TRANSLATOR: do NOT change the position of {0}!
260                     builder.AppendText(
261                         _("Please open the following URL and click " +
262                           "\"Allow\" to allow Smuxi to connect to your " +
263                           "Twitter account: {0}"),
264                         String.Empty
265                     );
266                     Session.AddMessageToChat(f_ProtocolChat, builder.ToMessage());
267
268                     builder = CreateMessageBuilder();
269                     builder.AppendEventPrefix();
270                     builder.AppendText(" ");
271                     builder.AppendUrl(authUri.AbsoluteUri);
272                     Session.AddMessageToChat(f_ProtocolChat, builder.ToMessage());
273
274                     builder = CreateMessageBuilder();
275                     builder.AppendEventPrefix();
276                     builder.AppendText(
277                         _("Once you have allowed Smuxi to access your " +
278                           "Twitter account, Twitter will provide a PIN.")
279                     );
280                     Session.AddMessageToChat(f_ProtocolChat, builder.ToMessage());
281
282                     builder = CreateMessageBuilder();
283                     builder.AppendEventPrefix();
284                     builder.AppendText(_("Please type: /pin PIN_FROM_TWITTER"));
285                     Session.AddMessageToChat(f_ProtocolChat, builder.ToMessage());
286                 }
287             } catch (Exception ex) {
288 #if LOG4NET
289                 f_Logger.Error("Connect(): Exception", ex);
290 #endif
291                 if (fm != null) {
292                     fm.SetStatus(_("Connection failed!"));
293                 }
294                 msg = CreateMessageBuilder().
295                     AppendEventPrefix().
296                     AppendErrorText(
297                         _("Connection failed! Reason: {0}"),
298                         ex.Message).
299                     ToMessage();
300                 Session.AddMessageToChat(Chat, msg);
301                 return;
302             }
303
304             // twitter is sometimes pretty slow, so fetch this in the background
305             ThreadPool.QueueUserWorkItem(delegate {
306                 try {
307                     // FIXME: replace with AutoResetEvent
308                     while (!HasTokens) {
309                         Thread.Sleep(1000);
310                     }
311                     
312                     var message = _("Fetching user details from Twitter, please wait...");
313                     msg = CreateMessageBuilder().
314                         AppendEventPrefix().AppendText(message).ToMessage();
315                     Session.AddMessageToChat(Chat, msg);
316
317                     UpdateUser();
318
319                     message = _("Finished fetching user details.");
320                     msg = CreateMessageBuilder().
321                         AppendEventPrefix().AppendText(message).ToMessage();
322                     Session.AddMessageToChat(Chat, msg);
323
324                     f_IsConnected = true;
325                     message =_("Successfully connected to Twitter.");
326                     if (fm != null) {
327                         fm.UpdateNetworkStatus();
328                         fm.SetStatus(message);
329                     }
330
331                     msg = CreateMessageBuilder().
332                         AppendEventPrefix().AppendText(message).ToMessage();
333                     Session.AddMessageToChat(Chat, msg);
334                     f_Listening = true;
335
336                     f_FriendsTimelineChat.PersonCount = 
337                     f_RepliesChat.PersonCount = 
338                     f_DirectMessagesChat.PersonCount = (int) f_TwitterUser.NumberOfFriends;
339
340                     OnConnected(EventArgs.Empty);
341
342                 } catch (Exception ex) {
343                     var message = _("Failed to fetch user details from Twitter. Reason: ");
344 #if LOG4NET
345                     f_Logger.Error("Connect(): " + message, ex);
346 #endif
347                     msg = CreateMessageBuilder().
348                         AppendEventPrefix().
349                         AppendErrorText(message + ex.Message).
350                         ToMessage();
351                     Session.AddMessageToChat(Chat, msg);
352
353                     if (fm != null) {
354                         fm.SetStatus(_("Connection failed!"));
355                     }
356                     msg = CreateMessageBuilder().
357                         AppendEventPrefix().
358                         AppendErrorText(_("Connection failed! Reason: {0}"),
359                                         ex.Message).
360                         ToMessage();
361                     Session.AddMessageToChat(Chat, msg);
362                 }
363             });
364             ThreadPool.QueueUserWorkItem(delegate {
365                 try {
366                     // FIXME: replace with AutoResetEvent
367                     // f_TwitterUser needed for proper self detection in the
368                     // CreatePerson() method
369                     while (!HasTokens || f_TwitterUser == null) {
370                         Thread.Sleep(1000);
371                     }
372
373                     msg = CreateMessageBuilder().
374                         AppendEventPrefix().
375                         AppendText(
376                             _("Fetching friends from Twitter, please wait...")
377                         ).
378                         ToMessage();
379                     Session.AddMessageToChat(Chat, msg);
380
381                     UpdateFriends();
382
383                     msg = CreateMessageBuilder().
384                         AppendEventPrefix().
385                         AppendText(_("Finished fetching friends.")).
386                         ToMessage();
387                     Session.AddMessageToChat(Chat, msg);
388                 } catch (Exception ex) {
389                     var message = _("Failed to fetch friends from Twitter. Reason: ");
390 #if LOG4NET
391                     f_Logger.Error("Connect(): " + message, ex);
392 #endif
393                     msg = CreateMessageBuilder().
394                         AppendEventPrefix().
395                         AppendErrorText(message + ex.Message).
396                         ToMessage();
397                     Session.AddMessageToChat(Chat, msg);
398                 }
399             });
400
401             OpenFriendsTimelineChat();
402             OpenRepliesChat();
403             OpenDirectMessagesChat();
404         }
405
406         public override void Reconnect(FrontendManager fm)
407         {
408             Trace.Call(fm);
409         }
410
411         public override void Disconnect(FrontendManager fm)
412         {
413             Trace.Call(fm);
414
415             f_Listening = false;
416             f_FriendsTimelineEvent.Set();
417         }
418
419         public override IList<GroupChatModel> FindGroupChats(GroupChatModel filter)
420         {
421             Trace.Call(filter);
422
423             return f_GroupChats;
424         }
425
426         public override void OpenChat(FrontendManager fm, ChatModel chat)
427         {
428             Trace.Call(fm, chat);
429
430             if (chat.ChatType == ChatType.Group) {
431                TwitterChatType twitterChatType = (TwitterChatType)
432                     Enum.Parse(typeof(TwitterChatType), chat.ID);
433                switch (twitterChatType) {
434                     case TwitterChatType.FriendsTimeline:
435                         OpenFriendsTimelineChat();
436                         break;
437                     case TwitterChatType.Replies:
438                         OpenRepliesChat();
439                         break;
440                     case TwitterChatType.DirectMessages:
441                         OpenDirectMessagesChat();
442                         break;
443                 }
444                 return;
445             }
446
447             OpenPrivateChat(chat.ID);
448         }
449
450         private void OpenFriendsTimelineChat()
451         {
452             ChatModel chat =  Session.GetChat(
453                 TwitterChatType.FriendsTimeline.ToString(),
454                 ChatType.Group,
455                 this
456             );
457
458             if (chat != null) {
459                 return;
460             }
461
462             if (f_UpdateFriendsTimelineThread != null &&
463                 f_UpdateFriendsTimelineThread.IsAlive) {
464                 return;
465             }
466
467             // BUG: causes a race condition as the frontend syncs the
468             // unpopulated chat! So only add it if it's ready
469             //Session.AddChat(f_FriendsTimelineChat);
470             f_UpdateFriendsTimelineThread = new Thread(
471                 new ThreadStart(UpdateFriendsTimelineThread)
472             );
473             f_UpdateFriendsTimelineThread.IsBackground = true;
474             f_UpdateFriendsTimelineThread.Name =
475                 "TwitterProtocolManager friends timeline listener";
476             f_UpdateFriendsTimelineThread.Start();
477         }
478
479         private void OpenRepliesChat()
480         {
481             ChatModel chat =  Session.GetChat(
482                 TwitterChatType.Replies.ToString(),
483                 ChatType.Group,
484                 this
485             );
486
487             if (chat != null) {
488                 return;
489             }
490
491             if (f_UpdateRepliesThread != null &&
492                 f_UpdateRepliesThread.IsAlive) {
493                 return;
494             }
495
496             // BUG: causes a race condition as the frontend syncs the
497             // unpopulated chat! So only add it if it's ready
498             //Session.AddChat(f_RepliesChat);
499             f_UpdateRepliesThread = new Thread(
500                 new ThreadStart(UpdateRepliesThread)
501             );
502             f_UpdateRepliesThread.IsBackground = true;
503             f_UpdateRepliesThread.Name =
504                 "TwitterProtocolManager replies listener";
505             f_UpdateRepliesThread.Start();
506         }
507
508         private void OpenDirectMessagesChat()
509         {
510             ChatModel chat =  Session.GetChat(
511                 TwitterChatType.DirectMessages.ToString(),
512                 ChatType.Group,
513                 this
514             );
515
516             if (chat != null) {
517                 return;
518             }
519
520             if (f_UpdateDirectMessagesThread != null &&
521                 f_UpdateDirectMessagesThread.IsAlive) {
522                 return;
523             }
524
525             // BUG: causes a race condition as the frontend syncs the
526             // unpopulated chat! So only add it if it's ready
527             //Session.AddChat(f_DirectMessagesChat);
528             f_UpdateDirectMessagesThread = new Thread(
529                 new ThreadStart(UpdateDirectMessagesThread)
530             );
531             f_UpdateDirectMessagesThread.IsBackground = true;
532             f_UpdateDirectMessagesThread.Name =
533                 "TwitterProtocolManager direct messages listener";
534             f_UpdateDirectMessagesThread.Start();
535         }
536
537         private ChatModel OpenPrivateChat(string userId)
538         {
539             return OpenPrivateChat(Decimal.Parse(userId));
540         }
541
542         private ChatModel OpenPrivateChat(decimal userId)
543         {
544             ChatModel chat =  Session.GetChat(
545                 userId.ToString(),
546                 ChatType.Person,
547                 this
548             );
549
550             if (chat != null) {
551                 return chat;
552             }
553
554             var response = TwitterUser.Show(f_OAuthTokens, userId,
555                                             f_OptionalProperties);
556             CheckResponse(response);
557             var user = response.ResponseObject;
558             PersonModel person = CreatePerson(user);
559             PersonChatModel personChat = new PersonChatModel(
560                 person,
561                 user.Id.ToString(),
562                 user.ScreenName,
563                 this
564             );
565             personChat.InitMessageBuffer(
566                 MessageBufferPersistencyType.Volatile
567             );
568             personChat.ApplyConfig(Session.UserConfig);
569             Session.AddChat(personChat);
570             Session.SyncChat(personChat);
571             return personChat;
572         }
573
574         public override void CloseChat(FrontendManager fm, ChatModel chat)
575         {
576             Trace.Call(fm, chat);
577
578             TwitterChatType? chatType = null;
579             try {
580                 chatType = (TwitterChatType) Enum.Parse(
581                     typeof(TwitterChatType),
582                     chat.ID
583                 );
584             } catch (ArgumentException) {
585             }
586             if (chat.ChatType == ChatType.Group &&
587                 chatType.HasValue) {
588                switch (chatType.Value) {
589                     case TwitterChatType.FriendsTimeline:
590                         if (f_UpdateFriendsTimelineThread != null &&
591                             f_UpdateFriendsTimelineThread.IsAlive) {
592                             f_UpdateFriendsTimelineThread.Abort();
593                         }
594                         break;
595                     case TwitterChatType.Replies:
596                         if (f_UpdateRepliesThread != null &&
597                             f_UpdateRepliesThread.IsAlive) {
598                             f_UpdateRepliesThread.Abort();
599                         }
600                         break;
601                     case TwitterChatType.DirectMessages:
602                         if (f_UpdateDirectMessagesThread != null &&
603                             f_UpdateDirectMessagesThread.IsAlive) {
604                             f_UpdateDirectMessagesThread.Abort();
605                         }
606                         break;
607                 }
608             }
609
610             Session.RemoveChat(chat);
611         }
612
613         public override void SetPresenceStatus(PresenceStatus status,
614                                                string message)
615         {
616             Trace.Call(status, message);
617
618             // TODO: implement me
619
620             // should we send updates here?!?
621         }
622
623         public override bool Command(CommandModel command)
624         {
625             bool handled = false;
626             if (command.IsCommand) {
627                 if (f_IsConnected) {
628                     switch (command.Command) {
629                         case "msg":
630                         case "query":
631                             CommandMessage(command);
632                             handled = true;
633                             break;
634                         case "timeline":
635                             CommandTimeline(command);
636                             handled = true;
637                             break;
638                         case "follow":
639                             CommandFollow(command);
640                             handled = true;
641                             break;
642                         case "unfollow":
643                             CommandUnfollow(command);
644                             handled = true;
645                             break;
646                         case "search":
647                         case "join":
648                             CommandSearch(command);
649                             handled = true;
650                             break;
651                         case "rt":
652                         case "retweet":
653                             CommandRetweet(command);
654                             handled = true;
655                             break;
656                         case "reply":
657                             CommandReply(command);
658                             handled = true;
659                             break;
660                     }
661                 }
662                 switch (command.Command) {
663                     case "help":
664                         CommandHelp(command);
665                         handled = true;
666                         break;
667                     case "connect":
668                         CommandConnect(command);
669                         handled = true;
670                         break;
671                     case "pin":
672                         CommandPin(command);
673                         handled = true;
674                         break;
675                 }
676             } else {
677                 if (f_IsConnected) {
678                     CommandSay(command);
679                     handled = true;
680                 } else {
681                     NotConnected(command);
682                     handled = true;
683                 }
684             }
685
686             return handled;
687         }
688
689         public override string ToString()
690         {
691             if (f_TwitterUser == null) {
692                 return NetworkID;
693             }
694
695             return String.Format("{0} (Twitter)", f_TwitterUser.ScreenName);
696         }
697
698         public void CommandHelp(CommandModel cd)
699         {
700             var builder = CreateMessageBuilder();
701             // TRANSLATOR: this line is used as a label / category for a
702             // list of commands below
703             builder.AppendHeader(_("Twitter Commands"));
704             Session.AddMessageToFrontend(cd, builder.ToMessage());
705
706             string[] help = {
707                 "connect twitter username",
708                 "pin pin-number",
709                 "follow screen-name|user-id",
710                 "unfollow screen-name|user-id",
711                 "search keyword",
712                 "retweet/rt index-number|tweet-id",
713                 "reply index-number|tweet-id message",
714             };
715
716             foreach (string line in help) {
717                 builder = CreateMessageBuilder();
718                 builder.AppendEventPrefix();
719                 builder.AppendText(line);
720                 Session.AddMessageToFrontend(cd, builder.ToMessage());
721             }
722         }
723
724         public void CommandConnect(CommandModel cd)
725         {
726             var server = new ServerModel();
727             if (cd.DataArray.Length >= 3) {
728                 server.Username = cd.DataArray[2];
729             } else {
730                 NotEnoughParameters(cd);
731                 return;
732             }
733
734             Connect(cd.FrontendManager, server);
735         }
736
737         public void CommandPin(CommandModel cd)
738         {
739             if (String.IsNullOrEmpty(cd.Parameter)) {
740                 NotEnoughParameters(cd);
741                 return;
742             }
743             var pin = cd.Parameter.Trim();
744
745             MessageBuilder builder;
746             if (String.IsNullOrEmpty(f_RequestToken)) {
747                 builder = CreateMessageBuilder();
748                 builder.AppendEventPrefix();
749                 builder.AppendText(_("No pending authorization request!"));
750                 Session.AddMessageToChat(f_ProtocolChat, builder.ToMessage());
751                 return;
752             }
753             var reqToken = f_RequestToken;
754             f_RequestToken = null;
755
756             var key = GetApiKey();
757             OAuthTokenResponse response;
758             try {
759                 response = OAuthUtility.GetAccessToken(key[0], key[1],
760                                                        reqToken, pin,
761                                                        f_WebProxy);
762             } catch (Exception ex) {
763 #if LOG4NET
764                 f_Logger.Error("CommandPin(): GetAccessToken() threw Exception!", ex);
765 #endif
766                 builder = CreateMessageBuilder();
767                 builder.AppendEventPrefix();
768                 // TRANSLATOR: {0} contains the reason of the failure
769                 builder.AppendText(
770                     _("Failed to authorize with Twitter: {0}"),
771                     ex.Message
772                 );
773                 Session.AddMessageToChat(f_ProtocolChat, builder.ToMessage());
774
775                 builder = CreateMessageBuilder();
776                 builder.AppendEventPrefix();
777                 builder.AppendText(
778                     _("Twitter did not accept your PIN.  "  +
779                       "Did you enter it correctly?")
780                 );
781                 Session.AddMessageToChat(f_ProtocolChat, builder.ToMessage());
782
783                 builder = CreateMessageBuilder();
784                 builder.AppendEventPrefix();
785                 builder.AppendText(
786                     _("Please retry by closing this tab and reconnecting to " +
787                       "the Twitter \"{0}\" account."),
788                     f_Username
789                 );
790                 Session.AddMessageToChat(f_ProtocolChat, builder.ToMessage());
791
792                 // allow the user to re-enter the pin
793                 // LAME: An incorrect PIN invalidates the request token!
794                 //f_RequestToken = reqToken;
795                 return;
796             }
797 #if LOG4NET
798             f_Logger.Debug("CommandPin(): retrieved " +
799                            " AccessToken: " + response.Token + 
800                            " AccessTokenSecret: " + response.TokenSecret +
801                            " ScreenName: " + response.ScreenName +
802                            " UserId: " + response.UserId);
803 #endif
804             var servers = new ServerListController(Session.UserConfig);
805             var server = servers.GetServer(Protocol, response.ScreenName);
806             if (server == null) {
807                 server = new ServerModel() {
808                     Protocol = Protocol,
809                     Network  = String.Empty,
810                     Hostname = response.ScreenName,
811                     Username = response.ScreenName,
812                     Password = String.Format("{0}|{1}", response.Token,
813                                              response.TokenSecret),
814                     OnStartupConnect = true
815                 };
816                 servers.AddServer(server);
817                 
818                 var obsoleteServer = servers.GetServer(Protocol, String.Empty);
819                 if (obsoleteServer != null &&
820                     obsoleteServer.Username.ToLower() == response.ScreenName.ToLower()) {
821                     // found an old server entry for this user using basic auth
822                     servers.RemoveServer(Protocol, String.Empty);
823
824                     builder = CreateMessageBuilder();
825                     builder.AppendEventPrefix();
826                     builder.AppendText(
827                         _("Migrated Twitter account from basic auth to OAuth.")
828                     );
829                     Session.AddMessageToChat(f_ProtocolChat, builder.ToMessage());
830                 }
831             } else {
832                 // update token
833                 server.Password = String.Format("{0}|{1}", response.Token,
834                                                 response.TokenSecret);
835                 servers.SetServer(server);
836             }
837             servers.Save();
838
839             builder = CreateMessageBuilder();
840             builder.AppendEventPrefix();
841             builder.AppendText(_("Successfully authorized Twitter account " +
842                                  "\"{0}\" for Smuxi"), response.ScreenName);
843             Session.AddMessageToChat(f_ProtocolChat, builder.ToMessage());
844
845             f_OAuthTokens.AccessToken = response.Token;
846             f_OAuthTokens.AccessTokenSecret = response.TokenSecret;
847             f_Username = response.ScreenName;
848         }
849
850         public void CommandSay(CommandModel cmd)
851         {
852             if (cmd.Chat.ChatType == ChatType.Group) {
853                 TwitterChatType twitterChatType = (TwitterChatType)
854                     Enum.Parse(typeof(TwitterChatType), cmd.Chat.ID);
855                 switch (twitterChatType) {
856                     case TwitterChatType.FriendsTimeline:
857                     case TwitterChatType.Replies: {
858                         try {
859                             PostUpdate(cmd.Data);
860                         } catch (Exception ex) {
861                             var msg = CreateMessageBuilder().
862                                 AppendEventPrefix().
863                                 AppendErrorText(
864                                     _("Could not update status - Reason: {0}"),
865                                     ex.Message).
866                                 ToMessage();
867                             Session.AddMessageToFrontend(cmd, msg);
868                         }
869                         break;
870                     }
871                     case TwitterChatType.DirectMessages: {
872                         var msg = CreateMessageBuilder().
873                             AppendEventPrefix().
874                             AppendErrorText(
875                                 _("Cannot send message - no target specified. " +
876                                   "Use: /msg $nick message")).
877                             ToMessage();
878                         Session.AddMessageToFrontend(cmd, msg);
879                         break;
880                     }
881                 }
882             } else if (cmd.Chat.ChatType == ChatType.Person) {
883                 try {
884                     SendMessage(cmd.Chat.Name, cmd.Data);
885                 } catch (Exception ex) {
886 #if LOG4NET
887                     f_Logger.Error(ex);
888 #endif
889                     var msg = CreateMessageBuilder().
890                         AppendEventPrefix().
891                         AppendErrorText(
892                             _("Could not send message - Reason: {0}"),
893                             ex.Message).
894                         ToMessage();
895                     Session.AddMessageToFrontend(cmd, msg);
896                 }
897             } else {
898                 // ignore protocol chat
899             }
900         }
901
902         public void CommandTimeline(CommandModel cmd)
903         {
904             if (cmd.DataArray.Length < 2) {
905                 NotEnoughParameters(cmd);
906                 return;
907             }
908
909             string keyword = cmd.Parameter;
910             string[] users = cmd.Parameter.Split(',');
911
912             string chatName = users.Length > 1 ? _("Other timelines") : "@" + users[0];
913             ChatModel chat;
914
915             if (users.Length > 1) {
916                 chat = Session.CreateChat<GroupChatModel>(keyword, chatName, this);
917             } else {
918                 var userResponse = TwitterUser.Show(f_OAuthTokens, users [0], f_OptionalProperties);
919                 CheckResponse(userResponse);
920                 var person = GetPerson(userResponse.ResponseObject);
921                 chat = Session.CreatePersonChat(person, person.ID + "/timeline",
922                                                 chatName, this);
923             }
924
925             var statuses = new List<TwitterStatus>();
926             foreach (var user in users) {
927                 var opts = CreateOptions<UserTimelineOptions>();
928                 opts.ScreenName = user;
929                 var statusCollectionResponse = TwitterTimeline.UserTimeline(f_OAuthTokens, opts);
930                 CheckResponse(statusCollectionResponse);
931
932                 foreach (var status in statusCollectionResponse.ResponseObject) {
933                     statuses.Add(status);
934                 }
935             }
936
937             var sortedStatuses = SortTimeline(statuses);
938             foreach (var status in sortedStatuses) {
939                 AddIndexToStatus(status);
940                 var msg = CreateMessageBuilder().
941                     Append(status, GetPerson(status.User)).ToMessage();
942                 chat.MessageBuffer.Add(msg);
943                 var userId = status.User.Id.ToString();
944                 var groupChat = chat as GroupChatModel;
945                 if (groupChat != null) {
946                     if (!groupChat.UnsafePersons.ContainsKey(userId)) {
947                         groupChat.UnsafePersons.Add(userId, GetPerson(status.User));
948                     }
949                 }
950             }
951             Session.AddChat(chat);
952             Session.SyncChat(chat);
953         }
954
955         public void CommandMessage(CommandModel cmd)
956         {
957             string nickname;
958             if (cmd.DataArray.Length >= 2) {
959                 nickname = cmd.DataArray[1];
960             } else {
961                 NotEnoughParameters(cmd);
962                 return;
963             }
964
965             var response = TwitterUser.Show(f_OAuthTokens, nickname,
966                                             f_OptionalProperties);
967             if (response.Result != RequestResult.Success) {
968                 var msg = CreateMessageBuilder().
969                     AppendEventPrefix().
970                     AppendErrorText(_("Could not send message - the " +
971                                       "specified user does not exist.")).
972                     ToMessage();
973                 Session.AddMessageToFrontend(cmd, msg);
974                 return;
975             }
976             var user = response.ResponseObject;
977             var chat = OpenPrivateChat(user.Id);
978
979             if (cmd.DataArray.Length >= 3) {
980                 string message = String.Join(" ", cmd.DataArray, 2, cmd.DataArray.Length-2);
981                 try {
982                     SendMessage(user.ScreenName, message);
983                 } catch (Exception ex) {
984                     var msg = CreateMessageBuilder().
985                         AppendEventPrefix().
986                         AppendErrorText(
987                             _("Could not send message - Reason: {0}"),
988                             ex.Message).
989                         ToMessage();
990                     Session.AddMessageToFrontend(cmd.FrontendManager, chat, msg);
991                 }
992             }
993         }
994
995         public void CommandFollow(CommandModel cmd)
996         {
997             if (cmd.DataArray.Length < 2) {
998                 NotEnoughParameters(cmd);
999                 return;
1000             }
1001
1002             var chat = cmd.Chat as GroupChatModel;
1003             if (chat == null) {
1004                 return;
1005             }
1006
1007             var options = CreateOptions<CreateFriendshipOptions>();
1008             options.Follow = true;
1009             decimal userId;
1010             TwitterResponse<TwitterUser> res;
1011             if (Decimal.TryParse(cmd.Parameter, out userId)) {
1012                 // parameter is an ID
1013                 res = TwitterFriendship.Create(f_OAuthTokens, userId, options);
1014             } else {
1015                 // parameter is a screen name
1016                 var screenName = cmd.Parameter;
1017                 res = TwitterFriendship.Create(f_OAuthTokens, screenName, options);
1018             }
1019             CheckResponse(res);
1020             var person = CreatePerson(res.ResponseObject);
1021             if (chat.GetPerson(person.ID) == null) {
1022                 Session.AddPersonToGroupChat(chat, person);
1023             }
1024         }
1025
1026         public void CommandUnfollow(CommandModel cmd)
1027         {
1028             if (cmd.DataArray.Length < 2) {
1029                 NotEnoughParameters(cmd);
1030                 return;
1031             }
1032
1033             var chat = cmd.Chat as GroupChatModel;
1034             if (chat == null) {
1035                 return;
1036             }
1037
1038             PersonModel person;
1039             var persons = chat.Persons;
1040             if (persons.TryGetValue(cmd.Parameter, out person)) {
1041                 // parameter is an ID
1042                 decimal userId;
1043                 Decimal.TryParse(cmd.Parameter, out userId);
1044                 var res = TwitterFriendship.Delete(f_OAuthTokens, userId, f_OptionalProperties);
1045                 CheckResponse(res);
1046             } else {
1047                 // parameter is a screen name
1048                 var screenName = cmd.Parameter;
1049                 person = persons.SingleOrDefault((arg) => arg.Value.IdentityName == screenName).Value;
1050                 if (person == null) {
1051                     return;
1052                 }
1053                 var res = TwitterFriendship.Delete(f_OAuthTokens, screenName, f_OptionalProperties);
1054                 CheckResponse(res);
1055             }
1056             Session.RemovePersonFromGroupChat(chat, person);
1057         }
1058
1059         public bool IsHomeTimeLine(ChatModel chatModel)
1060         {
1061             return chatModel.Equals(f_FriendsTimelineChat);
1062         }
1063
1064         private List<TwitterStatus> SortTimeline(IList<TwitterStatus> timeline)
1065         {
1066             List<TwitterStatus> sortedTimeline =
1067                 new List<TwitterStatus>(
1068                     timeline
1069                 );
1070             sortedTimeline.Sort(
1071                 (a, b) => (a.CreatedDate.CompareTo(b.CreatedDate))
1072             );
1073             return sortedTimeline;
1074         }
1075
1076         public void CommandSearch(CommandModel cmd)
1077         {
1078             if (cmd.DataArray.Length < 2) {
1079                 NotEnoughParameters(cmd);
1080                 return;
1081             }
1082
1083             var keyword = cmd.Parameter;
1084             var chatName = String.Format(_("Search {0}"), keyword);
1085             var chat = Session.CreateChat<GroupChatModel>(keyword, chatName, this);
1086             Session.AddChat(chat);
1087             var options = CreateOptions<SearchOptions>();
1088             options.Count = 50;
1089             var response = TwitterSearch.Search(f_OAuthTokens, keyword, options);
1090             CheckResponse(response);
1091             var search = response.ResponseObject;
1092             var sortedSearch = SortTimeline(search);
1093             foreach (var status in sortedSearch) {
1094                 AddIndexToStatus(status);
1095                 var msg = CreateMessageBuilder().
1096                     Append(status, GetPerson(status.User)).
1097                     ToMessage();
1098                 chat.MessageBuffer.Add(msg);
1099                 var userId = status.User.Id.ToString();
1100                 if (!chat.UnsafePersons.ContainsKey(userId)) {
1101                     chat.UnsafePersons.Add(userId, GetPerson(status.User));
1102                 }
1103             }
1104             Session.SyncChat(chat);
1105         }
1106
1107         public void CommandRetweet(CommandModel cmd)
1108         {
1109             if (cmd.DataArray.Length < 2) {
1110                 NotEnoughParameters(cmd);
1111                 return;
1112             }
1113
1114
1115             TwitterStatus status = null;
1116             int indexId;
1117             if (Int32.TryParse(cmd.Parameter, out indexId)) {
1118                 status = GetStatusFromIndex(indexId);
1119             }
1120
1121             decimal statusId;
1122             if (status == null) {
1123                 if (!Decimal.TryParse(cmd.Parameter, out statusId)) {
1124                     return;
1125                 }
1126             } else {
1127                 statusId = status.Id;
1128             }
1129             var response = TwitterStatus.Retweet(f_OAuthTokens, statusId, f_OptionalProperties);
1130             CheckResponse(response);
1131             status = response.ResponseObject;
1132
1133             var msg = CreateMessageBuilder().
1134                 Append(status, GetPerson(status.User)).
1135                 ToMessage();
1136             Session.AddMessageToChat(f_FriendsTimelineChat, msg);
1137         }
1138
1139         public void CommandReply(CommandModel cmd)
1140         {
1141             if (cmd.DataArray.Length < 3) {
1142                 NotEnoughParameters(cmd);
1143                 return;
1144             }
1145
1146             var id = cmd.DataArray[1];
1147             TwitterStatus status = null;
1148             int indexId;
1149             if (Int32.TryParse(id, out indexId)) {
1150                 status = GetStatusFromIndex(indexId);
1151             }
1152
1153             decimal statusId;
1154             if (status == null) {
1155                 if (!Decimal.TryParse(id, out statusId)) {
1156                     return;
1157                 }
1158                 var response = TwitterStatus.Show(f_OAuthTokens, statusId,
1159                                                   f_OptionalProperties);
1160                 CheckResponse(response);
1161                 status = response.ResponseObject;
1162             }
1163
1164             var text = String.Join(" ", cmd.DataArray.Skip(2).ToArray());
1165             // the screen name must be somewhere in the message for replies
1166             if (!text.Contains("@" + status.User.ScreenName)) {
1167                 text = String.Format("@{0} {1}", status.User.ScreenName, text);
1168             }
1169             var options = CreateOptions<StatusUpdateOptions>();
1170             options.InReplyToStatusId = status.Id;
1171             PostUpdate(text, options);
1172         }
1173
1174         private List<TwitterDirectMessage> SortTimeline(TwitterDirectMessageCollection timeline)
1175         {
1176             var sortedTimeline = new List<TwitterDirectMessage>(timeline.Count);
1177             foreach (TwitterDirectMessage msg in timeline) {
1178                 sortedTimeline.Add(msg);
1179             }
1180             sortedTimeline.Sort(
1181                 (a, b) => (a.CreatedDate.CompareTo(b.CreatedDate))
1182             );
1183             return sortedTimeline;
1184         }
1185
1186         private void UpdateFriendsTimelineThread()
1187         {
1188             Trace.Call();
1189
1190             try {
1191                 // query the timeline only after we have fetched the user and friends
1192                 while (f_TwitterUser == null /*|| f_TwitterUser.IsEmpty*/ ||
1193                        f_Friends == null) {
1194                     Thread.Sleep(1000);
1195                 }
1196
1197                 // populate friend list
1198                 lock (f_Friends) {
1199                     foreach (PersonModel friend in f_Friends.Values) {
1200                         f_FriendsTimelineChat.UnsafePersons.Add(friend.ID, friend);
1201                     }
1202                 }
1203                 Session.AddChat(f_FriendsTimelineChat);
1204                 Session.SyncChat(f_FriendsTimelineChat);
1205
1206                 while (f_Listening) {
1207                     try {
1208                         UpdateFriendsTimeline();
1209                     } catch (TwitterizerException ex) {
1210                         CheckTwitterizerException(ex);
1211                     } catch (WebException ex) {
1212                         CheckWebException(ex);
1213                     }
1214
1215                     // only poll once per interval or when we get fired
1216                     f_FriendsTimelineEvent.WaitOne(
1217                         f_UpdateFriendsTimelineInterval * 1000, false
1218                     );
1219                 }
1220             } catch (ThreadAbortException) {
1221 #if LOG4NET
1222                 f_Logger.Debug("UpdateFriendsTimelineThread(): thread aborted");
1223 #endif
1224             } catch (Exception ex) {
1225 #if LOG4NET
1226                 f_Logger.Error("UpdateFriendsTimelineThread(): Exception", ex);
1227 #endif
1228                 var msg = CreateMessageBuilder().
1229                     AppendEventPrefix().
1230                     AppendErrorText(
1231                         _("An error occurred while fetching the friends " +
1232                           "timeline from Twitter. Reason: {0}"),
1233                         ex.Message).
1234                     ToMessage();
1235                 Session.AddMessageToChat(Chat, msg);
1236             } finally {
1237 #if LOG4NET
1238                 f_Logger.Debug("UpdateFriendsTimelineThread(): finishing thread.");
1239 #endif
1240                 lock (Session.Chats) {
1241                     if (Session.Chats.Contains(f_FriendsTimelineChat)) {
1242                         Session.RemoveChat(f_FriendsTimelineChat);
1243                     }
1244                 }
1245                 f_FriendsTimelineChat.UnsafePersons.Clear();
1246             }
1247         }
1248
1249         private void UpdateFriendsTimeline()
1250         {
1251             Trace.Call();
1252
1253 #if LOG4NET
1254             f_Logger.Debug("UpdateFriendsTimeline(): getting friend timeline from twitter...");
1255 #endif
1256             var options = CreateOptions<TimelineOptions>();
1257             options.SinceStatusId = f_LastFriendsTimelineStatusID;
1258             options.Count = 50;
1259             var response = TwitterTimeline.HomeTimeline(f_OAuthTokens,
1260                                                         options);
1261             // ignore temporarily issues
1262             if (IsTemporilyErrorResponse(response)) {
1263                 return;
1264             }
1265             CheckResponse(response);
1266             var timeline = response.ResponseObject;
1267 #if LOG4NET
1268             f_Logger.Debug("UpdateFriendsTimeline(): done. New tweets: " +
1269                            timeline.Count);
1270 #endif
1271             if (timeline.Count == 0) {
1272                 return;
1273             }
1274
1275             List<TwitterStatus> sortedTimeline = SortTimeline(timeline);
1276             foreach (TwitterStatus status in sortedTimeline) {
1277                 AddIndexToStatus(status);
1278                 var msg = CreateMessageBuilder().
1279                     Append(status, GetPerson(status.User)).
1280                     ToMessage();
1281                 Session.AddMessageToChat(f_FriendsTimelineChat, msg);
1282
1283                 if (status.User.Id.ToString() == Me.ID) {
1284                     OnMessageSent(
1285                         new MessageEventArgs(f_FriendsTimelineChat, msg, null,
1286                                              status.InReplyToScreenName ?? String.Empty)
1287                     );
1288                 } else {
1289                     OnMessageReceived(
1290                         new MessageEventArgs(f_FriendsTimelineChat, msg,
1291                                              status.User.ScreenName,
1292                                              status.InReplyToScreenName ?? String.Empty)
1293                     );
1294                 }
1295
1296                 f_LastFriendsTimelineStatusID = status.Id;
1297             }
1298         }
1299
1300         private void UpdateRepliesThread()
1301         {
1302             Trace.Call();
1303
1304             try {
1305                 // query the replies only after we have fetched the user and friends
1306                 while (f_TwitterUser == null /*|| f_TwitterUser.IsEmpty*/ ||
1307                        f_Friends == null) {
1308                     Thread.Sleep(1000);
1309                 }
1310
1311                 // populate friend list
1312                 lock (f_Friends) {
1313                     foreach (PersonModel friend in f_Friends.Values) {
1314                         f_RepliesChat.UnsafePersons.Add(friend.ID, friend);
1315                     }
1316                 }
1317                 Session.AddChat(f_RepliesChat);
1318                 Session.SyncChat(f_RepliesChat);
1319
1320                 while (f_Listening) {
1321                     try {
1322                         UpdateReplies();
1323                     } catch (TwitterizerException ex) {
1324                         CheckTwitterizerException(ex);
1325                     } catch (WebException ex) {
1326                         CheckWebException(ex);
1327                     }
1328
1329                     // only poll once per interval
1330                     Thread.Sleep(f_UpdateRepliesInterval * 1000);
1331                 }
1332             } catch (ThreadAbortException) {
1333 #if LOG4NET
1334                 f_Logger.Debug("UpdateRepliesThread(): thread aborted");
1335 #endif
1336             } catch (Exception ex) {
1337 #if LOG4NET
1338                 f_Logger.Error("UpdateRepliesThread(): Exception", ex);
1339 #endif
1340                 var msg = CreateMessageBuilder().
1341                     AppendEventPrefix().
1342                     AppendErrorText(
1343                         _("An error occurred while fetching the replies " +
1344                           "from Twitter. Reason: {0}"),
1345                         ex.Message).
1346                     ToMessage();
1347                 Session.AddMessageToChat(Chat, msg);
1348             } finally {
1349 #if LOG4NET
1350                 f_Logger.Debug("UpdateRepliesThread(): finishing thread.");
1351 #endif
1352                 lock (Session.Chats) {
1353                     if (Session.Chats.Contains(f_RepliesChat)) {
1354                         Session.RemoveChat(f_RepliesChat);
1355                     }
1356                 }
1357                 f_RepliesChat.UnsafePersons.Clear();
1358             }
1359         }
1360
1361         private void UpdateReplies()
1362         {
1363             Trace.Call();
1364
1365 #if LOG4NET
1366             f_Logger.Debug("UpdateReplies(): getting replies from twitter...");
1367 #endif
1368             var options = CreateOptions<TimelineOptions>();
1369             options.SinceStatusId = f_LastReplyStatusID;
1370             var response = TwitterTimeline.Mentions(f_OAuthTokens, options);
1371             // ignore temporarily issues
1372             if (IsTemporilyErrorResponse(response)) {
1373                 return;
1374             }
1375             CheckResponse(response);
1376             var timeline = response.ResponseObject;
1377 #if LOG4NET
1378             f_Logger.Debug("UpdateReplies(): done. New replies: " + timeline.Count);
1379 #endif
1380             if (timeline.Count == 0) {
1381                 return;
1382             }
1383
1384             // if this isn't the first time we receive replies, this is new!
1385             bool highlight = f_LastReplyStatusID != 0;
1386             List<TwitterStatus> sortedTimeline = SortTimeline(timeline);
1387             foreach (TwitterStatus status in sortedTimeline) {
1388                 AddIndexToStatus(status);
1389                 var msg = CreateMessageBuilder().
1390                     Append(status, GetPerson(status.User), highlight).
1391                     ToMessage();
1392                 Session.AddMessageToChat(f_RepliesChat, msg);
1393
1394                 OnMessageReceived(
1395                     new MessageEventArgs(f_RepliesChat, msg,
1396                                          status.User.ScreenName,
1397                                          status.InReplyToScreenName ?? String.Empty)
1398                 );
1399
1400                 f_LastReplyStatusID = status.Id;
1401             }
1402         }
1403
1404         private void UpdateDirectMessagesThread()
1405         {
1406             Trace.Call();
1407
1408             try {
1409                 // query the messages only after we have fetched the user and friends
1410                 while (f_TwitterUser == null ||
1411                        f_Friends == null) {
1412                     Thread.Sleep(1000);
1413                 }
1414
1415                 // populate friend list
1416                 lock (f_Friends) {
1417                     foreach (PersonModel friend in f_Friends.Values) {
1418                         f_DirectMessagesChat.UnsafePersons.Add(friend.ID, friend);
1419                     }
1420                 }
1421                 Session.AddChat(f_DirectMessagesChat);
1422                 Session.SyncChat(f_DirectMessagesChat);
1423
1424                 while (f_Listening) {
1425                     try {
1426                         UpdateDirectMessages();
1427                     } catch (TwitterizerException ex) {
1428                         CheckTwitterizerException(ex);
1429                     } catch (WebException ex) {
1430                         CheckWebException(ex);
1431                     }
1432
1433                     // only poll once per interval or when we get fired
1434                     f_DirectMessageEvent.WaitOne(
1435                         f_UpdateDirectMessagesInterval * 1000, false
1436                     );
1437                 }
1438             } catch (ThreadAbortException) {
1439 #if LOG4NET
1440                 f_Logger.Debug("UpdateDirectMessagesThread(): thread aborted");
1441 #endif
1442             } catch (Exception ex) {
1443 #if LOG4NET
1444                 f_Logger.Error("UpdateDirectMessagesThread(): Exception", ex);
1445 #endif
1446                 var msg = CreateMessageBuilder().
1447                     AppendEventPrefix().
1448                     AppendErrorText(
1449                         _("An error occurred while fetching direct messages " +
1450                           "from Twitter. Reason: {0}"),
1451                         ex.Message).
1452                     ToMessage();
1453                 Session.AddMessageToChat(Chat, msg);
1454             } finally {
1455 #if LOG4NET
1456                 f_Logger.Debug("UpdateDirectMessagesThread(): finishing thread.");
1457 #endif
1458                 lock (Session.Chats) {
1459                     if (Session.Chats.Contains(f_DirectMessagesChat)) {
1460                         Session.RemoveChat(f_DirectMessagesChat);
1461                     }
1462                 }
1463                 f_DirectMessagesChat.UnsafePersons.Clear();
1464             }
1465         }
1466
1467         private void UpdateDirectMessages()
1468         {
1469             Trace.Call();
1470
1471 #if LOG4NET
1472             f_Logger.Debug("UpdateDirectMessages(): getting received direct messages from twitter...");
1473 #endif
1474             var options = CreateOptions<DirectMessagesOptions>();
1475             options.SinceStatusId = f_LastDirectMessageReceivedStatusID;
1476             options.Count = 50;
1477             var response = TwitterDirectMessage.DirectMessages(
1478                 f_OAuthTokens, options
1479             );
1480             // ignore temporarily issues
1481             if (IsTemporilyErrorResponse(response)) {
1482                 return;
1483             }
1484             CheckResponse(response);
1485             var receivedTimeline = response.ResponseObject;
1486 #if LOG4NET
1487             f_Logger.Debug("UpdateDirectMessages(): done. New messages: " +
1488                 (receivedTimeline == null ? 0 : receivedTimeline.Count));
1489 #endif
1490
1491 #if LOG4NET
1492             f_Logger.Debug("UpdateDirectMessages(): getting sent direct messages from twitter...");
1493 #endif
1494             var sentOptions = CreateOptions<DirectMessagesSentOptions>();
1495             sentOptions.SinceStatusId = f_LastDirectMessageSentStatusID;
1496             sentOptions.Count = 50;
1497             response = TwitterDirectMessage.DirectMessagesSent(
1498                 f_OAuthTokens, sentOptions
1499             );
1500             // ignore temporarily issues
1501             if (IsTemporilyErrorResponse(response)) {
1502                 return;
1503             }
1504             CheckResponse(response);
1505             var sentTimeline = response.ResponseObject;
1506 #if LOG4NET
1507             f_Logger.Debug("UpdateDirectMessages(): done. New messages: " +
1508                 (sentTimeline == null ? 0 : sentTimeline.Count));
1509 #endif
1510
1511             var timeline = new TwitterDirectMessageCollection();
1512             if (receivedTimeline != null) {
1513                 foreach (TwitterDirectMessage msg in receivedTimeline) {
1514                     timeline.Add(msg);
1515                 }
1516             }
1517             if (sentTimeline != null) {
1518                 foreach (TwitterDirectMessage msg in sentTimeline) {
1519                     timeline.Add(msg);
1520                 }
1521             }
1522
1523             if (timeline.Count == 0) {
1524                 // nothing to do
1525                 return;
1526             }
1527
1528             var sortedTimeline = SortTimeline(timeline);
1529             foreach (TwitterDirectMessage directMsg in sortedTimeline) {
1530                 // if this isn't the first time a receive a direct message,
1531                 // this is a new one!
1532                 bool highlight = receivedTimeline.Contains(directMsg) &&
1533                                  f_LastDirectMessageReceivedStatusID != 0;
1534                 var msg = CreateMessageBuilder().
1535                     Append(directMsg, GetPerson(directMsg.Sender), highlight).
1536                     ToMessage();
1537                 Session.AddMessageToChat(f_DirectMessagesChat, msg);
1538
1539                 // if there is a tab open for this user put the message there too
1540                 string userId;
1541                 if (receivedTimeline.Contains(directMsg)) {
1542                     // this is a received message
1543                     userId =  directMsg.SenderId.ToString();
1544
1545                     OnMessageReceived(
1546                         new MessageEventArgs(f_DirectMessagesChat, msg,
1547                                              directMsg.SenderScreenName, null)
1548                     );
1549                 } else {
1550                     // this is a sent message
1551                     userId = directMsg.RecipientId.ToString();
1552
1553                     OnMessageSent(
1554                         new MessageEventArgs(f_DirectMessagesChat, msg,
1555                                              null, directMsg.RecipientScreenName)
1556                     );
1557                 }
1558                 ChatModel chat =  Session.GetChat(
1559                     userId,
1560                     ChatType.Person,
1561                     this
1562                 );
1563                 if (chat != null) {
1564                     Session.AddMessageToChat(chat, msg);
1565                 }
1566             }
1567
1568             if (receivedTimeline != null) {
1569                 // first one is the newest
1570                 foreach (TwitterDirectMessage msg in receivedTimeline) {
1571                     f_LastDirectMessageReceivedStatusID = msg.Id;
1572                     break;
1573                 }
1574             }
1575             if (sentTimeline != null) {
1576                 // first one is the newest
1577                 foreach (TwitterDirectMessage msg in sentTimeline) {
1578                     f_LastDirectMessageSentStatusID = msg.Id;
1579                     break;
1580                 }
1581             }
1582         }
1583
1584         private void UpdateFriends()
1585         {
1586             Trace.Call();
1587
1588             if (f_Friends != null) {
1589                 return;
1590             }
1591
1592 #if LOG4NET
1593             f_Logger.Debug("UpdateFriends(): fetching friend IDs from twitter...");
1594 #endif
1595             var options = CreateOptions<UsersIdsOptions>();
1596             options.UserId = f_TwitterUser.Id;
1597             var response = TwitterFriendship.FriendsIds(
1598                 f_OAuthTokens, options
1599             );
1600             CheckResponse(response);
1601             var friendIds = response.ResponseObject;
1602 #if LOG4NET
1603             f_Logger.Debug("UpdateFriends(): done. Fetched IDs: " + friendIds.Count);
1604 #endif
1605
1606             var persons = new Dictionary<string, PersonModel>(friendIds.Count);
1607             // users/lookup only permits 100 users per call
1608             var pageSize = 100;
1609             var idList = new List<decimal>(friendIds);
1610             var idPages = new List<List<decimal>>();
1611             for (int offset = 0; offset < idList.Count; offset += pageSize) {
1612                 var count = Math.Min(pageSize, idList.Count - offset);
1613                 idPages.Add(idList.GetRange(offset, count));
1614             }
1615             foreach (var idPage in idPages) {
1616 #if LOG4NET
1617                 f_Logger.Debug("UpdateFriends(): fetching friends from twitter...");
1618 #endif
1619                 var userIds = new TwitterIdCollection(idPage);
1620                 var lookupOptions = CreateOptions<LookupUsersOptions>();
1621                 lookupOptions.UserIds = userIds;
1622                 var lookupResponse = TwitterUser.Lookup(f_OAuthTokens, lookupOptions);
1623                 CheckResponse(lookupResponse);
1624                 var friends = lookupResponse.ResponseObject;
1625 #if LOG4NET
1626                 f_Logger.Debug("UpdateFriends(): done. Fetched friends: " + friends.Count);
1627 #endif
1628                 foreach (var friend in friends) {
1629                     var person = CreatePerson(friend);
1630                     persons.Add(person.ID, person);
1631                 }
1632             }
1633             f_Friends = persons;
1634         }
1635
1636         private void UpdateUser()
1637         {
1638 #if LOG4NET
1639             f_Logger.Debug("UpdateUser(): getting user details from twitter...");
1640 #endif
1641             var response = TwitterUser.Show(f_OAuthTokens, f_Username,
1642                                             f_OptionalProperties);
1643             CheckResponse(response);
1644             var user = response.ResponseObject;
1645             f_TwitterUser = user;
1646             Me = CreatePerson(f_TwitterUser);
1647 #if LOG4NET
1648             f_Logger.Debug("UpdateUser(): done.");
1649 #endif
1650         }
1651
1652         protected new TwitterMessageBuilder CreateMessageBuilder()
1653         {
1654             return CreateMessageBuilder<TwitterMessageBuilder>();
1655         }
1656
1657         private T CreateOptions<T>() where T : OptionalProperties, new()
1658         {
1659             var options = new T() {
1660                 Proxy = f_WebProxy
1661             };
1662             return options;
1663         }
1664
1665         void PostUpdate(string text)
1666         {
1667             PostUpdate(text, null);
1668         }
1669
1670         void PostUpdate(string text, StatusUpdateOptions options)
1671         {
1672             if (options == null) {
1673                 options = CreateOptions<StatusUpdateOptions>();
1674             }
1675             var res = TwitterStatus.Update(f_OAuthTokens, text, options);
1676             CheckResponse(res);
1677             f_FriendsTimelineEvent.Set();
1678         }
1679
1680         private void SendMessage(string target, string text)
1681         {
1682             var res = TwitterDirectMessage.Send(f_OAuthTokens, target, text,
1683                                                 f_OptionalProperties);
1684             CheckResponse(res);
1685             f_DirectMessageEvent.Set();
1686         }
1687
1688         void AddIndexToStatus(TwitterStatus status)
1689         {
1690             lock (StatusIndex) {
1691                 var slot = ++StatusIndexOffset;
1692                 if (slot > StatusIndex.Length) {
1693                     StatusIndexOffset = 1;
1694                     slot = 1;
1695                 }
1696                 StatusIndex[slot - 1] = status;
1697                 status.Text = String.Format("[{0:00}] {1}", slot, status.Text);
1698                 var rtStatus = status.RetweetedStatus;
1699                 if (rtStatus != null) {
1700                     rtStatus.Text = String.Format("[{0:00}] {1}", slot,
1701                                                   rtStatus.Text);
1702                 }
1703             }
1704         }
1705
1706         TwitterStatus GetStatusFromIndex(int slot)
1707         {
1708             lock (StatusIndex) {
1709                 if (slot > StatusIndex.Length || slot < 1) {
1710                     return null;
1711                 }
1712                 return StatusIndex[slot - 1];
1713             }
1714         }
1715
1716         private void CheckTwitterizerException(TwitterizerException exception)
1717         {
1718             Trace.Call(exception == null ? null : exception.GetType());
1719
1720             if (exception.InnerException is WebException) {
1721                 CheckWebException((WebException) exception.InnerException);
1722                 return;
1723             } else if (exception.InnerException != null) {
1724 #if LOG4NET
1725                 f_Logger.Warn("CheckTwitterizerException(): unknown inner exception: " + exception.InnerException.GetType(), exception.InnerException);
1726 #endif
1727             }
1728
1729             throw exception;
1730         }
1731         
1732         private void CheckWebException(WebException exception)
1733         {
1734             Trace.Call(exception == null ? null : exception.GetType());
1735
1736             switch (exception.Status) {
1737                 case WebExceptionStatus.ConnectFailure:
1738                 case WebExceptionStatus.ConnectionClosed:
1739                 case WebExceptionStatus.Timeout:
1740                 case WebExceptionStatus.ReceiveFailure:
1741                 case WebExceptionStatus.NameResolutionFailure:
1742                 case WebExceptionStatus.ProxyNameResolutionFailure:
1743                     // ignore temporarly issues
1744 #if LOG4NET
1745                     f_Logger.Warn("CheckWebException(): ignored exception", exception);
1746 #endif
1747                     return;
1748             }
1749
1750             /*
1751             http://apiwiki.twitter.com/HTTP-Response-Codes-and-Errors
1752             * 200 OK: Success!
1753             * 304 Not Modified: There was no new data to return.
1754             * 400 Bad Request: The request was invalid.  An accompanying error
1755             *     message will explain why. This is the status code will be
1756             *     returned during rate limiting.
1757             * 401 Unauthorized: Authentication credentials were missing or
1758             *     incorrect.
1759             * 403 Forbidden: The request is understood, but it has been
1760             *     refused.  An accompanying error message will explain why.
1761             *     This code is used when requests are being denied due to
1762             *     update limits.
1763             * 404 Not Found: The URI requested is invalid or the resource
1764             *     requested, such as a user, does not exists.
1765             * 406 Not Acceptable: Returned by the Search API when an invalid
1766             *     format is specified in the request.
1767             * 500 Internal Server Error: Something is broken.  Please post to
1768             *     the group so the Twitter team can investigate.
1769             * 502 Bad Gateway: Twitter is down or being upgraded.
1770             * 503 Service Unavailable: The Twitter servers are up, but
1771             *     overloaded with requests. Try again later. The search and
1772             *     trend methods use this to indicate when you are being rate
1773             *     limited.
1774             */
1775             HttpWebResponse httpRes = exception.Response as HttpWebResponse;
1776             if (httpRes == null) {
1777                 throw exception;
1778             }
1779             switch (httpRes.StatusCode) {
1780                 case HttpStatusCode.BadGateway:
1781                 case HttpStatusCode.BadRequest:
1782                 case HttpStatusCode.Forbidden:
1783                 case HttpStatusCode.ServiceUnavailable:
1784                 case HttpStatusCode.GatewayTimeout:
1785                     // ignore temporarly issues
1786 #if LOG4NET
1787                     f_Logger.Warn("CheckWebException(): ignored exception", exception);
1788 #endif
1789                     return;
1790                 default:
1791 #if LOG4NET
1792                     f_Logger.Error("CheckWebException(): " +
1793                                    "Status: " + exception.Status + " " +
1794                                    "ResponseUri: " + exception.Response.ResponseUri);
1795 #endif
1796                     throw exception;
1797             }
1798         }
1799
1800         private void CheckResponse<T>(TwitterResponse<T> response) where T : ITwitterObject
1801         {
1802             if (response == null) {
1803                 throw new ArgumentNullException("response");
1804             }
1805
1806             if (response.Result == RequestResult.Success) {
1807                 return;
1808             }
1809
1810 #if LOG4NET
1811             f_Logger.Error("CheckResponse(): " +
1812                            "RequestUrl: " + response.RequestUrl + " " +
1813                            "Result: " + response.Result + " " +
1814                            "Content:\n" + response.Content);
1815 #endif
1816
1817             // HACK: Twitter returns HTML code saying they are overloaded o_O
1818             if (response.Result == RequestResult.Unknown &&
1819                 response.ErrorMessage == null) {
1820                 response.ErrorMessage = _("Twitter didn't send a valid response, they're probably overloaded");
1821             }
1822             throw new TwitterizerException(response.ErrorMessage);
1823         }
1824
1825         private bool IsTemporilyErrorResponse<T>(TwitterResponse<T> response)
1826                                         where T : ITwitterObject
1827         {
1828             if (response == null) {
1829                 throw new ArgumentNullException("response");
1830             }
1831
1832             switch (response.Result) {
1833                 case RequestResult.Success:
1834                     // no error at all
1835                     ErrorResponseCount = 0;
1836                     return false;
1837                 case RequestResult.ConnectionFailure:
1838                 case RequestResult.RateLimited:
1839                 case RequestResult.TwitterIsDown:
1840                 case RequestResult.TwitterIsOverloaded:
1841                 // probably "Twitter is over capacity"
1842                 case RequestResult.Unknown:
1843 #if LOG4NET
1844                     f_Logger.Debug("IsTemporilyErrorResponse(): " +
1845                                    "Detected temporily error " +
1846                                    "RequestUrl: " + response.RequestUrl + " " +
1847                                    "Result: " + response.Result + " " +
1848                                    "Content:\n" + response.Content);
1849 #endif
1850                     return true;
1851             }
1852
1853             if (ErrorResponseCount++ < MaxErrorResponseCount) {
1854 #if LOG4NET
1855                 f_Logger.WarnFormat(
1856                     "IsTemporilyErrorResponse(): Ignoring permanent error " +
1857                     "({0}/{1}) " +
1858                     "RequestUrl: {2} " +
1859                     "Result: {3} " +
1860                     "Content:\n{4}",
1861                     ErrorResponseCount,
1862                     MaxErrorResponseCount,
1863                     response.RequestUrl,
1864                     response.Result,
1865                     response.Content
1866                 );
1867 #endif
1868                 return true;
1869             }
1870
1871 #if LOG4NET
1872             f_Logger.ErrorFormat(
1873                 "IsTemporilyErrorResponse(): Detected permanent error " +
1874                 "RequestUrl: {0} Result: {1} " +
1875                 "Content:\n{2}",
1876                 response.RequestUrl,
1877                 response.Result,
1878                 response.Content
1879             );
1880 #endif
1881             return false;
1882         }
1883
1884         private PersonModel GetPerson(TwitterUser user)
1885         {
1886             if (user == null) {
1887                 throw new ArgumentNullException("user");
1888             }
1889
1890             PersonModel person;
1891             if (!f_Friends.TryGetValue(user.Id.ToString(), out person)) {
1892                 return CreatePerson(user);
1893             }
1894             return person;
1895         }
1896
1897         private PersonModel CreatePerson(decimal userId)
1898         {
1899             var res = TwitterUser.Show(f_OAuthTokens, userId, f_OptionalProperties);
1900             CheckResponse(res);
1901             var user = res.ResponseObject;
1902             return CreatePerson(user);
1903         }
1904
1905         private PersonModel CreatePerson(TwitterUser user)
1906         {
1907             if (user == null) {
1908                 throw new ArgumentNullException("user");
1909             }
1910
1911             var person = new PersonModel(
1912                 user.Id.ToString(),
1913                 user.ScreenName,
1914                 NetworkID,
1915                 Protocol,
1916                 this
1917             );
1918             if (f_TwitterUser != null &&
1919                 f_TwitterUser.ScreenName == user.ScreenName) {
1920                 person.IdentityNameColored.ForegroundColor = f_BlueTextColor;
1921                 person.IdentityNameColored.BackgroundColor = TextColor.None;
1922                 person.IdentityNameColored.Bold = true;
1923             }
1924             return person;
1925         }
1926
1927         protected override T CreateMessageBuilder<T>()
1928         {
1929             var builder = new TwitterMessageBuilder();
1930             builder.ApplyConfig(Session.UserConfig);
1931             return (T)(object) builder;
1932         }
1933
1934         private string[] GetApiKey()
1935         {
1936             var key = Defines.TwitterApiKey.Split('|');
1937             if (key.Length != 2) {
1938                 throw new InvalidOperationException("Invalid Twitter API key!");
1939             }
1940
1941             return key;
1942         }
1943
1944         private static string _(string msg)
1945         {
1946             return LibraryCatalog.GetString(msg, f_LibraryTextDomain);
1947         }
1948     }
1949 }