diff --git a/SlackAPI.Tests/Configuration/IntegrationFixture.cs b/SlackAPI.Tests/Configuration/IntegrationFixture.cs index cc542af7..99e4b94c 100755 --- a/SlackAPI.Tests/Configuration/IntegrationFixture.cs +++ b/SlackAPI.Tests/Configuration/IntegrationFixture.cs @@ -101,10 +101,12 @@ private SlackSocketClient CreateClient(string authToken, IWebProxy proxySettings { SlackSocketClient client; + var slackNowTakesAgesToConnectTimeout = TimeSpan.FromSeconds(60); + LoginResponse loginResponse = null; - using (var syncClient = new InSync($"{nameof(SlackClient.Connect)} - Connected callback")) - using (var syncClientSocket = new InSync($"{nameof(SlackClient.Connect)} - SocketConnected callback")) - using (var syncClientSocketHello = new InSync($"{nameof(SlackClient.Connect)} - SocketConnected hello callback")) + using (var syncClient = new InSync($"{nameof(SlackClient.Connect)} - Connected callback", slackNowTakesAgesToConnectTimeout)) + using (var syncClientSocket = new InSync($"{nameof(SlackClient.Connect)} - SocketConnected callback", slackNowTakesAgesToConnectTimeout)) + using (var syncClientSocketHello = new InSync($"{nameof(SlackClient.Connect)} - SocketConnected hello callback", slackNowTakesAgesToConnectTimeout)) { client = new SlackSocketClient(authToken, proxySettings, maintainPresenceChanges); diff --git a/SlackAPI.Tests/Update.cs b/SlackAPI.Tests/Update.cs index a5ed7151..b2f3066b 100644 --- a/SlackAPI.Tests/Update.cs +++ b/SlackAPI.Tests/Update.cs @@ -69,7 +69,7 @@ public void UpdatePresence() var client = this.fixture.UserClient; using (var sync = new InSync(nameof(SlackClient.EmitPresence))) { - client.EmitPresence((presence) => + client.EmitPresence(presence => { presence.AssertOk(); sync.Proceed(); diff --git a/SlackAPI.sln b/SlackAPI.sln index f3f74ac0..07009a02 100644 --- a/SlackAPI.sln +++ b/SlackAPI.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30320.27 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32526.322 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlackAPI", "SlackAPI\SlackAPI.csproj", "{7EED3D9B-9B7A-49A4-AFBF-599153A47DDA}" EndProject diff --git a/SlackAPI/RPCMessages/BotInfoResponse.cs b/SlackAPI/RPCMessages/BotInfoResponse.cs new file mode 100644 index 00000000..f0aa24c9 --- /dev/null +++ b/SlackAPI/RPCMessages/BotInfoResponse.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.RPCMessages +{ + [RequestPath("bots.info")] + public class BotInfoResponse : Response + { + public Bot bot; + } +} diff --git a/SlackAPI/RPCMessages/ChannelListResponse.cs b/SlackAPI/RPCMessages/ChannelListResponse.cs index 22280c1f..0fc68fd9 100644 --- a/SlackAPI/RPCMessages/ChannelListResponse.cs +++ b/SlackAPI/RPCMessages/ChannelListResponse.cs @@ -3,6 +3,7 @@ namespace SlackAPI { [RequestPath("channels.list")] + [Obsolete("Replaced by ConversationsListResponse", true)] public class ChannelListResponse : Response { public Channel[] channels; diff --git a/SlackAPI/RPCMessages/ConversationsOpenResponse.cs b/SlackAPI/RPCMessages/ConversationsOpenResponse.cs index 527f8f16..a455eb16 100644 --- a/SlackAPI/RPCMessages/ConversationsOpenResponse.cs +++ b/SlackAPI/RPCMessages/ConversationsOpenResponse.cs @@ -12,6 +12,5 @@ public class ConversationsOpenResponse : Response public string no_op; public string already_open; public Channel channel; - public string error; } } diff --git a/SlackAPI/RPCMessages/DialogOpenResponse.cs b/SlackAPI/RPCMessages/DialogOpenResponse.cs index 926a339a..367e130d 100644 --- a/SlackAPI/RPCMessages/DialogOpenResponse.cs +++ b/SlackAPI/RPCMessages/DialogOpenResponse.cs @@ -9,11 +9,5 @@ namespace SlackAPI.RPCMessages [RequestPath("dialog.open")] public class DialogOpenResponse : Response { - public ResponseMetadata response_metadata { get; set; } - - public class ResponseMetadata - { - public string[] messages { get; set; } - } } } diff --git a/SlackAPI/RPCMessages/DirectMessageConversationListResponse.cs b/SlackAPI/RPCMessages/DirectMessageConversationListResponse.cs index 59746ad0..bfb53696 100644 --- a/SlackAPI/RPCMessages/DirectMessageConversationListResponse.cs +++ b/SlackAPI/RPCMessages/DirectMessageConversationListResponse.cs @@ -7,6 +7,7 @@ namespace SlackAPI { [RequestPath("im.list")] + [Obsolete("Replaced by ConversationsListResponse", true)] public class DirectMessageConversationListResponse : Response { public DirectMessageConversation[] ims; diff --git a/SlackAPI/RPCMessages/GroupListResponse.cs b/SlackAPI/RPCMessages/GroupListResponse.cs index d8ad777e..61f96b25 100644 --- a/SlackAPI/RPCMessages/GroupListResponse.cs +++ b/SlackAPI/RPCMessages/GroupListResponse.cs @@ -7,6 +7,7 @@ namespace SlackAPI { [RequestPath("groups.list")] + [Obsolete("Replaced by ConversationsListResponse", true)] public class GroupListResponse : Response { public Channel[] groups; diff --git a/SlackAPI/RPCMessages/LoginResponse.cs b/SlackAPI/RPCMessages/LoginResponse.cs index d048f316..db557f12 100644 --- a/SlackAPI/RPCMessages/LoginResponse.cs +++ b/SlackAPI/RPCMessages/LoginResponse.cs @@ -6,27 +6,17 @@ namespace SlackAPI { - [RequestPath("rtm.start")] + [RequestPath("rtm.connect")] public class LoginResponse : Response - { - public Bot[] bots; - public Channel[] channels; - public Channel[] groups; - public DirectMessageConversation[] ims; - public Self self; - public int svn_rev; - public int min_svn_rev; - public Team team; - public string url; - public User[] users; - } + { + public string url; + public Team team; + public Self self; + } public class Self { - public DateTime created; public string id; - public string manual_presence; public string name; - public Preferences prefs; } } diff --git a/SlackAPI/SlackClient.cs b/SlackAPI/SlackClient.cs index e81a55c3..efe566ab 100644 --- a/SlackAPI/SlackClient.cs +++ b/SlackAPI/SlackClient.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Threading; using SlackAPI.RPCMessages; namespace SlackAPI @@ -25,8 +26,8 @@ public class SlackClient : SlackClientBase public List starredChannels; + public List Bots; public List Users; - public List Bots; public List Channels; public List Groups; public List DirectMessages; @@ -34,8 +35,8 @@ public class SlackClient : SlackClientBase public Dictionary UserLookup; public Dictionary ChannelLookup; public Dictionary GroupLookup; - public Dictionary DirectMessageLookup; public Dictionary ConversationLookup; + public Dictionary DirectMessageLookup; public SlackClient(string token) { @@ -48,60 +49,114 @@ public SlackClient(string token, IWebProxy proxySettings) APIToken = token; } - public virtual void Connect(Action onConnected = null, Action onSocketConnected = null) + public virtual void Connect(Action onConnected = null, Action onSocketConnected = null, int timeoutSeconds = 60) { - EmitLogin((loginDetails) => + EmitLogin(loginDetails => { if (loginDetails.ok) - Connected(loginDetails); + Connected(loginDetails, timeoutSeconds); if (onConnected != null) onConnected(loginDetails); }); } - protected virtual void Connected(LoginResponse loginDetails) + protected virtual void Connected(LoginResponse loginDetails, int timeoutSeconds) { MySelf = loginDetails.self; - MyData = loginDetails.users.First((c) => c.id == MySelf.id); MyTeam = loginDetails.team; - Users = new List(loginDetails.users.Where((c) => !c.deleted)); - Bots = new List(loginDetails.bots.Where((c) => !c.deleted)); - Channels = new List(loginDetails.channels); - Groups = new List(loginDetails.groups); - DirectMessages = new List(loginDetails.ims.Where((c) => Users.Exists((a) => a.id == c.user) && c.id != MySelf.id)); - starredChannels = - Groups.Where((c) => c.is_starred).Select((c) => c.id) - .Union( - DirectMessages.Where((c) => c.is_starred).Select((c) => c.user) - ).Union( - Channels.Where((c) => c.is_starred).Select((c) => c.id) - ).ToList(); - - UserLookup = new Dictionary(); - foreach (User u in Users) UserLookup.Add(u.id, u); - - ChannelLookup = new Dictionary(); - ConversationLookup = new Dictionary(); - foreach (Channel c in Channels) + var taskWaiter = new CountdownEvent(2); + var channels = new List(); + + GetUserList(response => { - ChannelLookup.Add(c.id, c); - ConversationLookup.Add(c.id, c); - } + if (response.ok) + { + MyData = response.members.First(c => c.id == MySelf.id); + Users = new List(response.members.Where(c => !c.deleted && !c.IsSlackBot)); + Bots = new List(response.members.Where(c => !c.deleted && c.IsSlackBot)); + } + else + { + loginDetails.ok = false; + loginDetails.error = response.error; + } - GroupLookup = new Dictionary(); - foreach (Channel g in Groups) + taskWaiter.Signal(); + }); + + GetConversationsList(response => { - GroupLookup.Add(g.id, g); - ConversationLookup.Add(g.id, g); - } + if (response.ok) + { + channels.AddRange(response.channels); + } + else + { + loginDetails.ok = false; + loginDetails.error = response.error; + } + + taskWaiter.Signal(); + }, ExcludeArchived: true); + + var released = taskWaiter.Wait(TimeSpan.FromSeconds(timeoutSeconds)); - DirectMessageLookup = new Dictionary(); - foreach (DirectMessageConversation im in DirectMessages) + if (released && loginDetails.ok) { - DirectMessageLookup.Add(im.id, im); - ConversationLookup.Add(im.id, im); + Channels = new List(channels.Where(x => x.is_channel)); + Groups = new List(channels.Where(x => x.is_group)); + starredChannels = + Channels.Where(c => c.is_starred).Select(c => c.id) + .Union( + DirectMessages.Where(c => c.is_starred).Select(c => c.user) + ).Union( + Channels.Where(c => c.is_starred).Select(c => c.id) + ).ToList(); + DirectMessages = new List( + channels + .Where(x => x.is_im && Users.Exists(u => u.id == x.user) && x.id != MySelf.id) + .Select(x => new DirectMessageConversation + { + created = x.created, + id = x.id, + is_open = x.is_open, + is_starred = x.is_starred, + is_user_deleted = UserLookup[x.user].deleted, + last_read = x.last_read, + latest = x.latest, + unread_count = x.unread_count, + user = x.user + })); + + UserLookup = new Dictionary(); + foreach (User u in Users) UserLookup.Add(u.id, u); + + ChannelLookup = new Dictionary(); + ConversationLookup = new Dictionary(); + foreach (Channel c in Groups) + { + ChannelLookup.Add(c.id, c); + ConversationLookup.Add(c.id, c); + } + + GroupLookup = new Dictionary(); + foreach (Channel g in Channels) + { + GroupLookup.Add(g.id, g); + ConversationLookup.Add(g.id, g); + } + + DirectMessageLookup = new Dictionary(); + foreach (DirectMessageConversation im in DirectMessages) + DirectMessageLookup.Add(im.id, im); + + } + else if (!released && loginDetails.ok) + { + loginDetails.ok = false; + loginDetails.error = "Timed out loading login details"; } } @@ -170,16 +225,20 @@ public void GetConversationsMembers(Action callbac APIRequestWithToken(callback, parameters.ToArray()); } + + [Obsolete("Replaced by GetConversionsList", true)] public void GetChannelList(Action callback, bool ExcludeArchived = true) { APIRequestWithToken(callback, new Tuple("exclude_archived", ExcludeArchived ? "1" : "0")); } + [Obsolete("Replaced by GetConversionsList", true)] public void GetGroupsList(Action callback, bool ExcludeArchived = true) { APIRequestWithToken(callback, new Tuple("exclude_archived", ExcludeArchived ? "1" : "0")); } + [Obsolete("Replaced by GetConversionsList", true)] public void GetDirectMessageList(Action callback) { APIRequestWithToken(callback); @@ -606,6 +665,16 @@ public void GetInfo(Action callback, string user) APIRequestWithToken(callback, new Tuple("user", user)); } + public void GetBotInfo(Action callback, string botUser, string teamId = null) + { + var parameters = new List> { new Tuple("bot", botUser) }; + + if (!string.IsNullOrWhiteSpace(teamId)) + parameters.Add(new Tuple("team_id", teamId)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + #endregion public void EmitLogin(Action callback, string agent = "Inumedia.SlackAPI") diff --git a/SlackAPI/SlackClientBase.cs b/SlackAPI/SlackClientBase.cs index 1e6d357b..d77140de 100644 --- a/SlackAPI/SlackClientBase.cs +++ b/SlackAPI/SlackClientBase.cs @@ -148,7 +148,7 @@ public void RegisterConverter(JsonConverter converter) { if (converter == null) { - throw new ArgumentNullException("converter"); + throw new ArgumentNullException(nameof(converter)); } Extensions.Converters.Add(converter); diff --git a/SlackAPI/SlackSocket.cs b/SlackAPI/SlackSocket.cs index 75903825..6de993a0 100644 --- a/SlackAPI/SlackSocket.cs +++ b/SlackAPI/SlackSocket.cs @@ -92,7 +92,7 @@ public SlackSocket(LoginResponse loginDetails, object routingTo, Action onConnec currentId = 1; cts = new CancellationTokenSource(); - socket.ConnectAsync(new Uri(string.Format("{0}?svn_rev={1}&login_with_boot_data-0-{2}&on_login-0-{2}&connect-1-{2}", loginDetails.url, loginDetails.svn_rev, DateTime.Now.Subtract(new DateTime(1970, 1, 1)).TotalSeconds)), cts.Token).Wait(); + socket.ConnectAsync(new Uri(string.Format("{0}?login_with_boot_data-0-{1}&on_login-0-{1}&connect-1-{1}", loginDetails.url, DateTime.Now.Subtract(new DateTime(1970, 1, 1)).TotalSeconds)), cts.Token).Wait(); if(onConnected != null) onConnected(); SetupReceiving(); @@ -136,7 +136,7 @@ public void Send(SlackSocketMessage message, Action callback) { int sendingId = Interlocked.Increment(ref currentId); message.id = sendingId; - callbacks.Add(sendingId, (c) => + callbacks.Add(sendingId, c => { K obj = c.Deserialize(); callback(obj); @@ -232,7 +232,7 @@ void SetupReceiving() continue; } - string data = string.Join("", buffers.Select((c) => Encoding.UTF8.GetString(c).TrimEnd('\0'))); + string data = string.Join("", buffers.Select(c => Encoding.UTF8.GetString(c).TrimEnd('\0'))); //Console.WriteLine("SlackSocket data = " + data); SlackSocketMessage message = null; try diff --git a/SlackAPI/SlackSocketClient.cs b/SlackAPI/SlackSocketClient.cs index 687ae4a7..e1ed3426 100644 --- a/SlackAPI/SlackSocketClient.cs +++ b/SlackAPI/SlackSocketClient.cs @@ -33,20 +33,23 @@ public SlackSocketClient(string token, IWebProxy proxySettings = null, bool main this.maintainPresenceChanges = maintainPresenceChanges; } - public override void Connect(Action onConnected, Action onSocketConnected = null) + public override void Connect(Action onConnected, Action onSocketConnected = null, int timeoutSeconds = 60) { - base.Connect((s) => { + base.Connect(s => { if (s.ok) + { ConnectSocket(onSocketConnected); + Connected(s, timeoutSeconds); + } onConnected(s); }); } - protected override void Connected(LoginResponse loginDetails) + protected override void Connected(LoginResponse login, int timeoutSeconds) { - this.loginDetails = loginDetails; - base.Connected(loginDetails); + this.loginDetails = login; + base.Connected(loginDetails, timeoutSeconds); } public void ConnectSocket(Action onSocketConnected){ diff --git a/SlackAPI/SlackTaskClient.cs b/SlackAPI/SlackTaskClient.cs index fd65669e..8c2f5505 100644 --- a/SlackAPI/SlackTaskClient.cs +++ b/SlackAPI/SlackTaskClient.cs @@ -21,6 +21,7 @@ public class SlackTaskClient : SlackClientBase public List starredChannels; + public List Bots; public List Users; public List Channels; public List Groups; @@ -46,40 +47,82 @@ public virtual async Task ConnectAsync() { var loginDetails = await EmitLoginAsync().ConfigureAwait(false); if(loginDetails.ok) - Connected(loginDetails); + await Connected(loginDetails); return loginDetails; } - protected virtual void Connected(LoginResponse loginDetails) + protected virtual async Task Connected(LoginResponse loginDetails) { - MySelf = loginDetails.self; - MyData = loginDetails.users.First((c) => c.id == MySelf.id); - MyTeam = loginDetails.team; + try + { + MySelf = loginDetails.self; + MyTeam = loginDetails.team; - Users = new List(loginDetails.users.Where((c) => !c.deleted)); - Channels = new List(loginDetails.channels); - Groups = new List(loginDetails.groups); - DirectMessages = new List(loginDetails.ims.Where((c) => Users.Exists((a) => a.id == c.user) && c.id != MySelf.id)); - starredChannels = - Groups.Where((c) => c.is_starred).Select((c) => c.id) - .Union( - DirectMessages.Where((c) => c.is_starred).Select((c) => c.user) - ).Union( - Channels.Where((c) => c.is_starred).Select((c) => c.id) - ).ToList(); + var userList = GetUserListAsync(); + var conversionsListAsync = GetConversationsListAsync(ExcludeArchived: true); - UserLookup = new Dictionary(); - foreach (User u in Users) UserLookup.Add(u.id, u); + await Task.WhenAll(userList, conversionsListAsync).ConfigureAwait(false); - ChannelLookup = new Dictionary(); - foreach (Channel c in Channels) ChannelLookup.Add(c.id, c); + if (!userList.Result.ok) + { + loginDetails.ok = false; + loginDetails.error = userList.Result.error; + return; + } - GroupLookup = new Dictionary(); - foreach (Channel g in Groups) GroupLookup.Add(g.id, g); + if (!conversionsListAsync.Result.ok) + { + loginDetails.ok = false; + loginDetails.error = conversionsListAsync.Result.error; + return; + } - DirectMessageLookup = new Dictionary(); - foreach (DirectMessageConversation im in DirectMessages) DirectMessageLookup.Add(im.id, im); + MyData = userList.Result.members.First(c => c.id == MySelf.id); + Bots = new List(userList.Result.members.Where(c => !c.deleted && c.IsSlackBot)); + Users = new List(userList.Result.members.Where(c => !c.deleted && !c.IsSlackBot)); + Channels = new List(conversionsListAsync.Result.channels.Where(x => x.is_channel)); + Groups = new List(conversionsListAsync.Result.channels.Where(x => x.is_group)); + DirectMessages = new List( + conversionsListAsync.Result.channels + .Where(x => x.is_im && Users.Exists(u => u.id == x.user) && x.id != MySelf.id) + .Select(x => new DirectMessageConversation + { + created = x.created, + id = x.id, + is_open = x.is_open, + is_starred = x.is_starred, + is_user_deleted = UserLookup[x.user].deleted, + last_read = x.last_read, + latest = x.latest, + unread_count = x.unread_count, + user = x.user + })); + starredChannels = + Groups.Where(c => c.is_starred).Select(c => c.id) + .Union( + DirectMessages.Where(c => c.is_starred).Select(c => c.user) + ).Union( + Channels.Where(c => c.is_starred).Select(c => c.id) + ).ToList(); + + UserLookup = new Dictionary(); + foreach (User u in Users) UserLookup.Add(u.id, u); + + ChannelLookup = new Dictionary(); + foreach (Channel c in Channels) ChannelLookup.Add(c.id, c); + + GroupLookup = new Dictionary(); + foreach (Channel g in Groups) GroupLookup.Add(g.id, g); + + DirectMessageLookup = new Dictionary(); + foreach (DirectMessageConversation im in DirectMessages) DirectMessageLookup.Add(im.id, im); + } + catch (Exception ex) + { + loginDetails.ok = false; + loginDetails.error = ex.Message; + } } public Task APIRequestWithTokenAsync() @@ -168,16 +211,19 @@ public Task GetConversationsMembersAsync(string ch return APIRequestWithTokenAsync(parameters.ToArray()); } + [Obsolete("Replaced by GetConversionsListAsync", true)] public Task GetChannelListAsync(bool ExcludeArchived = true) { return APIRequestWithTokenAsync(new Tuple("exclude_archived", ExcludeArchived ? "1" : "0")); } + [Obsolete("Replaced by GetConversionsListAsync", true)] public Task GetGroupsListAsync(bool ExcludeArchived = true) { return APIRequestWithTokenAsync(new Tuple("exclude_archived", ExcludeArchived ? "1" : "0")); } + [Obsolete("Replaced by GetConversionsListAsync", true)] public Task GetDirectMessageListAsync() { return APIRequestWithTokenAsync(); @@ -601,6 +647,21 @@ public Task GetCountsAsync() return APIRequestWithTokenAsync(); } + public Task GetInfoAsync(string user) + { + return APIRequestWithTokenAsync(new Tuple("user", user)); + } + + public Task GetBotInfoAsync(string botUser, string teamId = null) + { + var parameters = new List> { new Tuple("bot", botUser) }; + + if (!string.IsNullOrWhiteSpace(teamId)) + parameters.Add(new Tuple("team_id", teamId)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + public Task EmitLoginAsync(string agent = "Inumedia.SlackAPI") { return APIRequestWithTokenAsync(new Tuple("agent", agent));