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