diff --git a/.gitignore b/.gitignore index b9d6bd92..451fd09a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ ## Eclipse ################# +config.json + *.pydevproject .project .metadata @@ -213,3 +215,19 @@ pip-log.txt #Mr Developer .mr.developer.cfg +*.chk +*.ide +packages +*.private +.vs/config/applicationhost.config + +#.Net core +*.lock.json + +# Cake +tools/* +!tools/packages.config + + +.vs/ +artifacts/ \ No newline at end of file diff --git a/Attachment.cs b/Attachment.cs deleted file mode 100644 index 25a5626c..00000000 --- a/Attachment.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SlackAPI -{ - public class Attachment - { - public string Pretext; - public string text; - public string fallback; - public string color; - public Field[] fields; - ///I have absolutely no idea what goes on in here. - } - - public class Field{ - public string title; - public string value; - public string @short; - } -} diff --git a/Bot.cs b/Bot.cs deleted file mode 100644 index 87e00662..00000000 --- a/Bot.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SlackAPI -{ - public class Bot - { - 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; - } -} diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..71125298 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,19 @@ + + + A Slack wrapper for direct interaction with their APIs. + Inumedia - Copyright © 2018 + SlackAPI + en-US + Inumedia + Inumedia + https://github.com/Inumedia/SlackAPI + http://choosealicense.com/licenses/mit/ + true + git + https://github.com/Inumedia/SlackAPI + + false + false + false + + \ No newline at end of file diff --git a/Extensions.cs b/Extensions.cs deleted file mode 100644 index b0b25a3e..00000000 --- a/Extensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SlackAPI -{ - public static class Extensions - { - /// - /// Converts to a propert JavaScript timestamp interpretted by Slack. Also handles converting to UTC. - /// - /// - /// - public static string ToProperTimeStamp(this DateTime that, bool toUTC = true) - { - if (toUTC) - return that.ToUniversalTime().Subtract(new DateTime(1970, 1, 1)).TotalSeconds.ToString(); - else - return that.Subtract(new DateTime(1970, 1, 1)).TotalSeconds.ToString(); - } - } -} 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/JavascriptBotsToArray.cs b/JavascriptBotsToArray.cs deleted file mode 100644 index c8d0a38a..00000000 --- a/JavascriptBotsToArray.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SlackAPI -{ - public class JavascriptBotsToArray : Newtonsoft.Json.JsonConverter - { - public override bool CanConvert(Type objectType) - { - return true; - } - - public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) - { - List bots = new List(); - int d = reader.Depth; - - while (reader.Read() && reader.Depth > d) - { - Bot current = new Bot(); - int depth = reader.Depth; - - current.name = reader.Value.ToString(); - - reader.Read(); - while (reader.Read() && reader.Depth > depth) - { - if (reader.Value == null) break; - switch (reader.Value.ToString()) - { - case "image_48": - reader.Read(); - current.image_48 = reader.Value.ToString(); - break; - - case "image_64": - reader.Read(); - current.image_48 = reader.Value.ToString(); - break; - - case "emoji": - reader.Read(); - current.emoji = reader.Value.ToString(); - break; - } - } - - bots.Add(current); - } - - return bots.ToArray(); - } - - public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) - { - //Not sure if this is correct :D - throw new NotSupportedException("Too hackish for this shi."); - } - } -} diff --git a/JavascriptDateTimeConverter.cs b/JavascriptDateTimeConverter.cs deleted file mode 100644 index f68b108b..00000000 --- a/JavascriptDateTimeConverter.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SlackAPI -{ - class JavascriptDateTimeConverter : Newtonsoft.Json.JsonConverter - { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(DateTime); - } - - public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) - { - double value = double.Parse(reader.Value.ToString()); - return new DateTime(1970, 1, 1).Add(TimeSpan.FromSeconds(value)).ToLocalTime(); - } - - public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) - { - //Not sure if this is correct :D - writer.WriteValue(((DateTime)value).Subtract(new DateTime(1970, 1, 1)).TotalSeconds); - } - } -} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..ee687600 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Andrew Turner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE-2.0 b/LICENSE-2.0 deleted file mode 100644 index 0b42f8f2..00000000 --- a/LICENSE-2.0 +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/LoginResponse.cs b/LoginResponse.cs deleted file mode 100644 index 97acd515..00000000 --- a/LoginResponse.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SlackAPI -{ - [RequestPath("users.login")] - public class LoginResponse : Response - { - public Bot[] bots; - public Channel[] channels; - public Channel[] groups; - public DirectMessageConversation[] ims; - public Self self; - public int svn_rev; - public int min_svn_rev; - public Team team; - public string url; - public User[] users; - } - - public class Self - { - public DateTime created; - public string id; - public string manual_presence; - public string name; - public Preferences prefs; - } -} diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs deleted file mode 100644 index fd34e2f2..00000000 --- a/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("SlackAPI")] -[assembly: AssemblyDescription("A Slack wrapper for direct interaction with their APIs.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Inumedia")] -[assembly: AssemblyProduct("SlackAPI")] -[assembly: AssemblyCopyright("Copyright © 2014")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("80fe3ab4-f0d5-4fee-a6ae-524b523cebcc")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/README.md b/README.md new file mode 100644 index 00000000..ebae38ab --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +[![Build status](https://ci.appveyor.com/api/projects/status/5n9e7sruxpo0mw79/branch/master?svg=true)](https://ci.appveyor.com/project/Inumedia/slackapi/branch/master) +[![NuGet](https://img.shields.io/nuget/v/SlackAPI.svg)](https://www.nuget.org/packages/SlackAPI/) +[![MyGet Pre Release](https://img.shields.io/myget/slackapi/vpre/SlackAPI.svg)](https://www.myget.org/feed/slackapi/package/nuget/SlackAPI) + +# SlackAPI + +This is a third party implementation of Slack's API written in C#. This supports their WebAPI as well as their Real Time Messaging API. + +# Examples + +Some examples can be found on the Wiki: https://github.com/Inumedia/SlackAPI/wiki/Examples + +# Issues and Bugs + +Please log an issue if you find any bugs or think something isn't correct. + +# Getting in touch + +I have a Slack setup for personal projects with a few friends, this includes this Github and a few others as public channels. If you want access, shoot me a quick email inumedia@inumedia.net. + +# Committer access + +Want committer access? Feel like I'm too lazy to keep up with Slack's ever changing API? Want a bug fixed but don't want to log an issue for it? + +Create some pull requests, give me a reason to give you access. + +# How to build the solution +###### (aka where is the config.json file?) +The project **SlackAPI.Tests** requires a valid `config.json` file for tests. You have two options to build the solution: +- Unload SlackAPI.Tests project and you're able to build SlackAPI solution. +- Create your own config.json file to be able to run tests and validate your changes. + - Copy/paste `config.default.json` to `config.json` + - Update `config.json` file with your settings + - *userAuthToken* : Visit https://api.slack.com/docs/oauth-test-tokens to generate a token for your user + - *botAuthToken* : Visit https://my.slack.com/services/new/bot to create a bot for your Slack team and retrieve associated token + - *testChannel* : A channel ID (user associated to *userAuthToken* must be member of the channel) + - *directMessageUser* : A Slack member username + - *clientId*/*clientSecret*/*authCode* : Not used + +# NuGet package +SlackAPI NuGet package is build with following platforms support: +- .NET Framework 4.5 +- .NET Standard 1.3 (UWP support). + - The version cannot detect SlackSocketRouting attributes in loaded assemblies (used to extend SlackAPI to handle custom messages). +- .NET Standard 1.6 +- .NET Standard 2.0 + +[(.NET implementation compatibility table)](https://docs.microsoft.com/en-us/dotnet/standard/net-standard#net-implementation-support) diff --git a/Response.cs b/Response.cs deleted file mode 100644 index 3627fa70..00000000 --- a/Response.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SlackAPI -{ - public abstract class Response - { - /// - /// Should always be checked before trying to process a response. - /// - public bool ok; - - /// - /// Purely speculative. Might not be bools, and might not always be included when a request fails. - /// - public bool invalid_auth; - /// - /// Purely speculative. Might not be bools, and might not always be included when a request fails. - /// - public bool account_inactive; - } -} 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/IntegrationCollection.cs b/SlackAPI.Tests/Configuration/IntegrationCollection.cs new file mode 100644 index 00000000..2b8c8e0c --- /dev/null +++ b/SlackAPI.Tests/Configuration/IntegrationCollection.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace SlackAPI.Tests.Configuration +{ + [CollectionDefinition("Integration tests")] + public class IntegrationCollection : ICollectionFixture + { + } +} diff --git a/SlackAPI.Tests/Configuration/IntegrationFixture.cs b/SlackAPI.Tests/Configuration/IntegrationFixture.cs new file mode 100755 index 00000000..cc542af7 --- /dev/null +++ b/SlackAPI.Tests/Configuration/IntegrationFixture.cs @@ -0,0 +1,148 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using Newtonsoft.Json; +using Polly; +using SlackAPI.Tests.Helpers; +using SlackAPI.WebSocketMessages; +using Xunit; + +namespace SlackAPI.Tests.Configuration +{ + public class IntegrationFixture : IDisposable + { + 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.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 + { + Assert.True(userClient.Value.IsReady); + return userClient.Value; + } + } + + public SlackSocketClient BotClient + { + get + { + Assert.True(botClient.Value.IsReady); + return botClient.Value; + } + } + + public SlackSocketClient CreateUserClient(IWebProxy proxySettings = null, bool maintainPresenceChangesStatus = false, Action presenceChanged = null) + { + return this.connectRetryPolicy.Execute(() => this.CreateClient(this.Config.UserAuthToken, proxySettings, maintainPresenceChangesStatus, presenceChanged)); + } + + public SlackSocketClient CreateBotClient(IWebProxy proxySettings = null) + { + return this.connectRetryPolicy.Execute(() => this.CreateClient(this.Config.BotAuthToken, proxySettings)); + } + + public SlackTaskClient UserClientAsync => userClientAsync.Value; + + public SlackTaskClient BotClientAsync => botClientAsync.Value; + + public void Dispose() + { + if (this.userClient.IsValueCreated) + { + this.UserClient.CloseSocket(); + } + + if (this.botClient.IsValueCreated) + { + this.BotClient.CloseSocket(); + } + } + + private SlackConfig GetConfig() + { + var currentAssembly = this.GetType().GetTypeInfo().Assembly.Location; + var assemblyDirectory = Path.GetDirectoryName(currentAssembly); + string fileName = Path.Combine(assemblyDirectory, @"configuration\config.json"); + string json = System.IO.File.ReadAllText(fileName); + + var jsonObject = new {slack = (SlackConfig)null }; + return JsonConvert.DeserializeAnonymousType(json, jsonObject).slack; + } + + private SlackSocketClient CreateClient(string authToken, IWebProxy proxySettings = null, bool maintainPresenceChanges = false, Action presenceChanged = null) + { + SlackSocketClient client; + + LoginResponse loginResponse = null; + using (var syncClient = new InSync($"{nameof(SlackClient.Connect)} - Connected callback")) + using (var syncClientSocket = new InSync($"{nameof(SlackClient.Connect)} - SocketConnected callback")) + using (var syncClientSocketHello = new InSync($"{nameof(SlackClient.Connect)} - SocketConnected hello callback")) + { + client = new SlackSocketClient(authToken, proxySettings, maintainPresenceChanges); + + void OnPresenceChanged(PresenceChange x) + { + presenceChanged?.Invoke(client, x); + } + + client.OnPresenceChanged += OnPresenceChanged; + client.OnHello += () => syncClientSocketHello.Proceed(); + client.Connect(x => + { + loginResponse = x; + + Console.WriteLine($"Connected {x.ok}"); + syncClient.Proceed(); + if (!x.ok) + { + // If connect fails, socket connect callback is not called + syncClientSocket.Proceed(); + syncClientSocketHello.Proceed(); + } + }, () => + { + Console.WriteLine("Socket Connected"); + syncClientSocket.Proceed(); + }); + } + + loginResponse.AssertOk(); + + 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/Configuration/SlackConfig.cs b/SlackAPI.Tests/Configuration/SlackConfig.cs new file mode 100644 index 00000000..6f45de62 --- /dev/null +++ b/SlackAPI.Tests/Configuration/SlackConfig.cs @@ -0,0 +1,17 @@ +namespace SlackAPI.Tests.Configuration +{ + public class SlackConfig + { + public string UserAuthToken { get; set; } + public string BotAuthToken { get; set; } + public string TestChannel { get; set; } + public string DirectMessageUser { get; set; } + + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string RedirectUrl { get; set; } + public string AuthUsername { get; set; } + public string AuthPassword { get; set; } + public string AuthWorkspace { get; set; } + } +} \ No newline at end of file diff --git a/SlackAPI.Tests/Configuration/config.default.json b/SlackAPI.Tests/Configuration/config.default.json new file mode 100644 index 00000000..01d4e493 --- /dev/null +++ b/SlackAPI.Tests/Configuration/config.default.json @@ -0,0 +1,16 @@ +{ + "slack": { + "userAuthToken": "token-tokentoken-tokentoken-tokentoken-token", + "botAuthToken": "bottoken-bottoken-bottoken-bottoken", + "testChannel": "SuperSecretChannel", + "directMessageUser": "someUserId", + + // Application GetAccessToken + "clientId": "your-special-id", + "clientSecret": "such-special-secret-key", + "redirectUrl": "http://redirecturl", + "authUsername": "user@domain.com", + "authPassword": "userpassword", + "authWorkspace": "workspace" + } +} diff --git a/SlackAPI.Tests/Connect.cs b/SlackAPI.Tests/Connect.cs new file mode 100644 index 00000000..3aa193f8 --- /dev/null +++ b/SlackAPI.Tests/Connect.cs @@ -0,0 +1,167 @@ +using System; +using System.Linq; +using System.Net; +using System.Threading; +using SlackAPI.Tests.Configuration; +using SlackAPI.Tests.Helpers; +using SlackAPI.WebSocketMessages; +using Xunit; + +namespace SlackAPI.Tests +{ + [Collection("Integration tests")] + 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() + { + slackClient = this.fixture.CreateUserClient(); + Assert.True(slackClient.IsConnected, "Invalid, doesn't think it's connected."); + } + + [Fact] + public void TestConnectAsBot() + { + slackClient = this.fixture.CreateBotClient(); + Assert.True(slackClient.IsConnected, "Invalid, doesn't think it's connected."); + } + + [Fact] + public void TestConnectWithWrongProxySettings() + { + var proxySettings = new WebProxy { Address = new Uri("http://127.0.0.1:8080")}; + Assert.Throws(() => this.fixture.CreateUserClient(proxySettings)); + Assert.Throws(() => this.fixture.CreateBotClient(proxySettings)); + } + + [Fact] + public void TestConnectPostAndDelete() + { + // given + slackClient = this.fixture.CreateUserClient(); + string channel = this.fixture.Config.TestChannel; + + // when + DateTime messageTimestamp = PostMessage(slackClient, channel); + DeletedResponse deletedResponse = DeleteMessage(slackClient, channel, messageTimestamp); + + // then + Assert.NotNull(deletedResponse); + Assert.True(deletedResponse.ok); + Assert.Equal(channel, deletedResponse.channel); + 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; + + using (var sync = new InSync(nameof(SlackSocketClient.SendMessage))) + { + client.SendMessage(response => + { + sendMessageResponse = response; + sync.Proceed(); + }, channel, TestText); + } + + Assert.NotNull(sendMessageResponse); + Assert.Equal(TestText, sendMessageResponse.text); + + return sendMessageResponse.ts; + } + + private static DeletedResponse DeleteMessage(SlackSocketClient client, string channel, DateTime messageTimestamp) + { + DeletedResponse deletedResponse = null; + + using (var sync = new InSync(nameof(SlackClient.DeleteMessage))) + { + client.DeleteMessage(response => + { + deletedResponse = response; + sync.Proceed(); + }, channel, messageTimestamp); + } + + return deletedResponse; + } + } +} 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 new file mode 100644 index 00000000..83a77ec8 --- /dev/null +++ b/SlackAPI.Tests/Helpers/InSync.cs @@ -0,0 +1,34 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Runtime.CompilerServices; +using Xunit; + +namespace SlackAPI.Tests.Helpers +{ + public class InSync : IDisposable + { + 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, TimeSpan? waitTimeout = null) + { + this.message = message; + this.waitTimeout = waitTimeout.GetValueOrDefault(DefaultWaitTimeout); + this.waiter = new ManualResetEventSlim(); + } + + public void Proceed() + { + this.waiter.Set(); + } + + public void Dispose() + { + 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 new file mode 100644 index 00000000..70f750ef --- /dev/null +++ b/SlackAPI.Tests/Helpers/SlackMother.cs @@ -0,0 +1,253 @@ +namespace SlackAPI.Tests.Helpers +{ + 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() + { + fallback = "Required plain-text summary of the attachment.", + color = "#36a64f", + pretext = "Optional text that appears above the attachment block", + author_name = "Bobby Tables", + author_link = "http://flickr.com/bobby/", + author_icon = "http://flickr.com/icons/bobby.jpg", + title = "Slack API Documentation", + title_link = "https://api.slack.com/", + text = "Optional text that appears within the attachment", + fields = new[] + { + new Field() { title = "Priority", value = "High", @short = false }, + new Field() { title = "Priority", value = "High", @short = true }, + new Field() { title = "Priority", value = "High", @short = true } + } + } + }; + + public static Attachment[] SomeAttachmentsWithActions => new[] + { + new Attachment() + { + fallback = "Required plain-text summary of the attachment.", + color = "#36a64f", + pretext = "Optional text that appears above the attachment block", + author_name = "Bobby Tables", + author_link = "http://flickr.com/bobby/", + author_icon = "http://flickr.com/icons/bobby.jpg", + title = "Slack API Documentation", + title_link = "https://api.slack.com/", + text = "Optional text that appears within the attachment", + fields = new[] + { + new Field() { title = "Priority", value = "High", @short = false }, + new Field() { title = "Priority", value = "High", @short = true }, + new Field() { title = "Priority", value = "High", @short = true } + }, + actions = new [] + { + new AttachmentAction("Button 1", "Button 1 Text") , + new AttachmentAction("Button 2", "Button 2 Text") {style = "primary"}, + new AttachmentAction("Button 3", "Button 3 Text") {style = "danger"}, + new AttachmentAction("Button 4", "Button 4 Text") {style = "danger", confirm = new ActionConfirm {text = "Are you sure?????"} }, + new AttachmentAction("Button 5", "Button 5 Text") {style = "danger", confirm = new ActionConfirm {text = "Do you really want to do this", dismiss_text = "No I don't", ok_text = "Sure I do", title = "Just checking"} } + } + } + }; + } +} \ No newline at end of file diff --git a/SlackAPI.Tests/JoinDirectMessageChannel.cs b/SlackAPI.Tests/JoinDirectMessageChannel.cs new file mode 100644 index 00000000..237ede30 --- /dev/null +++ b/SlackAPI.Tests/JoinDirectMessageChannel.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using SlackAPI.Tests.Configuration; +using SlackAPI.Tests.Helpers; +using Xunit; + +namespace SlackAPI.Tests +{ + [Collection("Integration tests")] + public class JoinDirectMessageChannel + { + private readonly IntegrationFixture fixture; + + public JoinDirectMessageChannel(IntegrationFixture fixture) + { + this.fixture = fixture; + } + + [Fact] + public void ShouldJoinDirectMessageChannel() + { + // given + var client = this.fixture.UserClient; + JoinDirectMessageChannelResponse actual = null; + + string userName = this.fixture.Config.DirectMessageUser; + string user = client.Users.First(x => x.name.Equals(userName, StringComparison.OrdinalIgnoreCase)).id; + + // when + using (var sync = new InSync(nameof(SlackClient.JoinDirectMessageChannel))) + { + client.JoinDirectMessageChannel(response => + { + actual = response; + sync.Proceed();; + }, user); + } + + // then + Assert.True(actual.ok, "Error while joining user channel"); + Assert.NotEmpty(actual.channel.id); + } + } +} \ No newline at end of file diff --git a/SlackAPI.Tests/PostMessage.cs b/SlackAPI.Tests/PostMessage.cs new file mode 100644 index 00000000..9766660a --- /dev/null +++ b/SlackAPI.Tests/PostMessage.cs @@ -0,0 +1,125 @@ +using System; +using SlackAPI.RPCMessages; +using SlackAPI.Tests.Configuration; +using SlackAPI.Tests.Helpers; +using System.Linq; +using Xunit; + +namespace SlackAPI.Tests +{ + [Collection("Integration tests")] + public class PostMessage + { + private readonly IntegrationFixture fixture; + + public PostMessage(IntegrationFixture fixture) + { + this.fixture = fixture; + } + + [Fact] + public void SimpleMessageDelivery() + { + // 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, + "Hi there!"); + } + + // then + Assert.True(actual.ok, "Error while posting message to channel. "); + Assert.Equal("Hi there!", actual.message.text); + Assert.Equal("message", actual.message.type); + } + + [Fact] + public void Attachments() + { + // 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, + attachments: SlackMother.SomeAttachments); + } + + // then + Assert.True(actual.ok, "Error while posting message to channel. "); + } + + [Fact] + public void AttachmentsWithActions() + { + // 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, + attachments: SlackMother.SomeAttachmentsWithActions); + } + + // then + Assert.True(actual.ok, "Error while posting message to channel. "); + } + + [Fact] + public void PostEphemeralMessage() + { + // given + var client = this.fixture.UserClient; + PostEphemeralResponse actual = null; + + string userName = this.fixture.Config.DirectMessageUser; + string userId = client.Users.First(x => x.name.Equals(userName, StringComparison.OrdinalIgnoreCase)).id; + + // when + using (var sync = new InSync(nameof(SlackClient.PostEphemeralMessage))) + { + client.PostEphemeralMessage( + response => + { + actual = response; + sync.Proceed(); + }, + this.fixture.Config.TestChannel, + "Hi there!", + userId); + } + + // then + Assert.True(actual.ok, "Error while posting message to channel. "); + Assert.Null(actual.error); + } + } +} \ No newline at end of file diff --git a/SlackAPI.Tests/SlackAPI.Tests.csproj b/SlackAPI.Tests/SlackAPI.Tests.csproj new file mode 100644 index 00000000..bc4dc3b4 --- /dev/null +++ b/SlackAPI.Tests/SlackAPI.Tests.csproj @@ -0,0 +1,43 @@ + + + + net452;netcoreapp2.1 + Library + Full + SlackAPI.Tests + SlackAPI.Tests + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + PreserveNewest + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + $(DefineConstants);RELEASE + + + diff --git a/SlackAPI.Tests/Timestamp.cs b/SlackAPI.Tests/Timestamp.cs new file mode 100644 index 00000000..8f9e2ef0 --- /dev/null +++ b/SlackAPI.Tests/Timestamp.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; +using System; +using System.IO; +using Xunit; + +namespace SlackAPI.Tests +{ + [Collection("Unit tests")] + public class Timestamp + { + [Theory] + [InlineData("12345.000000")] + [InlineData("12345.900000")] + [InlineData("12345.123456")] + [InlineData("12345.123450")] + [InlineData("12345.12345")] + [InlineData("12345")] + public void TestTimestampConversion(string originalTimestamp) + { + // Arrange + JavascriptDateTimeConverter converter = new JavascriptDateTimeConverter(); + var jsonReader = new JsonTextReader(new StringReader($"\"{originalTimestamp}\"")); + jsonReader.Read(); + + // Act + DateTime timestampDateTime = (DateTime)converter.ReadJson(jsonReader, null, null, null); + var newTimestamp = timestampDateTime.ToProperTimeStamp(); + + // Assert + Assert.Equal(double.Parse(originalTimestamp), double.Parse(newTimestamp)); + Assert.Equal(6, newTimestamp.Substring(newTimestamp.IndexOf(".") + 1).Length); + } + } +} diff --git a/SlackAPI.Tests/Update.cs b/SlackAPI.Tests/Update.cs new file mode 100644 index 00000000..a5ed7151 --- /dev/null +++ b/SlackAPI.Tests/Update.cs @@ -0,0 +1,80 @@ +using SlackAPI.Tests.Configuration; +using SlackAPI.Tests.Helpers; +using Xunit; + +namespace SlackAPI.Tests +{ + [Collection("Integration tests")] + public class Update + { + private readonly IntegrationFixture fixture; + + public Update(IntegrationFixture fixture) + { + this.fixture = fixture; + } + + [Fact] + public void SimpleUpdate() + { + // given + var client = this.fixture.UserClient; + var messageId = PostedMessage(client); + UpdateResponse actual = null; + + // when + using (var sync = new InSync(nameof(SlackClient.Update))) + { + client.Update( + response => + { + actual = response; + sync.Proceed(); + }, + messageId, + this.fixture.Config.TestChannel, + "[changed]", + attachments: SlackMother.SomeAttachments, + as_user: true); + } + + // then + Assert.True(actual.ok, "Error while posting message to channel. "); + Assert.Equal("[changed]", actual.message.text); + Assert.Equal("message", actual.message.type); + } + + private string PostedMessage(SlackSocketClient client) + { + string messageId = null; + using (var sync = new InSync(nameof(SlackClient.PostMessage))) + { + client.PostMessage( + response => + { + messageId = response.ts; + Assert.True(response.ok, "Error while posting message to channel. "); + sync.Proceed(); + }, + this.fixture.Config.TestChannel, + "Hi there!", + as_user: true); + } + return messageId; + } + + [Fact] + public void UpdatePresence() + { + var client = this.fixture.UserClient; + using (var sync = new InSync(nameof(SlackClient.EmitPresence))) + { + client.EmitPresence((presence) => + { + presence.AssertOk(); + sync.Proceed(); + }, Presence.away); + } + } + } +} \ No newline at end of file diff --git a/SlackAPI.Tests/UploadFile.cs b/SlackAPI.Tests/UploadFile.cs new file mode 100644 index 00000000..28a5b943 --- /dev/null +++ b/SlackAPI.Tests/UploadFile.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using SlackAPI.Tests.Configuration; +using SlackAPI.Tests.Helpers; +using Xunit; + +namespace SlackAPI.Tests +{ + [Collection("Integration tests")] + public class UploadFile + { + private readonly IntegrationFixture fixture; + + public UploadFile(IntegrationFixture fixture) + { + this.fixture = fixture; + } + + [Fact] + public void UploadFile_Succeeds() + { + // Arrange + const int FileSize = 500; + const string FileName = "MyFile.bin"; + byte[] data = new byte[FileSize]; + + // Act + FileUploadResponse fileUploadResponse = null; + using (var sync = new InSync(nameof(SlackClient.UploadFile))) + { + this.fixture.UserClient.UploadFile(c => + { + fileUploadResponse = c; + Assert.True(c.ok); + sync.Proceed(); + }, + data, FileName, new[] {this.fixture.Config.TestChannel}); + } + + // Assert + using (var sync = new InSync(nameof(SlackClient.UploadFile))) + { + this.fixture.UserClient.GetFileInfo(c => + { + Assert.True(c.ok); + Assert.Equal(FileSize, c.file.size); + Assert.Equal(FileName, c.file.name); + Assert.Equal(new[] { this.fixture.Config.TestChannel}, c.file.channels); + sync.Proceed(); + }, fileUploadResponse.file.id); + } + } + + [Fact] + public async Task UploadFileAsync_Succeeds() + { + // Arrange + const int FileSize = 500; + const string FileName = "MyFile.bin"; + byte[] data = new byte[FileSize]; + + // Act + var fileUploadResponse = await this.fixture.UserClientAsync.UploadFileAsync(data, FileName, new[] { this.fixture.Config.TestChannel }); + + // Assert + Assert.True(fileUploadResponse.ok); + var fileInfoResponse = await this.fixture.UserClientAsync.GetFileInfoAsync(fileUploadResponse.file.id); + Assert.True(fileInfoResponse.ok); + Assert.Equal(FileSize, fileInfoResponse.file.size); + Assert.Equal(FileName, fileInfoResponse.file.name); + Assert.Equal(new[] { this.fixture.Config.TestChannel}, fileInfoResponse.file.channels); + } + } +} diff --git a/SlackAPI.Tests/UserUIInteraction.cs b/SlackAPI.Tests/UserUIInteraction.cs new file mode 100644 index 00000000..0ece63cf --- /dev/null +++ b/SlackAPI.Tests/UserUIInteraction.cs @@ -0,0 +1,77 @@ +using System.IO; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading; +using OpenQA.Selenium; +using OpenQA.Selenium.Chrome; +using SlackAPI.Tests.Configuration; +using SlackAPI.Tests.Helpers; +using Xunit; + +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 + { + private readonly IntegrationFixture fixture; + + public UserUIInteraction(IntegrationFixture fixture) + { + this.fixture = fixture; + } + + [Fact] + public void TestGetAccessToken() + { + var clientId = this.fixture.Config.ClientId; + var clientSecret = this.fixture.Config.ClientSecret; + var redirectUrl = this.fixture.Config.RedirectUrl; + var authUsername = this.fixture.Config.AuthUsername; + var authPassword = this.fixture.Config.AuthPassword; + var authWorkspace = this.fixture.Config.AuthWorkspace; + + using (var driver = new ChromeDriver(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))) + { + driver.Navigate().GoToUrl($"https://{authWorkspace}.slack.com"); + + // Wait a bit to ensure we can properly fill username and password fields + Thread.Sleep(1000); + driver.FindElement(By.Id("email")).SendKeys(authUsername); + driver.FindElement(By.Id("password")).SendKeys(authPassword); + driver.FindElement(By.Id("signin_btn")).Click(); + + var slackClientHelpers = new SlackClientHelpers(); + var uri = slackClientHelpers.GetAuthorizeUri(clientId, SlackScope.Identify); + driver.Navigate().GoToUrl(uri); + 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.Contains("identify", accessTokenResponse.scope); + } + } + + private AccessTokenResponse GetAccessToken(SlackClientHelpers slackClientHelpers, string clientId, string clientSecret, string redirectUri, string authCode) + { + AccessTokenResponse accessTokenResponse = null; + + using (var sync = new InSync(nameof(slackClientHelpers.GetAccessToken))) + { + slackClientHelpers.GetAccessToken(response => + { + accessTokenResponse = response; + sync.Proceed(); + + }, clientId, clientSecret, redirectUri, authCode); + } + + return accessTokenResponse; + } + } +#endif +} diff --git a/SlackAPI.Tests/Users.cs b/SlackAPI.Tests/Users.cs new file mode 100644 index 00000000..3970f6aa --- /dev/null +++ b/SlackAPI.Tests/Users.cs @@ -0,0 +1,45 @@ +using System.Linq; +using SlackAPI.Tests.Configuration; +using SlackAPI.Tests.Helpers; +using Xunit; + +namespace SlackAPI.Tests +{ + [Collection("Integration tests")] + public class Users + { + private readonly IntegrationFixture fixture; + + public Users(IntegrationFixture fixture) + { + this.fixture = fixture; + } + [Fact] + public void UserList() + { + var client = this.fixture.UserClient; + UserListResponse actual = null; + using (var sync = new InSync(nameof(SlackClient.UserLookup))) + { + client.GetUserList(response => + { + actual = response; + sync.Proceed(); + }); + } + + Assert.True(actual.ok, "Error while fetching user list."); + Assert.True(actual.members.Any()); + + // apparently deleted users do indeed have null values, so if the first user returned is deleted then there are failures. + var someMember = actual.members.Where(x => !x.deleted).First(); + Assert.NotNull(someMember.id); + Assert.NotNull(someMember.color); + Assert.NotNull(someMember.real_name); + Assert.NotNull(someMember.name); + Assert.NotNull(someMember.team_id); + Assert.NotNull(someMember.tz); + Assert.NotNull(someMember.tz_label); + } + } +} \ No newline at end of file diff --git a/SlackAPI.Tests/app.config b/SlackAPI.Tests/app.config new file mode 100755 index 00000000..21f024a7 --- /dev/null +++ b/SlackAPI.Tests/app.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SlackAPI.csproj b/SlackAPI.csproj deleted file mode 100644 index c2d6854e..00000000 --- a/SlackAPI.csproj +++ /dev/null @@ -1,107 +0,0 @@ - - - - - Debug - AnyCPU - {0C0A58A8-174E-4A4C-907B-C3569144D15D} - Library - Properties - SlackAPI - SlackAPI - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - False - ..\packages\Newtonsoft.Json.6.0.3\lib\net45\Newtonsoft.Json.dll - - - - - - - - - - - ..\packages\WebSocket4Net.0.9\lib\net45\WebSocket4Net.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/SlackAPI.sln b/SlackAPI.sln index 2192a644..f3f74ac0 100644 --- a/SlackAPI.sln +++ b/SlackAPI.sln @@ -1,9 +1,20 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.21005.1 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30320.27 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlackAPI", "SlackAPI.csproj", "{0C0A58A8-174E-4A4C-907B-C3569144D15D}" +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 @@ -11,12 +22,19 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0C0A58A8-174E-4A4C-907B-C3569144D15D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0C0A58A8-174E-4A4C-907B-C3569144D15D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0C0A58A8-174E-4A4C-907B-C3569144D15D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0C0A58A8-174E-4A4C-907B-C3569144D15D}.Release|Any CPU.Build.0 = Release|Any CPU + {7EED3D9B-9B7A-49A4-AFBF-599153A47DDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EED3D9B-9B7A-49A4-AFBF-599153A47DDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EED3D9B-9B7A-49A4-AFBF-599153A47DDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EED3D9B-9B7A-49A4-AFBF-599153A47DDA}.Release|Any CPU.Build.0 = Release|Any CPU + {DEFA9559-0F8F-4C38-9644-67A080EDC46D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DEFA9559-0F8F-4C38-9644-67A080EDC46D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEFA9559-0F8F-4C38-9644-67A080EDC46D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEFA9559-0F8F-4C38-9644-67A080EDC46D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {367D3666-0DC5-4B8A-832F-57279BE4DCB0} + EndGlobalSection EndGlobal diff --git a/SlackAPI/Attachment.cs b/SlackAPI/Attachment.cs new file mode 100644 index 00000000..64b28e1d --- /dev/null +++ b/SlackAPI/Attachment.cs @@ -0,0 +1,59 @@ +namespace SlackAPI +{ + //See: https://api.slack.com/docs/attachments + public class Attachment + { + public string callback_id; + public string fallback; + public string color; + public string pretext; + public string author_name; + public string author_link; + public string author_icon; + public string title; + public string title_link; + public string text; + public Field[] fields; + public IBlock[] blocks; + + public string image_url; + public string thumb_url; + public string[] mrkdwn_in; + public AttachmentAction[] actions; + + public string footer; + public string footer_icon; + } + + public class Field{ + public string title; + public string value; + public bool @short; + } + + //See: https://api.slack.com/docs/message-buttons#action_fields + public class AttachmentAction + { + public AttachmentAction(string name, string text) + { + this.name = name; + this.text = text; + } + public string name { get; } + public string text { get; } + public string style; + public string type = "button"; + public string value; + public ActionConfirm confirm; + public string url; + } + + //see: https://api.slack.com/docs/message-buttons#confirmation_fields + public class ActionConfirm + { + public string title; + public string text; + public string ok_text; + public string dismiss_text; + } +} 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/Bot.cs b/SlackAPI/Bot.cs new file mode 100644 index 00000000..c11ae22d --- /dev/null +++ b/SlackAPI/Bot.cs @@ -0,0 +1,12 @@ +namespace SlackAPI +{ + public class Bot + { + public string id; + public bool deleted; + public string name; + public string updated; + public string app_id; + public ProfileIcons icons; + } +} diff --git a/Channel.cs b/SlackAPI/Channel.cs similarity index 71% rename from Channel.cs rename to SlackAPI/Channel.cs index bdb562ac..c45d1c23 100644 --- a/Channel.cs +++ b/SlackAPI/Channel.cs @@ -6,28 +6,25 @@ namespace SlackAPI { - public class Channel + public class Channel : Conversation { - public string id; - public string name; public string creator; - public DateTime created; - public DateTime last_read; + public string user; public bool is_archived; public bool is_member; public bool is_general; - public bool is_starred; + 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'; } } public int num_members; - public int unread_count; public OwnedStampedMessage topic; public OwnedStampedMessage purpose; - public Message latest; - public string[] members; } } diff --git a/ContextMessage.cs b/SlackAPI/ContextMessage.cs similarity index 65% rename from ContextMessage.cs rename to SlackAPI/ContextMessage.cs index 27e7fd52..764c17a3 100644 --- a/ContextMessage.cs +++ b/SlackAPI/ContextMessage.cs @@ -8,16 +8,16 @@ namespace SlackAPI { public class ContextMessage : Message { - public string type; + //public string type; /// /// Only contains partial channel data. /// - public Channel channel; - public string user; - public string username; - public DateTime ts; - public string text; - public string permalink; + //public Channel channel; + //public string user; + //public string username; + //public DateTime ts; + //public string text; + //public string permalink; public Message previous_2; public Message previous; public Message next; diff --git a/DirectMessageConversation.cs b/SlackAPI/Conversation.cs similarity index 77% rename from DirectMessageConversation.cs rename to SlackAPI/Conversation.cs index 57b0aad9..def8d1a2 100644 --- a/DirectMessageConversation.cs +++ b/SlackAPI/Conversation.cs @@ -6,16 +6,14 @@ namespace SlackAPI { - public class DirectMessageConversation + public class Conversation { public string id; - public string user; public DateTime created; - public bool is_user_deleted; + public DateTime last_read; public bool is_open; public bool is_starred; - public DateTime last_read; - public Message latest; public int unread_count; + public Message latest; } } 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/PresenseResponse.cs b/SlackAPI/DirectMessageConversation.cs similarity index 51% rename from PresenseResponse.cs rename to SlackAPI/DirectMessageConversation.cs index f8189cad..2241fe6b 100644 --- a/PresenseResponse.cs +++ b/SlackAPI/DirectMessageConversation.cs @@ -6,13 +6,9 @@ namespace SlackAPI { - [RequestPath("presense.set")] - public class PresenceResponse : Response + public class DirectMessageConversation : Conversation { - } - public enum Presence - { - Active, - Away + public string user; + public bool is_user_deleted; } } diff --git a/SlackAPI/Extensions.cs b/SlackAPI/Extensions.cs new file mode 100644 index 00000000..02d8d6e4 --- /dev/null +++ b/SlackAPI/Extensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json; + +namespace SlackAPI +{ + public static class Extensions + { + internal static readonly IList Converters = new List { new JavascriptDateTimeConverter() }; + + /// + /// Converts to a propert JavaScript timestamp interpretted by Slack. Also handles converting to UTC. + /// + /// + /// + public static string ToProperTimeStamp(this DateTime that, bool toUTC = true) + { + if (toUTC) + { + return ((that.ToUniversalTime().Ticks - 621355968000000000m) / 10000000m).ToString("F6", CultureInfo.InvariantCulture); + } + else + return that.Subtract(new DateTime(1970, 1, 1)).TotalSeconds.ToString(CultureInfo.InvariantCulture); + } + + public static K Deserialize(this string data) + where K : class + { + return JsonConvert.DeserializeObject(data, CreateSettings()); + } + + public static object Deserialize(this string data, Type type) + { + return JsonConvert.DeserializeObject(data, type, CreateSettings()); + } + + private static JsonSerializerSettings CreateSettings() + { + JsonSerializerSettings settings = new JsonSerializerSettings(); + settings.Converters = Converters; + + return settings; + } + } +} diff --git a/File.cs b/SlackAPI/File.cs similarity index 81% rename from File.cs rename to SlackAPI/File.cs index 62b4d622..3f1dd33b 100644 --- a/File.cs +++ b/SlackAPI/File.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SlackAPI { @@ -13,6 +9,7 @@ namespace SlackAPI public class File { public string id; + public DateTime created; public DateTime timestamp; public string name; @@ -26,9 +23,10 @@ public class File public bool editable; public bool is_external; public string external_type; - + public string username; + /// - /// Looks it's in bytes? + /// File size in bytes /// public int size; @@ -39,12 +37,17 @@ public class File public string thumb_64; public string thumb_80; + public string thumb_160; public string thumb_360; public string thumb_360_gif; public int thumb_360_w; public int thumb_360_h; + public string thumb_480; + public int thumb_480_w; + public int thumb_480_h; public string permalink; + public string permalink_public; public string edit_link; public string preview; public string preview_highlight; @@ -53,12 +56,17 @@ public class File public bool is_public; public bool public_url_shared; + public bool display_as_bot; public string[] channels; public string[] groups; public string[] ims; public FileComment initial_comment; + public int comments_count; public int num_stars; public bool is_starred; + public string[] pinned_to; + + public Reaction[] reactions; } [Flags] diff --git a/SlackAPI/JavascriptDateTimeConverter.cs b/SlackAPI/JavascriptDateTimeConverter.cs new file mode 100644 index 00000000..438df1a8 --- /dev/null +++ b/SlackAPI/JavascriptDateTimeConverter.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + internal class JavascriptDateTimeConverter : Newtonsoft.Json.JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(DateTime) || objectType == typeof(DateTime?); + } + + public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) + { + decimal value = decimal.Parse(reader.Value.ToString(), CultureInfo.InvariantCulture); + DateTime res = new DateTime(621355968000000000 + (long)(value * 10000000m)).ToLocalTime(); + System.Diagnostics.Debug.Assert( + Decimal.Equals( + Decimal.Parse(res.ToProperTimeStamp(), CultureInfo.InvariantCulture), + Decimal.Parse(reader.Value.ToString(), CultureInfo.InvariantCulture)), + "Precision loss :("); + return res; + } + + public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) + { + //Not sure if this is correct :D + writer.WriteValue(((DateTime)value).Subtract(new DateTime(1970, 1, 1)).TotalSeconds); + } + } +} diff --git a/Message.cs b/SlackAPI/Message.cs similarity index 72% rename from Message.cs rename to SlackAPI/Message.cs index 81c9ba27..6077ec6c 100644 --- a/Message.cs +++ b/SlackAPI/Message.cs @@ -6,9 +6,9 @@ namespace SlackAPI { - public class Message + public class Message : SlackSocketMessage { - public string type; + public string channel; public DateTime ts; public string user; /// @@ -16,8 +16,12 @@ public class Message /// 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/MimeTypes.cs b/SlackAPI/MimeTypes.cs similarity index 100% rename from MimeTypes.cs rename to SlackAPI/MimeTypes.cs diff --git a/OwnedStampedMessage.cs b/SlackAPI/OwnedStampedMessage.cs similarity index 100% rename from OwnedStampedMessage.cs rename to SlackAPI/OwnedStampedMessage.cs diff --git a/Preferences.cs b/SlackAPI/Preferences.cs similarity index 96% rename from Preferences.cs rename to SlackAPI/Preferences.cs index e1b91b94..f0931eec 100644 --- a/Preferences.cs +++ b/SlackAPI/Preferences.cs @@ -53,9 +53,9 @@ public class Preferences public bool mark_msgs_read_immediately; public string tz; public string emoji_mode; - public string hightlight_words; + 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/ProfileIcons.cs b/SlackAPI/ProfileIcons.cs new file mode 100644 index 00000000..db849a48 --- /dev/null +++ b/SlackAPI/ProfileIcons.cs @@ -0,0 +1,12 @@ +namespace SlackAPI +{ + public class ProfileIcons + { + public string image_24; + public string image_32; + public string image_48; + public string image_72; + public string image_192; + public string image_512; + } +} diff --git a/SlackAPI/Properties/InternalsVisibleTo.cs b/SlackAPI/Properties/InternalsVisibleTo.cs new file mode 100644 index 00000000..d747d594 --- /dev/null +++ b/SlackAPI/Properties/InternalsVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("SlackAPI.Tests")] \ No newline at end of file diff --git a/SlackAPI/RPCMessages/AccessTokenResponse.cs b/SlackAPI/RPCMessages/AccessTokenResponse.cs new file mode 100644 index 00000000..9c4266bb --- /dev/null +++ b/SlackAPI/RPCMessages/AccessTokenResponse.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("oauth.access")] + public class AccessTokenResponse : Response + { + public string access_token; + public string scope; + public string team_name; + 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/AuthSigninResponse.cs b/SlackAPI/RPCMessages/AuthSigninResponse.cs similarity index 100% rename from AuthSigninResponse.cs rename to SlackAPI/RPCMessages/AuthSigninResponse.cs diff --git a/AuthStartResponse.cs b/SlackAPI/RPCMessages/AuthStartResponse.cs similarity index 90% rename from AuthStartResponse.cs rename to SlackAPI/RPCMessages/AuthStartResponse.cs index 6e2e044d..93dde36d 100644 --- a/AuthStartResponse.cs +++ b/SlackAPI/RPCMessages/AuthStartResponse.cs @@ -8,7 +8,6 @@ public class AuthStartResponse : Response public string email; public string domain; public UserTeamCombo[] users; - //string[] teams; //Not sure? /// /// Path to create a new team? /// diff --git a/AuthTestResponse.cs b/SlackAPI/RPCMessages/AuthTestResponse.cs similarity index 100% rename from AuthTestResponse.cs rename to SlackAPI/RPCMessages/AuthTestResponse.cs diff --git a/SlackAPI/RPCMessages/ChannelCreateResponse.cs b/SlackAPI/RPCMessages/ChannelCreateResponse.cs new file mode 100644 index 00000000..b5273651 --- /dev/null +++ b/SlackAPI/RPCMessages/ChannelCreateResponse.cs @@ -0,0 +1,10 @@ +using System; + +namespace SlackAPI +{ + [RequestPath("channels.create")] + public class ChannelCreateResponse : Response + { + public Channel channel; + } +} 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/ChannelListResponse.cs b/SlackAPI/RPCMessages/ChannelListResponse.cs similarity index 100% rename from ChannelListResponse.cs rename to SlackAPI/RPCMessages/ChannelListResponse.cs diff --git a/ChannelMessageHistory.cs b/SlackAPI/RPCMessages/ChannelMessageHistory.cs similarity index 100% rename from ChannelMessageHistory.cs rename to SlackAPI/RPCMessages/ChannelMessageHistory.cs diff --git a/SlackAPI/RPCMessages/ChannelSetTopicResponse.cs b/SlackAPI/RPCMessages/ChannelSetTopicResponse.cs new file mode 100644 index 00000000..540f1d06 --- /dev/null +++ b/SlackAPI/RPCMessages/ChannelSetTopicResponse.cs @@ -0,0 +1,8 @@ +namespace SlackAPI +{ + [RequestPath("channels.setTopic")] + public class ChannelSetTopicResponse : Response + { + public string topic; + } +} \ No newline at end of file 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/DeletedResponse.cs b/SlackAPI/RPCMessages/DeletedResponse.cs new file mode 100644 index 00000000..89b5af83 --- /dev/null +++ b/SlackAPI/RPCMessages/DeletedResponse.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("chat.delete")] + public class DeletedResponse : Response + { + public string channel; + public DateTime ts; + } +} 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/DirectMessageConversationListResponse.cs b/SlackAPI/RPCMessages/DirectMessageConversationListResponse.cs similarity index 100% rename from DirectMessageConversationListResponse.cs rename to SlackAPI/RPCMessages/DirectMessageConversationListResponse.cs 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/FileInfoResponse.cs b/SlackAPI/RPCMessages/FileInfoResponse.cs similarity index 100% rename from FileInfoResponse.cs rename to SlackAPI/RPCMessages/FileInfoResponse.cs diff --git a/FileListResponse.cs b/SlackAPI/RPCMessages/FileListResponse.cs similarity index 100% rename from FileListResponse.cs rename to SlackAPI/RPCMessages/FileListResponse.cs diff --git a/FileUploadResponse.cs b/SlackAPI/RPCMessages/FileUploadResponse.cs similarity index 100% rename from FileUploadResponse.cs rename to SlackAPI/RPCMessages/FileUploadResponse.cs diff --git a/SlackAPI/RPCMessages/FindTeamResponse.cs b/SlackAPI/RPCMessages/FindTeamResponse.cs new file mode 100644 index 00000000..d0b2d092 --- /dev/null +++ b/SlackAPI/RPCMessages/FindTeamResponse.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + /// + /// This is an undocumented response from an undocumented API. If anyone finds more info on this, please create a pull request. + /// + [RequestPath("auth.findTeam")] + public class FindTeamResponse : Response + { + public string sso_required, sso_type, team_id, url; + public string[] email_domains; + public bool sso; + public SSOProvider[] sso_provider; + + public static implicit operator Team(FindTeamResponse resp) + { + Team end = new Team(); + end.sso_required = resp.sso_required; + end.sso_type = resp.sso_type; + end.id = resp.team_id; + end.url = resp.url; + end.sso = resp.sso; + end.email_domains = resp.email_domains; + end.sso_provider = resp.sso_provider; + return end; + } + } +} diff --git a/SlackAPI/RPCMessages/GroupArchiveResponse.cs b/SlackAPI/RPCMessages/GroupArchiveResponse.cs new file mode 100644 index 00000000..d043012f --- /dev/null +++ b/SlackAPI/RPCMessages/GroupArchiveResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("groups.archive")] + public class GroupArchiveResponse : Response + { + } +} diff --git a/SlackAPI/RPCMessages/GroupCloseResponse.cs b/SlackAPI/RPCMessages/GroupCloseResponse.cs new file mode 100644 index 00000000..d0fdcd20 --- /dev/null +++ b/SlackAPI/RPCMessages/GroupCloseResponse.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("groups.close")] + public class GroupCloseResponse : Response + { + public string no_op; + public string already_closed; + } +} diff --git a/SlackAPI/RPCMessages/GroupCreateChildResponse.cs b/SlackAPI/RPCMessages/GroupCreateChildResponse.cs new file mode 100644 index 00000000..51d495e9 --- /dev/null +++ b/SlackAPI/RPCMessages/GroupCreateChildResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("groups.createChild")] + public class GroupCreateChildResponse : GroupResponse + { + } +} diff --git a/SlackAPI/RPCMessages/GroupCreateResponse.cs b/SlackAPI/RPCMessages/GroupCreateResponse.cs new file mode 100644 index 00000000..b5082e01 --- /dev/null +++ b/SlackAPI/RPCMessages/GroupCreateResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("groups.create")] + public class GroupCreateResponse : GroupResponse + { + } +} diff --git a/SlackAPI/RPCMessages/GroupInviteResponse.cs b/SlackAPI/RPCMessages/GroupInviteResponse.cs new file mode 100644 index 00000000..21551786 --- /dev/null +++ b/SlackAPI/RPCMessages/GroupInviteResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("groups.invite")] + public class GroupInviteResponse : GroupResponse + { + } +} diff --git a/SlackAPI/RPCMessages/GroupKickResponse.cs b/SlackAPI/RPCMessages/GroupKickResponse.cs new file mode 100644 index 00000000..3213fb22 --- /dev/null +++ b/SlackAPI/RPCMessages/GroupKickResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("groups.kick")] + public class GroupKickResponse : Response + { + } +} diff --git a/SlackAPI/RPCMessages/GroupLeaveResponse.cs b/SlackAPI/RPCMessages/GroupLeaveResponse.cs new file mode 100644 index 00000000..9c961224 --- /dev/null +++ b/SlackAPI/RPCMessages/GroupLeaveResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("groups.leave")] + public class GroupLeaveResponse : Response + { + } +} diff --git a/GroupListResponse.cs b/SlackAPI/RPCMessages/GroupListResponse.cs similarity index 100% rename from GroupListResponse.cs rename to SlackAPI/RPCMessages/GroupListResponse.cs diff --git a/SlackAPI/RPCMessages/GroupMarkResponse.cs b/SlackAPI/RPCMessages/GroupMarkResponse.cs new file mode 100644 index 00000000..b4cb7429 --- /dev/null +++ b/SlackAPI/RPCMessages/GroupMarkResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("groups.mark")] + public class GroupMarkResponse : Response + { + } +} diff --git a/GroupMessageHistory.cs b/SlackAPI/RPCMessages/GroupMessageHistory.cs similarity index 100% rename from GroupMessageHistory.cs rename to SlackAPI/RPCMessages/GroupMessageHistory.cs diff --git a/SlackAPI/RPCMessages/GroupOpenResponse.cs b/SlackAPI/RPCMessages/GroupOpenResponse.cs new file mode 100644 index 00000000..70c2ffcc --- /dev/null +++ b/SlackAPI/RPCMessages/GroupOpenResponse.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("groups.open")] + public class GroupOpenResponse : Response + { + public string no_op; + public string already_closed; + } +} diff --git a/SlackAPI/RPCMessages/GroupRenameResponse.cs b/SlackAPI/RPCMessages/GroupRenameResponse.cs new file mode 100644 index 00000000..65905767 --- /dev/null +++ b/SlackAPI/RPCMessages/GroupRenameResponse.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("groups.rename")] + public class GroupRenameResponse : Response + { + public Channel channel; + } +} diff --git a/SlackAPI/RPCMessages/GroupResponse.cs b/SlackAPI/RPCMessages/GroupResponse.cs new file mode 100644 index 00000000..f8834bc8 --- /dev/null +++ b/SlackAPI/RPCMessages/GroupResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + public class GroupResponse : Response + { + public Channel group; + } +} diff --git a/SlackAPI/RPCMessages/GroupSetPurposeResponse.cs b/SlackAPI/RPCMessages/GroupSetPurposeResponse.cs new file mode 100644 index 00000000..93a061c7 --- /dev/null +++ b/SlackAPI/RPCMessages/GroupSetPurposeResponse.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("groups.setPurpose")] + public class GroupSetPurposeResponse : Response + { + public string purpose; + } +} diff --git a/SlackAPI/RPCMessages/GroupSetTopicResponse.cs b/SlackAPI/RPCMessages/GroupSetTopicResponse.cs new file mode 100644 index 00000000..f8adab97 --- /dev/null +++ b/SlackAPI/RPCMessages/GroupSetTopicResponse.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("groups.setTopic")] + public class GroupSetTopicResponse : Response + { + public string topic; + } +} diff --git a/SlackAPI/RPCMessages/GroupUnarchiveResponse.cs b/SlackAPI/RPCMessages/GroupUnarchiveResponse.cs new file mode 100644 index 00000000..d68061c2 --- /dev/null +++ b/SlackAPI/RPCMessages/GroupUnarchiveResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("groups.unarchive")] + public class GroupUnarchiveResponse : Response + { + } +} diff --git a/SlackAPI/RPCMessages/JoinDirectMessageChannelResponse.cs b/SlackAPI/RPCMessages/JoinDirectMessageChannelResponse.cs new file mode 100644 index 00000000..a7095398 --- /dev/null +++ b/SlackAPI/RPCMessages/JoinDirectMessageChannelResponse.cs @@ -0,0 +1,8 @@ +namespace SlackAPI +{ + [RequestPath("conversations.open")] + public class JoinDirectMessageChannelResponse : Response + { + public Channel channel; + } +} \ No newline at end of file diff --git a/SlackAPI/RPCMessages/LoginResponse.cs b/SlackAPI/RPCMessages/LoginResponse.cs new file mode 100644 index 00000000..d048f316 --- /dev/null +++ b/SlackAPI/RPCMessages/LoginResponse.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [RequestPath("rtm.start")] + public class LoginResponse : Response + { + public Bot[] bots; + public Channel[] channels; + public Channel[] groups; + public DirectMessageConversation[] ims; + public Self self; + public int svn_rev; + public int min_svn_rev; + public Team team; + public string url; + public User[] users; + } + + public class Self + { + public DateTime created; + public string id; + public string manual_presence; + public string name; + public Preferences prefs; + } +} diff --git a/MarkResponse.cs b/SlackAPI/RPCMessages/MarkResponse.cs similarity index 89% rename from MarkResponse.cs rename to SlackAPI/RPCMessages/MarkResponse.cs index 982fbbc7..976b0c91 100644 --- a/MarkResponse.cs +++ b/SlackAPI/RPCMessages/MarkResponse.cs @@ -9,7 +9,7 @@ namespace SlackAPI /// /// This is used for moving the read cursor in the channel. /// - [RequestPath("channels.marks")] + [RequestPath("channels.mark")] public class MarkResponse : Response { } diff --git a/MessageHistory.cs b/SlackAPI/RPCMessages/MessageHistory.cs similarity index 92% rename from MessageHistory.cs rename to SlackAPI/RPCMessages/MessageHistory.cs index ba2541ca..8fb797a1 100644 --- a/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/PostEphemeralResponse.cs b/SlackAPI/RPCMessages/PostEphemeralResponse.cs new file mode 100644 index 00000000..bf48a50e --- /dev/null +++ b/SlackAPI/RPCMessages/PostEphemeralResponse.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.RPCMessages +{ + [RequestPath("chat.postEphemeral")] + public class PostEphemeralResponse : Response + { + public string message_ts; + } +} diff --git a/SlackAPI/RPCMessages/PostMessageResponse.cs b/SlackAPI/RPCMessages/PostMessageResponse.cs new file mode 100644 index 00000000..c7f4b908 --- /dev/null +++ b/SlackAPI/RPCMessages/PostMessageResponse.cs @@ -0,0 +1,20 @@ +namespace SlackAPI +{ + [RequestPath("chat.postMessage")] + public class PostMessageResponse : Response + { + public string ts; + public string channel; + public Message message; + + public class Message + { + public string text; + public string user; + public string username; + public string type; + public string subtype; + public string ts; + } + } +} diff --git a/SlackAPI/RPCMessages/PresenseResponse.cs b/SlackAPI/RPCMessages/PresenseResponse.cs new file mode 100644 index 00000000..7b3cd5de --- /dev/null +++ b/SlackAPI/RPCMessages/PresenseResponse.cs @@ -0,0 +1,14 @@ +namespace SlackAPI +{ + [RequestPath("users.setPresence")] + public class PresenceResponse : Response + { + } + + public enum Presence + { + active, + 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/SearchResponseAll.cs b/SlackAPI/RPCMessages/SearchResponseAll.cs similarity index 100% rename from SearchResponseAll.cs rename to SlackAPI/RPCMessages/SearchResponseAll.cs diff --git a/SearchResponseFiles.cs b/SlackAPI/RPCMessages/SearchResponseFiles.cs similarity index 91% rename from SearchResponseFiles.cs rename to SlackAPI/RPCMessages/SearchResponseFiles.cs index 9b8b284a..4cf8ebe1 100644 --- a/SearchResponseFiles.cs +++ b/SlackAPI/RPCMessages/SearchResponseFiles.cs @@ -10,7 +10,7 @@ namespace SlackAPI public class SearchResponseFiles : Response { public string query; - SearchResponseMessagesContainer messages; + public SearchResponseFilesContainer files; } public class SearchResponseFilesContainer diff --git a/SearchResponseMessages.cs b/SlackAPI/RPCMessages/SearchResponseMessages.cs similarity index 95% rename from SearchResponseMessages.cs rename to SlackAPI/RPCMessages/SearchResponseMessages.cs index 6131f949..8b1eef70 100644 --- a/SearchResponseMessages.cs +++ b/SlackAPI/RPCMessages/SearchResponseMessages.cs @@ -10,7 +10,6 @@ namespace SlackAPI public class SearchResponseMessages : Response { public string query; - SearchResponseMessagesContainer messages; } public class SearchResponseMessagesContainer @@ -56,8 +55,10 @@ public class PaginationInformation public enum SearchSort { + not_set, score, - timestamp + timestamp, + not_set_new_user } public enum SearchSortDirection diff --git a/StarListResponse.cs b/SlackAPI/RPCMessages/StarListResponse.cs similarity index 100% rename from StarListResponse.cs rename to SlackAPI/RPCMessages/StarListResponse.cs diff --git a/SlackAPI/RPCMessages/UpdateResponse.cs b/SlackAPI/RPCMessages/UpdateResponse.cs new file mode 100644 index 00000000..4514edfc --- /dev/null +++ b/SlackAPI/RPCMessages/UpdateResponse.cs @@ -0,0 +1,18 @@ +namespace SlackAPI +{ + [RequestPath("chat.update")] + public class UpdateResponse : Response + { + public string channel; + public string ts; + public string text; + public Message message; + + public class Message + { + public string type; + public string user; + public string text; + } + } +} \ No newline at end of file diff --git a/UserCountsResponse.cs b/SlackAPI/RPCMessages/UserCountsResponse.cs similarity index 100% rename from UserCountsResponse.cs rename to SlackAPI/RPCMessages/UserCountsResponse.cs 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/RPCMessages/UserGetPresenceResponse.cs b/SlackAPI/RPCMessages/UserGetPresenceResponse.cs new file mode 100644 index 00000000..62b43272 --- /dev/null +++ b/SlackAPI/RPCMessages/UserGetPresenceResponse.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.RPCMessages +{ + [RequestPath("users.getPresence")] + public class UserGetPresenceResponse : Response + { + public Presence presence; + } +} diff --git a/SlackAPI/RPCMessages/UserInfoResponse.cs b/SlackAPI/RPCMessages/UserInfoResponse.cs new file mode 100644 index 00000000..2948f507 --- /dev/null +++ b/SlackAPI/RPCMessages/UserInfoResponse.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.RPCMessages +{ + [RequestPath("users.info")] + public class UserInfoResponse : Response + { + public User user; + } +} diff --git a/UserListResponse.cs b/SlackAPI/RPCMessages/UserListResponse.cs similarity index 100% rename from UserListResponse.cs rename to SlackAPI/RPCMessages/UserListResponse.cs diff --git a/UserPreferencesResponse.cs b/SlackAPI/RPCMessages/UserPreferencesResponse.cs similarity index 100% rename from UserPreferencesResponse.cs rename to SlackAPI/RPCMessages/UserPreferencesResponse.cs diff --git a/SlackAPI/Reaction.cs b/SlackAPI/Reaction.cs new file mode 100644 index 00000000..5cae7099 --- /dev/null +++ b/SlackAPI/Reaction.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace SlackAPI +{ + public class Reaction + { + public string name; + public int count; + public List users; + } +} diff --git a/PostMessageResponse.cs b/SlackAPI/ReactionAddedResponse.cs similarity index 64% rename from PostMessageResponse.cs rename to SlackAPI/ReactionAddedResponse.cs index 45386c87..b641ac32 100644 --- a/PostMessageResponse.cs +++ b/SlackAPI/ReactionAddedResponse.cs @@ -6,8 +6,8 @@ namespace SlackAPI { - [RequestPath("chat.postMessage")] - public class PostMessageResponse : Response + [RequestPath("reactions.add")] + public class ReactionAddedResponse : Response { } diff --git a/Request.cs b/SlackAPI/Request.cs similarity index 50% rename from Request.cs rename to SlackAPI/Request.cs index 5a0f1d90..e7ed01a5 100644 --- a/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; @@ -9,6 +10,8 @@ namespace SlackAPI { + + class RequestState where K : Response { @@ -21,6 +24,7 @@ class RequestState public RequestState(HttpWebRequest requestData, Tuple[] postParameters, Action toCallback) { + if (requestData == null) throw new ArgumentNullException("requestData can not be null"); request = requestData; Post = postParameters; callback = toCallback; @@ -31,12 +35,12 @@ public void Begin() if (Post.Length == 0) { request.Method = "GET"; - request.BeginGetResponse(GotResponse, this); + IAsyncResult result = request.BeginGetResponse(GotResponse, this); } else { request.Method = "POST"; - request.BeginGetRequestStream(GotRequest, this); + IAsyncResult result = request.BeginGetRequestStream(GotRequest, this); } } @@ -68,26 +72,61 @@ internal void GotResponse(IAsyncResult result) { try { - response = (HttpWebResponse)request.EndGetResponse(result); + response = (HttpWebResponse)request?.EndGetResponse(result); Success = true; } catch (WebException we) { + // If we don't get a response, let the exception bubble up as we can't do anything + if (we.Response == null) + { + var defaultResponse = CreateDefaultResponseForError(we); + callback?.Invoke(defaultResponse); + return; + } + //Anything that doesn't return error 200 throws an exception. Sucks. :l response = (HttpWebResponse)we.Response; //TODO: Handle timeouts, etc? } + catch (Exception e) + { + var defaultResponse = CreateDefaultResponseForError(e); + callback?.Invoke(defaultResponse); + return; + } K responseObj; + if (response == null) + { + responseObj = CreateDefaultResponseForError(new Exception("Empty response")); + callback?.Invoke(responseObj); + return; + } - using(Stream responseReading = response.GetResponseStream()) - using (StreamReader reader = new StreamReader(responseReading)) + try + { + using (Stream responseReading = response.GetResponseStream()) + using (StreamReader reader = new StreamReader(responseReading)) + { + string responseData = reader.ReadToEnd(); + responseObj = responseData.Deserialize(); + } + } + catch (Exception e) { - string responseData = reader.ReadToEnd(); - responseObj = JsonConvert.DeserializeObject(responseData, new JavascriptDateTimeConverter()); + responseObj = CreateDefaultResponseForError(e); } - callback(responseObj); + callback?.Invoke(responseObj); + } + + private K CreateDefaultResponseForError(Exception e) + { + var defaultResponse = (K)Activator.CreateInstance(); + defaultResponse.ok = false; + defaultResponse.error = e.ToString(); + return defaultResponse; } } @@ -103,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 new file mode 100644 index 00000000..f535f5a4 --- /dev/null +++ b/SlackAPI/RequestStateForTask.cs @@ -0,0 +1,94 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + public class RequestStateForTask + where K : Response + { + public HttpWebRequest request; + Tuple[] Post; + public bool Success; + + public RequestStateForTask(HttpWebRequest requestData, Tuple[] postParameters) + { + request = requestData; + Post = postParameters; + } + + internal Task Execute() + { + if (Post == null || Post.Length == 0) + { + request.Method = "GET"; + return this.ExecuteResult(); + } + else + { + return this.ExecutePost(); + } + } + + private async Task ExecuteResult() + { + HttpWebResponse response = null; + try + { + response = (HttpWebResponse)await this.request.GetResponseAsync().ConfigureAwait(false); + Success = true; + } + catch (WebException we) + { + //Anything that doesn't return error 200 throws an exception. Sucks. :l + response = (HttpWebResponse)we.Response; + //TODO: Handle timeouts, etc? + } + + K responseObj; + + using (Stream responseReading = response.GetResponseStream()) + { + using (StreamReader reader = new StreamReader(responseReading)) + { + string responseData = reader.ReadToEnd(); + responseObj = responseData.Deserialize(); + } + } + + return responseObj; + } + + private async Task ExecutePost() + { + request.Method = "POST"; + request.ContentType = "application/x-www-form-urlencoded"; + using (Stream requestStream = await request.GetRequestStreamAsync().ConfigureAwait(false)) + { + if (Post.Length > 0) + { + using (StreamWriter writer = new StreamWriter(requestStream)) + { + bool first = true; + foreach (Tuple postEntry in Post) + { + if (!first) + writer.Write('&'); + + await writer.WriteAsync(string.Format("{0}={1}", Uri.EscapeDataString(postEntry.Item1), Uri.EscapeDataString(postEntry.Item2))).ConfigureAwait(false); + + first = false; + } + } + } + } + + return await this.ExecuteResult().ConfigureAwait(false); + } + } +} diff --git a/SlackAPI/Response.cs b/SlackAPI/Response.cs new file mode 100644 index 00000000..8cd5e2c7 --- /dev/null +++ b/SlackAPI/Response.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + public abstract class Response + { + /// + /// Should always be checked before trying to process a response. + /// + public bool ok; + + /// + /// 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 new file mode 100644 index 00000000..934c5356 --- /dev/null +++ b/SlackAPI/SlackAPI.csproj @@ -0,0 +1,40 @@ + + + + net45;netstandard2.0 + Full + SlackAPI + SlackAPI + + + + + + + + + + + + + + + + + + + + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + $(DefineConstants);RELEASE + + + diff --git a/SlackAPI/SlackClient.cs b/SlackAPI/SlackClient.cs new file mode 100644 index 00000000..e81a55c3 --- /dev/null +++ b/SlackAPI/SlackClient.cs @@ -0,0 +1,939 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using SlackAPI.RPCMessages; + +namespace SlackAPI +{ + /// + /// SlackClient is intended to solely handle RPC (HTTP-based) functionality. Does not handle WebSocket connectivity. + /// + /// For WebSocket connectivity, refer to + /// + public class SlackClient : SlackClientBase + { + private readonly string APIToken; + + public Self MySelf; + public User MyData; + public Team MyTeam; + + public List starredChannels; + + public List Users; + public List Bots; + public List Channels; + public List Groups; + public List DirectMessages; + + public Dictionary UserLookup; + public Dictionary ChannelLookup; + public Dictionary GroupLookup; + public Dictionary DirectMessageLookup; + public Dictionary ConversationLookup; + + public SlackClient(string token) + { + APIToken = token; + } + + public SlackClient(string token, IWebProxy proxySettings) + : base(proxySettings) + { + APIToken = token; + } + + public virtual void Connect(Action onConnected = null, Action onSocketConnected = null) + { + EmitLogin((loginDetails) => + { + if (loginDetails.ok) + Connected(loginDetails); + + if (onConnected != null) + onConnected(loginDetails); + }); + } + + protected virtual void Connected(LoginResponse loginDetails) + { + MySelf = loginDetails.self; + MyData = loginDetails.users.First((c) => c.id == MySelf.id); + MyTeam = loginDetails.team; + + Users = new List(loginDetails.users.Where((c) => !c.deleted)); + Bots = new List(loginDetails.bots.Where((c) => !c.deleted)); + Channels = new List(loginDetails.channels); + Groups = new List(loginDetails.groups); + DirectMessages = new List(loginDetails.ims.Where((c) => Users.Exists((a) => a.id == c.user) && c.id != MySelf.id)); + starredChannels = + Groups.Where((c) => c.is_starred).Select((c) => c.id) + .Union( + DirectMessages.Where((c) => c.is_starred).Select((c) => c.user) + ).Union( + Channels.Where((c) => c.is_starred).Select((c) => c.id) + ).ToList(); + + UserLookup = new Dictionary(); + foreach (User u in Users) UserLookup.Add(u.id, u); + + ChannelLookup = new Dictionary(); + ConversationLookup = new Dictionary(); + foreach (Channel c in Channels) + { + ChannelLookup.Add(c.id, c); + ConversationLookup.Add(c.id, c); + } + + GroupLookup = new Dictionary(); + foreach (Channel g in Groups) + { + GroupLookup.Add(g.id, g); + ConversationLookup.Add(g.id, g); + } + + DirectMessageLookup = new Dictionary(); + foreach (DirectMessageConversation im in DirectMessages) + { + DirectMessageLookup.Add(im.id, im); + ConversationLookup.Add(im.id, im); + } + } + + public void APIRequestWithToken(Action callback, params Tuple[] getParameters) + where K : Response + { + APIRequest(callback, getParameters, new Tuple[0], APIToken); + } + + public void TestAuth(Action callback) + { + APIRequestWithToken(callback); + } + + public void GetUserList(Action callback) + { + APIRequestWithToken(callback); + } + + 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 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")); + } + + public void GetGroupsList(Action callback, bool ExcludeArchived = true) + { + APIRequestWithToken(callback, new Tuple("exclude_archived", ExcludeArchived ? "1" : "0")); + } + + public void GetDirectMessageList(Action callback) + { + 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, string channel = null) + { + List> parameters = new List>(); + + if (!string.IsNullOrEmpty(userId)) + parameters.Add(new Tuple("user", userId)); + + if (from.HasValue) + parameters.Add(new Tuple("ts_from", from.Value.ToProperTimeStamp())); + + if (to.HasValue) + parameters.Add(new Tuple("ts_to", to.Value.ToProperTimeStamp())); + + if (!types.HasFlag(FileTypes.all)) + { + FileTypes[] values = (FileTypes[])Enum.GetValues(typeof(FileTypes)); + + StringBuilder building = new StringBuilder(); + bool first = true; + for (int i = 0; i < values.Length; ++i) + { + if (types.HasFlag(values[i])) + { + if (!first) building.Append(","); + + building.Append(values[i].ToString()); + + first = false; + } + } + + if (building.Length > 0) + parameters.Add(new Tuple("types", building.ToString())); + } + + if (count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + + 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, bool? unreads = false) + where K : MessageHistory + { + List> parameters = new List>(); + parameters.Add(new Tuple("channel", channel)); + + if(latest.HasValue) + parameters.Add(new Tuple("latest", latest.Value.ToProperTimeStamp())); + if(oldest.HasValue) + parameters.Add(new Tuple("oldest", oldest.Value.ToProperTimeStamp())); + if(count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + if (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, bool? unreads = false) + { + GetHistory(callback, channelInfo.id, latest, oldest, count, unreads); + } + + 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, unreads); + } + + 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, unreads); + } + + public void GetConversationsHistory(Action callback, Channel conversationInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) + { + GetHistory(callback, conversationInfo.id, latest, oldest, count, unreads); + } + + public void MarkChannel(Action callback, string channelId, DateTime ts) + { + APIRequestWithToken(callback, + new Tuple("channel", channelId), + new Tuple("ts", ts.ToProperTimeStamp()) + ); + } + + public void GetFileInfo(Action callback, string fileId, int? page = null, int? count = null) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("file", fileId)); + + if(count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + + if (page.HasValue) + parameters.Add(new Tuple("page", page.Value.ToString())); + + APIRequestWithToken(callback, parameters.ToArray()); + } + #region Groups + public void GroupsArchive(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void GroupsClose(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void GroupsCreate(Action callback, string name) + { + APIRequestWithToken(callback, new Tuple("name", name)); + } + + public void GroupsCreateChild(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void GroupsInvite(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 GroupsKick(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 GroupsLeave(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void GroupsMark(Action callback, string channelId, DateTime ts) + { + APIRequestWithToken(callback, new Tuple("channel", channelId), new Tuple("ts", ts.ToProperTimeStamp())); + } + + public void GroupsOpen(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void GroupsRename(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 GroupsSetPurpose(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 GroupsSetTopic(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 GroupsUnarchive(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + #endregion + + #region Conversations + public void ConversationsArchive(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void ConversationsClose(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void ConversationsCreate(Action callback, string name) + { + APIRequestWithToken(callback, new Tuple("name", name)); + } + + public void ConversationsInvite(Action callback, string channelId, string[] userIds) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("users", string.Join(",", userIds))); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void ConversationsJoin(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void ConversationsKick(Action callback, string channelId, string userId) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("user", userId)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void ConversationsLeave(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void ConversationsMark(Action callback, string channelId, DateTime ts) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("ts", ts.ToProperTimeStamp())); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void ConversationsOpen(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + public void ConversationsRename(Action callback, string channelId, string name) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("name", name)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void ConversationsSetPurpose(Action callback, string channelId, string purpose) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("purpose", purpose)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void ConversationsSetTopic(Action callback, string channelId, string topic) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("topic", topic)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void ConversationsUnarchive(Action callback, string channelId) + { + APIRequestWithToken(callback, new Tuple("channel", channelId)); + } + + #endregion + + + public void SearchAll(Action callback, string query, string sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) + { + List> parameters = new List>(); + parameters.Add(new Tuple("query", query)); + + if (sorting != null) + parameters.Add(new Tuple("sort", sorting)); + + if (direction.HasValue) + parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); + + if (enableHighlights) + parameters.Add(new Tuple("highlight", "1")); + + if (count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + + if (page.HasValue) + parameters.Add(new Tuple("page", page.Value.ToString())); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + 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 != null) + parameters.Add(new Tuple("sort", sorting)); + + if (direction.HasValue) + parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); + + if (enableHighlights) + parameters.Add(new Tuple("highlight", "1")); + + if (count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + + if (page.HasValue) + parameters.Add(new Tuple("page", page.Value.ToString())); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + 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 != null) + parameters.Add(new Tuple("sort", sorting)); + + if (direction.HasValue) + parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); + + if (enableHighlights) + parameters.Add(new Tuple("highlight", "1")); + + if (count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + + if (page.HasValue) + parameters.Add(new Tuple("page", page.Value.ToString())); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void GetStars(Action callback, string userId = null, int? count = null, int? page = null){ + List> parameters = new List>(); + + if(!string.IsNullOrEmpty(userId)) + parameters.Add(new Tuple("user", userId)); + + if(count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + + if(page.HasValue) + parameters.Add(new Tuple("page", page.Value.ToString())); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void DeleteMessage(Action callback, string channelId, DateTime ts) + { + List> parameters = new List>() + { + new Tuple("ts", ts.ToProperTimeStamp()), + new Tuple("channel", channelId) + }; + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void EmitPresence(Action callback, Presence status) + { + APIRequestWithToken(callback, new Tuple("presence", status.ToString())); + } + + public void GetPreferences(Action callback) + { + APIRequestWithToken(callback); + } + + #region Users + + public void GetCounts(Action callback) + { + APIRequestWithToken(callback); + } + + public void GetPresence(Action callback, string user) + { + APIRequestWithToken(callback, new Tuple("user", user)); + } + + public void GetInfo(Action callback, string user) + { + APIRequestWithToken(callback, new Tuple("user", user)); + } + + #endregion + + public void EmitLogin(Action callback, string agent = "Inumedia.SlackAPI") + { + APIRequestWithToken(callback, new Tuple("agent", agent)); + } + + public void Update( + Action callback, + string ts, + string channelId, + string text, + string botName = null, + string parse = null, + bool linkNames = false, + IBlock[] blocks = null, + Attachment[] attachments = null, + bool? as_user = null) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("ts", ts)); + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("text", text)); + + if (!string.IsNullOrEmpty(botName)) + parameters.Add(new Tuple("username", botName)); + + if (!string.IsNullOrEmpty(parse)) + parameters.Add(new Tuple("parse", parse)); + + if (linkNames) + parameters.Add(new Tuple("link_names", "1")); + + if (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 (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("users", user); + APIRequestWithToken(callback, param); + } + + public void PostMessage( + Action callback, + string channelId, + string text, + 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)); + + 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 PostEphemeralMessage( + Action callback, + string channelId, + string text, + string targetuser, + string parse = null, + bool linkNames = false, + Block[] blocks = null, + 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 (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("as_user", as_user.ToString())); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + + public void ScheduleMessage( + Action callback, + string channelId, + string text, + DateTime post_at, + string botName = null, + string parse = null, + bool linkNames = false, + IBlock[] blocks = null, + Attachment[] attachments = null, + bool? unfurl_links = null, + string icon_url = null, + string icon_emoji = null, + bool? as_user = null, + string thread_ts = null) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("text", text)); + parameters.Add(new Tuple("post_at", Convert.ToUInt64((post_at - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds).ToString())); + + if (!string.IsNullOrEmpty(botName)) + parameters.Add(new Tuple("username", botName)); + + if (!string.IsNullOrEmpty(parse)) + parameters.Add(new Tuple("parse", parse)); + + if (linkNames) + parameters.Add(new Tuple("link_names", "1")); + + if (blocks != null && blocks.Length > 0) + parameters.Add(new Tuple("blocks", + JsonConvert.SerializeObject(blocks, Formatting.None, + new JsonSerializerSettings // Shouldn't include a not set property + { + NullValueHandling = NullValueHandling.Ignore + }))); + + if (attachments != null && attachments.Length > 0) + parameters.Add(new Tuple("attachments", + JsonConvert.SerializeObject(attachments, Formatting.None, + new JsonSerializerSettings // Shouldn't include a not set property + { + NullValueHandling = NullValueHandling.Ignore + }))); + + if (unfurl_links.HasValue) + parameters.Add(new Tuple("unfurl_links", unfurl_links.Value ? "true" : "false")); + + if (!string.IsNullOrEmpty(icon_url)) + parameters.Add(new Tuple("icon_url", icon_url)); + + if (!string.IsNullOrEmpty(icon_emoji)) + parameters.Add(new Tuple("icon_emoji", icon_emoji)); + + if (as_user.HasValue) + parameters.Add(new Tuple("as_user", as_user.ToString())); + + if (!string.IsNullOrEmpty(thread_ts)) + parameters.Add(new Tuple("thread_ts", thread_ts)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void DialogOpen( + Action callback, + string triggerId, + 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, + 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)); + + APIRequestWithToken(callback, parameters.ToArray()); + } + + public void UploadFile(Action callback, byte[] fileData, string fileName, string[] channelIds, string title = null, string initialComment = null, bool useAsync = false, string fileType = null) + { + Uri target = new Uri(Path.Combine(APIBaseLocation, useAsync ? "files.uploadAsync" : "files.upload")); + + List parameters = new List(); + + //File/Content + if (!string.IsNullOrEmpty(fileType)) + parameters.Add(string.Format("{0}={1}", "filetype", fileType)); + + if (!string.IsNullOrEmpty(fileName)) + parameters.Add(string.Format("{0}={1}", "filename", fileName)); + + if (!string.IsNullOrEmpty(title)) + parameters.Add(string.Format("{0}={1}", "title", title)); + + if (!string.IsNullOrEmpty(initialComment)) + parameters.Add(string.Format("{0}={1}", "initial_comment", initialComment)); + + parameters.Add(string.Format("{0}={1}", "channels", string.Join(",", channelIds))); + + using (MultipartFormDataContent form = new MultipartFormDataContent()) + { + form.Add(new ByteArrayContent(fileData), "file", fileName); + 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 new file mode 100644 index 00000000..1e6d357b --- /dev/null +++ b/SlackAPI/SlackClientBase.cs @@ -0,0 +1,157 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace SlackAPI +{ + public abstract class SlackClientBase + { + protected readonly IWebProxy proxySettings; + private readonly HttpClient httpClient; + public string APIBaseLocation { get; set; } = "https://slack.com/api/"; + + static SlackClientBase() + { + // Force Tls 1.2 for Slack + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + } + + protected SlackClientBase() + { + this.httpClient = new HttpClient(); + } + + protected SlackClientBase(IWebProxy proxySettings) + { + this.proxySettings = proxySettings; + this.httpClient = new HttpClient(new HttpClientHandler { UseProxy = true, Proxy = proxySettings }); + } + + protected Uri GetSlackUri(string path, Tuple[] getParameters) + { + string parameters = default; + + if (getParameters != null && getParameters.Length > 0) + { + parameters = getParameters + .Where(x => x.Item2 != null) + .Select(new Func, string>(a => + { + try + { + return string.Format("{0}={1}", Uri.EscapeDataString(a.Item1), Uri.EscapeDataString(a.Item2)); + } + catch (Exception ex) + { + throw new InvalidOperationException(string.Format("Failed when processing '{0}'.", a), ex); + } + })) + .Aggregate((a, b) => + { + if (string.IsNullOrEmpty(a)) + return b; + else + return string.Format("{0}&{1}", a, b); + }); + + } + + 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, string token = "") + where K : Response + { + RequestPath path = RequestPath.GetRequestPath(); + //TODO: Custom paths? Appropriate subdomain paths? Not sure. + //Maybe store custom path in the requestpath.path itself? + + 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, string token = "") + where K : Response + { + RequestPath path = RequestPath.GetRequestPath(); + //TODO: Custom paths? Appropriate subdomain paths? Not sure. + //Maybe store custom path in the requestpath.path itself? + + 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. + var state = new RequestStateForTask(request, postParameters); + return state.Execute(); + } + + protected void APIGetRequest(Action callback, params Tuple[] getParameters) + where K : Response + { + APIRequest(callback, getParameters, new Tuple[0]); + } + + public Task APIGetRequestAsync(params Tuple[] getParameters) + where K : Response + { + return APIRequestAsync(getParameters, new Tuple[0]); + } + + protected HttpWebRequest CreateWebRequest(Uri requestUri) + { + var httpWebRequest = (HttpWebRequest)HttpWebRequest.Create(requestUri); + if (proxySettings != null) + { + httpWebRequest.Proxy = this.proxySettings; + } + + return httpWebRequest; + } + + protected Task PostRequestAsync(string requestUri, MultipartFormDataContent form, string token) + { + 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) + { + if (converter == null) + { + throw new ArgumentNullException("converter"); + } + + Extensions.Converters.Add(converter); + } + } +} diff --git a/SlackAPI/SlackClientHelpers.cs b/SlackAPI/SlackClientHelpers.cs new file mode 100644 index 00000000..8b91e6f9 --- /dev/null +++ b/SlackAPI/SlackClientHelpers.cs @@ -0,0 +1,123 @@ +using System; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + public class SlackClientHelpers : SlackClientBase + { + public SlackClientHelpers() + { + } + + public SlackClientHelpers(IWebProxy proxySettings) + : base(proxySettings) + { + } + + [Obsolete("Please use the OAuth method for authenticating users")] + public void StartAuth(Action callback, string email) + { + APIRequest(callback, new Tuple[] { new Tuple("email", email) }, new Tuple[0]); + } + + [Obsolete("Please use the OAuth method for authenticating users")] + public Task StartAuthAsync(string email) + { + return APIRequestAsync(new Tuple[] { new Tuple("email", email) }, new Tuple[0]); + } + + public void FindTeam(Action callback, string team) + { + //This seems to accept both 'team.slack.com' and just plain 'team'. + //Going to go with the latter. + Tuple domainName = new Tuple("domain", team); + APIRequest(callback, new Tuple[] { domainName }, new Tuple[0]); + } + + public Task FindTeamAsync(string team) + { + //This seems to accept both 'team.slack.com' and just plain 'team'. + //Going to go with the latter. + Tuple domainName = new Tuple("domain", team); + return APIRequestAsync(new Tuple[] { domainName }, new Tuple[0]); + } + + public void AuthSignin(Action callback, string userId, string teamId, string password) + { + APIRequest(callback, new Tuple[] { + new Tuple("user", userId), + new Tuple("team", teamId), + new Tuple("password", password) + }, new Tuple[0]); + } + + public Task AuthSigninAsync(string userId, string teamId, string password) + { + return APIRequestAsync(new Tuple[] { + new Tuple("user", userId), + new Tuple("team", teamId), + new Tuple("password", password) + }, new Tuple[0]); + } + + public Uri GetAuthorizeUri(string clientId, SlackScope scopes, string redirectUri = null, string state = null, string team = null) + { + string theScopes = BuildScope(scopes); + + return GetSlackUri("https://slack.com/oauth/authorize", new Tuple[] { new Tuple("client_id", clientId), + new Tuple("redirect_uri", redirectUri), + new Tuple("state", state), + new Tuple("scope", theScopes), + new Tuple("team", team)}); + } + + public void GetAccessToken(Action callback, string clientId, string clientSecret, string redirectUri, string code) + { + APIRequest(callback, new Tuple[] { new Tuple("client_id", clientId), + new Tuple("client_secret", clientSecret), new Tuple("code", code), + new Tuple("redirect_uri", redirectUri) }, new Tuple[] { }); + } + + public Task GetAccessTokenAsync(string clientId, string clientSecret, string redirectUri, string code) + { + return APIRequestAsync(new Tuple[] { new Tuple("client_id", clientId), + new Tuple("client_secret", clientSecret), new Tuple("code", code), + new Tuple("redirect_uri", redirectUri) }, new Tuple[] { }); + } + + private string BuildScope(SlackScope scope) + { + var builder = new StringBuilder(); + if ((int)(scope & SlackScope.Identify) != 0) + builder.Append("identify"); + if ((int)(scope & SlackScope.Read) != 0) + { + if (builder.Length > 0) + builder.Append(","); + builder.Append("read"); + } + if ((int)(scope & SlackScope.Post) != 0) + { + if (builder.Length > 0) + builder.Append(","); + builder.Append("post"); + } + if ((int)(scope & SlackScope.Client) != 0) + { + if (builder.Length > 0) + builder.Append(","); + builder.Append("client"); + } + if ((int)(scope & SlackScope.Admin) != 0) + { + if (builder.Length > 0) + builder.Append(","); + builder.Append("admin"); + } + + return builder.ToString(); + } + } +} diff --git a/SlackAPI/SlackScope.cs b/SlackAPI/SlackScope.cs new file mode 100644 index 00000000..3819c05a --- /dev/null +++ b/SlackAPI/SlackScope.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI +{ + [Flags] + public enum SlackScope + { + Identify = 1, + Read = 2, + Post = 4, + Client = 8, + Admin = 16, + } +} diff --git a/SlackAPI/SlackSocket.cs b/SlackAPI/SlackSocket.cs new file mode 100644 index 00000000..75903825 --- /dev/null +++ b/SlackAPI/SlackSocket.cs @@ -0,0 +1,364 @@ +using System.IO; +using Newtonsoft.Json; +using SlackAPI.Utilities; +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; +using System.Net; + +namespace SlackAPI +{ + public class SlackSocket + { + LockFreeQueue sendingQueue; + int currentlySending; + int closedEmitted; + CancellationTokenSource cts; + + Dictionary> callbacks; + internal ClientWebSocket socket; + int currentId; + + Dictionary> routes; + public bool Connected { get { return socket != null && socket.State == WebSocketState.Open; } } + public event Action ErrorSending; + public event Action ErrorReceiving; + public event Action ErrorReceivingDesiralization; + public event Action ErrorHandlingMessage; + public event Action ConnectionClosed; + + //This would be done for hinting but I don't think we really need this. + + static Dictionary> routing; + static SlackSocket() + { + routing = new Dictionary>(); + var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(x => x.GlobalAssemblyCache == false); + foreach (Assembly assembly in assemblies) + { + Type[] assemblyTypes; + try + { + assemblyTypes = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException) + { + return; + } + + foreach (Type type in assemblyTypes) + { + foreach (SlackSocketRouting route in type.GetTypeInfo().GetCustomAttributes()) + { + if (!routing.ContainsKey(route.Type)) + { + routing.Add(route.Type, new Dictionary() + { + {route.SubType ?? "null", type} + }); + } + else + { + if (!routing[route.Type].ContainsKey(route.SubType ?? "null")) + { + routing[route.Type].Add(route.SubType ?? "null", type); + } + else + { + throw new InvalidProgramException("Cannot have two socket message types with the same type and subtype!"); + } + } + } + } + } + } + + public SlackSocket(LoginResponse loginDetails, object routingTo, Action onConnected = null, IWebProxy proxySettings = null) + { + BuildRoutes(routingTo); + socket = new ClientWebSocket(); + if (proxySettings != null) + { + socket.Options.Proxy = proxySettings; + } + + callbacks = new Dictionary>(); + sendingQueue = new LockFreeQueue(); + currentId = 1; + + cts = new CancellationTokenSource(); + socket.ConnectAsync(new Uri(string.Format("{0}?svn_rev={1}&login_with_boot_data-0-{2}&on_login-0-{2}&connect-1-{2}", loginDetails.url, loginDetails.svn_rev, DateTime.Now.Subtract(new DateTime(1970, 1, 1)).TotalSeconds)), cts.Token).Wait(); + if(onConnected != null) + onConnected(); + SetupReceiving(); + } + + void BuildRoutes(object routingTo) + { + routes = new Dictionary>(); + + Type routingToType = routingTo.GetType(); + Type slackMessage = typeof(SlackSocketMessage); + foreach (MethodInfo m in routingTo.GetType().GetMethods(BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Public)) + { + ParameterInfo[] parameters = m.GetParameters(); + if (parameters.Length != 1) continue; + if (parameters[0].ParameterType.GetTypeInfo().IsSubclassOf(slackMessage)) + { + Type t = parameters[0].ParameterType; + foreach (SlackSocketRouting route in t.GetTypeInfo().GetCustomAttributes()) + { + Type genericAction = typeof(Action<>).MakeGenericType(parameters[0].ParameterType); + Delegate d = m.CreateDelegate(genericAction, routingTo); + if (d == null) + { + System.Diagnostics.Debug.WriteLine(string.Format("Couldn't create delegate for {0}.{1}", routingToType.FullName, m.Name)); + continue; + } + if (!routes.ContainsKey(route.Type)) + routes.Add(route.Type, new Dictionary()); + if (!routes[route.Type].ContainsKey(route.SubType ?? "null")) + routes[route.Type].Add(route.SubType ?? "null", d); + else + routes[route.Type][route.SubType ?? "null"] = Delegate.Combine(routes[route.Type][route.SubType ?? "null"], d); + } + } + } + } + + public void Send(SlackSocketMessage message, Action callback) + where K : SlackSocketMessage + { + int sendingId = Interlocked.Increment(ref currentId); + message.id = sendingId; + callbacks.Add(sendingId, (c) => + { + K obj = c.Deserialize(); + callback(obj); + }); + Send(message); + } + + public void Send(SlackSocketMessage message) + { + if (message.id == 0) + message.id = Interlocked.Increment(ref currentId); + //socket.Send(JsonConvert.SerializeObject(message)); + + if (string.IsNullOrEmpty(message.type)){ + IEnumerable routes = message.GetType().GetTypeInfo().GetCustomAttributes(); + + SlackSocketRouting route = null; + foreach (SlackSocketRouting r in routes) + { + route = r; + } + if (route == null) throw new InvalidProgramException("Cannot send without a proper route!"); + else + { + message.type = route.Type; + message.subtype = route.SubType; + } + } + + sendingQueue.Push(JsonConvert.SerializeObject(message, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); + if (Interlocked.CompareExchange(ref currentlySending, 1, 0) == 0) + Task.Factory.StartNew(HandleSending); + } + + public void BindCallback(Action callback) + { + Type t = typeof(K); + + foreach (SlackSocketRouting route in t.GetTypeInfo().GetCustomAttributes()) + { + if (!routes.ContainsKey(route.Type)) + routes.Add(route.Type, new Dictionary()); + if (!routes[route.Type].ContainsKey(route.SubType ?? "null")) + routes[route.Type].Add(route.SubType ?? "null", callback); + else + routes[route.Type][route.SubType ?? "null"] = Delegate.Combine(routes[route.Type][route.SubType ?? "null"], callback); + } + } + + public void UnbindCallback(Action callback) + { + Type t = typeof(K); + foreach (SlackSocketRouting route in t.GetTypeInfo().GetCustomAttributes()) + { + Delegate d = routes.ContainsKey(route.Type) ? (routes.ContainsKey(route.SubType ?? "null") ? routes[route.Type][route.SubType ?? "null"] : null) : null; + if (d != null) + { + Delegate newd = Delegate.Remove(d, callback); + routes[route.Type][route.SubType ?? "null"] = newd; + } + } + } + + void SetupReceiving() + { + Task.Factory.StartNew( + async () => + { + List buffers = new List(); + byte[] bytes = new byte[1024]; + buffers.Add(bytes); + ArraySegment buffer = new ArraySegment(bytes); + while (socket.State == WebSocketState.Open) + { + WebSocketReceiveResult result = null; + try + { + result = await socket.ReceiveAsync(buffer, cts.Token).ConfigureAwait(false); + } + catch (WebSocketException wex) + { + if (ErrorReceiving != null) + ErrorReceiving(wex); + Close(); + break; + } + + if (!result.EndOfMessage && buffer.Count == buffer.Array.Length) + { + bytes = new byte[1024]; + buffers.Add(bytes); + buffer = new ArraySegment(bytes); + continue; + } + + string data = string.Join("", buffers.Select((c) => Encoding.UTF8.GetString(c).TrimEnd('\0'))); + //Console.WriteLine("SlackSocket data = " + data); + SlackSocketMessage message = null; + try + { + message = data.Deserialize(); + } + catch (JsonException jsonExcep) + { + if (ErrorReceivingDesiralization != null) + ErrorReceivingDesiralization(jsonExcep); + continue; + } + + if (message == null) + continue; + else + { + HandleMessage(message, data); + buffers = new List(); + bytes = new byte[1024]; + buffers.Add(bytes); + buffer = new ArraySegment(bytes); + } + } + }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + + void HandleMessage(SlackSocketMessage message, string data) + { + if (callbacks.ContainsKey(message.reply_to)) + callbacks[message.reply_to](data); + else if (routes.ContainsKey(message.type) && routes[message.type].ContainsKey(message.subtype ?? "null")) + { + try + { + object o = null; + if (routing.ContainsKey(message.type) && + routing[message.type].ContainsKey(message.subtype ?? "null")) + o = data.Deserialize(routing[message.type][message.subtype ?? "null"]); + else + { + //I believe this method is slower than the former. If I'm wrong we can just use this instead. :D + Type t = routes[message.type][message.subtype ?? "null"].GetMethodInfo().GetParameters()[0].ParameterType; + o = data.Deserialize(t); + } + routes[message.type][message.subtype ?? "null"].DynamicInvoke(o); + } + catch (Exception e) + { + if (ErrorHandlingMessage != null) + ErrorHandlingMessage(e); + throw e; + } + } + else + { + System.Diagnostics.Debug.WriteLine(string.Format("No valid route for {0} - {1}", message.type, message.subtype ?? "null")); + if (ErrorHandlingMessage != null) + ErrorHandlingMessage(new InvalidDataException(string.Format("No valid route for {0} - {1}", message.type, message.subtype ?? "null"))); + } + } + + void HandleSending() + { + string message; + while (sendingQueue.Pop(out message) && socket.State == WebSocketState.Open && !cts.Token.IsCancellationRequested) + { + byte[] sending = Encoding.UTF8.GetBytes(message); + ArraySegment buffer = new ArraySegment(sending); + try + { + socket.SendAsync(buffer, WebSocketMessageType.Text, true, cts.Token).Wait(); + } + catch (WebSocketException wex) + { + if (ErrorSending != null) + ErrorSending(wex); + Close(); + break; + } + } + + currentlySending = 0; + } + + public void Close() + { + try + { + this.socket.Abort(); + } + catch (Exception) + { + // ignored + } + + if (Interlocked.CompareExchange(ref closedEmitted, 1, 0) == 0 && ConnectionClosed != null) + ConnectionClosed(); + } + } + + public class SlackSocketMessage + { + public int id; + public int reply_to; + public string type; + public string subtype; + public bool ok = true; + public Error error; + } + + public class Error + { + public int code; + public string msg; + } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] + public class SlackSocketRouting : Attribute + { + public string Type; + public string SubType; + public SlackSocketRouting(string type, string subtype = null) + { + this.Type = type; + this.SubType = subtype; + } + } +} \ No newline at end of file diff --git a/SlackAPI/SlackSocketClient.cs b/SlackAPI/SlackSocketClient.cs new file mode 100644 index 00000000..687ae4a7 --- /dev/null +++ b/SlackAPI/SlackSocketClient.cs @@ -0,0 +1,266 @@ +using System.Net.WebSockets; +using System; +using System.Linq; +using System.Net; +using SlackAPI.WebSocketMessages; + +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; + + public long PingRoundTripMilliseconds { get; private set; } + public bool IsReady { get { return HelloReceived; } } + public bool IsConnected { get { return underlyingSocket != null && underlyingSocket.Connected; } } + + public event Action OnHello; + private LoginResponse loginDetails; + + 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) + { + base.Connect((s) => { + if (s.ok) + ConnectSocket(onSocketConnected); + + onConnected(s); + }); + } + + protected override void Connected(LoginResponse loginDetails) + { + this.loginDetails = loginDetails; + base.Connected(loginDetails); + } + + public void ConnectSocket(Action onSocketConnected){ + underlyingSocket = new SlackSocket(loginDetails, this, onSocketConnected, this.proxySettings); + underlyingSocket.ConnectionClosed += UnderlyingSocket_ConnectionClosed; + } + + private void UnderlyingSocket_ConnectionClosed() + { + OnConnectionLost?.Invoke(); + } + + public void ErrorReceiving(Action callback) + { + if (callback != null) underlyingSocket.ErrorReceiving += callback; + } + + public void ErrorReceivingDesiralization(Action callback) + { + if (callback != null) underlyingSocket.ErrorReceivingDesiralization += callback; + } + + public void ErrorHandlingMessage(Action callback) + { + if (callback != null) underlyingSocket.ErrorHandlingMessage += callback; + } + + public void BindCallback(Action callback) + { + underlyingSocket.BindCallback(callback); + } + + public void UnbindCallback(Action callback) + { + underlyingSocket.UnbindCallback(callback); + } + + public void SendPresence(Presence status) + { + underlyingSocket.Send(new PresenceChange() { presence = status, user = base.MySelf.id }); + } + + public void SendTyping(string channelId) + { + underlyingSocket.Send(new Typing() { channel = channelId }); + } + + public void SendMessage(Action onSent, string channelId, string textData, string userName = null) + { + if (userName == null) + { + userName = MySelf.id; + } + + if (onSent != null) { + underlyingSocket.Send( new Message() {channel = channelId, text = textData, user = userName, type = "message"}, onSent); + } else { + underlyingSocket.Send(new Message() { channel = channelId, text = textData, user = userName, type = "message" }); + } + } + + public void SendPing() + { + underlyingSocket.Send(new Ping()); + } + + public void SubscribePresenceChange(params string[] usersIds) + { + underlyingSocket.Send(new PresenceChangeSubscription(usersIds)); + } + + public void HandlePongReceived(Pong pong) + { + if (OnPongReceived != null) + OnPongReceived(pong); + } + + public void HandleReactionAdded(ReactionAdded reactionAdded) + { + if (OnReactionAdded != null) + OnReactionAdded(reactionAdded); + } + + public void HandleHello(Hello hello) + { + if (maintainPresenceChanges) + { + // Subscribe presence change event for all the users on startup to maintain status in the the lookup table + SubscribePresenceChange(UserLookup.Keys.ToArray()); + } + + HelloReceived = true; + + if (OnHello != null) + OnHello(); + } + + public void HandlePresence(PresenceChange change) + { + UserLookup[change.user].presence = change.presence.ToString().ToLower(); + } + + public void HandleManualPresence(ManualPresenceChange change) + { + change.user = MySelf.id; + HandlePresence(change); + } + + public void HandleUserChange(UserChange change) + { + UserLookup[change.user.id] = change.user; + } + + public void HandleTeamJoin(TeamJoin newuser) + { + UserLookup.Add(newuser.user.id, newuser.user); + } + + public void HandleChannelCreated(ChannelCreated created) + { + ChannelLookup.Add(created.channel.id, created.channel); + } + + public void HandleChannelRename(ChannelRename rename) + { + ChannelLookup[rename.channel.id].name = rename.channel.name; + } + + public void HandleChannelDeleted(ChannelDeleted deleted) + { + ChannelLookup.Remove(deleted.channel); + } + + public void HandleChannelArchive(ChannelArchive archive) + { + ChannelLookup[archive.channel].is_archived = true; + } + + public void HandleChannelUnarchive(ChannelUnarchive unarchive) + { + ChannelLookup[unarchive.channel].is_archived = false; + } + + public void HandleGroupJoined(GroupJoined joined) + { + GroupLookup.Add(joined.channel.id, joined.channel); + } + + public void HandleGroupLeft(GroupLeft left) + { + GroupLookup.Remove(left.channel); + } + + public void HandleGroupOpen(GroupOpen open) + { + GroupLookup[open.channel].is_open = true; + } + + public void HandleGroupClose(GroupClose close) + { + GroupLookup[close.channel].is_open = false; + } + + public void HandleGroupArchive(GroupArchive archive) + { + GroupLookup[archive.channel].is_archived = true; + } + + public void HandleGroupUnarchive(GroupUnarchive unarchive) + { + GroupLookup[unarchive.channel].is_archived = false; + } + + public void HandleGroupRename(GroupRename rename) + { + GroupLookup[rename.channel.id].name = rename.channel.name; + GroupLookup[rename.channel.id].created = rename.channel.created; + } + + public void UserTyping(Typing t) + { + + } + + public void Message(NewMessage m) + { + if (OnMessageReceived != null) + OnMessageReceived(m); + } + + public void FileShareMessage(FileShareMessage m) + { + Message(m); + } + + public void PresenceChange(PresenceChange p) + { + OnPresenceChanged?.Invoke(p); + } + + public void ManualPresenceChange(ManualPresenceChange p) + { + p.user = MySelf.id; + PresenceChange(p); + } + + public void ChannelMarked(ChannelMarked m) + { + + } + + public void CloseSocket() + { + underlyingSocket.Close(); + } + } +} diff --git a/SlackAPI/SlackTaskClient.cs b/SlackAPI/SlackTaskClient.cs new file mode 100644 index 00000000..fd65669e --- /dev/null +++ b/SlackAPI/SlackTaskClient.cs @@ -0,0 +1,905 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using SlackAPI.RPCMessages; + +namespace SlackAPI +{ + public class SlackTaskClient : SlackClientBase + { + private readonly string APIToken; + + public Self MySelf; + public User MyData; + public Team MyTeam; + + public List starredChannels; + + public List Users; + public List Channels; + public List Groups; + public List DirectMessages; + + public Dictionary UserLookup; + public Dictionary ChannelLookup; + public Dictionary GroupLookup; + public Dictionary DirectMessageLookup; + + public SlackTaskClient(string token) + { + APIToken = token; + } + + public SlackTaskClient(string token, IWebProxy proxySettings) + : base(proxySettings) + { + APIToken = token; + } + + public virtual async Task ConnectAsync() + { + var loginDetails = await EmitLoginAsync().ConfigureAwait(false); + if(loginDetails.ok) + Connected(loginDetails); + + return loginDetails; + } + + protected virtual void Connected(LoginResponse loginDetails) + { + MySelf = loginDetails.self; + MyData = loginDetails.users.First((c) => c.id == MySelf.id); + MyTeam = loginDetails.team; + + Users = new List(loginDetails.users.Where((c) => !c.deleted)); + Channels = new List(loginDetails.channels); + Groups = new List(loginDetails.groups); + DirectMessages = new List(loginDetails.ims.Where((c) => Users.Exists((a) => a.id == c.user) && c.id != MySelf.id)); + starredChannels = + Groups.Where((c) => c.is_starred).Select((c) => c.id) + .Union( + DirectMessages.Where((c) => c.is_starred).Select((c) => c.user) + ).Union( + Channels.Where((c) => c.is_starred).Select((c) => c.id) + ).ToList(); + + UserLookup = new Dictionary(); + foreach (User u in Users) UserLookup.Add(u.id, u); + + ChannelLookup = new Dictionary(); + foreach (Channel c in Channels) ChannelLookup.Add(c.id, c); + + GroupLookup = new Dictionary(); + foreach (Channel g in Groups) GroupLookup.Add(g.id, g); + + DirectMessageLookup = new Dictionary(); + foreach (DirectMessageConversation im in DirectMessages) DirectMessageLookup.Add(im.id, im); + } + + public Task APIRequestWithTokenAsync() + where K : Response + { + return APIRequestWithTokenAsync(new Tuple[] { }); + } + + public Task APIRequestWithTokenAsync(params Tuple[] postParameters) + where K : Response + { + return APIRequestAsync(new Tuple[] { }, postParameters, APIToken); + } + + public Task TestAuthAsync() + { + return APIRequestWithTokenAsync(); + } + + public Task GetUserListAsync(int limit = 0, bool include_locale = false, string cursor = null, string team_id = null) + { + if (limit < 0) + { + throw new ArgumentException(nameof(limit)); + } + var args = new List>(); + args.Add(new Tuple("limit", limit.ToString())); + args.Add(new Tuple("include_locale", include_locale.ToString())); + if (cursor != null) + { + args.Add(new Tuple("cursor", cursor)); + } + if (team_id != null) + { + args.Add(new Tuple("team_id", team_id)); + } + return APIRequestWithTokenAsync(args.ToArray()); + } + + public Task GetUserByEmailAsync(string email) + { + return APIRequestWithTokenAsync(new Tuple("email", email)); + } + + public Task ChannelsCreateAsync(string name) { + return APIRequestWithTokenAsync(new Tuple("name", name)); + } + + public Task ChannelsInviteAsync(string userId, string channelId) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("user", userId)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task GetConversationsListAsync(string cursor = "", bool ExcludeArchived = true, int limit = 100, string[] types = null) + { + List> parameters = new List>() + { + Tuple.Create("exclude_archived", ExcludeArchived ? "1" : "0") + }; + if (limit > 0) + parameters.Add(Tuple.Create("limit", limit.ToString())); + if (types != null && types.Any()) + parameters.Add(Tuple.Create("types", string.Join(",", types))); + if (!string.IsNullOrEmpty(cursor)) + parameters.Add(new Tuple("cursor", cursor)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task GetConversationsMembersAsync(string channelId, string cursor = "", int limit = 100) + { + List> parameters = new List> + { + new Tuple("channel", channelId) + }; + if (limit > 0) + parameters.Add(Tuple.Create("limit", limit.ToString())); + if (!string.IsNullOrEmpty(cursor)) + parameters.Add(new Tuple("cursor", cursor)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task GetChannelListAsync(bool ExcludeArchived = true) + { + return APIRequestWithTokenAsync(new Tuple("exclude_archived", ExcludeArchived ? "1" : "0")); + } + + public Task GetGroupsListAsync(bool ExcludeArchived = true) + { + return APIRequestWithTokenAsync(new Tuple("exclude_archived", ExcludeArchived ? "1" : "0")); + } + + public Task GetDirectMessageListAsync() + { + return APIRequestWithTokenAsync(); + } + + public Task GetFilesAsync(string userId = null, DateTime? from = null, DateTime? to = null, int? count = null, int? page = null, FileTypes types = FileTypes.all) + { + List> parameters = new List>(); + + if (!string.IsNullOrEmpty(userId)) + parameters.Add(new Tuple("user", userId)); + + if (from.HasValue) + parameters.Add(new Tuple("ts_from", from.Value.ToProperTimeStamp())); + + if (to.HasValue) + parameters.Add(new Tuple("ts_to", to.Value.ToProperTimeStamp())); + + if (!types.HasFlag(FileTypes.all)) + { + FileTypes[] values = (FileTypes[])Enum.GetValues(typeof(FileTypes)); + + StringBuilder building = new StringBuilder(); + bool first = true; + for (int i = 0; i < values.Length; ++i) + { + if (types.HasFlag(values[i])) + { + if (!first) building.Append(","); + + building.Append(values[i].ToString()); + + first = false; + } + } + + if (building.Length > 0) + parameters.Add(new Tuple("types", building.ToString())); + } + + if (count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + + if (page.HasValue) + parameters.Add(new Tuple("page", page.Value.ToString())); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + private Task GetHistoryAsync(string channel, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) + where K : MessageHistory + { + List> parameters = new List>(); + parameters.Add(new Tuple("channel", channel)); + + if(latest.HasValue) + parameters.Add(new Tuple("latest", latest.Value.ToProperTimeStamp())); + if(oldest.HasValue) + parameters.Add(new Tuple("oldest", oldest.Value.ToProperTimeStamp())); + if (count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + if (unreads.HasValue) + parameters.Add(new Tuple("unreads", unreads.Value ? "1" : "0")); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task GetChannelHistoryAsync(Channel channelInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) + { + return GetHistoryAsync(channelInfo.id, latest, oldest, count, unreads); + } + + public Task GetDirectMessageHistoryAsync(DirectMessageConversation conversationInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) + { + return GetHistoryAsync(conversationInfo.id, latest, oldest, count, unreads); + } + + public Task GetGroupHistoryAsync(Channel groupInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) + { + return GetHistoryAsync(groupInfo.id, latest, oldest, count, unreads); + } + + public Task GetConversationsHistoryAsync(Channel conversationInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null, bool? unreads = false) + { + return GetHistoryAsync(conversationInfo.id, latest, oldest, count, unreads); + } + + public Task MarkChannelAsync(string channelId, DateTime ts) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId), + new Tuple("ts", ts.ToProperTimeStamp()) + ); + } + + public Task GetFileInfoAsync(string fileId, int? page = null, int? count = null) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("file", fileId)); + + if(count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + + if (page.HasValue) + parameters.Add(new Tuple("page", page.Value.ToString())); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + #region Groups + public Task GroupsArchiveAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task GroupsCloseAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task GroupsCreateAsync(string name) + { + return APIRequestWithTokenAsync(new Tuple("name", name)); + } + + public Task GroupsCreateChildAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task GroupsInviteAsync(string userId, string channelId) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("user", userId)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task GroupsKickAsync(string userId, string channelId) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("user", userId)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task GroupsLeaveAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task GroupsMarkAsync(string channelId, DateTime ts) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId), new Tuple("ts", ts.ToProperTimeStamp())); + } + + public Task GroupsOpenAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task GroupsRenameAsync(string channelId, string name) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("name", name)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task GroupsSetPurposeAsync(string channelId, string purpose) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("purpose", purpose)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task GroupsSetTopicAsync(string channelId, string topic) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("topic", topic)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task GroupsUnarchiveAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + #endregion + + #region Conversations + public Task ConversationsArchiveAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task ConversationsCloseAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task ConversationsCreateAsync(string name, bool? isPrivate = null, string teamId = null) + { + List> parameters = new List>(); + parameters.Add(new Tuple("name", name)); + + if (isPrivate.HasValue) + parameters.Add(new Tuple("is_private", isPrivate.Value ? "true" : "false")); + + if (!string.IsNullOrEmpty(teamId)) + parameters.Add(new Tuple("team_id", teamId)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsInviteAsync(string channelId, string[] userIds) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("users", string.Join(",", userIds))); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsJoinAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task ConversationsKickAsync(string channelId, string userId) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("user", userId)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsLeaveAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task ConversationsMarkAsync(string channelId, DateTime ts) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("ts", ts.ToProperTimeStamp())); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsOpenAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + public Task ConversationsOpenAsync(string[] userIds) + { + return APIRequestWithTokenAsync(new Tuple("users", string.Join(",", userIds))); + } + + public Task ConversationsRenameAsync(string channelId, string name) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("name", name)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsSetPurposeAsync(string channelId, string purpose) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("purpose", purpose)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsSetTopicAsync(string channelId, string topic) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("topic", topic)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task ConversationsUnarchiveAsync(string channelId) + { + return APIRequestWithTokenAsync(new Tuple("channel", channelId)); + } + + #endregion + + public Task SearchAllAsync(string query, string sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) + { + List> parameters = new List>(); + parameters.Add(new Tuple("query", query)); + + if (sorting != null) + parameters.Add(new Tuple("sort", sorting)); + + if (direction.HasValue) + parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); + + if (enableHighlights) + parameters.Add(new Tuple("highlight", "1")); + + if (count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + + if (page.HasValue) + parameters.Add(new Tuple("page", page.Value.ToString())); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task SearchMessagesAsync(string query, string sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) + { + List> parameters = new List>(); + parameters.Add(new Tuple("query", query)); + + if (sorting != null) + parameters.Add(new Tuple("sort", sorting)); + + if (direction.HasValue) + parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); + + if (enableHighlights) + parameters.Add(new Tuple("highlight", "1")); + + if (count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + + if (page.HasValue) + parameters.Add(new Tuple("page", page.Value.ToString())); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task SearchFilesAsync(string query, string sorting = null, SearchSortDirection? direction = null, bool enableHighlights = false, int? count = null, int? page = null) + { + List> parameters = new List>(); + parameters.Add(new Tuple("query", query)); + + if (sorting != null) + parameters.Add(new Tuple("sort", sorting)); + + if (direction.HasValue) + parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); + + if (enableHighlights) + parameters.Add(new Tuple("highlight", "1")); + + if (count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + + if (page.HasValue) + parameters.Add(new Tuple("page", page.Value.ToString())); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task GetStarsAsync(string userId = null, int? count = null, int? page = null){ + List> parameters = new List>(); + + if(!string.IsNullOrEmpty(userId)) + parameters.Add(new Tuple("user", userId)); + + if(count.HasValue) + parameters.Add(new Tuple("count", count.Value.ToString())); + + if(page.HasValue) + parameters.Add(new Tuple("page", page.Value.ToString())); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task DeleteMessageAsync(string channelId, DateTime ts) + { + List> parameters = new List>() + { + new Tuple("ts", ts.ToProperTimeStamp()), + new Tuple("channel", channelId) + }; + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task EmitPresence(Presence status) + { + return APIRequestWithTokenAsync(new Tuple("presence", status.ToString())); + } + + public Task GetPreferencesAsync() + { + return APIRequestWithTokenAsync(); + } + + public Task GetCountsAsync() + { + return APIRequestWithTokenAsync(); + } + + public Task EmitLoginAsync(string agent = "Inumedia.SlackAPI") + { + return APIRequestWithTokenAsync(new Tuple("agent", agent)); + } + public Task UpdateAsync(string ts, + string channelId, + string text, + string botName = null, + string parse = null, + bool linkNames = false, + Attachment[] attachments = null, + bool? as_user = null, + IBlock[] blocks = null) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("ts", ts)); + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("text", text)); + + if (!string.IsNullOrEmpty(botName)) + parameters.Add(new Tuple("username", botName)); + + if (!string.IsNullOrEmpty(parse)) + parameters.Add(new Tuple("parse", parse)); + + if (linkNames) + parameters.Add(new Tuple("link_names", "1")); + + if (attachments != null && attachments.Length > 0) + parameters.Add(new Tuple("attachments", JsonConvert.SerializeObject(attachments))); + + if (as_user.HasValue) + parameters.Add(new Tuple("as_user", as_user.ToString())); + + if (blocks != null && blocks.Length > 0) + parameters.Add(new Tuple("blocks", JsonConvert.SerializeObject(blocks, + new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore + }))); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task JoinDirectMessageChannelAsync(string user) + { + var param = new Tuple("users", user); + return APIRequestWithTokenAsync(param); + } + + public Task PostMessageAsync( + string channelId, + string text, + 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)); + + if(!string.IsNullOrEmpty(botName)) + parameters.Add(new Tuple("username", botName)); + + if (!string.IsNullOrEmpty(parse)) + parameters.Add(new Tuple("parse", parse)); + + if (linkNames) + parameters.Add(new Tuple("link_names", "1")); + + if (blocks != null && blocks.Length > 0) + parameters.Add(new Tuple("blocks", JsonConvert.SerializeObject(blocks, + new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore + }))); + + if (attachments != null && attachments.Length > 0) + parameters.Add(new Tuple("attachments", JsonConvert.SerializeObject(attachments, + new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore + }))); + + if (unfurl_links.HasValue) + parameters.Add(new Tuple("unfurl_links", unfurl_links.Value ? "true" : "false")); + + if (!string.IsNullOrEmpty(icon_url)) + parameters.Add(new Tuple("icon_url", icon_url)); + + if (!string.IsNullOrEmpty(icon_emoji)) + parameters.Add(new Tuple("icon_emoji", icon_emoji)); + + if (as_user.HasValue) + parameters.Add(new Tuple("as_user", as_user.ToString())); + + if (!string.IsNullOrEmpty(thread_ts)) + parameters.Add(new Tuple("thread_ts", thread_ts)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task PostEphemeralMessageAsync( + string channelId, + string text, + string targetuser, + string parse = null, + bool linkNames = false, + Attachment[] attachments = null, + bool as_user = false, + string thread_ts = null) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("text", text)); + parameters.Add(new Tuple("user", targetuser)); + + if (!string.IsNullOrEmpty(parse)) + parameters.Add(new Tuple("parse", parse)); + + if (linkNames) + parameters.Add(new Tuple("link_names", "1")); + + if (attachments != null && attachments.Length > 0) + parameters.Add(new Tuple("attachments", + JsonConvert.SerializeObject(attachments, Formatting.None, + new JsonSerializerSettings // Shouldn't include a not set property + { + NullValueHandling = NullValueHandling.Ignore + }))); + + parameters.Add(new Tuple("as_user", as_user.ToString())); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + + public Task ScheduleMessageAsync( + string channelId, + string text, + DateTime post_at, + string botName = null, + string parse = null, + bool linkNames = false, + IBlock[] blocks = null, + Attachment[] attachments = null, + bool? unfurl_links = null, + string icon_url = null, + string icon_emoji = null, + bool as_user = false, + string thread_ts = null) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("channel", channelId)); + parameters.Add(new Tuple("text", text)); + parameters.Add(new Tuple("post_at", Convert.ToUInt64((post_at - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds).ToString())); + + if (!string.IsNullOrEmpty(botName)) + parameters.Add(new Tuple("username", botName)); + + if (!string.IsNullOrEmpty(parse)) + parameters.Add(new Tuple("parse", parse)); + + if (linkNames) + parameters.Add(new Tuple("link_names", "1")); + + if (blocks != null && blocks.Length > 0) + parameters.Add(new Tuple("blocks", JsonConvert.SerializeObject(blocks, + new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore + }))); + + if (attachments != null && attachments.Length > 0) + parameters.Add(new Tuple("attachments", JsonConvert.SerializeObject(attachments, + new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore + }))); + + if (unfurl_links.HasValue) + parameters.Add(new Tuple("unfurl_links", unfurl_links.Value ? "true" : "false")); + + if (!string.IsNullOrEmpty(icon_url)) + parameters.Add(new Tuple("icon_url", icon_url)); + + if (!string.IsNullOrEmpty(icon_emoji)) + parameters.Add(new Tuple("icon_emoji", icon_emoji)); + + if (as_user) + parameters.Add(new Tuple("as_user", true.ToString())); + + if (!string.IsNullOrEmpty(thread_ts)) + parameters.Add(new Tuple("thread_ts", thread_ts)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task AddReactionAsync( + string name = null, + string channel = null, + string timestamp = null) + { + List> parameters = new List>(); + + if (!string.IsNullOrEmpty(name)) + parameters.Add(new Tuple("name", name)); + + if (!string.IsNullOrEmpty(channel)) + parameters.Add(new Tuple("channel", channel)); + + if (!string.IsNullOrEmpty(timestamp)) + parameters.Add(new Tuple("timestamp", timestamp)); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public Task DialogOpenAsync( + string triggerId, + Dialog dialog) + { + List> parameters = new List>(); + + parameters.Add(new Tuple("trigger_id", triggerId)); + + parameters.Add(new Tuple("dialog", + JsonConvert.SerializeObject(dialog, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }))); + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + + public async Task UploadFileAsync(byte[] fileData, string fileName, string[] channelIds, string title = null, string initialComment = null, bool useAsync = false, string fileType = null) + { + Uri target = new Uri(Path.Combine(APIBaseLocation, useAsync ? "files.uploadAsync" : "files.upload")); + + List parameters = new List(); + + //File/Content + if (!string.IsNullOrEmpty(fileType)) + parameters.Add(string.Format("{0}={1}", "filetype", fileType)); + + if (!string.IsNullOrEmpty(fileName)) + parameters.Add(string.Format("{0}={1}", "filename", fileName)); + + if (!string.IsNullOrEmpty(title)) + parameters.Add(string.Format("{0}={1}", "title", title)); + + if (!string.IsNullOrEmpty(initialComment)) + parameters.Add(string.Format("{0}={1}", "initial_comment", initialComment)); + + parameters.Add(string.Format("{0}={1}", "channels", string.Join(",", channelIds))); + + using (MultipartFormDataContent form = new MultipartFormDataContent()) + { + form.Add(new ByteArrayContent(fileData), "file", fileName); + HttpResponseMessage response = await PostRequestAsync(string.Format("{0}?{1}", target, string.Join("&", parameters.ToArray())), form, APIToken); + string result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + return result.Deserialize(); + } + } + + public Task ChannelSetTopicAsync(string channelId, string newTopic) + { + return APIRequestWithTokenAsync( + new Tuple("channel", channelId), + new Tuple("topic", newTopic)); + } + + public Task PublishAppHomeTab( + string userId, + View view) + { + view.type = ViewTypes.Home; + var parameters = new List> + { + new Tuple("user_id", userId), + new Tuple("view", JsonConvert.SerializeObject(view, Formatting.None, + new JsonSerializerSettings // Shouldn't include a not set property + { + NullValueHandling = NullValueHandling.Ignore + })) + }; + + return APIRequestWithTokenAsync(parameters.ToArray()); + } + } +} diff --git a/Team.cs b/SlackAPI/Team.cs similarity index 88% rename from Team.cs rename to SlackAPI/Team.cs index e145089e..e0e1a318 100644 --- a/Team.cs +++ b/SlackAPI/Team.cs @@ -4,21 +4,34 @@ namespace SlackAPI { public class Team { - [Newtonsoft.Json.JsonConverter(typeof(JavascriptBotsToArray))] - public Bot[] bots; public string domain; /// /// Supported domains emails can be registered from. /// + /// TODO: Is this obsolete? public string email_domain; + /// + /// Supported domains emails can be registered from. + /// + public string[] email_domains; public string id; public long limit_ts; public DateTime LimitTimestamp { get { return new DateTime(1970, 1, 1).AddMilliseconds(limit_ts); } } public int msg_edit_window_mins; public string name; - public bool over_storage_limit; + public bool over_storage_limit, sso; public TeamPreferences prefs; + public string sso_required; + public string sso_type; + public string url; + public SSOProvider[] sso_provider; + } + + public class SSOProvider + { + public string name; + public string type; } public class BotList diff --git a/TeamPreferences.cs b/SlackAPI/TeamPreferences.cs similarity index 67% rename from TeamPreferences.cs rename to SlackAPI/TeamPreferences.cs index 2b641d66..e157d951 100644 --- a/TeamPreferences.cs +++ b/SlackAPI/TeamPreferences.cs @@ -11,17 +11,19 @@ public class TeamPreferences public AuthMode auth_mode; public string[] default_channels; public bool display_real_names; - public int gateway_allow_irc_plain; - public int gateway_allow_irc_ssl; - public int gateway_allow_xmpp_ssl; + public bool gateway_allow_irc_plain; + public bool gateway_allow_irc_ssl; + public bool gateway_allow_xmpp_ssl; public bool hide_referers; public int msg_edit_window_mins; public bool srvices_only_admins; - public bool stats_only_admins; + public bool? stats_only_admins; public enum AuthMode { - normal + normal, + saml, + google } } } diff --git a/User.cs b/SlackAPI/User.cs similarity index 63% rename from User.cs rename to SlackAPI/User.cs index e0aaf39a..eba1f1a3 100644 --- a/User.cs +++ b/SlackAPI/User.cs @@ -22,7 +22,18 @@ public bool IsSlackBot public UserProfile profile; public bool is_admin; public bool is_owner; + public bool is_primary_owner; + public bool is_restricted; + public bool is_ultra_restricted; + public bool has_2fa; + public string two_factor_type; public bool has_files; public string presence; + public bool is_bot; + public string tz; + public string tz_label; + public int tz_offset; + public string team_id; + public string real_name; } } diff --git a/SlackAPI/UserProfile.cs b/SlackAPI/UserProfile.cs new file mode 100644 index 00000000..6a3d6932 --- /dev/null +++ b/SlackAPI/UserProfile.cs @@ -0,0 +1,21 @@ +namespace SlackAPI +{ + public class UserProfile : ProfileIcons + { + public string title; + public string display_name; + public string first_name; + public string last_name; + public string real_name; + public string email; + public string skype; + public string status_emoji; + public string status_text; + public string phone; + + public override string ToString() + { + return real_name; + } + } +} diff --git a/UserTeamCombo.cs b/SlackAPI/UserTeamCombo.cs similarity index 100% rename from UserTeamCombo.cs rename to SlackAPI/UserTeamCombo.cs diff --git a/SlackAPI/Utilities/ILockFree.cs b/SlackAPI/Utilities/ILockFree.cs new file mode 100644 index 00000000..781e0739 --- /dev/null +++ b/SlackAPI/Utilities/ILockFree.cs @@ -0,0 +1,18 @@ +using System; + +namespace SlackAPI.Utilities +{ + public abstract class ILockFree where T : class + { + internal class SingleLinkNode + { + public SingleLinkNode Next; + public T Item; + } + + public abstract void Push(T pItem); + public virtual void Push(ILockFree pItems) { T obj; while (pItems.Pop(out obj)) Push(obj); } + public abstract bool Pop(out T pItem); + public abstract T Pop(); + } +} diff --git a/SlackAPI/Utilities/LockFreeQueue.cs b/SlackAPI/Utilities/LockFreeQueue.cs new file mode 100644 index 00000000..824c72e1 --- /dev/null +++ b/SlackAPI/Utilities/LockFreeQueue.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; + +namespace SlackAPI.Utilities +{ + public class LockFreeQueue : ILockFree + where T : class + { + private SingleLinkNode mHead; + private SingleLinkNode mTail; + public int Count; + public T Latest + { + get + { + return mHead.Next == null ? default(T) : mTail.Item; + } + } + + public LockFreeQueue() + { + mHead = new SingleLinkNode(); + mTail = mHead; + } + + private static bool CompareAndExchange(ref SingleLinkNode pLocation, SingleLinkNode pComparand, SingleLinkNode pNewValue) + { + return + pComparand == + Interlocked.CompareExchange(ref pLocation, pNewValue, pComparand); + } + + public T Next { get { return mHead.Next == null ? default(T) : mHead.Next.Item; } } + public void Unshift(T pItem) + { + SingleLinkNode oldHead = null; + + SingleLinkNode newNode = new SingleLinkNode(); + newNode.Item = pItem; + + bool newNodeWasAdded = false; + while (!newNodeWasAdded) + { + oldHead = mHead.Next; + newNode.Next = oldHead; + + if (mHead.Next == oldHead) + newNodeWasAdded = CompareAndExchange(ref mHead.Next, oldHead, newNode); + } + + CompareAndExchange(ref mHead, oldHead, newNode); + } + public override void Push(T pItem) + { + SingleLinkNode oldTail = null; + SingleLinkNode oldTailNext; + + SingleLinkNode newNode = new SingleLinkNode(); + newNode.Item = pItem; + + bool newNodeWasAdded = false; + while (!newNodeWasAdded) + { + oldTail = mTail; + oldTailNext = oldTail.Next; + + if (mTail == oldTail) + if (oldTailNext == null) + newNodeWasAdded = CompareAndExchange(ref mTail.Next, null, newNode); + else + CompareAndExchange(ref mTail, oldTail, oldTailNext); + } + + CompareAndExchange(ref mTail, oldTail, newNode); + Interlocked.Increment(ref Count); + } + + public override bool Pop(out T pItem) + { + pItem = default(T); + SingleLinkNode oldHead = null; + + bool haveAdvancedHead = false; + while (!haveAdvancedHead) + { + oldHead = mHead; + SingleLinkNode oldTail = mTail; + SingleLinkNode oldHeadNext = oldHead.Next; + + if (oldHead == mHead) + { + if (oldHead == oldTail) + { + if (oldHeadNext == null) + return false; + CompareAndExchange(ref mTail, oldTail, oldHeadNext); + } + + else + { + pItem = oldHeadNext.Item; + haveAdvancedHead = + CompareAndExchange(ref mHead, oldHead, oldHeadNext); + } + } + } + Interlocked.Decrement(ref Count); + return true; + } + + public T Shift() + { + T result; + Shift(out result); + return result; + } + + public bool Shift(out T pItem) + { + pItem = default(T); + if (mHead == null) + return false; + SingleLinkNode oldHead = null; + + bool haveAdvancedHead = false; + while (!haveAdvancedHead) + { + oldHead = mHead; + if (oldHead != null) + { + SingleLinkNode oldHeadNext = oldHead.Next; + if (CompareAndExchange(ref mHead, oldHead, oldHeadNext)) + { + pItem = oldHead.Item; + return true; + } + } + } + return false; + } + + public override T Pop() + { + T result; + Pop(out result); + return result; + } + + public override string ToString() + { + return String.Format("Item count: {0}", Count); + } + + public IEnumerator GetEnumerator() + { + return new LockFreeEnumerator(this); + } + + /// + /// Does *not* provide any kind of stateful guarantee. Should only be used in cases where we know that the queue is not volatile. + /// + internal class LockFreeEnumerator : IEnumerator + { + LockFreeQueue parent; + SingleLinkNode currentNode; + + T IEnumerator.Current + { + get + { + return currentNode.Item; + } + } + object IEnumerator.Current + { + get + { + return currentNode.Item; + } + } + + public bool MoveNext() + { + if (currentNode == null) + currentNode = parent.mHead.Next; + else + currentNode = currentNode.Next; + return currentNode != null; + } + + public LockFreeEnumerator(LockFreeQueue list) + { + parent = list; + } + + public void Dispose() + { + parent = null; + currentNode = null; + } + + public void Reset() + { + currentNode = parent.mHead.Next; + } + } + } +} diff --git a/SlackAPI/WebSocketMessages/ChannelArchive.cs b/SlackAPI/WebSocketMessages/ChannelArchive.cs new file mode 100644 index 00000000..72d503b7 --- /dev/null +++ b/SlackAPI/WebSocketMessages/ChannelArchive.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("channel_archive")] + public class ChannelArchive + { + public string channel; + public string user; + } +} diff --git a/SlackAPI/WebSocketMessages/ChannelCreated.cs b/SlackAPI/WebSocketMessages/ChannelCreated.cs new file mode 100644 index 00000000..eedd6083 --- /dev/null +++ b/SlackAPI/WebSocketMessages/ChannelCreated.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("channel_created")] + public class ChannelCreated + { + public Channel channel; + } +} diff --git a/SlackAPI/WebSocketMessages/ChannelDeleted.cs b/SlackAPI/WebSocketMessages/ChannelDeleted.cs new file mode 100644 index 00000000..509caa48 --- /dev/null +++ b/SlackAPI/WebSocketMessages/ChannelDeleted.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("channel_deleted")] + public class ChannelDeleted + { + public string channel; + } +} diff --git a/SlackAPI/WebSocketMessages/ChannelHistoryChanged.cs b/SlackAPI/WebSocketMessages/ChannelHistoryChanged.cs new file mode 100644 index 00000000..fb30be65 --- /dev/null +++ b/SlackAPI/WebSocketMessages/ChannelHistoryChanged.cs @@ -0,0 +1,10 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("channel_history_changed")] + public class ChannelHistoryChanged + { + public string latest; + public string ts; + public string event_ts; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/ChannelMarked.cs b/SlackAPI/WebSocketMessages/ChannelMarked.cs new file mode 100644 index 00000000..1dc92d20 --- /dev/null +++ b/SlackAPI/WebSocketMessages/ChannelMarked.cs @@ -0,0 +1,11 @@ +using System; + +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("channel_marked")] + public class ChannelMarked : SlackSocketMessage + { + public string channel; + public DateTime ts; + } +} diff --git a/SlackAPI/WebSocketMessages/ChannelRename.cs b/SlackAPI/WebSocketMessages/ChannelRename.cs new file mode 100644 index 00000000..fe197f6d --- /dev/null +++ b/SlackAPI/WebSocketMessages/ChannelRename.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("channel_rename")] + public class ChannelRename + { + public Channel channel; + } +} diff --git a/SlackAPI/WebSocketMessages/ChannelUnarchive.cs b/SlackAPI/WebSocketMessages/ChannelUnarchive.cs new file mode 100644 index 00000000..f91ff962 --- /dev/null +++ b/SlackAPI/WebSocketMessages/ChannelUnarchive.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("channel_unarchive")] + public class ChannelUnarchive + { + public string channel; + public string user; + } +} diff --git a/SlackAPI/WebSocketMessages/DeletedMessage.cs b/SlackAPI/WebSocketMessages/DeletedMessage.cs new file mode 100644 index 00000000..300808bc --- /dev/null +++ b/SlackAPI/WebSocketMessages/DeletedMessage.cs @@ -0,0 +1,13 @@ +using System; + +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("message", "message_deleted")] + public class DeletedMessage : SlackSocketMessage + { + public string channel; + public DateTime ts; + public DateTime deleted_ts; + public bool hidden; + } +} diff --git a/SlackAPI/WebSocketMessages/DndUpdatedUser.cs b/SlackAPI/WebSocketMessages/DndUpdatedUser.cs new file mode 100644 index 00000000..e9441365 --- /dev/null +++ b/SlackAPI/WebSocketMessages/DndUpdatedUser.cs @@ -0,0 +1,15 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("dnd_updated_user")] + public class DndUpdatedUser + { + public string user; + /* + dnd_status": { + "dnd_enabled": true, + "next_dnd_start_ts": 1450387800, + "next_dnd_end_ts": 1450423800 + } + */ + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/EmailDomainChanged.cs b/SlackAPI/WebSocketMessages/EmailDomainChanged.cs new file mode 100644 index 00000000..7f19be2e --- /dev/null +++ b/SlackAPI/WebSocketMessages/EmailDomainChanged.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("email_domain_changed")] + public class EmailDomainChanged + { + public string email_domain; + public string event_ts; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/EmojiChangedAdd.cs b/SlackAPI/WebSocketMessages/EmojiChangedAdd.cs new file mode 100644 index 00000000..36c2a1cd --- /dev/null +++ b/SlackAPI/WebSocketMessages/EmojiChangedAdd.cs @@ -0,0 +1,10 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("emoji_changed", "add")] + public class EmojiChangedAdd + { + public string name; + public string value; + public string event_ts; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/EmojiChangedRemove.cs b/SlackAPI/WebSocketMessages/EmojiChangedRemove.cs new file mode 100644 index 00000000..b8dd73bf --- /dev/null +++ b/SlackAPI/WebSocketMessages/EmojiChangedRemove.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("emoji_changed", "remove")] + public class EmojiChangedRemove + { + public string[] names; + public string event_ts; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/FileChange.cs b/SlackAPI/WebSocketMessages/FileChange.cs new file mode 100644 index 00000000..63a343e6 --- /dev/null +++ b/SlackAPI/WebSocketMessages/FileChange.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("file_change")] + public class FileChange + { + public File file; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/FileCommentDeleted.cs b/SlackAPI/WebSocketMessages/FileCommentDeleted.cs new file mode 100644 index 00000000..087c1c30 --- /dev/null +++ b/SlackAPI/WebSocketMessages/FileCommentDeleted.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("file_comment_deleted")] + public class FileCommentDeleted + { + public File file; + public string comment; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/FileCommentEdited.cs b/SlackAPI/WebSocketMessages/FileCommentEdited.cs new file mode 100644 index 00000000..6389fb7b --- /dev/null +++ b/SlackAPI/WebSocketMessages/FileCommentEdited.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("file_comment_edited")] + public class FileCommentEdited + { + public File file; + public string comment; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/FileCreated.cs b/SlackAPI/WebSocketMessages/FileCreated.cs new file mode 100644 index 00000000..35aff5ff --- /dev/null +++ b/SlackAPI/WebSocketMessages/FileCreated.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("file_created")] + public class FileCreated + { + public File file; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/FileDeleted.cs b/SlackAPI/WebSocketMessages/FileDeleted.cs new file mode 100644 index 00000000..303c1db7 --- /dev/null +++ b/SlackAPI/WebSocketMessages/FileDeleted.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("file_deleted")] + public class FileDeleted + { + public string file_id; + public string event_ts; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/FilePublic.cs b/SlackAPI/WebSocketMessages/FilePublic.cs new file mode 100644 index 00000000..8d0dfeb2 --- /dev/null +++ b/SlackAPI/WebSocketMessages/FilePublic.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("file_public")] + public class FilePublic + { + public File file; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/FileShareMessage.cs b/SlackAPI/WebSocketMessages/FileShareMessage.cs new file mode 100644 index 00000000..2918d68b --- /dev/null +++ b/SlackAPI/WebSocketMessages/FileShareMessage.cs @@ -0,0 +1,10 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("message", "file_share")] + public class FileShareMessage : NewMessage + { + public bool upload; + + public File file; + } +} diff --git a/SlackAPI/WebSocketMessages/FileUnshared.cs b/SlackAPI/WebSocketMessages/FileUnshared.cs new file mode 100644 index 00000000..e46a5d57 --- /dev/null +++ b/SlackAPI/WebSocketMessages/FileUnshared.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("file_unshared")] + public class FileUnshared + { + public File file; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/GroupArchive.cs b/SlackAPI/WebSocketMessages/GroupArchive.cs new file mode 100644 index 00000000..2689ace0 --- /dev/null +++ b/SlackAPI/WebSocketMessages/GroupArchive.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("group_archive")] + public class GroupArchive : SlackSocketMessage + { + public string channel; + } +} + diff --git a/SlackAPI/WebSocketMessages/GroupClose.cs b/SlackAPI/WebSocketMessages/GroupClose.cs new file mode 100644 index 00000000..9856a808 --- /dev/null +++ b/SlackAPI/WebSocketMessages/GroupClose.cs @@ -0,0 +1,10 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("group_close")] + public class GroupClose : SlackSocketMessage + { + public string user; + public string channel; + } +} + diff --git a/SlackAPI/WebSocketMessages/GroupHistoryChanged.cs b/SlackAPI/WebSocketMessages/GroupHistoryChanged.cs new file mode 100644 index 00000000..b5f40ddf --- /dev/null +++ b/SlackAPI/WebSocketMessages/GroupHistoryChanged.cs @@ -0,0 +1,10 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("group_history_changed")] + public class GroupHistoryChanged + { + public string latest; + public string ts; + public string event_ts; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/GroupJoined.cs b/SlackAPI/WebSocketMessages/GroupJoined.cs new file mode 100644 index 00000000..12518c9e --- /dev/null +++ b/SlackAPI/WebSocketMessages/GroupJoined.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("group_joined")] + public class GroupJoined : SlackSocketMessage + { + public Channel channel; + } +} + diff --git a/SlackAPI/WebSocketMessages/GroupLeft.cs b/SlackAPI/WebSocketMessages/GroupLeft.cs new file mode 100644 index 00000000..988bbd3c --- /dev/null +++ b/SlackAPI/WebSocketMessages/GroupLeft.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("group_left")] + public class GroupLeft : SlackSocketMessage + { + public string channel; + } +} + diff --git a/SlackAPI/WebSocketMessages/GroupOpen.cs b/SlackAPI/WebSocketMessages/GroupOpen.cs new file mode 100644 index 00000000..e86a7fe3 --- /dev/null +++ b/SlackAPI/WebSocketMessages/GroupOpen.cs @@ -0,0 +1,10 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("group_open")] + public class GroupOpen : SlackSocketMessage + { + public string user; + public string channel; + } +} + diff --git a/SlackAPI/WebSocketMessages/GroupRename.cs b/SlackAPI/WebSocketMessages/GroupRename.cs new file mode 100644 index 00000000..13a5a468 --- /dev/null +++ b/SlackAPI/WebSocketMessages/GroupRename.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("group_rename")] + public class GroupRename : SlackSocketMessage + { + public Channel channel; + } +} + diff --git a/SlackAPI/WebSocketMessages/GroupUnarchive.cs b/SlackAPI/WebSocketMessages/GroupUnarchive.cs new file mode 100644 index 00000000..37a69d55 --- /dev/null +++ b/SlackAPI/WebSocketMessages/GroupUnarchive.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("group_unarchive")] + public class GroupUnarchive : SlackSocketMessage + { + public string channel; + } +} + diff --git a/SlackAPI/WebSocketMessages/Hello.cs b/SlackAPI/WebSocketMessages/Hello.cs new file mode 100644 index 00000000..beb4825a --- /dev/null +++ b/SlackAPI/WebSocketMessages/Hello.cs @@ -0,0 +1,9 @@ +using System; + +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("hello")] + public class Hello : SlackSocketMessage + { + } +} diff --git a/SlackAPI/WebSocketMessages/ImClosed.cs b/SlackAPI/WebSocketMessages/ImClosed.cs new file mode 100644 index 00000000..06a4bdba --- /dev/null +++ b/SlackAPI/WebSocketMessages/ImClosed.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("im_close")] + public class ImClosed + { + public string user; + public string channel; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/ImHistoryChanged.cs b/SlackAPI/WebSocketMessages/ImHistoryChanged.cs new file mode 100644 index 00000000..592875c3 --- /dev/null +++ b/SlackAPI/WebSocketMessages/ImHistoryChanged.cs @@ -0,0 +1,10 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("im_history_changed")] + public class ImHistoryChanged + { + public string latest; + public string ts; + public string event_ts; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/ImOpen.cs b/SlackAPI/WebSocketMessages/ImOpen.cs new file mode 100644 index 00000000..83966b09 --- /dev/null +++ b/SlackAPI/WebSocketMessages/ImOpen.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("im_open")] + public class ImOpen + { + public string user; + public string channel; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/ManualPresenceChange.cs b/SlackAPI/WebSocketMessages/ManualPresenceChange.cs new file mode 100644 index 00000000..72b00771 --- /dev/null +++ b/SlackAPI/WebSocketMessages/ManualPresenceChange.cs @@ -0,0 +1,7 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("manual_presence_change")] + public class ManualPresenceChange : PresenceChange + { + } +} diff --git a/SlackAPI/WebSocketMessages/MessageReceived.cs b/SlackAPI/WebSocketMessages/MessageReceived.cs new file mode 100644 index 00000000..f2934051 --- /dev/null +++ b/SlackAPI/WebSocketMessages/MessageReceived.cs @@ -0,0 +1,10 @@ +using System; + +namespace SlackAPI.WebSocketMessages +{ + public class MessageReceived : SlackSocketMessage + { + public string text; + public DateTime ts; + } +} diff --git a/SlackAPI/WebSocketMessages/NewMessage.cs b/SlackAPI/WebSocketMessages/NewMessage.cs new file mode 100644 index 00000000..ac597887 --- /dev/null +++ b/SlackAPI/WebSocketMessages/NewMessage.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("message")] + [SlackSocketRouting("message", "bot_message")] + public class NewMessage : SlackSocketMessage + { + public string user; + public string channel; + public string text; + public string team; + public DateTime ts; + public DateTime thread_ts; + public string username; + public string bot_id; + public UserProfile icons; + public List blocks; + public List attachments; + + public NewMessage() + { + type = "message"; + } + } +} diff --git a/SlackAPI/WebSocketMessages/Ping.cs b/SlackAPI/WebSocketMessages/Ping.cs new file mode 100644 index 00000000..01322536 --- /dev/null +++ b/SlackAPI/WebSocketMessages/Ping.cs @@ -0,0 +1,10 @@ +using System; + +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("ping")] + public class Ping : SlackSocketMessage + { + public int ping_interv_ms = 3000; + } +} diff --git a/SlackAPI/WebSocketMessages/Pong.cs b/SlackAPI/WebSocketMessages/Pong.cs new file mode 100644 index 00000000..6979303f --- /dev/null +++ b/SlackAPI/WebSocketMessages/Pong.cs @@ -0,0 +1,10 @@ +using System; + +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("pong")] + public class Pong : SlackSocketMessage + { + public int ping_interv_ms; + } +} diff --git a/SlackAPI/WebSocketMessages/PresenceChange.cs b/SlackAPI/WebSocketMessages/PresenceChange.cs new file mode 100644 index 00000000..1cbb2e1c --- /dev/null +++ b/SlackAPI/WebSocketMessages/PresenceChange.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("presence_change")] + public class PresenceChange : SlackSocketMessage + { + public string user; + public Presence presence; + } +} diff --git a/SlackAPI/WebSocketMessages/PresenceChangeSubscription.cs b/SlackAPI/WebSocketMessages/PresenceChangeSubscription.cs new file mode 100644 index 00000000..7f533e11 --- /dev/null +++ b/SlackAPI/WebSocketMessages/PresenceChangeSubscription.cs @@ -0,0 +1,15 @@ +using System.Linq; + +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("presence_sub")] + public class PresenceChangeSubscription : SlackSocketMessage + { + public PresenceChangeSubscription(string[] usersIds) + { + this.ids = usersIds; + } + + public string[] ids { get; } + } +} diff --git a/SlackAPI/WebSocketMessages/ReactionAdded.cs b/SlackAPI/WebSocketMessages/ReactionAdded.cs new file mode 100644 index 00000000..277e721c --- /dev/null +++ b/SlackAPI/WebSocketMessages/ReactionAdded.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("reaction_added")] + public class ReactionAdded : SlackSocketMessage + { + public string user; + public string reaction; + public string item_user; + public Item item; + public string event_ts; + + public ReactionAdded(){} + } + + public class Item + { + public string type; + public string channel; + public string file; + public string file_comment; + public string ts; + } +} + diff --git a/SlackAPI/WebSocketMessages/SubteamSelfAdded.cs b/SlackAPI/WebSocketMessages/SubteamSelfAdded.cs new file mode 100644 index 00000000..5c0948c7 --- /dev/null +++ b/SlackAPI/WebSocketMessages/SubteamSelfAdded.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("subteam_self_added")] + public class SubteamSelfAdded + { + public string subteam_id; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/SubteamSelfRemoved.cs b/SlackAPI/WebSocketMessages/SubteamSelfRemoved.cs new file mode 100644 index 00000000..442642a7 --- /dev/null +++ b/SlackAPI/WebSocketMessages/SubteamSelfRemoved.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("subteam_self_removed")] + public class SubteamSelfRemoved + { + public string subteam_id; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/TeamDomainChange.cs b/SlackAPI/WebSocketMessages/TeamDomainChange.cs new file mode 100644 index 00000000..a48b41cd --- /dev/null +++ b/SlackAPI/WebSocketMessages/TeamDomainChange.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("team_domain_change")] + public class TeamDomainChange + { + public string url; + public string domain; + } +} \ No newline at end of file diff --git a/SlackAPI/WebSocketMessages/TeamJoin.cs b/SlackAPI/WebSocketMessages/TeamJoin.cs new file mode 100644 index 00000000..4a41e007 --- /dev/null +++ b/SlackAPI/WebSocketMessages/TeamJoin.cs @@ -0,0 +1,8 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("team_join")] + public class TeamJoin : SlackSocketMessage + { + public User user; + } +} diff --git a/SlackAPI/WebSocketMessages/Typing.cs b/SlackAPI/WebSocketMessages/Typing.cs new file mode 100644 index 00000000..29794096 --- /dev/null +++ b/SlackAPI/WebSocketMessages/Typing.cs @@ -0,0 +1,12 @@ +using System; + +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("typing")] + [SlackSocketRouting("user_typing")] + public class Typing : SlackSocketMessage + { + public string user; + public string channel; + } +} diff --git a/SlackAPI/WebSocketMessages/UserChange.cs b/SlackAPI/WebSocketMessages/UserChange.cs new file mode 100644 index 00000000..9bbac456 --- /dev/null +++ b/SlackAPI/WebSocketMessages/UserChange.cs @@ -0,0 +1,9 @@ +namespace SlackAPI.WebSocketMessages +{ + [SlackSocketRouting("user_change")] + public class UserChange : SlackSocketMessage + { + public User user; + } +} + diff --git a/SlackClient.cs b/SlackClient.cs deleted file mode 100644 index 70cfc218..00000000 --- a/SlackClient.cs +++ /dev/null @@ -1,619 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using WebSocket4Net; -using System.Text; -using System.Threading; - -namespace SlackAPI -{ - public class SlackClient - { - readonly string APIToken; - bool authWorks = false; - - const string APIBaseLocation = "https://slack.com/api/"; - const int Timeout = 5000; - - const char StartHighlight = '\uE001'; - const char EndHightlight = '\uE001'; - - static List> replacers = new List>(){ - new Tuple("&", "&"), - new Tuple("<", "<"), - new Tuple(">", ">") - }; - - public bool Connected { get { return MySelf != null && socket != null && socket.State == WebSocketState.Open; } } - public bool IsReady { get { return helloReceived == 1; } } - - int helloReceived; - int pingBlocking; - int currentId = 1; - Timer pingTimer; - WebSocket socket; - Dictionary> socketCallbacks; - - public Self MySelf; - public User MyData; - public Team MyTeam; - - public List starredChannels; - - public List Users; - public List Channels; - public List Groups; - public List DirectMessages; - - public Dictionary UserLookup; - public Dictionary ChannelLookup; - public Dictionary GroupLookup; - public Dictionary DirectMessageLookup; - - public event Action OnUserTyping; - public event Action OnMessageReceived; - public event Action OnPresenceChanged; - public event Action OnHello; - - public SlackClient(string token) - { - APIToken = token; - } - - public void Connect(Action onConnected) - { - EmitLogin((loginDetails) => - { - MySelf = loginDetails.self; - MyData = loginDetails.users.First((c) => c.id == MySelf.id); - MyTeam = loginDetails.team; - - Users = new List(loginDetails.users.Where((c) => !c.deleted)); - Channels = new List(loginDetails.channels); - Groups = new List(loginDetails.groups); - DirectMessages = new List(loginDetails.ims.Where((c) => Users.Exists((a) => a.id == c.user) && c.id != MySelf.id)); - starredChannels = - Groups.Where((c) => c.is_starred).Select((c) => c.id) - .Union( - DirectMessages.Where((c) => c.is_starred).Select((c) => c.user) - ).Union( - Channels.Where((c) => c.is_starred).Select((c) => c.id) - ).ToList(); - - UserLookup = new Dictionary(); - foreach (User u in Users) UserLookup.Add(u.id, u); - - ChannelLookup = new Dictionary(); - foreach (Channel c in Channels) ChannelLookup.Add(c.id, c); - - GroupLookup = new Dictionary(); - foreach (Channel g in Groups) GroupLookup.Add(g.id, g); - - DirectMessageLookup = new Dictionary(); - foreach (DirectMessageConversation im in DirectMessages) DirectMessageLookup.Add(im.id, im); - - //socket = new ClientWebSocket(); - //clientBuffer = ClientWebSocket.CreateClientBuffer(4096, 4096); - //socket.ConnectAsync(new Uri(string.Format("{0}?svn_rev={1}&login_with_boot_data-0-{2}&on_login-0-{2}&connect-1-{2}", loginDetails.url, loginDetails.svn_rev, DateTime.Now.Subtract(new DateTime(1970,1,1)).TotalSeconds)), token); - - //socket.SendAsync() - - //socket.ReceiveAsync(clientBuffer, token); - - socket = new WebSocket(string.Format("{0}?svn_rev={1}&login_with_boot_data-0-{2}&on_login-0-{2}&connect-1-{2}", loginDetails.url, loginDetails.svn_rev, DateTime.Now.Subtract(new DateTime(1970, 1, 1)).TotalSeconds)); - socket.MessageReceived += socket_MessageReceived; - socket.Closed += socket_Closed; - socket.Opened += socket_Opened; - - socket.Open(); - - if (onConnected != null) - onConnected(loginDetails); - }); - } - - void socket_Opened(object sender, EventArgs e) - { - - } - - //TODO: Attempt to reconnect. - void socket_Closed(object sender, EventArgs e) - { - - } - - //send: ping, message (channel, text, id), typing (channel) - /// - /// Hacked together. Please revise. - /// - /// - /// - void socket_MessageReceived(object sender, MessageReceivedEventArgs e) - { - ReceivingMessage message = JsonConvert.DeserializeObject(e.Message, new JavascriptDateTimeConverter()); - - if (message.reply_to != 0) - { - if (socketCallbacks.ContainsKey(message.reply_to)) - { - socketCallbacks[message.reply_to](message); - socketCallbacks.Remove(message.reply_to); - } - return; - } - - switch (message.type) - { - case "channel_marked": - //TODO? - break; - - case "user_typing": - if (OnUserTyping != null) - OnUserTyping(message); - break; - - case "message": - if (OnMessageReceived != null) - OnMessageReceived(message); - break; - - case "presence_change": - Users.Find((c) => c.id == message.user).presence = message.presence; - - if (OnPresenceChanged != null) - OnPresenceChanged(message); - break; - - case "hello": - if (Interlocked.CompareExchange(ref helloReceived, 1, 0) == 0) - { - socketCallbacks = new Dictionary>(); - new Timer((o) => - { - if (socket != null && Interlocked.CompareExchange(ref pingBlocking, 1, 0) == 0) - { - SendSocket(new SendingMessage() { type = "ping" }, (r) => - { - - }); - pingBlocking = 0; - } - }, null, 2000, 2000); - } - if (OnHello != null) - OnHello(message); - break; - } - } - - //TODO: Check connected, reconnect if possible. - internal void SendSocket(SendingMessage message, Action callback) - { - if (callback != null) - { - int id = Interlocked.Increment(ref currentId); - message.id = id; - socketCallbacks.Add(id, callback); - } - - socket.Send(JsonConvert.SerializeObject(message)); - } - - public class SendingMessage - { - public string type; - public int id; - public string channel; - public string text; - public string user; - public string presence; - } - - /// - /// Hacking ftw! - /// - public class ReceivingMessage - { - public string type; - public int reply_to; - public string user; - public string presence; - public DateTime ts; - public string text; - public string team; - public string channel; - } - - public static void APIRequest(Action callback, Tuple[] getParameters, Tuple[] postParameters) - where K : Response - { - RequestPath path = RequestPath.GetRequestPath(); - //TODO: Custom paths? Appropriate subdomain paths? Not sure. - //Maybe store custom path in the requestpath.path itself? - - string parameters = getParameters - .Select(new Func, string>(a => string.Format("{0}={1}", WebUtility.UrlEncode(a.Item1), WebUtility.UrlEncode(a.Item2)))) - .Aggregate((a, b) => - { - if (string.IsNullOrEmpty(a)) - return b; - else - return string.Format("{0}&{1}", a, b); - }); - - Uri requestUri = new Uri(string.Format("{0}?{1}", Path.Combine(APIBaseLocation, path.Path), parameters)); - - HttpWebRequest request = WebRequest.CreateHttp(requestUri); - - //This will handle all of the processing. - RequestState state = new RequestState(request, postParameters, callback); - state.Begin(); - } - - public static void APIGetRequest(Action callback, params Tuple[] getParameters) - where K : Response - { - APIRequest(callback, getParameters, new Tuple[0]); - } - - 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]); - } - - public static void StartAuth(Action callback, string email) - { - APIRequest(callback, new Tuple[] { new Tuple("email", email) }, new Tuple[0]); - } - - public static void AuthSignin(Action callback, string userId, string teamId, string password) - { - APIRequest(callback, new Tuple[] { - new Tuple("user", userId), - new Tuple("team", teamId), - new Tuple("password", password) - }, new Tuple[0]); - } - - public void TestAuth(Action callback) - { - APIRequestWithToken(callback); - } - - public void GetUserList(Action callback) - { - APIRequestWithToken(callback); - } - - public void GetChannelList(Action callback, bool ExcludeArchived = true) - { - APIRequestWithToken(callback, new Tuple("exclude_archived", ExcludeArchived ? "1" : "0")); - } - - public void GetGroupsList(Action callback, bool ExcludeArchived = true) - { - APIRequestWithToken(callback, new Tuple("exclude_archived", ExcludeArchived ? "1" : "0")); - } - - public void GetDirectMessageList(Action callback) - { - 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) - { - List> parameters = new List>(); - - if (!string.IsNullOrEmpty(userId)) - parameters.Add(new Tuple("user", userId)); - - if (from.HasValue) - parameters.Add(new Tuple("ts_from", from.Value.ToProperTimeStamp())); - - if (to.HasValue) - parameters.Add(new Tuple("ts_to", to.Value.ToProperTimeStamp())); - - if (!types.HasFlag(FileTypes.all)) - { - FileTypes[] values = (FileTypes[])Enum.GetValues(typeof(FileTypes)); - - StringBuilder building = new StringBuilder(); - bool first = true; - for (int i = 0; i < values.Length; ++i) - { - if (types.HasFlag(values[i])) - { - if (!first) building.Append(","); - - building.Append(values[i].ToString()); - - first = false; - } - } - - if (building.Length > 0) - parameters.Add(new Tuple("types", building.ToString())); - } - - if (count.HasValue) - parameters.Add(new Tuple("count", count.Value.ToString())); - - if (page.HasValue) - parameters.Add(new Tuple("page", page.Value.ToString())); - - APIRequestWithToken(callback, parameters.ToArray()); - } - - void GetHistory(Action historyCallback, string channel, DateTime? latest = null, DateTime? oldest = null, int? count = null) - where K : MessageHistory - { - List> parameters = new List>(); - parameters.Add(new Tuple("channel", channel)); - - if(latest.HasValue) - parameters.Add(new Tuple("latest", latest.Value.ToProperTimeStamp())); - if(oldest.HasValue) - parameters.Add(new Tuple("oldest", oldest.Value.ToProperTimeStamp())); - if(count.HasValue) - parameters.Add(new Tuple("count", count.Value.ToString())); - - APIRequestWithToken(historyCallback, parameters.ToArray()); - } - - public void GetChannelHistory(Action callback, Channel channelInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null) - { - GetHistory(callback, channelInfo.id, latest, oldest, count); - } - - public void GetDirectMessageHistory(Action callback, DirectMessageConversation conversationInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null) - { - GetHistory(callback, conversationInfo.id, latest, oldest, count); - } - - public void GetGroupHistory(Action callback, Channel groupInfo, DateTime? latest = null, DateTime? oldest = null, int? count = null) - { - GetHistory(callback, groupInfo.id, latest, oldest, count); - } - - public void MarkChannel(Action callback, string channelId, DateTime ts) - { - APIRequestWithToken(callback, - new Tuple("channel", channelId), - new Tuple("ts", ts.ToProperTimeStamp()) - ); - } - - public void GetFileInfo(Action callback, string fileId, int? page = null, int? count = null) - { - List> parameters = new List>(); - - parameters.Add(new Tuple("file", fileId)); - - if(count.HasValue) - parameters.Add(new Tuple("count", count.Value.ToString())); - - if (page.HasValue) - parameters.Add(new Tuple("page", page.Value.ToString())); - - APIRequestWithToken(callback, parameters.ToArray()); - } - - public void SearchAll(Action callback, string query, SearchSort? 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 (direction.HasValue) - parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); - - if (enableHighlights) - parameters.Add(new Tuple("highlight", "1")); - - if (count.HasValue) - parameters.Add(new Tuple("count", count.Value.ToString())); - - if (page.HasValue) - parameters.Add(new Tuple("page", page.Value.ToString())); - - 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) - { - List> parameters = new List>(); - parameters.Add(new Tuple("query", query)); - - if (sorting.HasValue) - parameters.Add(new Tuple("sort", sorting.Value.ToString())); - - if (direction.HasValue) - parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); - - if (enableHighlights) - parameters.Add(new Tuple("highlight", "1")); - - if (count.HasValue) - parameters.Add(new Tuple("count", count.Value.ToString())); - - if (page.HasValue) - parameters.Add(new Tuple("page", page.Value.ToString())); - - 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) - { - List> parameters = new List>(); - parameters.Add(new Tuple("query", query)); - - if (sorting.HasValue) - parameters.Add(new Tuple("sort", sorting.Value.ToString())); - - if (direction.HasValue) - parameters.Add(new Tuple("sort_dir", direction.Value.ToString())); - - if (enableHighlights) - parameters.Add(new Tuple("highlight", "1")); - - if (count.HasValue) - parameters.Add(new Tuple("count", count.Value.ToString())); - - if (page.HasValue) - parameters.Add(new Tuple("page", page.Value.ToString())); - - APIRequestWithToken(callback, parameters.ToArray()); - } - - public void GetStars(Action callback, string userId = null, int? count = null, int? page = null){ - List> parameters = new List>(); - - if(!string.IsNullOrEmpty(userId)) - parameters.Add(new Tuple("user", userId)); - - if(count.HasValue) - parameters.Add(new Tuple("count", count.Value.ToString())); - - if(page.HasValue) - parameters.Add(new Tuple("page", page.Value.ToString())); - - APIRequestWithToken(callback, parameters.ToArray()); - } - - public void PostMessage( - Action callback, - string channelId, - string text, - string botName = null, - string parse = null, - bool linkNames = false, - Attachment[] attachments = null, - bool unfurl_links = false, - string icon_url = null, - string icon_emoji = null) - { - List> parameters = new List>(); - - parameters.Add(new Tuple("channel", channelId)); - parameters.Add(new Tuple("text", text)); - - if(!string.IsNullOrEmpty(botName)) - parameters.Add(new Tuple("username", botName)); - - if (!string.IsNullOrEmpty(parse)) - parameters.Add(new Tuple("parse", parse)); - - if (linkNames) - parameters.Add(new Tuple("link_names", "1")); - - if (attachments != null && attachments.Length > 0) - parameters.Add(new Tuple("attachments", JsonConvert.SerializeObject(attachments))); - - if (unfurl_links) - parameters.Add(new Tuple("unfurl_links", "1")); - - 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)); - - APIRequestWithToken(callback, parameters.ToArray()); - } - - public void UploadFile(Action callback, byte[] fileData, string fileName, string[] channelIds, string title = null, string initialComment = null, bool useAsync = false) - { - 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 - //FileType? - - if (!string.IsNullOrEmpty(fileName)) - parameters.Add(string.Format("{0}={1}", "filename", fileName)); - - if (!string.IsNullOrEmpty(title)) - parameters.Add(string.Format("{0}={1}", "title", title)); - - if (!string.IsNullOrEmpty(initialComment)) - parameters.Add(string.Format("{0}={1}", "initial_comment", initialComment)); - - parameters.Add(string.Format("{0}={1}", "channels", string.Join(",", channelIds))); - - using(HttpClient client = new HttpClient()) - using (MultipartFormDataContent form = new MultipartFormDataContent()) - { - form.Add(new ByteArrayContent(fileData), "file", fileName); - HttpResponseMessage response = client.PostAsync(string.Format("{0}?{1}", target, string.Join("&", parameters.ToArray())), form).Result; - string result = response.Content.ReadAsStringAsync().Result; - callback(JsonConvert.DeserializeObject(result, new JavascriptDateTimeConverter())); - } - } - - public void EmitPresence(Action callback, Presence status) - { - APIRequestWithToken(callback, new Tuple("presence", status.ToString())); - } - - public void SendPresence(Presence status) - { - SendSocket(new SendingMessage() - { - type = "presence_change", - presence = status.ToString() - }, null); - } - - public void SendTyping(string channelId) - { - SendSocket(new SendingMessage() - { - type = "typing", - channel = channelId - }, null); - } - - public void SendMessage(Action onSent, string channelId, string textData) - { - SendSocket(new SendingMessage() - { - type = "message", - channel = channelId, - text = textData - }, onSent); - } - - public void GetPreferences(Action callback) - { - APIRequestWithToken(callback); - } - - public void GetCounts(Action callback) - { - APIRequestWithToken(callback); - } - - public void EmitLogin(Action callback, string agent = "Inumedia<3") - { - APIRequestWithToken(callback, new Tuple("agent", agent)); - } - } -} \ No newline at end of file diff --git a/UserProfile.cs b/UserProfile.cs deleted file mode 100644 index 32b0fb8c..00000000 --- a/UserProfile.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SlackAPI -{ - public class UserProfile - { - public string first_name; - public string last_name; - public string real_name; - public string email; - public string skype; - public string phone; - public string image_24; - public string image_32; - public string image_48; - public string image_72; - public string image_192; - - public override string ToString() - { - return real_name; - } - } -} diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..23ec30c1 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,15 @@ +image: Visual Studio 2017 + +pull_requests: + do_not_increment_build_number: false + +skip_branch_with_pr: true + +build_script: +- ps: .\build.ps1 + +test: off + +artifacts: +- path: '.\artifacts\**\*.nupkg' + name: Nuget packages diff --git a/build.cake b/build.cake new file mode 100644 index 00000000..2859cd18 --- /dev/null +++ b/build.cake @@ -0,0 +1,195 @@ +#tool "nuget:?package=GitVersion.CommandLine&version=5.1.3" +#addin "Cake.FileHelpers&version=3.2.1" +#addin "Cake.Incubator&version=5.1.0" + +using System.Text.RegularExpressions; + +var configuration = Argument("configuration", "Release"); +var target = Argument("target", "Default"); + +var project = File("./SlackAPI/SlackApi.csproj"); +var testProject = File("./SlackAPI.Tests/SlackApi.Tests.csproj"); +var testConfig = File("./SlackAPI.Tests/Configuration/config.json"); +var projects = new[] { project, testProject }; +var artifactsDirectory = "./artifacts"; +GitVersion gitVersion = null; +var isReleaseBuild = false; + +Task("Clean") + .Does(() => +{ + CleanDirectory(artifactsDirectory); +}); + + +Task("Configure") + .Does(() => +{ + gitVersion = GitVersion(); + + GitVersion(new GitVersionSettings { + UpdateAssemblyInfo = true, + UpdateAssemblyInfoFilePath = "GlobalAssemblyInfo.cs" + }); + + isReleaseBuild = AppVeyor.IsRunningOnAppVeyor + ? AppVeyor.Environment.Repository.Branch == "master" + : false; + + Information("Is release build: '{0}'", isReleaseBuild); + Information("GitVersion details:\n{0}", gitVersion.Dump()); + + if (AppVeyor.IsRunningOnAppVeyor) + { + var buildVersion = gitVersion.SemVer + ".ci." + AppVeyor.Environment.Build.Number; + Information("Using build version: {0}", buildVersion); + AppVeyor.UpdateBuildVersion(buildVersion); + } +}); + + +Task("Build") + .IsDependentOn("Configure") + .Does(() => +{ + foreach(var project in projects) + { + DotNetCoreBuild( + project, + new DotNetCoreBuildSettings + { + Configuration = configuration + } + ); + } +}); + + +Task("ConfigureTest") + .Does(() => +{ + if (AppVeyor.IsRunningOnAppVeyor) + { + FileWriteText(testConfig, $@" + {{ + ""slack"": + {{ + ""userAuthToken"": ""{EnvironmentVariable("userAuthToken")}"", + ""botAuthToken"": ""{EnvironmentVariable("botAuthToken")}"", + ""testChannel"": ""{EnvironmentVariable("testChannel")}"", + ""directMessageUser"": ""{EnvironmentVariable("directMessageUser")}"", + ""clientId"": ""{EnvironmentVariable("clientId")}"", + ""clientSecret"": ""{EnvironmentVariable("clientSecret")}"", + ""redirectUrl"": ""{EnvironmentVariable("redirectUrl")}"", + ""authUsername"": ""{EnvironmentVariable("authUsername")}"", + ""authPassword"": ""{EnvironmentVariable("authPassword")}"", + ""authWorkspace"": ""{EnvironmentVariable("authWorkspace")}"" + }} + }}"); + } +}); + + +Task("Test") + .IsDependentOn("ConfigureTest") + .IsDependentOn("Build") + .Does(() => +{ + // AppVeyor is unable to differentiate tests from multiple frameworks + // To push all test results on AppVeyor: + // - disable builtin AppVeyor push from XUnit + // - generate MSTest report + // - replace assembly name in test report + // - manualy push test result + + foreach (var framework in new[] { "net452", "netcoreapp2.1"}) + { + DotNetCoreTest( + testProject, + new DotNetCoreTestSettings + { + Configuration = configuration, + Framework = framework, + ArgumentCustomization = args => args.Append("--logger \"trx;LogFileName=result_" + framework + ".trx\""), + EnvironmentVariables = new Dictionary{ + { "APPVEYOR_API_URL", null } + } + } + ); + + if (AppVeyor.IsRunningOnAppVeyor) + { + var testResult = File("./SlackAPI.Tests/TestResults/result_" + framework + ".trx"); + + ReplaceRegexInFiles( + testResult, + @"slackapi\.tests\.dll", + "SlackAPI.Tests." + framework + ".dll", + RegexOptions.IgnoreCase); + + AppVeyor.UploadTestResults(testResult, AppVeyorTestResultsType.MSTest); + } + } +}); + + +Task("Package") + .IsDependentOn("Clean") + .IsDependentOn("Build") + .IsDependentOn("Test") + .Does(() => +{ + DotNetCorePack( + project, + new DotNetCorePackSettings + { + Configuration = configuration, + OutputDirectory = artifactsDirectory, + IncludeSymbols = !isReleaseBuild, + IncludeSource = !isReleaseBuild, + ArgumentCustomization = args => args.Append("/p:Version=\"" + gitVersion.NuGetVersion + "\"") + } + ); + +}); + + +Task("Publish") + .IsDependentOn("Package") + .WithCriteria(() => AppVeyor.IsRunningOnAppVeyor && !AppVeyor.Environment.PullRequest.IsPullRequest, "Publishing is supported only from CI for non PR") + .Does(() => +{ + // Publish on Nuget if it's a release build or on MyGet for others builds + var mapping = new Dictionary + { + { true, ("NUGET_APITOKEN", "NuGet", "https://nuget.org/api/v2/package") }, + { false, ("MYGET_APITOKEN", "MyGet", "https://www.myget.org/F/slackapi/api/v2") }, + }; + + var config = mapping[isReleaseBuild]; + + var apiToken = EnvironmentVariable(config.token); + if (string.IsNullOrEmpty(apiToken)) + { + Warning("{0} environment variable not found. Unable to push package on {1}", config.token, config.provider); + } + else + { + var packages = GetFiles(artifactsDirectory + "/**/*.nupkg"); + + NuGetPush(packages, new NuGetPushSettings + { + Source = config.source, + ApiKey = apiToken, + Verbosity = NuGetVerbosity.Detailed, + }); + } +}); + + +Task("Default") + .IsDependentOn("Package") + .IsDependentOn("Publish"); + + +RunTarget(target); diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 00000000..44de5793 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,189 @@ +########################################################################## +# This is the Cake bootstrapper script for PowerShell. +# This file was downloaded from https://github.com/cake-build/resources +# Feel free to change this file to fit your needs. +########################################################################## + +<# + +.SYNOPSIS +This is a Powershell script to bootstrap a Cake build. + +.DESCRIPTION +This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) +and execute your Cake build script with the parameters you provide. + +.PARAMETER Script +The build script to execute. +.PARAMETER Target +The build script target to run. +.PARAMETER Configuration +The build configuration to use. +.PARAMETER Verbosity +Specifies the amount of information to be displayed. +.PARAMETER Experimental +Tells Cake to use the latest Roslyn release. +.PARAMETER WhatIf +Performs a dry run of the build script. +No tasks will be executed. +.PARAMETER Mono +Tells Cake to use the Mono scripting engine. +.PARAMETER SkipToolPackageRestore +Skips restoring of packages. +.PARAMETER ScriptArgs +Remaining arguments are added here. + +.LINK +http://cakebuild.net + +#> + +[CmdletBinding()] +Param( + [string]$Script = "build.cake", + [string]$Target = "Default", + [ValidateSet("Release", "Debug")] + [string]$Configuration = "Release", + [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] + [string]$Verbosity = "Verbose", + [switch]$Experimental, + [Alias("DryRun","Noop")] + [switch]$WhatIf, + [switch]$Mono, + [switch]$SkipToolPackageRestore, + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$ScriptArgs +) + +[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null +function MD5HashFile([string] $filePath) +{ + if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) + { + return $null + } + + [System.IO.Stream] $file = $null; + [System.Security.Cryptography.MD5] $md5 = $null; + try + { + $md5 = [System.Security.Cryptography.MD5]::Create() + $file = [System.IO.File]::OpenRead($filePath) + return [System.BitConverter]::ToString($md5.ComputeHash($file)) + } + finally + { + if ($file -ne $null) + { + $file.Dispose() + } + } +} + +Write-Host "Preparing to run build script..." + +if(!$PSScriptRoot){ + $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent +} + +$TOOLS_DIR = Join-Path $PSScriptRoot "tools" +$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" +$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" +$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" +$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" +$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" + +# Should we use mono? +$UseMono = ""; +if($Mono.IsPresent) { + Write-Verbose -Message "Using the Mono based scripting engine." + $UseMono = "-mono" +} + +# Should we use the new Roslyn? +$UseExperimental = ""; +if($Experimental.IsPresent -and !($Mono.IsPresent)) { + Write-Verbose -Message "Using experimental version of Roslyn." + $UseExperimental = "-experimental" +} + +# Is this a dry run? +$UseDryRun = ""; +if($WhatIf.IsPresent) { + $UseDryRun = "-dryrun" +} + +# Make sure tools folder exists +if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { + Write-Verbose -Message "Creating tools directory..." + New-Item -Path $TOOLS_DIR -Type directory | out-null +} + +# Make sure that packages.config exist. +if (!(Test-Path $PACKAGES_CONFIG)) { + Write-Verbose -Message "Downloading packages.config..." + try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { + Throw "Could not download packages.config." + } +} + +# Try find NuGet.exe in path if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Trying to find nuget.exe in PATH..." + $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) } + $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 + if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { + Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." + $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName + } +} + +# Try download NuGet.exe if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Downloading NuGet.exe..." + try { + (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) + } catch { + Throw "Could not download NuGet.exe." + } +} + +# Save nuget.exe path to environment to be available to child processed +$ENV:NUGET_EXE = $NUGET_EXE + +# Restore tools from NuGet? +if(-Not $SkipToolPackageRestore.IsPresent) { + Push-Location + Set-Location $TOOLS_DIR + + # Check for changes in packages.config and remove installed tools if true. + [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) + if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or + ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { + Write-Verbose -Message "Missing or changed package.config hash..." + Remove-Item * -Recurse -Exclude packages.config,nuget.exe + } + + Write-Verbose -Message "Restoring tools from NuGet..." + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" + + if ($LASTEXITCODE -ne 0) { + Throw "An error occured while restoring NuGet tools." + } + else + { + $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" + } + Write-Verbose -Message ($NuGetOutput | out-string) + Pop-Location +} + +# Make sure that Cake has been installed. +if (!(Test-Path $CAKE_EXE)) { + Throw "Could not find Cake.exe at $CAKE_EXE" +} + +# Start Cake +Write-Host "Running build script..." +Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" +exit $LASTEXITCODE \ No newline at end of file diff --git a/packages.config b/packages.config deleted file mode 100644 index 00b2764b..00000000 --- a/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/tools/packages.config b/tools/packages.config new file mode 100644 index 00000000..cedcc6ab --- /dev/null +++ b/tools/packages.config @@ -0,0 +1,4 @@ + + + +