-
Notifications
You must be signed in to change notification settings - Fork 3
/
SlackListener.cs
186 lines (161 loc) · 8.91 KB
/
SlackListener.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
using System;
using System.Linq;
using Countersoft.Gemini.Extensibility.Events;
using Countersoft.Gemini.Extensibility.Apps;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Collections;
using Countersoft.Foundation.Commons.Extensions;
using Countersoft.Gemini;
using Countersoft.Gemini.Contracts;
using Countersoft.Gemini.Commons.Entity;
using Countersoft.Gemini.Infrastructure.Managers;
using System.Text;
using Countersoft.Gemini.Commons.Permissions;
using Countersoft.Foundation.Data;
namespace TSJ.Gemini.Slack
{
/**
* Messages are sent to slack on 3 events:
* - Create
* - Comment
* - Immediately published to a slack channel
*
* - Change
* - this create a thread (per user/ticket#) which will wait for X seconds
* of no changes flushing out all changes made in that time period. This is
* to accomodate several changes being made at the same time without flooding a channel.
* */
[AppType(AppTypeEnum.Event),
AppGuid("ABBADABB-AD00-4151-A177-1F0529EEE7E1"),
AppName("Slack Integration"),
AppDescription("Provides slack integration by posting updates to gemini to a channel in slack.")]
public class SlackListener : AbstractIssueListener
{
//tuple is user and issueid
private static Dictionary<Tuple<string, int>, IdleTimeoutExecutor> _executorDictionary =
new Dictionary<Tuple<string, int>, IdleTimeoutExecutor>();
private static GlobalConfigurationWidgetData<SlackConfigData> GetConfig(GeminiContext ctx)
{
try
{
return ctx.GlobalConfigurationWidgetStore.Get<SlackConfigData>(AppConstants.AppId);
}
catch
{
return null;
}
}
private static string GetProjectChannel(int projectId, Dictionary<int, string> projectChannels)
{
string channel = null;
projectChannels.TryGetValue(projectId, out channel);
if (channel.IsEmpty())
{
// Try and get the all projects channel.
projectChannels.TryGetValue(0, out channel);
}
return channel;
}
public static string GetIssueKey(IssueEventArgs args)
{
var project = args.Context.Projects.Get(args.Entity.ProjectId);
if (project == null) return string.Empty;
return string.Concat(project.Code, '-', args.Entity.Id);
}
public override void AfterComment(IssueCommentEventArgs args)
{
var data = GetConfig(args.Context);
if (data == null || data.Value == null) return;
string channel = GetProjectChannel(args.Issue.Project.Id, data.Value.ProjectChannels);
if (channel == null || channel.Trim().Length == 0) return;
QuickSlack.Send(data.Value.SlackAPIEndpoint, channel, string.Format("{0} added a comment to <{1}|{2} - {3}>"
, args.User.Fullname, args.BuildIssueUrl(args.Issue), args.Issue.IssueKey, args.Issue.Title),
"more details attached",
"good",
new[] { new { title = "Comment", value = StripHTML(args.Entity.Comment), _short = false } }, StripHTML(args.Entity.Comment));
base.AfterComment(args);
}
public override void AfterCreate(IssueEventArgs args)
{
var data = GetConfig(args.Context);
if (data == null || data.Value == null) return;
string channel = GetProjectChannel(args.Entity.ProjectId, data.Value.ProjectChannels);
if (channel == null || channel.Trim().Length == 0) return;
QuickSlack.Send(data.Value.SlackAPIEndpoint, channel, string.Format("{0} created <{1}|{2} - {3}>"
, args.User.Fullname, args.BuildIssueUrl(args.Entity), GetIssueKey(args), args.Entity.Title),
"more details attached",
"good",
new[] { new { title = "Description", value = StripHTML(args.Entity.Description), _short = false } },
StripHTML(args.Entity.Description));
base.AfterCreate(args);
}
/***
* the functionality here hinges on the "changelog" that is provided from the gemini api
* We don't have to keep track of changes.
* This method looks for a recent change for this user/issue and extends the timeout if there is
* a match, otherwise it creates an executor to post to slack after 60 seconds
* */
public override void AfterUpdateFull(IssueDtoEventArgs args)
{
var data = GetConfig(args.Context);
if (data == null || data.Value == null) return;
string channel = GetProjectChannel(args.Issue.Entity.ProjectId, data.Value.ProjectChannels);
if (channel == null || channel.Trim().Length == 0) return;
lock (_executorDictionary)
{
var key = Tuple.Create(args.User.Username, args.Issue.Id);
//look for an existing username/issue# combination indicating that a change was recently
//made in which case we just extend the timeout
IdleTimeoutExecutor ex = null;
if (!_executorDictionary.TryGetValue(key, out ex))
{
DateTime createDate = DateTime.Now.AddSeconds(-1);
_executorDictionary[key] = new IdleTimeoutExecutor(DateTime.Now.AddSeconds(data.Value.SecondsToQueueChanges),
//this executes x seconds after the last update, initially set above ^^ then adjusted on subsequent
//updates further below (in the else) based on the key being found
() => { PostChangesToSlack(args, data, channel, createDate); },
() => {
_executorDictionary.Remove(key);
new SessionManager().CloseSession(); // Need to close the DB connection as we span a new thread.
},
_executorDictionary);
}
else
{
//we found a pending executor, just update the timeout to be later
ex.Timeout = DateTime.Now.AddSeconds(data.Value.SecondsToQueueChanges);
}
}
base.AfterUpdateFull(args);
}
//called when the timeout has expired which was waiting for pending changes.
private static void PostChangesToSlack(IssueDtoEventArgs args, GlobalConfigurationWidgetData<SlackConfigData> data, string channel, DateTime createDate)
{
var issueManager = GeminiApp.GetManager<IssueManager>(args.User);
var userManager = GeminiApp.GetManager<UserManager>(args.User);
var userDto = userManager.Convert(args.User);
var issue = issueManager.Get(args.Issue.Id);
//get the changelog of all changes since the create date (minus a second to avoid missing the initial change)
var changelog = issueManager.GetChangeLog(issue, userDto, userDto, createDate.AddSeconds(-1));
changelog.RemoveAll(c => c.Entity.AttributeChanged == ItemAttributeVisibility.AssociatedComments); // No need to show comments in updates as we already do that in the AfterComment event.
if (changelog.Count == 0) return; // No changes made!
var fields = changelog
.Select(a => new
{
title = a.Field,
value = StripHTML(a.FullChange),
_short = a.Entity.AttributeChanged != ItemAttributeVisibility.Description && a.Entity.AttributeChanged != ItemAttributeVisibility.AssociatedComments
});
QuickSlack.Send(data.Value.SlackAPIEndpoint, channel, string.Format("{0} updated issue <{1}|{2} - {3}>"
, args.User.Fullname, args.BuildIssueUrl(args.Issue), args.Issue.IssueKey, args.Issue.Title),
"details attached",
"good", //todo colors here based on something
fields.ToArray());
}
public static string StripHTML(string htmlString)
{
return Countersoft.Foundation.Utility.Helpers.HtmlHelper.ConvertHtmlToText2(htmlString).Replace((char)160, ' '); // Replace unicode NBSP with normal space as it breaks slack....
}
}
}