3
// Smuxi - Smart MUltipleXed Irc
5
// Copyright (c) 2009 Mirco Bauer <meebey@meebey.net>
7
// Full GPL License: <http://www.gnu.org/licenses/gpl.txt>
9
// This program is free software; you can redistribute it and/or modify
10
// it under the terms of the GNU General Public License as published by
11
// the Free Software Foundation; either version 2 of the License, or
12
// (at your option) any later version.
14
// This program is distributed in the hope that it will be useful,
15
// but WITHOUT ANY WARRANTY; without even the implied warranty of
16
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
// GNU General Public License for more details.
19
// You should have received a copy of the GNU General Public License
20
// along with this program; if not, write to the Free Software
21
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24
using System.Threading;
25
using System.Collections.Generic;
26
using Twitterizer.Framework;
29
namespace Smuxi.Engine
31
public enum TwitterChatType {
37
[ProtocolManagerInfo(Name = "Twitter", Description = "Twitter Micro-Blogging", Alias = "twitter")]
38
public class TwitterProtocolManager : ProtocolManagerBase
41
private static readonly log4net.ILog f_Logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
43
static readonly string f_LibraryTextDomain = "smuxi-engine-twitter";
44
static readonly TextColor f_BlueTextColor = new TextColor(0x0000FF);
46
TwitterUser f_TwitterUser;
48
ProtocolChatModel f_ProtocolChat;
49
List<PersonModel> f_Friends;
50
List<GroupChatModel> f_GroupChats = new List<GroupChatModel>();
52
GroupChatModel f_FriendsTimelineChat;
53
AutoResetEvent f_FriendsTimelineEvent = new AutoResetEvent(false);
54
Thread f_UpdateFriendsTimelineThread;
55
int f_UpdateFriendsTimelineInterval = 120;
56
Int64? f_LastFriendsTimelineStatusID;
57
DateTime f_LastFriendsUpdate;
59
GroupChatModel f_RepliesChat;
60
Thread f_UpdateRepliesThread;
61
int f_UpdateRepliesInterval = 120;
62
Int64? f_LastReplyStatusID;
64
GroupChatModel f_DirectMessagesChat;
65
AutoResetEvent f_DirectMessageEvent = new AutoResetEvent(false);
66
Thread f_UpdateDirectMessagesThread;
67
int f_UpdateDirectMessagesInterval = 120;
68
Int64? f_LastDirectMessageReceivedStatusID;
69
Int64? f_LastDirectMessageSentStatusID;
74
public override string NetworkID {
80
public override string Protocol {
86
public override ChatModel Chat {
88
return f_ProtocolChat;
92
public TwitterProtocolManager(Session session) : base(session)
96
f_ProtocolChat = new ProtocolChatModel(NetworkID, "Twitter", this);
98
f_FriendsTimelineChat = new GroupChatModel(
99
TwitterChatType.FriendsTimeline.ToString(),
100
_("Friends Timeline"),
103
f_GroupChats.Add(f_FriendsTimelineChat);
105
f_RepliesChat = new GroupChatModel(
106
TwitterChatType.Replies.ToString(),
110
f_GroupChats.Add(f_RepliesChat);
112
f_DirectMessagesChat = new GroupChatModel(
113
TwitterChatType.DirectMessages.ToString(),
114
_("Direct Messages"),
117
f_GroupChats.Add(f_DirectMessagesChat);
120
public override void Connect(FrontendManager fm, string host, int port,
121
string username, string password)
123
Trace.Call(fm, host, port, username, "XXX");
125
f_Username = username;
126
f_Twitter = new Twitter(username, password, "Smuxi");
128
Session.AddChat(f_ProtocolChat);
129
Session.SyncChat(f_ProtocolChat);
132
msg = String.Format(_("Connecting to Twitter..."));
134
Session.AddTextToChat(f_ProtocolChat, "-!- " + msg);
136
// for some reason VerifyCredentials() always fails
137
//bool login = Twitter.VerifyCredentials(username, password);
140
fm.SetStatus(_("Login failed!"));
141
Session.AddTextToChat(f_ProtocolChat,
142
"-!- " + _("Login failed! Username and/or password are " +
147
} catch (Exception ex) {
148
fm.SetStatus(_("Connection failed!"));
149
Session.AddTextToChat(f_ProtocolChat,
150
"-!- " + _("Connection failed! Reason: ") + ex.Message
154
f_IsConnected = true;
155
msg =_("Successfully connected to Twitter.");
157
Session.AddTextToChat(f_ProtocolChat, "-!- " + msg);
160
// twitter is sometimes pretty slow, so fetch this in the background
161
ThreadPool.QueueUserWorkItem(delegate {
163
msg =_("Fetching user details from Twitter, please wait...");
164
Session.AddTextToChat(f_ProtocolChat, "-!- " + msg);
168
msg =_("Finished fetching user details.");
169
Session.AddTextToChat(f_ProtocolChat, "-!- " + msg);
171
f_FriendsTimelineChat.PersonCount = f_TwitterUser.NumberOfFriends;
172
f_RepliesChat.PersonCount = f_TwitterUser.NumberOfFriends;
173
f_DirectMessagesChat.PersonCount = f_TwitterUser.NumberOfFriends;
174
} catch (Exception ex) {
175
msg =_("Failed to fetch user details from Twitter. Reason: ");
177
f_Logger.Error("Connect(): " + msg, ex);
179
Session.AddTextToChat(f_ProtocolChat, "-!- " + msg + ex.Message);
182
ThreadPool.QueueUserWorkItem(delegate {
184
msg =_("Fetching friends from Twitter, please wait...");
185
Session.AddTextToChat(f_ProtocolChat, "-!- " + msg);
189
msg =_("Finished fetching friends.");
190
Session.AddTextToChat(f_ProtocolChat, "-!- " + msg);
191
} catch (Exception ex) {
192
msg =_("Failed to fetch friends from Twitter. Reason: ");
194
f_Logger.Error("Connect(): " + msg, ex);
196
Session.AddTextToChat(f_ProtocolChat, "-!- " + msg + ex.Message);
200
OpenFriendsTimelineChat();
202
OpenDirectMessagesChat();
205
public override void Reconnect(FrontendManager fm)
210
public override void Disconnect(FrontendManager fm)
215
f_FriendsTimelineEvent.Set();
218
public override IList<GroupChatModel> FindGroupChats(GroupChatModel filter)
225
public override void OpenChat(FrontendManager fm, ChatModel chat)
227
Trace.Call(fm, chat);
229
if (chat.ChatType == ChatType.Group) {
230
TwitterChatType twitterChatType = (TwitterChatType)
231
Enum.Parse(typeof(TwitterChatType), chat.ID);
232
switch (twitterChatType) {
233
case TwitterChatType.FriendsTimeline:
234
OpenFriendsTimelineChat();
236
case TwitterChatType.Replies:
239
case TwitterChatType.DirectMessages:
240
OpenDirectMessagesChat();
246
OpenPrivateChat(chat.ID);
249
private void OpenFriendsTimelineChat()
251
ChatModel chat = Session.GetChat(
252
TwitterChatType.FriendsTimeline.ToString(),
261
// BUG: causes a race condition as the frontend syncs the
262
// unpopulated chat! So only add it if it's ready
263
//Session.AddChat(f_FriendsTimelineChat);
264
f_UpdateFriendsTimelineThread = new Thread(
265
new ThreadStart(UpdateFriendsTimelineThread)
267
f_UpdateFriendsTimelineThread.IsBackground = true;
268
f_UpdateFriendsTimelineThread.Name =
269
"TwitterProtocolManager friends timeline listener";
270
f_UpdateFriendsTimelineThread.Start();
273
private void OpenRepliesChat()
275
ChatModel chat = Session.GetChat(
276
TwitterChatType.Replies.ToString(),
285
// BUG: causes a race condition as the frontend syncs the
286
// unpopulated chat! So only add it if it's ready
287
//Session.AddChat(f_RepliesChat);
288
f_UpdateRepliesThread = new Thread(
289
new ThreadStart(UpdateRepliesThread)
291
f_UpdateRepliesThread.IsBackground = true;
292
f_UpdateRepliesThread.Name =
293
"TwitterProtocolManager replies listener";
294
f_UpdateRepliesThread.Start();
297
private void OpenDirectMessagesChat()
299
ChatModel chat = Session.GetChat(
300
TwitterChatType.DirectMessages.ToString(),
309
// BUG: causes a race condition as the frontend syncs the
310
// unpopulated chat! So only add it if it's ready
311
//Session.AddChat(f_DirectMessagesChat);
312
f_UpdateDirectMessagesThread = new Thread(
313
new ThreadStart(UpdateDirectMessagesThread)
315
f_UpdateDirectMessagesThread.IsBackground = true;
316
f_UpdateDirectMessagesThread.Name =
317
"TwitterProtocolManager direct messages listener";
318
f_UpdateDirectMessagesThread.Start();
321
private void OpenPrivateChat(int userId)
323
OpenPrivateChat(userId.ToString());
326
private void OpenPrivateChat(string userId)
328
ChatModel chat = Session.GetChat(
338
TwitterUser user = f_Twitter.User.Show(userId);
339
PersonModel person = new PersonModel(
346
PersonChatModel personChat = new PersonChatModel(
352
Session.AddChat(personChat);
353
Session.SyncChat(personChat);
356
public override void CloseChat(FrontendManager fm, ChatModel chat)
358
Trace.Call(fm, chat);
360
Session.RemoveChat(chat);
363
public override bool Command(CommandModel command)
365
bool handled = false;
366
if (command.IsCommand) {
368
switch (command.Command) {
371
CommandMessage(command);
376
switch (command.Command) {
378
CommandHelp(command);
382
CommandConnect(command);
391
NotConnected(command);
399
public override string ToString()
404
public void CommandHelp(CommandModel cd)
406
MessageModel fmsg = new MessageModel();
407
TextMessagePartModel fmsgti;
409
fmsgti = new TextMessagePartModel();
410
// TRANSLATOR: this line is used as a label / category for a
411
// list of commands below
412
fmsgti.Text = "[" + _("Twitter Commands") + "]";
414
fmsg.MessageParts.Add(fmsgti);
416
Session.AddMessageToChat(cd.Chat, fmsg);
420
"connect twitter username password",
423
foreach (string line in help) {
424
cd.FrontendManager.AddTextToChat(cd.Chat, "-!- " + line);
428
public void CommandConnect(CommandModel cd)
431
if (cd.DataArray.Length >= 3) {
432
user = cd.DataArray[2];
434
NotEnoughParameters(cd);
439
if (cd.DataArray.Length >= 4) {
440
pass = cd.DataArray[3];
442
NotEnoughParameters(cd);
446
Connect(cd.FrontendManager, null, 0, user, pass);
449
public void CommandSay(CommandModel cmd)
451
FrontendManager fm = cmd.FrontendManager;
452
if (cmd.Chat.ChatType == ChatType.Group) {
453
TwitterChatType twitterChatType = (TwitterChatType)
454
Enum.Parse(typeof(TwitterChatType), cmd.Chat.ID);
455
switch (twitterChatType) {
456
case TwitterChatType.FriendsTimeline:
457
case TwitterChatType.Replies:
458
PostUpdate(cmd.Data);
460
case TwitterChatType.DirectMessages:
464
_("Cannot send message - no target specified. "+
465
"Use: /msg $nick message")
469
} else if (cmd.Chat.ChatType == ChatType.Person) {
470
SendMessage(cmd.Chat.ID, cmd.Data);
472
// ignore protocol chat
476
public void CommandMessage(CommandModel cmd)
478
FrontendManager fm = cmd.FrontendManager;
480
if (cmd.DataArray.Length >= 2) {
481
nickname = cmd.DataArray[1];
483
NotEnoughParameters(cmd);
487
TwitterUser user = null;
489
user = f_Twitter.User.Show(nickname);
490
} catch (NullReferenceException) {
491
// HACK: User.Show() might throw an NRE if the user does not exist
492
// trying to handle this gracefully
495
fm.AddTextToChat(cmd.Chat, "-!- " +
496
_("Could not send message - the specified user does not exist.")
501
OpenPrivateChat(user.ID);
503
if (cmd.DataArray.Length >= 3) {
504
string message = String.Join(" ", cmd.DataArray, 2, cmd.DataArray.Length-2);
505
SendMessage(user.ID.ToString(), message);
509
private List<TwitterStatus> SortTimeline(TwitterStatusCollection timeline)
511
List<TwitterStatus> sortedTimeline =
512
new List<TwitterStatus>(
515
foreach (TwitterStatus status in timeline) {
516
sortedTimeline.Add(status);
519
(a, b) => (a.Created.CompareTo(b.Created))
521
return sortedTimeline;
524
private void UpdateFriendsTimelineThread()
529
// query the timeline only after we have fetched the user and friends
530
while (f_TwitterUser == null || f_Friends == null) {
534
// populate friend list
536
foreach (PersonModel friend in f_Friends) {
537
f_FriendsTimelineChat.UnsafePersons.Add(friend.ID, friend);
540
Session.AddChat(f_FriendsTimelineChat);
541
Session.SyncChat(f_FriendsTimelineChat);
543
while (f_Listening) {
545
UpdateFriendsTimeline();
546
} catch (TwitterizerException ex) {
547
if (ex.Message.Contains("rate limited")) {
548
// ignore and keep trying
554
// only poll once per interval or when we get fired
555
f_FriendsTimelineEvent.WaitOne(
556
f_UpdateFriendsTimelineInterval * 1000
559
} catch (ThreadAbortException) {
561
f_Logger.Debug("UpdateFriendsTimelineThread(): thread aborted");
563
} catch (Exception ex) {
565
f_Logger.Error("UpdateFriendsTimelineThread(): Exception", ex);
567
string msg =_("An error occurred while fetching the friends timeline from Twitter. Reason: ");
568
Session.AddTextToChat(f_ProtocolChat, "-!- " + msg + ex.Message);
571
f_Logger.Debug("UpdateFriendsTimelineThread(): finishing thread.");
575
private void UpdateFriendsTimeline()
580
f_Logger.Debug("UpdateFriendsTimeline(): getting friend timeline from twitter...");
582
TwitterParameters parameters = new TwitterParameters();
583
parameters.Add(TwitterParameterNames.Count, 50);
584
if (f_LastFriendsTimelineStatusID != null) {
585
parameters.Add(TwitterParameterNames.SinceID,
586
f_LastFriendsTimelineStatusID);
588
TwitterStatusCollection timeline =
589
f_Twitter.Status.FriendsTimeline(parameters);
591
f_Logger.Debug("UpdateFriendsTimeline(): done. New tweets: " +
592
(timeline == null ? 0 : timeline.Count));
594
if (timeline == null || timeline.Count == 0) {
598
List<TwitterStatus> sortedTimeline = SortTimeline(timeline);
599
foreach (TwitterStatus status in sortedTimeline) {
600
MessageModel msg = CreateMessage(
602
status.TwitterUser.ScreenName,
605
Session.AddMessageToChat(f_FriendsTimelineChat, msg);
607
f_LastFriendsTimelineStatusID = status.ID;
611
private void UpdateRepliesThread()
616
// query the replies only after we have fetched the user and friends
617
while (f_TwitterUser == null || f_Friends == null) {
621
// populate friend list
623
foreach (PersonModel friend in f_Friends) {
624
f_RepliesChat.UnsafePersons.Add(friend.ID, friend);
627
Session.AddChat(f_RepliesChat);
628
Session.SyncChat(f_RepliesChat);
630
while (f_Listening) {
633
} catch (TwitterizerException ex) {
634
if (ex.Message.Contains("rate limited")) {
635
// ignore and keep trying
641
// only poll once per interval
642
Thread.Sleep(f_UpdateRepliesInterval * 1000);
644
} catch (ThreadAbortException) {
646
f_Logger.Debug("UpdateRepliesThread(): thread aborted");
648
} catch (Exception ex) {
650
f_Logger.Error("UpdateRepliesThread(): Exception", ex);
652
string msg =_("An error occurred while fetching the replies from Twitter. Reason: ");
653
Session.AddTextToChat(f_ProtocolChat, "-!- " + msg + ex.Message);
656
f_Logger.Debug("UpdateRepliesThread(): finishing thread.");
660
private void UpdateReplies()
665
f_Logger.Debug("UpdateReplies(): getting replies from twitter...");
667
TwitterParameters parameters = new TwitterParameters();
668
parameters.Add(TwitterParameterNames.Count, 50);
669
if (f_LastReplyStatusID != null) {
670
parameters.Add(TwitterParameterNames.SinceID,
671
f_LastReplyStatusID);
673
TwitterStatusCollection timeline =
674
f_Twitter.Status.Replies(parameters);
676
f_Logger.Debug("UpdateReplies(): done. New replies: " +
677
(timeline == null ? 0 : timeline.Count));
679
if (timeline == null || timeline.Count == 0) {
683
// if this isn't the first time we receive replies, this is new!
684
bool highlight = f_LastReplyStatusID != null;
685
List<TwitterStatus> sortedTimeline = SortTimeline(timeline);
686
foreach (TwitterStatus status in sortedTimeline) {
687
MessageModel msg = CreateMessage(
689
status.TwitterUser.ScreenName,
693
Session.AddMessageToChat(f_RepliesChat, msg);
695
f_LastReplyStatusID = status.ID;
699
private void UpdateDirectMessagesThread()
704
// query the messages only after we have fetched the user and friends
705
while (f_TwitterUser == null || f_Friends == null) {
709
// populate friend list
711
foreach (PersonModel friend in f_Friends) {
712
f_DirectMessagesChat.UnsafePersons.Add(friend.ID, friend);
715
Session.AddChat(f_DirectMessagesChat);
716
Session.SyncChat(f_DirectMessagesChat);
718
while (f_Listening) {
720
UpdateDirectMessages();
721
} catch (TwitterizerException ex) {
722
if (ex.Message.Contains("rate limited")) {
723
// ignore and keep trying
729
// only poll once per interval or when we get fired
730
f_DirectMessageEvent.WaitOne(
731
f_UpdateDirectMessagesInterval * 1000
734
} catch (ThreadAbortException) {
736
f_Logger.Debug("UpdateDirectMessagesThread(): thread aborted");
738
} catch (Exception ex) {
740
f_Logger.Error("UpdateDirectMessagesThread(): Exception", ex);
742
string msg =_("An error occurred while fetching direct messages from Twitter. Reason: ");
743
Session.AddTextToChat(f_ProtocolChat, "-!- " + msg + ex.Message);
746
f_Logger.Debug("UpdateDirectMessagesThread(): finishing thread.");
750
private void UpdateDirectMessages()
754
TwitterParameters parameters;
756
f_Logger.Debug("UpdateDirectMessages(): getting received direct messages from twitter...");
758
parameters = new TwitterParameters();
759
parameters.Add(TwitterParameterNames.Count, 50);
760
if (f_LastDirectMessageReceivedStatusID != null) {
761
parameters.Add(TwitterParameterNames.SinceID,
762
f_LastDirectMessageReceivedStatusID);
764
TwitterStatusCollection receivedTimeline =
765
f_Twitter.DirectMessages.DirectMessages(parameters);
767
f_Logger.Debug("UpdateDirectMessages(): done. New messages: " +
768
(receivedTimeline == null ? 0 : receivedTimeline.Count));
772
f_Logger.Debug("UpdateDirectMessages(): getting sent direct messages from twitter...");
774
parameters = new TwitterParameters();
775
parameters.Add(TwitterParameterNames.Count, 50);
776
if (f_LastDirectMessageSentStatusID != null) {
777
parameters.Add(TwitterParameterNames.SinceID,
778
f_LastDirectMessageSentStatusID);
780
TwitterStatusCollection sentTimeline =
781
f_Twitter.DirectMessages.DirectMessagesSent(parameters);
783
f_Logger.Debug("UpdateDirectMessages(): done. New messages: " +
784
(sentTimeline == null ? 0 : sentTimeline.Count));
787
TwitterStatusCollection timeline = new TwitterStatusCollection();
788
if (receivedTimeline != null) {
789
foreach (TwitterStatus status in receivedTimeline) {
790
timeline.Add(status);
793
if (sentTimeline != null) {
794
foreach (TwitterStatus status in sentTimeline) {
795
timeline.Add(status);
799
if (timeline.Count == 0) {
804
List<TwitterStatus> sortedTimeline = SortTimeline(timeline);
805
foreach (TwitterStatus status in sortedTimeline) {
806
// if this isn't the first time a receive a direct message,
807
// this is a new one!
808
bool highlight = receivedTimeline.Contains(status) &&
809
f_LastDirectMessageReceivedStatusID != null;
810
MessageModel msg = CreateMessage(
812
status.TwitterUser.ScreenName,
816
Session.AddMessageToChat(f_DirectMessagesChat, msg);
818
// if there is a tab open for this user put the message there too
820
if (receivedTimeline.Contains(status)) {
821
// this is a received message
822
userId = status.TwitterUser.ID.ToString();
824
// this is a sent message
825
userId = status.Recipient.ID.ToString();
827
ChatModel chat = Session.GetChat(
833
Session.AddMessageToChat(chat, msg);
837
if (receivedTimeline != null) {
838
// first one is the newest
839
foreach (TwitterStatus status in receivedTimeline) {
840
f_LastDirectMessageReceivedStatusID = status.ID;
844
if (sentTimeline != null) {
845
// first one is the newest
846
foreach (TwitterStatus status in sentTimeline) {
847
f_LastDirectMessageSentStatusID = status.ID;
853
private void UpdateFriends()
857
if (f_Friends != null) {
862
f_Logger.Debug("UpdateFriends(): getting friends from twitter...");
864
TwitterUserCollection friends = f_Twitter.User.Friends();
866
f_Logger.Debug("UpdateFriends(): done. Friends: " +
867
(friends == null ? 0 : friends.Count));
869
if (friends == null || friends.Count == 0) {
873
List<PersonModel> persons = new List<PersonModel>(friends.Count);
874
foreach (TwitterUser friend in friends) {
875
PersonModel person = new PersonModel(
876
friend.ID.ToString(),
887
private void UpdateUser()
890
f_Logger.Debug("UpdateUser(): getting user details from twitter...");
892
f_TwitterUser = f_Twitter.User.Show(f_Username);
894
f_Logger.Debug("UpdateUser(): done.");
898
protected override TextColor GetIdentityNameColor(string identityName)
900
if (identityName == f_TwitterUser.ScreenName) {
901
return f_BlueTextColor;
904
return base.GetIdentityNameColor(identityName);
907
private MessageModel CreateMessage(DateTime when, string from,
910
return CreateMessage(when, from, message, false);
913
private MessageModel CreateMessage(DateTime when, string from,
914
string message, bool highlight)
916
MessageModel msg = new MessageModel();
917
TextMessagePartModel msgPart;
918
msg.TimeStamp = when;
920
msgPart = new TextMessagePartModel();
922
msg.MessageParts.Add(msgPart);
924
msgPart = new TextMessagePartModel();
926
msgPart.ForegroundColor = GetIdentityNameColor(from);
927
msg.MessageParts.Add(msgPart);
929
msgPart = new TextMessagePartModel();
931
msg.MessageParts.Add(msgPart);
933
msgPart = new TextMessagePartModel(message);
934
msgPart.IsHighlight = highlight;
935
msg.MessageParts.Add(msgPart);
942
private void PostUpdate(string text)
944
f_Twitter.Status.Update(text);
945
f_FriendsTimelineEvent.Set();
948
private void SendMessage(string target, string text)
950
f_Twitter.DirectMessages.New(target, text);
951
f_DirectMessageEvent.Set();
954
private static string _(string msg)
956
return LibraryCatalog.GetString(msg, f_LibraryTextDomain);