diff --git a/SlackAPI.Tests/Conversations.cs b/SlackAPI.Tests/Conversations.cs new file mode 100644 index 00000000..3c8b74dc --- /dev/null +++ b/SlackAPI.Tests/Conversations.cs @@ -0,0 +1,41 @@ +using System.Linq; +using SlackAPI.RPCMessages; +using SlackAPI.Tests.Configuration; +using SlackAPI.Tests.Helpers; +using Xunit; + +namespace SlackAPI.Tests +{ + [Collection("Integration tests")] + public class Conversations + { + private readonly IntegrationFixture fixture; + + public Conversations(IntegrationFixture fixture) + { + this.fixture = fixture; + } + [Fact] + public void ConversationList() + { + var client = this.fixture.UserClient; + ConversationsListResponse actual = null; + using (var sync = new InSync(nameof(SlackClient.ChannelLookup))) + { + client.GetConversationsList(response => + { + actual = response; + sync.Proceed(); + }); + } + + Assert.True(actual.ok, "Error while fetching conversation list."); + Assert.True(actual.channels.Any()); + + // check to null + var someChannel = actual.channels.First(); + Assert.NotNull(someChannel.id); + Assert.NotNull(someChannel.name); + } + } +} \ No newline at end of file diff --git a/SlackAPI.Tests/UserUIInteraction.cs b/SlackAPI.Tests/UserUIInteraction.cs index 60b7faf8..0ece63cf 100644 --- a/SlackAPI.Tests/UserUIInteraction.cs +++ b/SlackAPI.Tests/UserUIInteraction.cs @@ -52,7 +52,7 @@ public void TestGetAccessToken() var accessTokenResponse = GetAccessToken(slackClientHelpers, clientId, clientSecret, redirectUrl, code); Assert.True(accessTokenResponse.ok); - Assert.Equal("identify", accessTokenResponse.scope); + Assert.Contains("identify", accessTokenResponse.scope); } } diff --git a/SlackAPI.sln b/SlackAPI.sln index f3b012b7..f3f74ac0 100644 --- a/SlackAPI.sln +++ b/SlackAPI.sln @@ -1,20 +1,20 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26228.4 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30320.27 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlackAPI", "SlackAPI\SlackAPI.csproj", "{7EED3D9B-9B7A-49A4-AFBF-599153A47DDA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlackAPI.Tests", "SlackAPI.Tests\SlackAPI.Tests.csproj", "{DEFA9559-0F8F-4C38-9644-67A080EDC46D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution items", "Solution items", "{532C9828-A2CC-4281-950A-248B06D42E9C}" -ProjectSection(SolutionItems) = preProject - appveyor.yml = appveyor.yml - build.cake = build.cake - Directory.Build.props = Directory.Build.props - GlobalAssemblyInfo.cs = GlobalAssemblyInfo.cs - README.md = README.md -EndProjectSection + ProjectSection(SolutionItems) = preProject + appveyor.yml = appveyor.yml + build.cake = build.cake + Directory.Build.props = Directory.Build.props + GlobalAssemblyInfo.cs = GlobalAssemblyInfo.cs + README.md = README.md + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/SlackAPI/Attachment.cs b/SlackAPI/Attachment.cs index b9e9582f..64b28e1d 100644 --- a/SlackAPI/Attachment.cs +++ b/SlackAPI/Attachment.cs @@ -45,6 +45,7 @@ public AttachmentAction(string name, string text) public string type = "button"; public string value; public ActionConfirm confirm; + public string url; } //see: https://api.slack.com/docs/message-buttons#confirmation_fields diff --git a/SlackAPI/Block.cs b/SlackAPI/Block.cs index d2aa0403..5b05099f 100644 --- a/SlackAPI/Block.cs +++ b/SlackAPI/Block.cs @@ -46,6 +46,12 @@ public class ContextBlock : IBlock public string block_id { get; set; } public IElement[] elements { get; set; } } + public class HeaderBlock : IBlock + { + public string type { get; } = BlockTypes.Header; + public Text text { get; set; } + public string block_id { get; set; } + } public class Text : IElement { public string type { get; set; } = TextTypes.PlainText; @@ -176,6 +182,12 @@ public class DatePickerElement : IElement public Confirm confirm { get; set; } } + public class View + { + public string type { get; set; } + public IBlock[] blocks { get; set; } + } + public static class ButtonStyles { public const string Primary = "primary"; @@ -189,6 +201,13 @@ public static class BlockTypes public const string Actions = "actions"; public const string Context = "context"; public const string Image = "image"; + public const string Header = "header"; + } + + public static class ViewTypes + { + public const string Home = "home"; + public const string Modal = "modal"; } public static class TextTypes diff --git a/SlackAPI/Channel.cs b/SlackAPI/Channel.cs index 7335e8bd..c45d1c23 100644 --- a/SlackAPI/Channel.cs +++ b/SlackAPI/Channel.cs @@ -10,12 +10,14 @@ public class Channel : Conversation { public string name; public string creator; + public string user; public bool is_archived; public bool is_member; public bool is_general; public bool is_channel; public bool is_group; + public bool is_im; //Is this deprecated by is_open? public bool IsPrivateGroup { get { return id != null && id[0] == 'G'; } } diff --git a/SlackAPI/Message.cs b/SlackAPI/Message.cs index dfb25656..6077ec6c 100644 --- a/SlackAPI/Message.cs +++ b/SlackAPI/Message.cs @@ -16,6 +16,7 @@ public class Message : SlackSocketMessage /// public string username; public string text; + public Attachment[] attachments; public bool is_starred; public string permalink; public Reaction[] reactions; diff --git a/SlackAPI/Preferences.cs b/SlackAPI/Preferences.cs index 528f8c1d..f0931eec 100644 --- a/SlackAPI/Preferences.cs +++ b/SlackAPI/Preferences.cs @@ -73,5 +73,6 @@ public class Preferences public string mac_ssb_bounce; public string last_snippet_type; public int display_real_names_override; + public string muted_channels; } } \ No newline at end of file diff --git a/SlackAPI/RPCMessages/AppHomeTabResponse.cs b/SlackAPI/RPCMessages/AppHomeTabResponse.cs new file mode 100644 index 00000000..e0b8db02 --- /dev/null +++ b/SlackAPI/RPCMessages/AppHomeTabResponse.cs @@ -0,0 +1,38 @@ +namespace SlackAPI +{ + [RequestPath("views.publish")] + public class AppHomeTabResponse : Response + { + public AppHomeTabView view; + + public class AppHomeTabView + { + public string id; + public string team_id; + public string type; + public object close; + public object submit; + public Block[] blocks; + public string private_metadata; + public string callback_id; + public State state; + public string hash; + public bool clear_on_close; + public bool notify_on_close; + public string root_view_id; + public object previous_view_id; + public string app_id; + public string external_id; + public string bot_id; + } + + public class State + { + public Values values; + } + + public class Values + { + } + } +} diff --git a/SlackAPI/RPCMessages/ConversationsArchiveResponse.cs b/SlackAPI/RPCMessages/ConversationsArchiveResponse.cs new file mode 100644 index 00000000..d3203251 --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsArchiveResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("conversations.archive")] + public class ConversationsArchiveResponse : Response + { + } +} diff --git a/SlackAPI/RPCMessages/ConversationsCloseResponse.cs b/SlackAPI/RPCMessages/ConversationsCloseResponse.cs new file mode 100644 index 00000000..d8a7fca8 --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsCloseResponse.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("conversations.close")] + public class ConversationsCloseResponse : Response + { + public string no_op; + public string already_closed; + } +} diff --git a/SlackAPI/RPCMessages/ConversationsCreateResponse.cs b/SlackAPI/RPCMessages/ConversationsCreateResponse.cs new file mode 100644 index 00000000..ec71d2e3 --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsCreateResponse.cs @@ -0,0 +1,10 @@ +using System; + +namespace SlackAPI +{ + [RequestPath("conversations.create")] + public class ConversationsCreateResponse : Response + { + public Channel channel; + } +} diff --git a/SlackAPI/RPCMessages/ConversationsInviteResponse.cs b/SlackAPI/RPCMessages/ConversationsInviteResponse.cs new file mode 100644 index 00000000..95f04de4 --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsInviteResponse.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("conversations.invite")] + public class ConversationsInviteResponse : Response + { + public Channel channel; + } +} diff --git a/SlackAPI/RPCMessages/ConversationsJoinResponse.cs b/SlackAPI/RPCMessages/ConversationsJoinResponse.cs new file mode 100644 index 00000000..873818de --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsJoinResponse.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.RPCMessages +{ + + [RequestPath("conversations.join")] + public class ConversationsJoinResponse : Response + { + public Channel channel; + } +} \ No newline at end of file diff --git a/SlackAPI/RPCMessages/ConversationsKickResponse.cs b/SlackAPI/RPCMessages/ConversationsKickResponse.cs new file mode 100644 index 00000000..daa081e3 --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsKickResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("conversations.kick")] + public class ConversationsKickResponse : Response + { + } +} diff --git a/SlackAPI/RPCMessages/ConversationsLeaveResponse.cs b/SlackAPI/RPCMessages/ConversationsLeaveResponse.cs new file mode 100644 index 00000000..0f66be8f --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsLeaveResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("conversations.leave")] + public class ConversationsLeaveResponse : Response + { + } +} diff --git a/SlackAPI/RPCMessages/ConversationsListResponse.cs b/SlackAPI/RPCMessages/ConversationsListResponse.cs new file mode 100644 index 00000000..005938f6 --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsListResponse.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI.RPCMessages +{ + [RequestPath("conversations.list")] + public class ConversationsListResponse : Response + { + public Channel[] channels; + } +} diff --git a/SlackAPI/RPCMessages/ConversationsMarkResponse.cs b/SlackAPI/RPCMessages/ConversationsMarkResponse.cs new file mode 100644 index 00000000..fe7d6665 --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsMarkResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("conversations.mark")] + public class ConversationsMarkResponse : Response + { + } +} diff --git a/SlackAPI/RPCMessages/ConversationsMembersResponse.cs b/SlackAPI/RPCMessages/ConversationsMembersResponse.cs new file mode 100644 index 00000000..bd53a648 --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsMembersResponse.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.RPCMessages +{ + [RequestPath("conversations.members")] + public class ConversationsMembersResponse : Response + { + public string[] members; + } +} \ No newline at end of file diff --git a/SlackAPI/RPCMessages/ConversationsMessageHistory.cs b/SlackAPI/RPCMessages/ConversationsMessageHistory.cs new file mode 100644 index 00000000..ffff5abc --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsMessageHistory.cs @@ -0,0 +1,9 @@ +using System; + +namespace SlackAPI.RPCMessages +{ + [RequestPath("conversations.history")] + public class ConversationsMessageHistory : MessageHistory + { + } +} diff --git a/SlackAPI/RPCMessages/ConversationsOpenResponse.cs b/SlackAPI/RPCMessages/ConversationsOpenResponse.cs new file mode 100644 index 00000000..527f8f16 --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsOpenResponse.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("conversations.open")] + public class ConversationsOpenResponse : Response + { + public string no_op; + public string already_open; + public Channel channel; + public string error; + } +} diff --git a/SlackAPI/RPCMessages/ConversationsRenameResponse.cs b/SlackAPI/RPCMessages/ConversationsRenameResponse.cs new file mode 100644 index 00000000..e633eddd --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsRenameResponse.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("conversations.rename")] + public class ConversationsRenameResponse : Response + { + public Channel channel; + } +} diff --git a/SlackAPI/RPCMessages/ConversationsSetPurposeResponse.cs b/SlackAPI/RPCMessages/ConversationsSetPurposeResponse.cs new file mode 100644 index 00000000..20d4fafe --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsSetPurposeResponse.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("conversations.setPurpose")] + public class ConversationsSetPurposeResponse : Response + { + public string purpose; + } +} diff --git a/SlackAPI/RPCMessages/ConversationsSetTopicResponse.cs b/SlackAPI/RPCMessages/ConversationsSetTopicResponse.cs new file mode 100644 index 00000000..61ead8db --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsSetTopicResponse.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("conversations.setTopic")] + public class ConversationsSetTopicResponse : Response + { + public string topic; + } +} diff --git a/SlackAPI/RPCMessages/ConversationsUnarchiveResponse.cs b/SlackAPI/RPCMessages/ConversationsUnarchiveResponse.cs new file mode 100644 index 00000000..1d894e01 --- /dev/null +++ b/SlackAPI/RPCMessages/ConversationsUnarchiveResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("conversations.unarchive")] + public class ConversationsUnarchiveResponse : Response + { + } +} diff --git a/SlackAPI/RPCMessages/JoinDirectMessageChannelResponse.cs b/SlackAPI/RPCMessages/JoinDirectMessageChannelResponse.cs index 2e9d54bb..a7095398 100644 --- a/SlackAPI/RPCMessages/JoinDirectMessageChannelResponse.cs +++ b/SlackAPI/RPCMessages/JoinDirectMessageChannelResponse.cs @@ -1,6 +1,6 @@ namespace SlackAPI { - [RequestPath("im.open")] + [RequestPath("conversations.open")] public class JoinDirectMessageChannelResponse : Response { public Channel channel; diff --git a/SlackAPI/RPCMessages/ScheduleMessageResponse.cs b/SlackAPI/RPCMessages/ScheduleMessageResponse.cs new file mode 100644 index 00000000..70a4b5c9 --- /dev/null +++ b/SlackAPI/RPCMessages/ScheduleMessageResponse.cs @@ -0,0 +1,21 @@ +namespace SlackAPI +{ + [RequestPath("chat.scheduleMessage")] + public class ScheduleMessageResponse : Response + { + public string ts; + public string channel; + public string scheduled_message_id; + public int post_at; + public Message message; + + public class Message + { + public string text; + public string user; + public string username; + public string type; + public string subtype; + } + } +} diff --git a/SlackAPI/RPCMessages/UserEmailLookupResponse.cs b/SlackAPI/RPCMessages/UserEmailLookupResponse.cs new file mode 100644 index 00000000..487adcda --- /dev/null +++ b/SlackAPI/RPCMessages/UserEmailLookupResponse.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.RPCMessages +{ + [RequestPath("users.lookupByEmail")] + public class UserEmailLookupResponse : Response + { + public User user; + } +} \ No newline at end of file diff --git a/SlackAPI/RequestStateForTask.cs b/SlackAPI/RequestStateForTask.cs index 63075b73..f535f5a4 100644 --- a/SlackAPI/RequestStateForTask.cs +++ b/SlackAPI/RequestStateForTask.cs @@ -40,7 +40,7 @@ private async Task ExecuteResult() HttpWebResponse response = null; try { - response = (HttpWebResponse)await this.request.GetResponseAsync(); + response = (HttpWebResponse)await this.request.GetResponseAsync().ConfigureAwait(false); Success = true; } catch (WebException we) @@ -68,7 +68,7 @@ private async Task ExecutePost() { request.Method = "POST"; request.ContentType = "application/x-www-form-urlencoded"; - using (Stream requestStream = await request.GetRequestStreamAsync()) + using (Stream requestStream = await request.GetRequestStreamAsync().ConfigureAwait(false)) { if (Post.Length > 0) { @@ -80,7 +80,7 @@ private async Task ExecutePost() if (!first) writer.Write('&'); - await writer.WriteAsync(string.Format("{0}={1}", Uri.EscapeDataString(postEntry.Item1), Uri.EscapeDataString(postEntry.Item2))); + await writer.WriteAsync(string.Format("{0}={1}", Uri.EscapeDataString(postEntry.Item1), Uri.EscapeDataString(postEntry.Item2))).ConfigureAwait(false); first = false; } @@ -88,7 +88,7 @@ private async Task ExecutePost() } } - return await this.ExecuteResult(); + return await this.ExecuteResult().ConfigureAwait(false); } } } diff --git a/SlackAPI/Response.cs b/SlackAPI/Response.cs index 6c60a0dc..8cd5e2c7 100644 --- a/SlackAPI/Response.cs +++ b/SlackAPI/Response.cs @@ -17,11 +17,22 @@ public abstract class Response /// if ok is false, then this is the reason-code /// public string error; + public string needed; + public string provided; + public string warning; public void AssertOk() { if (!(ok)) throw new InvalidOperationException(string.Format("An error occurred: {0}", this.error)); } + + public ResponseMetaData response_metadata; + } + + public class ResponseMetaData + { + public string next_cursor; + public string[] messages; } } diff --git a/SlackAPI/SlackAPI.csproj b/SlackAPI/SlackAPI.csproj index b93cc54f..934c5356 100644 --- a/SlackAPI/SlackAPI.csproj +++ b/SlackAPI/SlackAPI.csproj @@ -8,7 +8,7 @@ - + diff --git a/SlackAPI/SlackClient.cs b/SlackAPI/SlackClient.cs index 7340cb71..e81a55c3 100644 --- a/SlackAPI/SlackClient.cs +++ b/SlackAPI/SlackClient.cs @@ -108,14 +108,7 @@ protected virtual void Connected(LoginResponse loginDetails) public void APIRequestWithToken(Action callback, params Tuple[] getParameters) where K : Response { - Tuple[] tokenArray = new Tuple[]{ - new Tuple("token", APIToken) - }; - - if (getParameters != null && getParameters.Length > 0) - tokenArray = tokenArray.Concat(getParameters).ToArray(); - - APIRequest(callback, tokenArray, new Tuple[0]); + APIRequest(callback, getParameters, new Tuple[0], APIToken); } public void TestAuth(Action callback) @@ -128,7 +121,12 @@ public void GetUserList(Action callback) APIRequestWithToken(callback); } - public void ChannelsCreate(Action callback, string name) { + public void GetUserByEmail(Action callback, string email) + { + APIRequestWithToken(callback, new Tuple("email", email)); + } + + public void ChannelsCreate(Action callback, string name) { APIRequestWithToken(callback, new Tuple("name", name)); } @@ -142,6 +140,36 @@ public void ChannelsInvite(Action callback, string userId APIRequestWithToken(callback, parameters.ToArray()); } + public void GetConversationsList(Action callback, string cursor = "", bool ExcludeArchived = true, int limit = 100, string[] types = null) + { + List> parameters = new List>() + { + Tuple.Create("exclude_archived", ExcludeArchived ? "1" : "0") + }; + if (limit > 0) + parameters.Add(Tuple.Create("limit", limit.ToString())); + if ((types != null) && types.Any()) + parameters.Add(Tuple.Create("types", string.Join(",", types))); + if (!string.IsNullOrEmpty(cursor)) + parameters.Add(Tuple.Create("cursor", cursor)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void GetConversationsMembers(Action callback, string channelId, string cursor = "", int limit = 100) + { + List> parameters = new List> + { + new Tuple("channel", channelId) + }; + if (limit > 0) + parameters.Add(Tuple.Create("limit", limit.ToString())); + if (!string.IsNullOrEmpty(cursor)) + parameters.Add(Tuple.Create("cursor", cursor)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + public void GetChannelList(Action callback, bool ExcludeArchived = true) { APIRequestWithToken(callback, new Tuple("exclude_archived", ExcludeArchived ? "1" : "0")); @@ -237,6 +265,11 @@ public void GetGroupHistory(Action callback, Channel groupI GetHistory(callback, groupInfo.id, latest, oldest, count, unreads); } + public void GetConversationsHistory(Action callback, Channel conversationInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) + { + GetHistory(callback, conversationInfo.id, latest, oldest, count, unreads); + } + public void MarkChannel(Action callback, string channelId, DateTime ts) { APIRequestWithToken(callback, @@ -352,6 +385,105 @@ public void GroupsUnarchive(Action callback, string chan #endregion + #region Conversations + public void ConversationsArchive(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void ConversationsClose(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void ConversationsCreate(Action callback, string name) + { + APIRequestWithToken(callback, new Tuple("name", name)); + } + + public void ConversationsInvite(Action callback, string channelId, string[] userIds) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("users", string.Join(",", userIds))); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void ConversationsJoin(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void ConversationsKick(Action callback, string channelId, string userId) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("user", userId)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void ConversationsLeave(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void ConversationsMark(Action callback, string channelId, DateTime ts) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("ts", ts.ToProperTimeStamp())); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void ConversationsOpen(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void ConversationsRename(Action callback, string channelId, string name) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("name", name)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void ConversationsSetPurpose(Action callback, string channelId, string purpose) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("purpose", purpose)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void ConversationsSetTopic(Action callback, string channelId, string topic) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("topic", topic)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void ConversationsUnarchive(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + #endregion + + public void SearchAll(Action callback, string query, string sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) { List> parameters = new List>(); @@ -491,7 +623,7 @@ public void Update( bool linkNames = false, IBlock[] blocks = null, Attachment[] attachments = null, - bool as_user = false) + bool? as_user = null) { List> parameters = new List>(); @@ -521,16 +653,16 @@ public void Update( { NullValueHandling = NullValueHandling.Ignore }))); - - - parameters.Add(new Tuple("as_user", as_user.ToString())); + + if (as_user.HasValue) + parameters.Add(new Tuple("as_user", as_user.ToString())); APIRequestWithToken(callback, parameters.ToArray()); } public void JoinDirectMessageChannel(Action callback, string user) { - var param = new Tuple("user", user); + var param = new Tuple("users", user); APIRequestWithToken(callback, param); } @@ -641,6 +773,73 @@ public void PostEphemeralMessage( APIRequestWithToken(callback, parameters.ToArray()); } + + + public void ScheduleMessage( + Action callback, + string channelId, + string text, + DateTime post_at, + string botName = null, + string parse = null, + bool linkNames = false, + IBlock[] blocks = null, + Attachment[] attachments = null, + bool? unfurl_links = null, + string icon_url = null, + string icon_emoji = null, + bool? as_user = null, + string thread_ts = null) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("text", text)); + parameters.Add(new Tuple("post_at", Convert.ToUInt64((post_at - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds).ToString())); + + if (!string.IsNullOrEmpty(botName)) + parameters.Add(new Tuple("username", botName)); + + if (!string.IsNullOrEmpty(parse)) + parameters.Add(new Tuple("parse", parse)); + + if (linkNames) + parameters.Add(new Tuple("link_names", "1")); + + if (blocks != null && blocks.Length > 0) + parameters.Add(new Tuple("blocks", + JsonConvert.SerializeObject(blocks, Formatting.None, + new JsonSerializerSettings // Shouldn't include a not set property + { + NullValueHandling = NullValueHandling.Ignore + }))); + + if (attachments != null && attachments.Length > 0) + parameters.Add(new Tuple("attachments", + JsonConvert.SerializeObject(attachments, Formatting.None, + new JsonSerializerSettings // Shouldn't include a not set property + { + NullValueHandling = NullValueHandling.Ignore + }))); + + if (unfurl_links.HasValue) + parameters.Add(new Tuple("unfurl_links", unfurl_links.Value ? "true" : "false")); + + if (!string.IsNullOrEmpty(icon_url)) + parameters.Add(new Tuple("icon_url", icon_url)); + + if (!string.IsNullOrEmpty(icon_emoji)) + parameters.Add(new Tuple("icon_emoji", icon_emoji)); + + if (as_user.HasValue) + parameters.Add(new Tuple("as_user", as_user.ToString())); + + if (!string.IsNullOrEmpty(thread_ts)) + parameters.Add(new Tuple("thread_ts", thread_ts)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + public void DialogOpen( Action callback, string triggerId, @@ -685,7 +884,6 @@ public void UploadFile(Action callback, byte[] fileData, str Uri target = new Uri(Path.Combine(APIBaseLocation, useAsync ? "files.uploadAsync" : "files.upload")); List parameters = new List(); - parameters.Add(string.Format("token={0}", APIToken)); //File/Content if (!string.IsNullOrEmpty(fileType)) @@ -705,7 +903,7 @@ public void UploadFile(Action callback, byte[] fileData, str using (MultipartFormDataContent form = new MultipartFormDataContent()) { form.Add(new ByteArrayContent(fileData), "file", fileName); - HttpResponseMessage response = PostRequest(string.Format("{0}?{1}", target, string.Join("&", parameters.ToArray())), form); + HttpResponseMessage response = PostRequestAsync(string.Format("{0}?{1}", target, string.Join("&", parameters.ToArray())), form, APIToken).Result; string result = response.Content.ReadAsStringAsync().Result; callback(result.Deserialize()); } @@ -718,5 +916,24 @@ public void DeleteFile(Action callback, string file = null) APIRequestWithToken(callback, new Tuple("file", file)); } + + public void PublishAppHomeTab( + Action callback, + string userId, + View view) + { + view.type = ViewTypes.Home; + var parameters = new List> + { + new Tuple("user_id", userId), + new Tuple("view", JsonConvert.SerializeObject(view, Formatting.None, + new JsonSerializerSettings // Shouldn't include a not set property + { + NullValueHandling = NullValueHandling.Ignore + })) + }; + + APIRequestWithToken(callback, parameters.ToArray()); + } } } diff --git a/SlackAPI/SlackClientBase.cs b/SlackAPI/SlackClientBase.cs index 5b2f08f4..1e6d357b 100644 --- a/SlackAPI/SlackClientBase.cs +++ b/SlackAPI/SlackClientBase.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; using Newtonsoft.Json; @@ -33,7 +34,11 @@ protected SlackClientBase(IWebProxy proxySettings) protected Uri GetSlackUri(string path, Tuple[] getParameters) { - string parameters = getParameters + string parameters = default; + + if (getParameters != null && getParameters.Length > 0) + { + parameters = getParameters .Where(x => x.Item2 != null) .Select(new Func, string>(a => { @@ -54,11 +59,19 @@ protected Uri GetSlackUri(string path, Tuple[] getParameters) return string.Format("{0}&{1}", a, b); }); - Uri requestUri = new Uri(string.Format("{0}?{1}", path, parameters)); + } + + Uri requestUri = default; + + if (!string.IsNullOrEmpty(parameters)) + requestUri = new Uri(string.Format("{0}?{1}", path, parameters)); + else + requestUri = new Uri(path); + return requestUri; } - protected void APIRequest(Action callback, Tuple[] getParameters, Tuple[] postParameters) + protected void APIRequest(Action callback, Tuple[] getParameters, Tuple[] postParameters, string token = "") where K : Response { RequestPath path = RequestPath.GetRequestPath(); @@ -68,12 +81,15 @@ protected void APIRequest(Action callback, Tuple[] getPara Uri requestUri = GetSlackUri(Path.Combine(APIBaseLocation, path.Path), getParameters); HttpWebRequest request = CreateWebRequest(requestUri); + if (!string.IsNullOrEmpty(token)) + request.Headers.Add("Authorization", "Bearer " + token); + //This will handle all of the processing. RequestState state = new RequestState(request, postParameters, callback); state.Begin(); } - public Task APIRequestAsync(Tuple[] getParameters, Tuple[] postParameters) + public Task APIRequestAsync(Tuple[] getParameters, Tuple[] postParameters, string token = "") where K : Response { RequestPath path = RequestPath.GetRequestPath(); @@ -83,6 +99,9 @@ public Task APIRequestAsync(Tuple[] getParameters, Tuple(request, postParameters); return state.Execute(); @@ -111,9 +130,18 @@ protected HttpWebRequest CreateWebRequest(Uri requestUri) return httpWebRequest; } - protected HttpResponseMessage PostRequest(string requestUri, MultipartFormDataContent form) + protected Task PostRequestAsync(string requestUri, MultipartFormDataContent form, string token) { - return httpClient.PostAsync(requestUri, form).Result; + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + Content = form, + RequestUri = new Uri(requestUri), + }; + + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + + return httpClient.SendAsync(requestMessage); } public void RegisterConverter(JsonConverter converter) diff --git a/SlackAPI/SlackSocket.cs b/SlackAPI/SlackSocket.cs index 42b6e9ac..75903825 100644 --- a/SlackAPI/SlackSocket.cs +++ b/SlackAPI/SlackSocket.cs @@ -214,7 +214,7 @@ void SetupReceiving() WebSocketReceiveResult result = null; try { - result = await socket.ReceiveAsync(buffer, cts.Token); + result = await socket.ReceiveAsync(buffer, cts.Token).ConfigureAwait(false); } catch (WebSocketException wex) { diff --git a/SlackAPI/SlackSocketClient.cs b/SlackAPI/SlackSocketClient.cs index e067acc0..687ae4a7 100644 --- a/SlackAPI/SlackSocketClient.cs +++ b/SlackAPI/SlackSocketClient.cs @@ -15,6 +15,7 @@ public class SlackSocketClient : SlackClient public event Action OnReactionAdded; public event Action OnPongReceived; public event Action OnPresenceChanged; + public event Action OnConnectionLost; bool HelloReceived; public const int PingInterval = 3000; @@ -50,19 +51,25 @@ protected override void Connected(LoginResponse loginDetails) public void ConnectSocket(Action onSocketConnected){ underlyingSocket = new SlackSocket(loginDetails, this, onSocketConnected, this.proxySettings); + underlyingSocket.ConnectionClosed += UnderlyingSocket_ConnectionClosed; + } + + private void UnderlyingSocket_ConnectionClosed() + { + OnConnectionLost?.Invoke(); } - public void ErrorReceiving(Action callback) + public void ErrorReceiving(Action callback) { if (callback != null) underlyingSocket.ErrorReceiving += callback; } - public void ErrorReceivingDesiralization(Action callback) + public void ErrorReceivingDesiralization(Action callback) { if (callback != null) underlyingSocket.ErrorReceivingDesiralization += callback; } - public void ErrorHandlingMessage(Action callback) + public void ErrorHandlingMessage(Action callback) { if (callback != null) underlyingSocket.ErrorHandlingMessage += callback; } diff --git a/SlackAPI/SlackTaskClient.cs b/SlackAPI/SlackTaskClient.cs index 8051f6a4..fd65669e 100644 --- a/SlackAPI/SlackTaskClient.cs +++ b/SlackAPI/SlackTaskClient.cs @@ -44,7 +44,7 @@ public SlackTaskClient(string token, IWebProxy proxySettings) public virtual async Task ConnectAsync() { - var loginDetails = await EmitLoginAsync(); + var loginDetails = await EmitLoginAsync().ConfigureAwait(false); if(loginDetails.ok) Connected(loginDetails); @@ -91,11 +91,7 @@ public Task APIRequestWithTokenAsync() public Task APIRequestWithTokenAsync(params Tuple[] postParameters) where K : Response { - Tuple[] tokenArray = new Tuple[]{ - new Tuple("token", APIToken) - }; - - return APIRequestAsync(tokenArray, postParameters); + return APIRequestAsync(new Tuple[] { }, postParameters, APIToken); } public Task TestAuthAsync() @@ -103,9 +99,29 @@ public Task TestAuthAsync() return APIRequestWithTokenAsync(); } - public Task GetUserListAsync() + public Task GetUserListAsync(int limit = 0, bool include_locale = false, string cursor = null, string team_id = null) + { + if (limit < 0) + { + throw new ArgumentException(nameof(limit)); + } + var args = new List>(); + args.Add(new Tuple("limit", limit.ToString())); + args.Add(new Tuple("include_locale", include_locale.ToString())); + if (cursor != null) + { + args.Add(new Tuple("cursor", cursor)); + } + if (team_id != null) + { + args.Add(new Tuple("team_id", team_id)); + } + return APIRequestWithTokenAsync(args.ToArray()); + } + + public Task GetUserByEmailAsync(string email) { - return APIRequestWithTokenAsync(); + return APIRequestWithTokenAsync(new Tuple("email", email)); } public Task ChannelsCreateAsync(string name) { @@ -122,6 +138,36 @@ public Task ChannelsInviteAsync(string userId, string cha return APIRequestWithTokenAsync(parameters.ToArray()); } + public Task GetConversationsListAsync(string cursor = "", bool ExcludeArchived = true, int limit = 100, string[] types = null) + { + List> parameters = new List>() + { + Tuple.Create("exclude_archived", ExcludeArchived ? "1" : "0") + }; + if (limit > 0) + parameters.Add(Tuple.Create("limit", limit.ToString())); + if (types != null && types.Any()) + parameters.Add(Tuple.Create("types", string.Join(",", types))); + if (!string.IsNullOrEmpty(cursor)) + parameters.Add(new Tuple("cursor", cursor)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task GetConversationsMembersAsync(string channelId, string cursor = "", int limit = 100) + { + List> parameters = new List> + { + new Tuple("channel", channelId) + }; + if (limit > 0) + parameters.Add(Tuple.Create("limit", limit.ToString())); + if (!string.IsNullOrEmpty(cursor)) + parameters.Add(new Tuple("cursor", cursor)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + public Task GetChannelListAsync(bool ExcludeArchived = true) { return APIRequestWithTokenAsync(new Tuple("exclude_archived", ExcludeArchived ? "1" : "0")); @@ -214,6 +260,11 @@ public Task GetGroupHistoryAsync(Channel groupInfo, DateTim return GetHistoryAsync(groupInfo.id, latest, oldest, count, unreads); } + public Task GetConversationsHistoryAsync(Channel conversationInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) + { + return GetHistoryAsync(conversationInfo.id, latest, oldest, count, unreads); + } + public Task MarkChannelAsync(string channelId, DateTime ts) { return APIRequestWithTokenAsync(new Tuple("channel", channelId), @@ -328,6 +379,118 @@ public Task GroupsUnarchiveAsync(string channelId) #endregion + #region Conversations + public Task ConversationsArchiveAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task ConversationsCloseAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task ConversationsCreateAsync(string name, bool? isPrivate = null, string teamId = null) + { + List> parameters = new List>(); + parameters.Add(new Tuple("name", name)); + + if (isPrivate.HasValue) + parameters.Add(new Tuple("is_private", isPrivate.Value ? "true" : "false")); + + if (!string.IsNullOrEmpty(teamId)) + parameters.Add(new Tuple("team_id", teamId)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsInviteAsync(string channelId, string[] userIds) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("users", string.Join(",", userIds))); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsJoinAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task ConversationsKickAsync(string channelId, string userId) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("user", userId)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsLeaveAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task ConversationsMarkAsync(string channelId, DateTime ts) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("ts", ts.ToProperTimeStamp())); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsOpenAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task ConversationsOpenAsync(string[] userIds) + { + return APIRequestWithTokenAsync(new Tuple("users", string.Join(",", userIds))); + } + + public Task ConversationsRenameAsync(string channelId, string name) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("name", name)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsSetPurposeAsync(string channelId, string purpose) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("purpose", purpose)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsSetTopicAsync(string channelId, string topic) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("topic", topic)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsUnarchiveAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + #endregion + public Task SearchAllAsync(string query, string sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) { List> parameters = new List>(); @@ -449,7 +612,7 @@ public Task UpdateAsync(string ts, string parse = null, bool linkNames = false, Attachment[] attachments = null, - bool as_user = false, + bool? as_user = null, IBlock[] blocks = null) { List> parameters = new List>(); @@ -470,8 +633,9 @@ public Task UpdateAsync(string ts, if (attachments != null && attachments.Length > 0) parameters.Add(new Tuple("attachments", JsonConvert.SerializeObject(attachments))); - parameters.Add(new Tuple("as_user", as_user.ToString())); - + if (as_user.HasValue) + parameters.Add(new Tuple("as_user", as_user.ToString())); + if (blocks != null && blocks.Length > 0) parameters.Add(new Tuple("blocks", JsonConvert.SerializeObject(blocks, new JsonSerializerSettings() @@ -484,7 +648,7 @@ public Task UpdateAsync(string ts, public Task JoinDirectMessageChannelAsync(string user) { - var param = new Tuple("user", user); + var param = new Tuple("users", user); return APIRequestWithTokenAsync(param); } @@ -499,7 +663,8 @@ public Task PostMessageAsync( bool? unfurl_links = null, string icon_url = null, string icon_emoji = null, - bool as_user = false) + bool? as_user = null, + string thread_ts = null) { List> parameters = new List>(); @@ -538,8 +703,11 @@ public Task PostMessageAsync( if (!string.IsNullOrEmpty(icon_emoji)) parameters.Add(new Tuple("icon_emoji", icon_emoji)); - if (as_user) - parameters.Add(new Tuple("as_user", true.ToString())); + if (as_user.HasValue) + parameters.Add(new Tuple("as_user", as_user.ToString())); + + if (!string.IsNullOrEmpty(thread_ts)) + parameters.Add(new Tuple("thread_ts", thread_ts)); return APIRequestWithTokenAsync(parameters.ToArray()); } @@ -579,6 +747,69 @@ public Task PostEphemeralMessageAsync( return APIRequestWithTokenAsync(parameters.ToArray()); } + + public Task ScheduleMessageAsync( + string channelId, + string text, + DateTime post_at, + string botName = null, + string parse = null, + bool linkNames = false, + IBlock[] blocks = null, + Attachment[] attachments = null, + bool? unfurl_links = null, + string icon_url = null, + string icon_emoji = null, + bool as_user = false, + string thread_ts = null) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("text", text)); + parameters.Add(new Tuple("post_at", Convert.ToUInt64((post_at - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds).ToString())); + + if (!string.IsNullOrEmpty(botName)) + parameters.Add(new Tuple("username", botName)); + + if (!string.IsNullOrEmpty(parse)) + parameters.Add(new Tuple("parse", parse)); + + if (linkNames) + parameters.Add(new Tuple("link_names", "1")); + + if (blocks != null && blocks.Length > 0) + parameters.Add(new Tuple("blocks", JsonConvert.SerializeObject(blocks, + new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore + }))); + + if (attachments != null && attachments.Length > 0) + parameters.Add(new Tuple("attachments", JsonConvert.SerializeObject(attachments, + new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore + }))); + + if (unfurl_links.HasValue) + parameters.Add(new Tuple("unfurl_links", unfurl_links.Value ? "true" : "false")); + + if (!string.IsNullOrEmpty(icon_url)) + parameters.Add(new Tuple("icon_url", icon_url)); + + if (!string.IsNullOrEmpty(icon_emoji)) + parameters.Add(new Tuple("icon_emoji", icon_emoji)); + + if (as_user) + parameters.Add(new Tuple("as_user", true.ToString())); + + if (!string.IsNullOrEmpty(thread_ts)) + parameters.Add(new Tuple("thread_ts", thread_ts)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + public Task AddReactionAsync( string name = null, string channel = null, @@ -621,7 +852,6 @@ public async Task UploadFileAsync(byte[] fileData, string fi Uri target = new Uri(Path.Combine(APIBaseLocation, useAsync ? "files.uploadAsync" : "files.upload")); List parameters = new List(); - parameters.Add(string.Format("token={0}", APIToken)); //File/Content if (!string.IsNullOrEmpty(fileType)) @@ -641,8 +871,8 @@ public async Task UploadFileAsync(byte[] fileData, string fi using (MultipartFormDataContent form = new MultipartFormDataContent()) { form.Add(new ByteArrayContent(fileData), "file", fileName); - HttpResponseMessage response = PostRequest(string.Format("{0}?{1}", target, string.Join("&", parameters.ToArray())), form); - string result = await response.Content.ReadAsStringAsync(); + HttpResponseMessage response = await PostRequestAsync(string.Format("{0}?{1}", target, string.Join("&", parameters.ToArray())), form, APIToken); + string result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); return result.Deserialize(); } } @@ -653,5 +883,23 @@ public Task ChannelSetTopicAsync(string channelId, stri new Tuple("channel", channelId), new Tuple("topic", newTopic)); } + + public Task PublishAppHomeTab( + string userId, + View view) + { + view.type = ViewTypes.Home; + var parameters = new List> + { + new Tuple("user_id", userId), + new Tuple("view", JsonConvert.SerializeObject(view, Formatting.None, + new JsonSerializerSettings // Shouldn't include a not set property + { + NullValueHandling = NullValueHandling.Ignore + })) + }; + + return APIRequestWithTokenAsync(parameters.ToArray()); + } } -} \ No newline at end of file +} diff --git a/SlackAPI/TeamPreferences.cs b/SlackAPI/TeamPreferences.cs index c7da5453..e157d951 100644 --- a/SlackAPI/TeamPreferences.cs +++ b/SlackAPI/TeamPreferences.cs @@ -17,7 +17,7 @@ public class TeamPreferences public bool hide_referers; public int msg_edit_window_mins; public bool srvices_only_admins; - public bool stats_only_admins; + public bool? stats_only_admins; public enum AuthMode { diff --git a/SlackAPI/UserProfile.cs b/SlackAPI/UserProfile.cs index 65ab093a..6a3d6932 100644 --- a/SlackAPI/UserProfile.cs +++ b/SlackAPI/UserProfile.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SlackAPI +namespace SlackAPI { public class UserProfile : ProfileIcons { + public string title; + public string display_name; public string first_name; public string last_name; public string real_name;