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