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