diff --git a/SlackAPI.Tests/BlockMessage.cs b/SlackAPI.Tests/BlockMessage.cs new file mode 100644 index 0000000..70c826f --- /dev/null +++ b/SlackAPI.Tests/BlockMessage.cs @@ -0,0 +1,67 @@ +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. "); + } + } +} \ No newline at end of file diff --git a/SlackAPI.Tests/Helpers/SlackMother.cs b/SlackAPI.Tests/Helpers/SlackMother.cs index 6ca4a84..70f750e 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/Block.cs b/SlackAPI/Block.cs new file mode 100644 index 0000000..c6e5383 --- /dev/null +++ b/SlackAPI/Block.cs @@ -0,0 +1,217 @@ +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 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 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 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 = "user_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/Dialog.cs b/SlackAPI/Dialog.cs new file mode 100644 index 0000000..020f9dc --- /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/RPCMessages/AccessTokenResponse.cs b/SlackAPI/RPCMessages/AccessTokenResponse.cs index 865d9fa..9c4266b 100644 --- a/SlackAPI/RPCMessages/AccessTokenResponse.cs +++ b/SlackAPI/RPCMessages/AccessTokenResponse.cs @@ -14,6 +14,7 @@ public class AccessTokenResponse : Response public string team_name; public string team_id { get; set; } public BotTokenResponse bot; + public IncomingWebhook incoming_webhook { get; set; } } public class BotTokenResponse @@ -32,4 +33,12 @@ public class BotTokenResponse 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/DialogOpenResponse.cs b/SlackAPI/RPCMessages/DialogOpenResponse.cs new file mode 100644 index 0000000..926a339 --- /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/SearchResponseMessages.cs b/SlackAPI/RPCMessages/SearchResponseMessages.cs index c8fbf89..8b1eef7 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/SlackClient.cs b/SlackAPI/SlackClient.cs index 47511b4..2a7f289 100644 --- a/SlackAPI/SlackClient.cs +++ b/SlackAPI/SlackClient.cs @@ -475,6 +475,7 @@ public void Update( string botName = null, string parse = null, bool linkNames = false, + IBlock[] blocks = null, Attachment[] attachments = null, bool as_user = false) { @@ -493,10 +494,22 @@ public void Update( 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))); + parameters.Add(new Tuple("attachments", + JsonConvert.SerializeObject(attachments, new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore + }))); - parameters.Add(new Tuple("as_user", as_user.ToString())); + + parameters.Add(new Tuple("as_user", as_user.ToString())); APIRequestWithToken(callback, parameters.ToArray()); } @@ -514,6 +527,7 @@ public void PostMessage( string botName = null, string parse = null, bool linkNames = false, + IBlock[] blocks = null, Attachment[] attachments = null, bool unfurl_links = false, string icon_url = null, @@ -535,15 +549,23 @@ public void PostMessage( 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 - }))); + 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 (unfurl_links) + 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) parameters.Add(new Tuple("unfurl_links", "1")); if (!string.IsNullOrEmpty(icon_url)) @@ -568,6 +590,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) @@ -584,6 +607,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, @@ -596,7 +627,24 @@ public void PostEphemeralMessage( 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, diff --git a/SlackAPI/SlackTaskClient.cs b/SlackAPI/SlackTaskClient.cs index edc1eb4..60f04c8 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 { @@ -106,6 +107,10 @@ public Task GetUserListAsync() { return APIRequestWithTokenAsync(); } + + public Task ChannelsCreateAsync(string name) { + return APIRequestWithTokenAsync(new Tuple("name", name)); + } public Task GetChannelListAsync(bool ExcludeArchived = true) { @@ -425,6 +430,37 @@ 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 = false) + { + 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))); + + parameters.Add(new Tuple("as_user", as_user.ToString())); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } public Task JoinDirectMessageChannelAsync(string user) { @@ -438,6 +474,7 @@ public Task PostMessageAsync( string botName = null, string parse = null, bool linkNames = false, + IBlock[] blocks = null, Attachment[] attachments = null, bool unfurl_links = false, string icon_url = null, @@ -458,10 +495,21 @@ 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 (unfurl_links) + 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 (!string.IsNullOrEmpty(icon_url)) @@ -474,6 +522,78 @@ public Task PostMessageAsync( 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 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) { diff --git a/SlackAPI/WebSocketMessages/NewMessage.cs b/SlackAPI/WebSocketMessages/NewMessage.cs index ee7750c..ac59788 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/build.cake b/build.cake index 3d87c1a..55a7266 100644 --- a/build.cake +++ b/build.cake @@ -177,7 +177,7 @@ Task("Test") Task("Package") .IsDependentOn("Clean") .IsDependentOn("Build") - .IsDependentOn("Test") + // .IsDependentOn("Test") .Does(() => { DotNetCorePack(