forked from BotBuilderCommunity/botbuilder-community-dotnet
-
Notifications
You must be signed in to change notification settings - Fork 0
/
SlackClientWrapper.cs
227 lines (201 loc) · 10.9 KB
/
SlackClientWrapper.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
using System;
using System.Collections.Specialized;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Bot.Builder.Community.Adapters.Slack.Model;
using Bot.Builder.Community.Adapters.Slack.Model.Events;
using Microsoft.AspNetCore.Http;
using Microsoft.Bot.Schema;
using Newtonsoft.Json;
using SlackAPI;
using Attachment = SlackAPI.Attachment;
namespace Bot.Builder.Community.Adapters.Slack
{
public class SlackClientWrapper
{
private const string PostMessageUrl = "https://slack.com/api/chat.postMessage";
private const string PostEphemeralMessageUrl = "https://slack.com/api/chat.postEphemeral";
private readonly SlackTaskClient _api;
/// <summary>
/// Initializes a new instance of the <see cref="SlackClientWrapper"/> class.
/// Creates a Slack client by supplying the access token.
/// </summary>
/// <param name="options">An object containing API credentials, a webhook verification token and other options.</param>
public SlackClientWrapper(SlackClientWrapperOptions options)
{
Options = options ?? throw new ArgumentNullException(nameof(options));
if (string.IsNullOrWhiteSpace(options.SlackVerificationToken) && string.IsNullOrWhiteSpace(options.SlackClientSigningSecret))
{
const string message = "****************************************************************************************" +
"* WARNING: Your bot is operating without recommended security mechanisms in place. *" +
"* Initialize your adapter with a clientSigningSecret parameter to enable *" +
"* verification that all incoming webhooks originate with Slack: *" +
"* *" +
"* var adapter = new SlackAdapter({clientSigningSecret: <my secret from slack>}); *" +
"* *" +
"****************************************************************************************" +
">> Slack docs: https://api.slack.com/docs/verifying-requests-from-slack";
throw new InvalidOperationException(message + Environment.NewLine + "Required: include a verificationToken or clientSigningSecret to verify incoming Events API webhooks");
}
_api = new SlackTaskClient(options.SlackBotToken);
LoginWithSlackAsync(default).Wait();
}
/// <summary>
/// Gets the <see cref="SlackClientWrapperOptions"/>.
/// </summary>
/// <value>
/// An object containing API credentials, a webhook verification token and other options.
/// </value>
public SlackClientWrapperOptions Options { get; }
/// <summary>
/// Gets the user identity.
/// </summary>
/// <value>
/// A string containing the user identity.
/// </value>
public string Identity { get; private set; }
/// <summary>
/// Wraps Slack API's DeleteMessageAsync method.
/// </summary>
/// <param name="channelId">The channel to delete the message from.</param>
/// <param name="ts">The timestamp of the message.</param>
/// <param name="cancellationToken">A cancellation token for the task.</param>
/// <returns>A <see cref="DeletedResponse"/> representing the response to deleting the message.</returns>
public virtual async Task<DeletedResponse> DeleteMessageAsync(string channelId, DateTime ts, CancellationToken cancellationToken)
{
return await _api.DeleteMessageAsync(channelId, ts).ConfigureAwait(false);
}
/// <summary>
/// Wraps Slack API's TestAuthAsync method.
/// </summary>
/// <param name="cancellationToken">A cancellation token for the task.</param>
/// <returns>The user Id.</returns>
public virtual async Task<string> TestAuthAsync(CancellationToken cancellationToken)
{
var auth = await _api.TestAuthAsync().ConfigureAwait(false);
return auth.user_id;
}
/// <summary>
/// Wraps Slack API's UpdateAsync method.
/// </summary>
/// <param name="ts">The timestamp of the message.</param>
/// <param name="channelId">The channel to delete the message from.</param>
/// <param name="text">The text to update with.</param>
/// <param name="botName">The optional bot name.</param>
/// <param name="parse">Change how messages are treated.Defaults to 'none'. See https://api.slack.com/methods/chat.postMessage#formatting. </param>
/// <param name="linkNames">If to find and link channel names and username.</param>
/// <param name="attachments">The attachments, if any.</param>
/// <param name="asUser">If the message is being sent as user instead of as a bot.</param>
/// <param name="cancellationToken">A cancellation token for the task.</param>
/// <returns>A <see cref="UpdateResponse"/> representing the response to the operation.</returns>
public virtual async Task<SlackResponse> UpdateAsync(string ts, string channelId, string text, string botName = null, string parse = null, bool linkNames = false, Attachment[] attachments = null, bool asUser = false, CancellationToken cancellationToken = default)
{
var updateResponse = await _api.UpdateAsync(ts, channelId, text, botName, parse, linkNames, attachments, asUser).ConfigureAwait(false);
return new SlackResponse()
{
Ok = updateResponse.ok,
Message = new MessageEvent()
{
User = updateResponse.message.user,
Type = updateResponse.message.type,
Text = updateResponse.message.text
},
Channel = updateResponse.channel,
Ts = updateResponse.ts
};
}
/// <summary>
/// Validates the local secret against the one obtained from the request header.
/// </summary>
/// <param name="request">The <see cref="HttpRequest"/> with the signature.</param>
/// <param name="body">The raw body of the request.</param>
/// <returns>The result of the comparison between the signature in the request and hashed secret.</returns>
public virtual bool VerifySignature(HttpRequest request, string body)
{
if (request == null || string.IsNullOrWhiteSpace(body))
{
return false;
}
var timestamp = request.Headers["X-Slack-Request-Timestamp"];
object[] signature = { "v0", timestamp.ToString(), body };
var baseString = string.Join(":", signature);
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(Options.SlackClientSigningSecret)))
{
var hashArray = hmac.ComputeHash(Encoding.UTF8.GetBytes(baseString));
var hash = string.Concat("v0=", BitConverter.ToString(hashArray).Replace("-", string.Empty)).ToUpperInvariant();
var retrievedSignature = request.Headers["X-Slack-Signature"].ToString().ToUpperInvariant();
return hash == retrievedSignature;
}
}
/// <summary>
/// Posts a message to Slack.
/// </summary>
/// <param name="message">The message to be posted.</param>
/// <param name="cancellationToken">A cancellation token for the task.</param>
/// <returns>The <see cref="SlackResponse"/> to the posting operation.</returns>
public virtual async Task<SlackResponse> PostMessageAsync(NewSlackMessage message, CancellationToken cancellationToken)
{
if (message == null)
{
return null;
}
var data = new NameValueCollection
{
["token"] = Options.SlackBotToken,
["channel"] = message.Channel,
["text"] = message.Text,
["thread_ts"] = message.ThreadTs,
["user"] = message.User,
};
if (message.Blocks != null)
{
data["blocks"] = JsonConvert.SerializeObject(message.Blocks, new JsonSerializerSettings()
{
NullValueHandling = NullValueHandling.Ignore,
});
}
byte[] response;
using (var client = new WebClient())
{
var url = !string.IsNullOrWhiteSpace(message.Ephemeral)
? PostEphemeralMessageUrl
: PostMessageUrl;
response = await client.UploadValuesTaskAsync(url, "POST", data).ConfigureAwait(false);
}
return JsonConvert.DeserializeObject<SlackResponse>(Encoding.UTF8.GetString(response));
}
/// <summary>
/// Get the bot user id associated with the team on which an incoming activity originated. This is used internally by the SlackMessageTypeMiddleware to identify direct_mention and mention events.
/// In single-team mode, this will pull the information from the Slack API at launch.
/// In multi-team mode, this will use the `getBotUserByTeam` method passed to the constructor to pull the information from a developer-defined source.
/// </summary>
/// <param name="activity">An Activity.</param>
/// <returns>The identity of the bot's user.</returns>
public virtual string GetBotUserIdentity(Activity activity)
{
return Identity;
}
/// <summary>
/// Manages the login to Slack with the given credentials.
/// </summary>
/// <param name="cancellationToken">A cancellation token for the task.</param>
/// <returns>A Task representing the asynchronous operation.</returns>
public async Task LoginWithSlackAsync(CancellationToken cancellationToken)
{
if (Options.SlackBotToken != null)
{
Identity = await TestAuthAsync(cancellationToken).ConfigureAwait(false);
}
else if (string.IsNullOrWhiteSpace(Options.SlackClientId) ||
string.IsNullOrWhiteSpace(Options.SlackClientSecret) ||
Options.SlackRedirectUri == null ||
Options.SlackScopes.Count == 0)
{
throw new InvalidOperationException("Missing Slack API credentials! Provide SlackClientId, SlackClientSecret, scopes and SlackRedirectUri as part of the SlackAdapter options.");
}
}
}
}