diff --git a/.gitignore b/.gitignore index bff54b29..451fd09a 100644 --- a/.gitignore +++ b/.gitignore @@ -225,6 +225,9 @@ packages *.lock.json # Cake -tools/ +tools/* +!tools/packages.config + + .vs/ artifacts/ \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 5a366409..71125298 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,9 +1,5 @@  - 1.1.0 - - dev.0+sha.0 - A Slack wrapper for direct interaction with their APIs. Inumedia - Copyright © 2018 SlackAPI @@ -15,5 +11,9 @@ true git https://github.com/Inumedia/SlackAPI + + false + false + false \ No newline at end of file diff --git a/GlobalAssemblyInfo.cs b/GlobalAssemblyInfo.cs new file mode 100644 index 00000000..35472f28 --- /dev/null +++ b/GlobalAssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyVersion("1.1.0.0")] +[assembly: AssemblyFileVersion("1.1.0.0")] +[assembly: AssemblyInformationalVersion("1.1.0.0")] diff --git a/SlackAPI.Tests/BlockMessage.cs b/SlackAPI.Tests/BlockMessage.cs new file mode 100644 index 00000000..0f1d451d --- /dev/null +++ b/SlackAPI.Tests/BlockMessage.cs @@ -0,0 +1,98 @@ +using SlackAPI.Tests.Configuration; +using SlackAPI.Tests.Helpers; +using Xunit; + +namespace SlackAPI.Tests +{ + [Collection("Integration tests")] + public class BlockMessage + { + private readonly IntegrationFixture fixture; + + public BlockMessage(IntegrationFixture fixture) + { + this.fixture = fixture; + } + + [Fact] + public void Blocks() + { + // given + var client = this.fixture.UserClient; + PostMessageResponse actual = null; + + // when + using (var sync = new InSync(nameof(SlackClient.PostMessage))) + { + client.PostMessage( + response => + { + actual = response; + sync.Proceed(); + }, + this.fixture.Config.TestChannel, + string.Empty, + blocks: SlackMother.SomeBlocks); + } + + // then + Assert.True(actual.ok, "Error while posting message to channel. "); + } + + [Fact] + public void BlocksWithActions() + { + // given + var client = this.fixture.UserClient; + PostMessageResponse actual = null; + + // when + using (var sync = new InSync()) + { + client.PostMessage( + response => + { + actual = response; + sync.Proceed(); + }, + this.fixture.Config.TestChannel, + string.Empty, + blocks: SlackMother.SomeBlocksWithActions); + } + + // then + Assert.True(actual.ok, "Error while posting message to channel. "); + } + + [Fact] + public void BlocksInAttachment() + { + // given + var client = this.fixture.UserClient; + PostMessageResponse actual = null; + + // when + using (var sync = new InSync(nameof(SlackClient.PostMessage))) + { + client.PostMessage( + response => + { + actual = response; + sync.Proceed(); + }, + this.fixture.Config.TestChannel, + "These blocks are in an attachment", + attachments: new [] + { + new Attachment + { + blocks = SlackMother.SomeBlocks + } + }); + } + + // then + Assert.True(actual.ok, "Error while posting message to channel. "); + } + } +} \ No newline at end of file diff --git a/SlackAPI.Tests/Configuration/IntegrationFixture.cs b/SlackAPI.Tests/Configuration/IntegrationFixture.cs index 20da866d..cc542af7 100755 --- a/SlackAPI.Tests/Configuration/IntegrationFixture.cs +++ b/SlackAPI.Tests/Configuration/IntegrationFixture.cs @@ -1,32 +1,46 @@ using System; using System.IO; +using System.Linq; using System.Net; using System.Reflection; -using System.Threading; using Newtonsoft.Json; +using Polly; using SlackAPI.Tests.Helpers; +using SlackAPI.WebSocketMessages; using Xunit; namespace SlackAPI.Tests.Configuration { public class IntegrationFixture : IDisposable { - private Lazy userClient; - private Lazy botClient; - private Lazy userClientAsync; - private Lazy botClientAsync; + private const int MaxConnectionAttempts = 5; + + private readonly Lazy userClient; + private readonly Lazy botClient; + private readonly Lazy userClientAsync; + private readonly Lazy botClientAsync; + + private readonly Policy connectRetryPolicy; public IntegrationFixture() { this.Config = this.GetConfig(); - this.userClient = new Lazy(() => this.CreateClient(this.Config.UserAuthToken)); - this.botClient = new Lazy(() => this.CreateClient(this.Config.BotAuthToken)); + + this.connectRetryPolicy = Policy + .Handle(exception => exception.Message.Contains("ratelimited")) + .WaitAndRetry(MaxConnectionAttempts, retryAttempt => TimeSpan.FromSeconds(ComputeExponentialBackoff(retryAttempt)), + (exception, timeSpan, retryCount, context) => Console.WriteLine($"Connection failed ({exception.Message}). Retrying after {timeSpan.TotalSeconds}s ({retryCount}/5)")); + + this.userClient = new Lazy(() => connectRetryPolicy.Execute(() => this.CreateClient(this.Config.UserAuthToken))); + this.botClient = new Lazy(() => connectRetryPolicy.Execute(() => this.CreateClient(this.Config.BotAuthToken))); this.userClientAsync = new Lazy(() => new SlackTaskClient(this.Config.UserAuthToken)); this.botClientAsync = new Lazy(() => new SlackTaskClient(this.Config.BotAuthToken)); } public SlackConfig Config { get; } + public TimeSpan ConnectionTimeout => TimeSpan.FromSeconds(Enumerable.Range(1, MaxConnectionAttempts).Sum(ComputeExponentialBackoff)) + TimeSpan.FromSeconds(10); // Maximum exponential backoff + 10 seconds for connections attemps + public SlackSocketClient UserClient { get @@ -45,14 +59,14 @@ public SlackSocketClient BotClient } } - public SlackSocketClient CreateUserClient(IWebProxy proxySettings = null) + public SlackSocketClient CreateUserClient(IWebProxy proxySettings = null, bool maintainPresenceChangesStatus = false, Action presenceChanged = null) { - return this.CreateClient(this.Config.UserAuthToken, proxySettings); + return this.connectRetryPolicy.Execute(() => this.CreateClient(this.Config.UserAuthToken, proxySettings, maintainPresenceChangesStatus, presenceChanged)); } public SlackSocketClient CreateBotClient(IWebProxy proxySettings = null) { - return this.CreateClient(this.Config.BotAuthToken, proxySettings); + return this.connectRetryPolicy.Execute(() => this.CreateClient(this.Config.BotAuthToken, proxySettings)); } public SlackTaskClient UserClientAsync => userClientAsync.Value; @@ -83,7 +97,7 @@ private SlackConfig GetConfig() return JsonConvert.DeserializeAnonymousType(json, jsonObject).slack; } - private SlackSocketClient CreateClient(string authToken, IWebProxy proxySettings = null) + private SlackSocketClient CreateClient(string authToken, IWebProxy proxySettings = null, bool maintainPresenceChanges = false, Action presenceChanged = null) { SlackSocketClient client; @@ -92,7 +106,14 @@ private SlackSocketClient CreateClient(string authToken, IWebProxy proxySettings using (var syncClientSocket = new InSync($"{nameof(SlackClient.Connect)} - SocketConnected callback")) using (var syncClientSocketHello = new InSync($"{nameof(SlackClient.Connect)} - SocketConnected hello callback")) { - client = new SlackSocketClient(authToken, proxySettings); + client = new SlackSocketClient(authToken, proxySettings, maintainPresenceChanges); + + void OnPresenceChanged(PresenceChange x) + { + presenceChanged?.Invoke(client, x); + } + + client.OnPresenceChanged += OnPresenceChanged; client.OnHello += () => syncClientSocketHello.Proceed(); client.Connect(x => { @@ -117,5 +138,11 @@ private SlackSocketClient CreateClient(string authToken, IWebProxy proxySettings return client; } + + private int ComputeExponentialBackoff(int retryAttempt) + { + // Retries after 4, 8, 16, 32, 64... seconds + return 2 * (int)Math.Pow(2, retryAttempt); + } } } diff --git a/SlackAPI.Tests/Connect.cs b/SlackAPI.Tests/Connect.cs index 0aee94d0..3aa193f8 100644 --- a/SlackAPI.Tests/Connect.cs +++ b/SlackAPI.Tests/Connect.cs @@ -1,5 +1,7 @@ using System; +using System.Linq; using System.Net; +using System.Threading; using SlackAPI.Tests.Configuration; using SlackAPI.Tests.Helpers; using SlackAPI.WebSocketMessages; @@ -8,30 +10,38 @@ namespace SlackAPI.Tests { [Collection("Integration tests")] - public class Connect + public class Connect : IDisposable { const string TestText = "Test :D"; private readonly IntegrationFixture fixture; + private SlackSocketClient slackClient; + public Connect(IntegrationFixture fixture) { this.fixture = fixture; + + // Extra wait to mitigate Slack throttling + Thread.Sleep(2000); + } + + public void Dispose() + { + slackClient?.CloseSocket(); } [Fact] public void TestConnectAsUser() { - var client = this.fixture.CreateUserClient(); - Assert.True(client.IsConnected, "Invalid, doesn't think it's connected."); - client.CloseSocket(); + slackClient = this.fixture.CreateUserClient(); + Assert.True(slackClient.IsConnected, "Invalid, doesn't think it's connected."); } [Fact] public void TestConnectAsBot() { - var client = this.fixture.CreateBotClient(); - Assert.True(client.IsConnected, "Invalid, doesn't think it's connected."); - client.CloseSocket(); + slackClient = this.fixture.CreateBotClient(); + Assert.True(slackClient.IsConnected, "Invalid, doesn't think it's connected."); } [Fact] @@ -46,12 +56,12 @@ public void TestConnectWithWrongProxySettings() public void TestConnectPostAndDelete() { // given - SlackSocketClient client = this.fixture.CreateUserClient(); + slackClient = this.fixture.CreateUserClient(); string channel = this.fixture.Config.TestChannel; // when - DateTime messageTimestamp = PostMessage(client, channel); - DeletedResponse deletedResponse = DeleteMessage(client, channel, messageTimestamp); + DateTime messageTimestamp = PostMessage(slackClient, channel); + DeletedResponse deletedResponse = DeleteMessage(slackClient, channel, messageTimestamp); // then Assert.NotNull(deletedResponse); @@ -60,6 +70,65 @@ public void TestConnectPostAndDelete() Assert.Equal(messageTimestamp, deletedResponse.ts); } + [Fact] + public void TestConnectGetPresenceChanges() + { + // Arrange + int presenceChangesRaisedCount = 0; + using (var sync = new InSync(nameof(TestConnectGetPresenceChanges), this.fixture.ConnectionTimeout)) + { + void OnPresenceChanged(SlackSocketClient sender, PresenceChange e) + { + if (++presenceChangesRaisedCount == sender.Users.Count) + { + sync.Proceed(); + } + } + + // Act + slackClient = this.fixture.CreateUserClient(maintainPresenceChangesStatus: true, presenceChanged: OnPresenceChanged); + } + + // Assert + Assert.True(slackClient.Users.All(x => x.presence != null)); + } + + [Fact(Skip = "Not stable on AppVeyor")] + public void TestManualSubscribePresenceChangeAndManualPresenceChange() + { + // Arrange + slackClient = this.fixture.CreateUserClient(); + using (var sync = new InSync()) + { + slackClient.OnPresenceChanged += x => + { + if (x.user == slackClient.MySelf.id) + { + // Assert + sync.Proceed(); + } + }; + + slackClient.SubscribePresenceChange(slackClient.MySelf.id); + } + + using (var sync = new InSync()) + { + slackClient.OnPresenceChanged += x => + { + if (x is ManualPresenceChange && x.user == slackClient.MySelf.id) + { + // Assert + sync.Proceed(); + } + }; + + // Act + slackClient.EmitPresence(x => x.AssertOk(), Presence.away); + slackClient.EmitPresence(x => x.AssertOk(), Presence.auto); + } + } + private static DateTime PostMessage(SlackSocketClient client, string channel) { MessageReceived sendMessageResponse = null; 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/Helpers/InSync.cs b/SlackAPI.Tests/Helpers/InSync.cs index 82a0ba94..83a77ec8 100644 --- a/SlackAPI.Tests/Helpers/InSync.cs +++ b/SlackAPI.Tests/Helpers/InSync.cs @@ -8,14 +8,16 @@ namespace SlackAPI.Tests.Helpers { public class InSync : IDisposable { - private readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(15); + private readonly TimeSpan DefaultWaitTimeout = TimeSpan.FromSeconds(15); private readonly ManualResetEventSlim waiter; private readonly string message; + private readonly TimeSpan waitTimeout; - public InSync([CallerMemberName] string message = null) + public InSync([CallerMemberName] string message = null, TimeSpan? waitTimeout = null) { this.message = message; + this.waitTimeout = waitTimeout.GetValueOrDefault(DefaultWaitTimeout); this.waiter = new ManualResetEventSlim(); } @@ -26,7 +28,7 @@ public void Proceed() public void Dispose() { - Assert.True(this.waiter.Wait(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : this.WaitTimeout), $"Took too long to do '{this.message}'"); + Assert.True(this.waiter.Wait(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : this.waitTimeout), $"Took too long to do '{this.message}'"); } } } \ No newline at end of file diff --git a/SlackAPI.Tests/Helpers/SlackMother.cs b/SlackAPI.Tests/Helpers/SlackMother.cs index 6ca4a84f..70f750ef 100644 --- a/SlackAPI.Tests/Helpers/SlackMother.cs +++ b/SlackAPI.Tests/Helpers/SlackMother.cs @@ -2,6 +2,202 @@ { public class SlackMother { + public static IBlock[] SomeBlocks => new IBlock[] + { + new ContextBlock + { + elements = new IElement[]{ + new Text + { + type = TextTypes.Markdown, + text = "" + + } + } + }, + new SectionBlock + { + text = new Text + { + type = TextTypes.Markdown, + text = "" + }, + accessory = new ImageElement() + { + image_url = "https://imgs.xkcd.com/comics/exploits_of_a_mom.png", + alt_text = "Required for image elements" + } + }, + new DividerBlock(), + new SectionBlock + { + fields = new [] + { + new Text + { + type = TextTypes.Markdown, + text = "*Priority*\nHigh" + }, + new Text + { + type = TextTypes.Markdown, + text = "*Priority*\nHigh" + }, + new Text + { + type = TextTypes.Markdown, + text = "*Priority*\nHigh" + }, + new Text + { + type = TextTypes.PlainText, + text = "*Priority*\nHigh" + } + } + } + }; + + public static IBlock[] SomeBlocksWithActions => new IBlock[] + { + new ContextBlock + { + elements = new IElement[]{ + new Text + { + type = TextTypes.Markdown, + text = "" + } + }, + + }, + new SectionBlock + { + text = new Text + { + type = TextTypes.Markdown, + text = "" + }, + accessory = new OverflowElement + { + options = new [] + { + new Option + { + text = new Text + { + type = TextTypes.PlainText, + text = "Option 1 Text" + }, + value = "option 1" + }, + new Option + { + text = new Text + { + type = TextTypes.PlainText, + text = "Option 2 Text" + }, + value = "option 2" + }, + } + } + }, + new DividerBlock(), + new SectionBlock + { + fields = new [] + { + new Text + { + type = TextTypes.Markdown, + text = "*Priority*\nHigh" + }, + new Text + { + type = TextTypes.Markdown, + text = "*Priority*\nHigh" + }, + new Text + { + type = TextTypes.Markdown, + text = "*Priority*\nHigh" + }, + new Text + { + type = TextTypes.PlainText, + text = "*Priority*\nHigh" + } + } + }, + new SectionBlock + { + text = new Text + { + text = "Pick a date" + }, + accessory = new Element + { + type = ElementTypes.DatePicker, + initial_date = "1977-05-25", + placeholder = new Text + { + text = "Select a date" + } + } + }, + new ActionsBlock + { + block_id = "Optional unique identifier for a block", + elements = new IElement[] + { + new ButtonElement + { + text = new Text + { + text = "Button 1 Text" + }, + value = "Button 1", + style = ButtonStyles.Danger, + confirm = new Confirm + { + title = new Text + { + text = "Are you sure?" + }, + text = new Text + { + text = "Did you press Button 1?" + }, + confirm = new Text + { + text = "I did" + }, + deny = new Text + { + text = "I didn't" + } + } + }, + new ButtonElement + { + text = new Text + { + text = "Button 2 Text" + }, + value = "Button 2", + }, + new ButtonElement + { + text = new Text + { + text = "Button 3 Text" + }, + value = "Button 3", + style = ButtonStyles.Primary, + } + } + } + }; public static Attachment[] SomeAttachments => new[] { new Attachment() diff --git a/SlackAPI.Tests/SlackAPI.Tests.csproj b/SlackAPI.Tests/SlackAPI.Tests.csproj index 98acb69f..bc4dc3b4 100644 --- a/SlackAPI.Tests/SlackAPI.Tests.csproj +++ b/SlackAPI.Tests/SlackAPI.Tests.csproj @@ -10,6 +10,7 @@ + @@ -29,6 +30,12 @@ + + + Properties\GlobalAssemblyInfo.cs + + + $(DefineConstants);RELEASE diff --git a/SlackAPI.Tests/UserUIInteraction.cs b/SlackAPI.Tests/UserUIInteraction.cs index cea2692a..0ece63cf 100644 --- a/SlackAPI.Tests/UserUIInteraction.cs +++ b/SlackAPI.Tests/UserUIInteraction.cs @@ -10,6 +10,9 @@ namespace SlackAPI.Tests { +// Run UI tests on a single plateform to avoid Slack Captcha +// (captcha is displayed when trying to login too often) +#if NETFRAMEWORK [Collection("Integration tests")] public class UserUIInteraction { @@ -43,13 +46,13 @@ public void TestGetAccessToken() var slackClientHelpers = new SlackClientHelpers(); var uri = slackClientHelpers.GetAuthorizeUri(clientId, SlackScope.Identify); driver.Navigate().GoToUrl(uri); - driver.FindElement(By.Id("oauth_authorizify")).Click(); + driver.FindElement(By.CssSelector("button[type='submit']")).Click(); var code = Regex.Match(driver.Url, "code=(?[^&]+)&state").Groups["code"].Value; var accessTokenResponse = GetAccessToken(slackClientHelpers, clientId, clientSecret, redirectUrl, code); Assert.True(accessTokenResponse.ok); - Assert.Equal("identify", accessTokenResponse.scope); + Assert.Contains("identify", accessTokenResponse.scope); } } @@ -70,4 +73,5 @@ private AccessTokenResponse GetAccessToken(SlackClientHelpers slackClientHelpers return accessTokenResponse; } } +#endif } diff --git a/SlackAPI.sln b/SlackAPI.sln index 77a33f65..f3f74ac0 100644 --- a/SlackAPI.sln +++ b/SlackAPI.sln @@ -1,12 +1,21 @@  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 +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/SlackAPI/Attachment.cs b/SlackAPI/Attachment.cs index 25ae63f7..64b28e1d 100644 --- a/SlackAPI/Attachment.cs +++ b/SlackAPI/Attachment.cs @@ -14,6 +14,7 @@ public class Attachment public string title_link; public string text; public Field[] fields; + public IBlock[] blocks; public string image_url; public string thumb_url; @@ -44,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 new file mode 100644 index 00000000..5b05099f --- /dev/null +++ b/SlackAPI/Block.cs @@ -0,0 +1,236 @@ +namespace SlackAPI +{ + //see https://api.slack.com/reference/messaging/blocks + public class Block : IBlock + { + public string type { get; set; } + public string block_id { get; set; } + public Text text { get; set; } + public Element accessory { get; set; } + public Element[] elements { get; set; } + public Text title { get; set; } + public string image_url { get; set; } + public string alt_text { get; set; } + public Text[] fields { get; set; } + } + public class SectionBlock : IBlock + { + public string type { get; } = BlockTypes.Section; + public string block_id { get; set; } + public Text text { get; set; } + public IElement accessory { get; set; } + public Text[] fields { get; set; } + } + public class DividerBlock : IBlock + { + public string type { get; } = BlockTypes.Divider; + public string block_id { get; set; } + } + public class ImageBlock : IBlock + { + public string type { get; } = BlockTypes.Image; + public string block_id { get; set; } + public Text title { get; set; } + public string image_url { get; set; } + public string alt_text { get; set; } + } + public class ActionsBlock : IBlock + { + public string type { get; } = BlockTypes.Actions; + public string block_id { get; set; } + public IElement[] elements { get; set; } + } + public class ContextBlock : IBlock + { + public string type { get; } = BlockTypes.Context; + 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; + public string text { get; set; } + public bool? emoji { get; set; } + public bool? verbatim { get; set; } + } + + public class Option + { + public Text text { get; set; } + public string value { get; set; } + } + + public class OptionGroups + { + public Text label { get; set; } + public Option[] options { get; set; } + } + + public class Confirm + { + public Text title { get; set; } + public Text text { get; set; } + public Text confirm { get; set; } + public Text deny { get; set; } + } + + public class Element : IElement + { + public string type { get; set; } + public string action_id { get; set; } + public Text text { get; set; } + public string value { get; set; } + public Text placeholder { get; set; } + public Option[] options { get; set; } + public OptionGroups[] option_groups { get; set; } + public string image_url { get; set; } + public string alt_text { get; set; } + public string url { get; set; } + public string initial_date { get; set; } + public string initial_user { get; set; } + public string initial_channel { get; set; } + public string initial_conversation { get; set; } + public string initial_option { get; set; } + public int? min_query_length { get; set; } + public Confirm confirm { get; set; } + public string style { get; set; } + } + public class ImageElement : IElement + { + public string type { get; } = ElementTypes.Image; + public string image_url { get; set; } + public string alt_text { get; set; } + } + public class ButtonElement : IElement + { + public string type { get; } = ElementTypes.Button; + public string action_id { get; set; } + public Text text { get; set; } + public string value { get; set; } + public Text placeholder { get; set; } + public Option[] options { get; set; } + public OptionGroups[] option_groups { get; set; } + public string url { get; set; } + public Confirm confirm { get; set; } + public string style { get; set; } + } + public class StaticSelectElement : IElement + { + public string type { get; } = ElementTypes.StaticSelect; + public string action_id { get; set; } + public Text placeholder { get; set; } + public Option[] options { get; set; } + public OptionGroups[] option_groups { get; set; } + public string initial_option { get; set; } + public Confirm confirm { get; set; } + } + public class ExternalSelectElement : IElement + { + public string type { get; } = ElementTypes.ExternalSelect; + public string action_id { get; set; } + public Text placeholder { get; set; } + public string initial_option { get; set; } + public int min_query_length { get; set; } + public Confirm confirm { get; set; } + } + + + public class UserSelectElement : IElement + { + public string type { get; } = ElementTypes.UserSelect; + public string action_id { get; set; } + public Text placeholder { get; set; } + public string initial_user { get; set; } + public Confirm confirm { get; set; } + } + public class ConversationSelectElement : IElement + { + public string type { get; } = ElementTypes.ChannelSelect; + public string action_id { get; set; } + public Text placeholder { get; set; } + public string initial_conversation { get; set; } + public Confirm confirm { get; set; } + } + public class ChannelSelectElement : IElement + { + public string type { get; } = ElementTypes.ChannelSelect; + public string action_id { get; set; } + public Text placeholder { get; set; } + public string initial_channel { get; set; } + public Confirm confirm { get; set; } + } + public class OverflowElement : IElement + { + public string type { get; } = ElementTypes.Overflow; + public string action_id { get; set; } + public Option[] options { get; set; } + public Confirm confirm { get; set; } + } + + public class DatePickerElement : IElement + { + public string type { get; } = ElementTypes.DatePicker; + public string action_id { get; set; } + public Text placeholder { get; set; } + public string initial_date { get; set; } + 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"; + public const string Danger = "danger"; + } + + public static class BlockTypes + { + public const string Section = "section"; + public const string Divider = "divider"; + 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 + { + public const string Markdown = "mrkdwn"; + public const string PlainText = "plain_text"; + } + + public static class ElementTypes + { + public const string Image = "image"; + public const string Button = "button"; + public const string StaticSelect = "static_select"; + public const string ExternalSelect = "external_select"; + public const string UserSelect = "users_select"; + public const string ChannelSelect = "channel_select"; + public const string ConversationSelect = "conversation_select"; + public const string Overflow = "overflow"; + public const string DatePicker = "datepicker"; + } + + public interface IElement { } + + public interface IBlock { } + +} 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/Dialog.cs b/SlackAPI/Dialog.cs new file mode 100644 index 00000000..020f9dcf --- /dev/null +++ b/SlackAPI/Dialog.cs @@ -0,0 +1,80 @@ +namespace SlackAPI +{ + //see https://api.slack.com/dialogs + + public class Dialog + { + public string callback_id { get; set; } + public string title { get; set; } + public string submit_label { get; set; } + public bool? notify_on_cancel { get; set; } + public string state { get; set; } + public Element[] elements { get; set; } + + public abstract class Element + { + public string label { get; set; } + public string name { get; set; } + public string placeholder { get; set; } + public bool? optional { get; set; } + public string value { get; set; } + } + + public class TextElement : Element + { + public string type { get; } = "text"; + public string subtype { get; set; } + public int? max_length { get; set; } + public int? min_length { get; set; } + public string hint { get; set; } + + } + + public class TextAreaElement : Element + { + public string type { get; } = "textarea"; + public int? max_length { get; set; } + public int? min_length { get; set; } + public string hint { get; set; } + + } + + public class SelectElement : Element + { + public string type { get; } = "select"; + public string data_source { get; set; } + public Option[] options { get; set; } + public OptionGroup[] option_groups { get; set; } + public Option[] selected_options { get; set; } + public int? min_query_length { get; set; } + } + + public class Option + { + public string label { get; set; } + public string value { get; set; } + } + + public class OptionGroup + { + public string label { get; set; } + public Option[] options { get; set; } + } + + public static class DataSourceTypes + { + public static string Users = "users"; + public static string Channels = "channels"; + public static string Conversations = "conversations"; + public static string External = "external"; + } + + public static class TextElementSubTypes + { + public static string Email = "email"; + public static string Number = "number"; + public static string Telephone = "telephone"; + public static string Url = "url"; + } + } +} \ No newline at end of file diff --git a/SlackAPI/Extensions.cs b/SlackAPI/Extensions.cs index 194a9487..02d8d6e4 100644 --- a/SlackAPI/Extensions.cs +++ b/SlackAPI/Extensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using Newtonsoft.Json; namespace SlackAPI @@ -17,10 +18,10 @@ public static string ToProperTimeStamp(this DateTime that, bool toUTC = true) { if (toUTC) { - return ((that.ToUniversalTime().Ticks - 621355968000000000m) / 10000000m).ToString("F6"); + return ((that.ToUniversalTime().Ticks - 621355968000000000m) / 10000000m).ToString("F6", CultureInfo.InvariantCulture); } else - return that.Subtract(new DateTime(1970, 1, 1)).TotalSeconds.ToString(); + return that.Subtract(new DateTime(1970, 1, 1)).TotalSeconds.ToString(CultureInfo.InvariantCulture); } public static K Deserialize(this string data) diff --git a/SlackAPI/JavascriptDateTimeConverter.cs b/SlackAPI/JavascriptDateTimeConverter.cs index d70d3742..438df1a8 100644 --- a/SlackAPI/JavascriptDateTimeConverter.cs +++ b/SlackAPI/JavascriptDateTimeConverter.cs @@ -11,7 +11,7 @@ internal class JavascriptDateTimeConverter : Newtonsoft.Json.JsonConverter { public override bool CanConvert(Type objectType) { - return objectType == typeof(DateTime); + return objectType == typeof(DateTime) || objectType == typeof(DateTime?); } public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) @@ -20,7 +20,7 @@ public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectTy DateTime res = new DateTime(621355968000000000 + (long)(value * 10000000m)).ToLocalTime(); System.Diagnostics.Debug.Assert( Decimal.Equals( - Decimal.Parse(res.ToProperTimeStamp()), + Decimal.Parse(res.ToProperTimeStamp(), CultureInfo.InvariantCulture), Decimal.Parse(reader.Value.ToString(), CultureInfo.InvariantCulture)), "Precision loss :("); return res; diff --git a/SlackAPI/Message.cs b/SlackAPI/Message.cs index d829245a..6077ec6c 100644 --- a/SlackAPI/Message.cs +++ b/SlackAPI/Message.cs @@ -16,9 +16,12 @@ public class Message : SlackSocketMessage /// public string username; public string text; + public Attachment[] attachments; public bool is_starred; public string permalink; public Reaction[] reactions; //Wibblr? Not really sure what this applies to. :< + + public DateTime? thread_ts; } } diff --git a/SlackAPI/Preferences.cs b/SlackAPI/Preferences.cs index 92f48a58..f0931eec 100644 --- a/SlackAPI/Preferences.cs +++ b/SlackAPI/Preferences.cs @@ -55,7 +55,7 @@ public class Preferences public string emoji_mode; public string highlight_words; //public string newxp_slackbot_step; //I don't even... - public SearchSort search_sort; + public string search_sort; public string push_loud_channels; public string push_mention_channels; public string push_loud_channels_set; @@ -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/AccessTokenResponse.cs b/SlackAPI/RPCMessages/AccessTokenResponse.cs index ebf9fb9d..9c4266bb 100644 --- a/SlackAPI/RPCMessages/AccessTokenResponse.cs +++ b/SlackAPI/RPCMessages/AccessTokenResponse.cs @@ -12,6 +12,33 @@ public class AccessTokenResponse : Response public string access_token; public string scope; public string team_name; - public Bot bot; + public string team_id { get; set; } + public BotTokenResponse bot; + public IncomingWebhook incoming_webhook { get; set; } + } + + public class BotTokenResponse + { + public string emoji; + public string image_24; + public string image_32; + public string image_48; + public string image_72; + public string image_192; + + public bool deleted; + public UserProfile icons; + public string id; + public string name; + public string bot_user_id; + public string bot_access_token; + } + + public class IncomingWebhook + { + public string channel { get; set; } + public string channel_id { get; set; } + public string configuration_url { get; set; } + public string url { get; set; } } } 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/ChannelInviteResponse.cs b/SlackAPI/RPCMessages/ChannelInviteResponse.cs new file mode 100644 index 00000000..41b2108b --- /dev/null +++ b/SlackAPI/RPCMessages/ChannelInviteResponse.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("channels.invite")] + public class ChannelInviteResponse : Response + { + public Channel channel; + } +} 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/DialogOpenResponse.cs b/SlackAPI/RPCMessages/DialogOpenResponse.cs new file mode 100644 index 00000000..926a339a --- /dev/null +++ b/SlackAPI/RPCMessages/DialogOpenResponse.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +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/FileDeleteResponse.cs b/SlackAPI/RPCMessages/FileDeleteResponse.cs new file mode 100644 index 00000000..81c637a7 --- /dev/null +++ b/SlackAPI/RPCMessages/FileDeleteResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("files.delete")] + public class FileDeleteResponse : 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/MessageHistory.cs b/SlackAPI/RPCMessages/MessageHistory.cs index ba2541ca..8fb797a1 100644 --- a/SlackAPI/RPCMessages/MessageHistory.cs +++ b/SlackAPI/RPCMessages/MessageHistory.cs @@ -12,6 +12,7 @@ public class MessageHistory : Response public DateTime latest; public Message[] messages; public bool has_more; + public int unread_count_display; public bool channel_not_found; public bool invalid_ts_latest; diff --git a/SlackAPI/RPCMessages/PresenseResponse.cs b/SlackAPI/RPCMessages/PresenseResponse.cs index 5c3c333f..7b3cd5de 100644 --- a/SlackAPI/RPCMessages/PresenseResponse.cs +++ b/SlackAPI/RPCMessages/PresenseResponse.cs @@ -1,18 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SlackAPI +namespace SlackAPI { [RequestPath("users.setPresence")] public class PresenceResponse : Response { } + public enum Presence { active, - away + away, + auto } } 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/SearchResponseMessages.cs b/SlackAPI/RPCMessages/SearchResponseMessages.cs index c8fbf89e..8b1eef70 100644 --- a/SlackAPI/RPCMessages/SearchResponseMessages.cs +++ b/SlackAPI/RPCMessages/SearchResponseMessages.cs @@ -55,8 +55,10 @@ public class PaginationInformation public enum SearchSort { + not_set, score, - timestamp + timestamp, + not_set_new_user } public enum SearchSortDirection 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/Request.cs b/SlackAPI/Request.cs index 2af5854c..e7ed01a5 100644 --- a/SlackAPI/Request.cs +++ b/SlackAPI/Request.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using System; -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Diagnostics; using System.IO; using System.Net; using System.Reflection; @@ -141,20 +142,36 @@ public RequestPath(string requestPath, bool isPrimaryAPI = true) UsePrimaryAPI = isPrimaryAPI; } - static Dictionary paths = new Dictionary(); + static ConcurrentDictionary paths = new ConcurrentDictionary(); public static RequestPath GetRequestPath() { Type t = typeof(K); - if (paths.ContainsKey(t)) - return paths[t]; + if (paths.TryGetValue(t, out var path)) + return path; TypeInfo info = t.GetTypeInfo(); - RequestPath path = info.GetCustomAttribute(); + path = info.GetCustomAttribute(); if (path == null) throw new InvalidOperationException(string.Format("No valid request path for {0}", t.Name)); - paths.Add(t, path); + try + { + // Some other thread may have already placed the path to the dictionary, + // the original one will remain there and current one will be GCed once it + // gets out of scope of this request. + paths.TryAdd(t, path); + } + catch (Exception e) + { + // There is a slight chance of TryAdd throwing, we want to be extra safe + // so we just consume it and leave the dictionary as-is, next call of + // the same function will try to add the path again. + // This may be removed in the future if TryAdd is verified as safe. + // See #190. + Trace.TraceError(e.ToString()); + } + return path; } } 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 f6d875d0..934c5356 100644 --- a/SlackAPI/SlackAPI.csproj +++ b/SlackAPI/SlackAPI.csproj @@ -1,14 +1,14 @@  - net45;netstandard1.3;netstandard1.6;netstandard2.0 + net45;netstandard2.0 Full SlackAPI SlackAPI - + @@ -27,6 +27,12 @@ + + + Properties\GlobalAssemblyInfo.cs + + + $(DefineConstants);RELEASE diff --git a/SlackAPI/SlackClient.cs b/SlackAPI/SlackClient.cs index 3948d33a..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) @@ -127,10 +120,57 @@ 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)); } - public void GetChannelList(Action callback, bool ExcludeArchived = true) + + public void ChannelsInvite(Action callback, string userId, string channelId) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("user", 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")); } @@ -145,7 +185,7 @@ public void GetDirectMessageList(Action c APIRequestWithToken(callback); } - public void GetFiles(Action callback, string userId = null, DateTime? from = null, DateTime? to = null, int? count = null, int? page = null, FileTypes types = FileTypes.all) + public void GetFiles(Action callback, string userId = null, DateTime? from = null, DateTime? to = null, int? count = null, int? page = null, FileTypes types = FileTypes.all, string channel = null) { List> parameters = new List>(); @@ -186,10 +226,13 @@ public void GetFiles(Action callback, string userId = null, Da if (page.HasValue) parameters.Add(new Tuple("page", page.Value.ToString())); + if (!string.IsNullOrEmpty(channel)) + parameters.Add(new Tuple("channel", channel)); + APIRequestWithToken(callback, parameters.ToArray()); } - void GetHistory(Action historyCallback, string channel, DateTime? latest = null, DateTime? oldest = null, int? count = null) + void GetHistory(Action historyCallback, string channel, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) where K : MessageHistory { List> parameters = new List>(); @@ -201,23 +244,30 @@ void GetHistory(Action historyCallback, string channel, DateTime? latest = parameters.Add(new Tuple("oldest", oldest.Value.ToProperTimeStamp())); if(count.HasValue) parameters.Add(new Tuple("count", count.Value.ToString())); + if (unreads.HasValue) + parameters.Add(new Tuple("unreads", unreads.Value ? "1" : "0")); APIRequestWithToken(historyCallback, parameters.ToArray()); } - public void GetChannelHistory(Action callback, Channel channelInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null) + public void GetChannelHistory(Action callback, Channel channelInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) { - GetHistory(callback, channelInfo.id, latest, oldest, count); + GetHistory(callback, channelInfo.id, latest, oldest, count, unreads); } - public void GetDirectMessageHistory(Action callback, DirectMessageConversation conversationInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null) + public void GetDirectMessageHistory(Action callback, DirectMessageConversation conversationInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) { - GetHistory(callback, conversationInfo.id, latest, oldest, count); + GetHistory(callback, conversationInfo.id, latest, oldest, count, unreads); } - public void GetGroupHistory(Action callback, Channel groupInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null) + public void GetGroupHistory(Action callback, Channel groupInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) { - GetHistory(callback, groupInfo.id, latest, oldest, count); + 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) @@ -335,13 +385,112 @@ public void GroupsUnarchive(Action callback, string chan #endregion - public void SearchAll(Action callback, string query, SearchSort? sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) + #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>(); parameters.Add(new Tuple("query", query)); - if (sorting.HasValue) - parameters.Add(new Tuple("sort", sorting.Value.ToString())); + if (sorting != null) + parameters.Add(new Tuple("sort", sorting)); if (direction.HasValue) parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); @@ -358,13 +507,13 @@ public void SearchAll(Action callback, string query, SearchSo APIRequestWithToken(callback, parameters.ToArray()); } - public void SearchMessages(Action callback, string query, SearchSort? sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) + public void SearchMessages(Action callback, string query, string sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) { List> parameters = new List>(); parameters.Add(new Tuple("query", query)); - if (sorting.HasValue) - parameters.Add(new Tuple("sort", sorting.Value.ToString())); + if (sorting != null) + parameters.Add(new Tuple("sort", sorting)); if (direction.HasValue) parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); @@ -381,13 +530,13 @@ public void SearchMessages(Action callback, string query APIRequestWithToken(callback, parameters.ToArray()); } - public void SearchFiles(Action callback, string query, SearchSort? sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) + public void SearchFiles(Action callback, string query, string sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) { List> parameters = new List>(); parameters.Add(new Tuple("query", query)); - if (sorting.HasValue) - parameters.Add(new Tuple("sort", sorting.Value.ToString())); + if (sorting != null) + parameters.Add(new Tuple("sort", sorting)); if (direction.HasValue) parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); @@ -472,8 +621,9 @@ public void Update( string botName = null, string parse = null, bool linkNames = false, + IBlock[] blocks = null, Attachment[] attachments = null, - bool as_user = false) + bool? as_user = null) { List> parameters = new List>(); @@ -490,17 +640,29 @@ public void Update( if (linkNames) parameters.Add(new Tuple("link_names", "1")); - if (attachments != null && attachments.Length > 0) - parameters.Add(new Tuple("attachments", JsonConvert.SerializeObject(attachments))); + if (blocks != null && blocks.Length > 0) + parameters.Add(new Tuple("blocks", + JsonConvert.SerializeObject(blocks, new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore + }))); - parameters.Add(new Tuple("as_user", as_user.ToString())); + if (attachments != null && attachments.Length > 0) + parameters.Add(new Tuple("attachments", + JsonConvert.SerializeObject(attachments, new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore + }))); + + 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); } @@ -511,8 +673,9 @@ public void PostMessage( string botName = null, string parse = null, bool linkNames = false, + IBlock[] blocks = null, Attachment[] attachments = null, - bool unfurl_links = false, + bool? unfurl_links = null, string icon_url = null, string icon_emoji = null, bool? as_user = null, @@ -532,16 +695,24 @@ public void PostMessage( 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 - }))); + 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) - parameters.Add(new Tuple("unfurl_links", "1")); + 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)); @@ -565,6 +736,7 @@ public void PostEphemeralMessage( string targetuser, string parse = null, bool linkNames = false, + Block[] blocks = null, Attachment[] attachments = null, bool as_user = false, string thread_ts = null) @@ -581,6 +753,14 @@ public void PostEphemeralMessage( 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, @@ -595,6 +775,90 @@ public void PostEphemeralMessage( } + 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, + Dialog dialog) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("trigger_id", triggerId)); + + parameters.Add(new Tuple("dialog", + JsonConvert.SerializeObject(dialog, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }))); + + APIRequestWithToken(callback, parameters.ToArray()); + } + public void AddReaction( Action callback, string name = null, @@ -620,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)) @@ -640,10 +903,37 @@ 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()); } } + + public void DeleteFile(Action callback, string file = null) + { + if (string.IsNullOrEmpty(file)) + return; + + 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 6d0129d2..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; @@ -12,7 +13,13 @@ public abstract class SlackClientBase { protected readonly IWebProxy proxySettings; private readonly HttpClient httpClient; - protected const string APIBaseLocation = "https://slack.com/api/"; + public string APIBaseLocation { get; set; } = "https://slack.com/api/"; + + static SlackClientBase() + { + // Force Tls 1.2 for Slack + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + } protected SlackClientBase() { @@ -27,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 => { @@ -48,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(); @@ -62,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(); @@ -77,6 +99,9 @@ public Task APIRequestAsync(Tuple[] getParameters, Tuple(request, postParameters); return state.Execute(); @@ -105,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 c4915bee..75903825 100644 --- a/SlackAPI/SlackSocket.cs +++ b/SlackAPI/SlackSocket.cs @@ -11,10 +11,6 @@ using System.Linq; using System.Net; -#if NETSTANDARD1_6 -using Microsoft.Extensions.DependencyModel; -#endif - namespace SlackAPI { public class SlackSocket @@ -42,17 +38,7 @@ public class SlackSocket static SlackSocket() { routing = new Dictionary>(); - -#if NET45 || NETSTANDARD2_0 var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(x => x.GlobalAssemblyCache == false); -#elif NETSTANDARD1_6 - var assemblies = DependencyContext.Default.GetDefaultAssemblyNames().Select(Assembly.Load); -#elif NETSTANDARD1_3 - var assemblies = new[] { typeof(SlackSocket).GetType().GetTypeInfo().Assembly }; -#warning Routing messages in custom assemblies are not supported with .Net Standard 1.3 -#else -#error Platform not supported -#endif foreach (Assembly assembly in assemblies) { Type[] assemblyTypes; @@ -228,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 5aff2a32..687ae4a7 100644 --- a/SlackAPI/SlackSocketClient.cs +++ b/SlackAPI/SlackSocketClient.cs @@ -1,5 +1,6 @@ using System.Net.WebSockets; using System; +using System.Linq; using System.Net; using SlackAPI.WebSocketMessages; @@ -7,11 +8,14 @@ namespace SlackAPI { public class SlackSocketClient : SlackClient { + readonly bool maintainPresenceChanges; SlackSocket underlyingSocket; public event Action OnMessageReceived; public event Action OnReactionAdded; public event Action OnPongReceived; + public event Action OnPresenceChanged; + public event Action OnConnectionLost; bool HelloReceived; public const int PingInterval = 3000; @@ -23,14 +27,10 @@ public class SlackSocketClient : SlackClient public event Action OnHello; private LoginResponse loginDetails; - public SlackSocketClient(string token) - : base(token) - { - } - - public SlackSocketClient(string token, IWebProxy proxySettings) + public SlackSocketClient(string token, IWebProxy proxySettings = null, bool maintainPresenceChanges = false) : base(token, proxySettings) { + this.maintainPresenceChanges = maintainPresenceChanges; } public override void Connect(Action onConnected, Action onSocketConnected = null) @@ -51,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; } @@ -80,7 +86,7 @@ public void UnbindCallback(Action callback) public void SendPresence(Presence status) { - underlyingSocket.Send(new PresenceChange() { presence = Presence.active, user = base.MySelf.id }); + underlyingSocket.Send(new PresenceChange() { presence = status, user = base.MySelf.id }); } public void SendTyping(string channelId) @@ -107,6 +113,11 @@ public void SendPing() underlyingSocket.Send(new Ping()); } + public void SubscribePresenceChange(params string[] usersIds) + { + underlyingSocket.Send(new PresenceChangeSubscription(usersIds)); + } + public void HandlePongReceived(Pong pong) { if (OnPongReceived != null) @@ -121,6 +132,12 @@ public void HandleReactionAdded(ReactionAdded reactionAdded) public void HandleHello(Hello hello) { + if (maintainPresenceChanges) + { + // Subscribe presence change event for all the users on startup to maintain status in the the lookup table + SubscribePresenceChange(UserLookup.Keys.ToArray()); + } + HelloReceived = true; if (OnHello != null) @@ -132,6 +149,12 @@ public void HandlePresence(PresenceChange change) UserLookup[change.user].presence = change.presence.ToString().ToLower(); } + public void HandleManualPresence(ManualPresenceChange change) + { + change.user = MySelf.id; + HandlePresence(change); + } + public void HandleUserChange(UserChange change) { UserLookup[change.user.id] = change.user; @@ -216,13 +239,18 @@ public void Message(NewMessage m) public void FileShareMessage(FileShareMessage m) { - if (OnMessageReceived != null) - OnMessageReceived(m); + Message(m); } public void PresenceChange(PresenceChange p) { + OnPresenceChanged?.Invoke(p); + } + public void ManualPresenceChange(ManualPresenceChange p) + { + p.user = MySelf.id; + PresenceChange(p); } public void ChannelMarked(ChannelMarked m) diff --git a/SlackAPI/SlackTaskClient.cs b/SlackAPI/SlackTaskClient.cs index edc1eb41..fd65669e 100644 --- a/SlackAPI/SlackTaskClient.cs +++ b/SlackAPI/SlackTaskClient.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; +using SlackAPI.RPCMessages; namespace SlackAPI { @@ -43,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); @@ -86,15 +87,11 @@ public Task APIRequestWithTokenAsync() { return APIRequestWithTokenAsync(new Tuple[] { }); } - + 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() @@ -102,9 +99,73 @@ 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) { - return APIRequestWithTokenAsync(); + 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(new Tuple("email", email)); + } + + public Task ChannelsCreateAsync(string name) { + return APIRequestWithTokenAsync(new Tuple("name", name)); + } + + public Task ChannelsInviteAsync(string userId, string channelId) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("user", userId)); + + 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) @@ -138,7 +199,7 @@ public Task GetFilesAsync(string userId = null, DateTime? from if (!types.HasFlag(FileTypes.all)) { FileTypes[] values = (FileTypes[])Enum.GetValues(typeof(FileTypes)); - + StringBuilder building = new StringBuilder(); bool first = true; for (int i = 0; i < values.Length; ++i) @@ -166,35 +227,42 @@ public Task GetFilesAsync(string userId = null, DateTime? from return APIRequestWithTokenAsync(parameters.ToArray()); } - private Task GetHistoryAsync(string channel, DateTime? latest = null, DateTime? oldest = null, int? count = null) + private Task GetHistoryAsync(string channel, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) where K : MessageHistory { List> parameters = new List>(); parameters.Add(new Tuple("channel", channel)); - + if(latest.HasValue) parameters.Add(new Tuple("latest", latest.Value.ToProperTimeStamp())); if(oldest.HasValue) parameters.Add(new Tuple("oldest", oldest.Value.ToProperTimeStamp())); - if(count.HasValue) - parameters.Add(new Tuple("count", count.Value.ToString())); + if (count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + if (unreads.HasValue) + parameters.Add(new Tuple("unreads", unreads.Value ? "1" : "0")); return APIRequestWithTokenAsync(parameters.ToArray()); } - public Task GetChannelHistoryAsync(Channel channelInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null) + public Task GetChannelHistoryAsync(Channel channelInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) + { + return GetHistoryAsync(channelInfo.id, latest, oldest, count, unreads); + } + + public Task GetDirectMessageHistoryAsync(DirectMessageConversation conversationInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) { - return GetHistoryAsync(channelInfo.id, latest, oldest, count); + return GetHistoryAsync(conversationInfo.id, latest, oldest, count, unreads); } - public Task GetDirectMessageHistoryAsync(DirectMessageConversation conversationInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null) + public Task GetGroupHistoryAsync(Channel groupInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) { - return GetHistoryAsync(conversationInfo.id, latest, oldest, count); + return GetHistoryAsync(groupInfo.id, latest, oldest, count, unreads); } - public Task GetGroupHistoryAsync(Channel groupInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null) + public Task GetConversationsHistoryAsync(Channel conversationInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) { - return GetHistoryAsync(groupInfo.id, latest, oldest, count); + return GetHistoryAsync(conversationInfo.id, latest, oldest, count, unreads); } public Task MarkChannelAsync(string channelId, DateTime ts) @@ -209,7 +277,7 @@ public Task GetFileInfoAsync(string fileId, int? page = null, List> parameters = new List>(); parameters.Add(new Tuple("file", fileId)); - + if(count.HasValue) parameters.Add(new Tuple("count", count.Value.ToString())); @@ -311,13 +379,125 @@ public Task GroupsUnarchiveAsync(string channelId) #endregion - public Task SearchAllAsync(string query, SearchSort? sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) + #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>(); parameters.Add(new Tuple("query", query)); - if (sorting.HasValue) - parameters.Add(new Tuple("sort", sorting.Value.ToString())); + if (sorting != null) + parameters.Add(new Tuple("sort", sorting)); if (direction.HasValue) parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); @@ -334,13 +514,13 @@ public Task SearchAllAsync(string query, SearchSort? sorting return APIRequestWithTokenAsync(parameters.ToArray()); } - public Task SearchMessagesAsync(string query, SearchSort? sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) + public Task SearchMessagesAsync(string query, string sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) { List> parameters = new List>(); parameters.Add(new Tuple("query", query)); - if (sorting.HasValue) - parameters.Add(new Tuple("sort", sorting.Value.ToString())); + if (sorting != null) + parameters.Add(new Tuple("sort", sorting)); if (direction.HasValue) parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); @@ -357,13 +537,13 @@ public Task SearchMessagesAsync(string query, SearchSort return APIRequestWithTokenAsync(parameters.ToArray()); } - public Task SearchFilesAsync(string query, SearchSort? sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) + public Task SearchFilesAsync(string query, string sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) { List> parameters = new List>(); parameters.Add(new Tuple("query", query)); - if (sorting.HasValue) - parameters.Add(new Tuple("sort", sorting.Value.ToString())); + if (sorting != null) + parameters.Add(new Tuple("sort", sorting)); if (direction.HasValue) parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); @@ -382,7 +562,7 @@ public Task SearchFilesAsync(string query, SearchSort? sort public Task GetStarsAsync(string userId = null, int? count = null, int? page = null){ List> parameters = new List>(); - + if(!string.IsNullOrEmpty(userId)) parameters.Add(new Tuple("user", userId)); @@ -425,10 +605,50 @@ public Task EmitLoginAsync(string agent = "Inumedia.SlackAPI") { return APIRequestWithTokenAsync(new Tuple("agent", agent)); } + public Task UpdateAsync(string ts, + string channelId, + string text, + string botName = null, + string parse = null, + bool linkNames = false, + Attachment[] attachments = null, + bool? as_user = null, + IBlock[] blocks = null) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("ts", ts)); + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("text", text)); + + 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 (attachments != null && attachments.Length > 0) + parameters.Add(new Tuple("attachments", JsonConvert.SerializeObject(attachments))); + + 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() + { + NullValueHandling = NullValueHandling.Ignore + }))); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } public Task JoinDirectMessageChannelAsync(string user) { - var param = new Tuple("user", user); + var param = new Tuple("users", user); return APIRequestWithTokenAsync(param); } @@ -438,11 +658,13 @@ public Task PostMessageAsync( string botName = null, string parse = null, bool linkNames = false, + IBlock[] blocks = null, Attachment[] attachments = null, - bool unfurl_links = false, + 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>(); @@ -458,11 +680,22 @@ public Task PostMessageAsync( if (linkNames) parameters.Add(new Tuple("link_names", "1")); - if (attachments != null && attachments.Length > 0) - parameters.Add(new Tuple("attachments", JsonConvert.SerializeObject(attachments))); + 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) - parameters.Add(new Tuple("unfurl_links", "1")); + 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)); @@ -470,17 +703,155 @@ public Task PostMessageAsync( if (!string.IsNullOrEmpty(icon_emoji)) parameters.Add(new Tuple("icon_emoji", icon_emoji)); - parameters.Add(new Tuple("as_user", as_user.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()); } + public Task PostEphemeralMessageAsync( + string channelId, + string text, + string targetuser, + string parse = null, + bool linkNames = false, + Attachment[] attachments = 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("user", targetuser)); + + if (!string.IsNullOrEmpty(parse)) + parameters.Add(new Tuple("parse", parse)); + + if (linkNames) + parameters.Add(new Tuple("link_names", "1")); + + 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 + }))); + + parameters.Add(new Tuple("as_user", as_user.ToString())); + + 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, + string timestamp = null) + { + List> parameters = new List>(); + + if (!string.IsNullOrEmpty(name)) + parameters.Add(new Tuple("name", name)); + + if (!string.IsNullOrEmpty(channel)) + parameters.Add(new Tuple("channel", channel)); + + if (!string.IsNullOrEmpty(timestamp)) + parameters.Add(new Tuple("timestamp", timestamp)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task DialogOpenAsync( + string triggerId, + Dialog dialog) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("trigger_id", triggerId)); + + parameters.Add(new Tuple("dialog", + JsonConvert.SerializeObject(dialog, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }))); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + public async Task UploadFileAsync(byte[] fileData, string fileName, string[] channelIds, string title = null, string initialComment = null, bool useAsync = false, string fileType = null) { 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)) @@ -500,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(); } } @@ -512,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; diff --git a/SlackAPI/WebSocketMessages/ManualPresenceChange.cs b/SlackAPI/WebSocketMessages/ManualPresenceChange.cs new file mode 100644 index 00000000..72b00771 --- /dev/null +++ b/SlackAPI/WebSocketMessages/ManualPresenceChange.cs @@ -0,0 +1,7 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("manual_presence_change")] + public class ManualPresenceChange : PresenceChange + { + } +} diff --git a/SlackAPI/WebSocketMessages/NewMessage.cs b/SlackAPI/WebSocketMessages/NewMessage.cs index ee7750c7..ac597887 100644 --- a/SlackAPI/WebSocketMessages/NewMessage.cs +++ b/SlackAPI/WebSocketMessages/NewMessage.cs @@ -16,6 +16,7 @@ public class NewMessage : SlackSocketMessage public string username; public string bot_id; public UserProfile icons; + public List blocks; public List attachments; public NewMessage() diff --git a/SlackAPI/WebSocketMessages/PresenceChange.cs b/SlackAPI/WebSocketMessages/PresenceChange.cs index cf8578af..1cbb2e1c 100644 --- a/SlackAPI/WebSocketMessages/PresenceChange.cs +++ b/SlackAPI/WebSocketMessages/PresenceChange.cs @@ -1,6 +1,4 @@ -using System; - -namespace SlackAPI.WebSocketMessages +namespace SlackAPI.WebSocketMessages { [SlackSocketRouting("presence_change")] public class PresenceChange : SlackSocketMessage diff --git a/SlackAPI/WebSocketMessages/PresenceChangeSubscription.cs b/SlackAPI/WebSocketMessages/PresenceChangeSubscription.cs new file mode 100644 index 00000000..7f533e11 --- /dev/null +++ b/SlackAPI/WebSocketMessages/PresenceChangeSubscription.cs @@ -0,0 +1,15 @@ +using System.Linq; + +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("presence_sub")] + public class PresenceChangeSubscription : SlackSocketMessage + { + public PresenceChangeSubscription(string[] usersIds) + { + this.ids = usersIds; + } + + public string[] ids { get; } + } +} diff --git a/build.cake b/build.cake index 3d87c1a3..2859cd18 100644 --- a/build.cake +++ b/build.cake @@ -1,5 +1,6 @@ -#tool "nuget:?package=GitVersion.CommandLine" -#addin "Cake.FileHelpers" +#tool "nuget:?package=GitVersion.CommandLine&version=5.1.3" +#addin "Cake.FileHelpers&version=3.2.1" +#addin "Cake.Incubator&version=5.1.0" using System.Text.RegularExpressions; @@ -11,7 +12,7 @@ var testProject = File("./SlackAPI.Tests/SlackApi.Tests.csproj"); var testConfig = File("./SlackAPI.Tests/Configuration/config.json"); var projects = new[] { project, testProject }; var artifactsDirectory = "./artifacts"; -var versionSuffix = string.Empty; +GitVersion gitVersion = null; var isReleaseBuild = false; Task("Clean") @@ -24,67 +25,26 @@ Task("Clean") Task("Configure") .Does(() => { - var buildNumber = 0; - if (AppVeyor.IsRunningOnAppVeyor) - { - isReleaseBuild = AppVeyor.Environment.Repository.Branch == "master" && AppVeyor.Environment.Repository.Tag.IsTag; - buildNumber = AppVeyor.Environment.Build.Number; - Information("Build number is '{0}' (CI build)", buildNumber); - } - else - { - buildNumber = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; - Information("Build number is '{0}' (local build)", buildNumber); - } - - // If the build is a tag on master, generate a clean version (1.0.0) - // following SemVer 1.0.0 rules. NuGet supports only SemVer 1.0.0 - // In other cases, generate a prerelease version (1.0.0-branch.123+sha.abcdefg) - // following SevVer 2.0.0 rules. MyGet supports SemVer 2.0.0 - if (isReleaseBuild) - { - versionSuffix = "\"\""; - } - else - { - var gitVersion = GitVersion(); - var gitBranch = (AppVeyor.IsRunningOnAppVeyor - ? AppVeyor.Environment.Repository.Branch - : gitVersion.BranchName); - gitBranch = Regex.Replace(gitBranch, @"[/\-_]", string.Empty); - gitBranch = gitBranch.Substring(0, Math.Min(10, gitBranch.Length)); - - Information("Current git branch is '{0}' (normalized)", gitBranch); - - var gitCommitId = (AppVeyor.IsRunningOnAppVeyor - ? AppVeyor.Environment.Repository.Commit.Id - : gitVersion.Sha) - .Substring(0, 8); - - Information("Current git sha is '{0}' (normalized)", gitCommitId); - - var isPullRequest = AppVeyor.IsRunningOnAppVeyor && AppVeyor.Environment.PullRequest.IsPullRequest; - if (isPullRequest) - { - gitBranch = "PR"; - } + gitVersion = GitVersion(); - Information("Is Pull Request: '{0}'", isPullRequest); + GitVersion(new GitVersionSettings { + UpdateAssemblyInfo = true, + UpdateAssemblyInfoFilePath = "GlobalAssemblyInfo.cs" + }); + isReleaseBuild = AppVeyor.IsRunningOnAppVeyor + ? AppVeyor.Environment.Repository.Branch == "master" + : false; - versionSuffix = $"{gitBranch}.{buildNumber}+sha.{gitCommitId}"; - } + Information("Is release build: '{0}'", isReleaseBuild); + Information("GitVersion details:\n{0}", gitVersion.Dump()); - var versionPrefix = XmlPeek("./Directory.Build.props", "/Project/PropertyGroup/VersionPrefix"); - var version = isReleaseBuild ? $"{versionPrefix}-release.{buildNumber}" : string.Join("-", versionPrefix, versionSuffix); if (AppVeyor.IsRunningOnAppVeyor) { - // Update AppVeyor build version so it will match the build version in assemblies and package - AppVeyor.UpdateBuildVersion(version); + var buildVersion = gitVersion.SemVer + ".ci." + AppVeyor.Environment.Build.Number; + Information("Using build version: {0}", buildVersion); + AppVeyor.UpdateBuildVersion(buildVersion); } - - Information("Using version '{0}'", version); - Information("Release type build (skip symbols): {0}", isReleaseBuild); }); @@ -98,8 +58,7 @@ Task("Build") project, new DotNetCoreBuildSettings { - Configuration = configuration, - VersionSuffix = versionSuffix + Configuration = configuration } ); } @@ -186,9 +145,9 @@ Task("Package") { Configuration = configuration, OutputDirectory = artifactsDirectory, - VersionSuffix = versionSuffix, IncludeSymbols = !isReleaseBuild, - IncludeSource = !isReleaseBuild + IncludeSource = !isReleaseBuild, + ArgumentCustomization = args => args.Append("/p:Version=\"" + gitVersion.NuGetVersion + "\"") } ); diff --git a/tools/packages.config b/tools/packages.config new file mode 100644 index 00000000..cedcc6ab --- /dev/null +++ b/tools/packages.config @@ -0,0 +1,4 @@ + + + +