Skip to content

Commit

Permalink
Merge pull request #10 from thesoftwarejedi/dev-tsj
Browse files Browse the repository at this point in the history
Dev tsj
  • Loading branch information
thesoftwarejedi committed Jul 25, 2014
2 parents ed45e94 + 725c699 commit d304453
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 109 deletions.
76 changes: 76 additions & 0 deletions IdleTimeoutExecutor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace TSJ.Gemini.Slack
{

/**
* not the cleanest implementation ever of a way to have thread safety around a future action
* whose execution time may change. Thread safety is dependent on proper use of the locking
* around the given mutex from outside the object. Lock around creation, be sure to invalidate references
* on "removed" being called, and otherwise check the "Dead" property before adjusting the timeout
* **/
public class IdleTimeoutExecutor
{

private DateTime _timeout;
private object _mutex;

public DateTime Timeout
{
get { return _timeout; }
set
{
lock (_mutex)
{
if (Dead) throw new Exception("IdleTimeoutExecutor already fired");
_timeout = value;
//alert the waiting thread
Monitor.PulseAll(_mutex);
}
}
}

public bool Dead
{
get;
private set;
}

public IdleTimeoutExecutor(DateTime timeout, Action onTimeoutExpired, object mutex, Action onFinish)
{
DateTime created = DateTime.Now;
if (DateTime.Now > timeout)
{
onTimeoutExpired();
}
else
{
_timeout = timeout;
_mutex = mutex;
new Thread(() =>
{
lock (_mutex)
{
while (_timeout > DateTime.Now)
{
Monitor.Wait(_mutex, _timeout - DateTime.Now);
}
try
{
onTimeoutExpired();
}
catch { }
onFinish();
Dead = true;
}
}).Start();
}
}

}
}
180 changes: 71 additions & 109 deletions SlackListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,28 @@

namespace TSJ.Gemini.Slack
{

/**
* Messages are sent to slack on 3 events:
* - Create
* - Immediately published to a slack channel
*
* - Change (includes comments)
* - 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
{
{

//not sure of scope here, is a listener treated as a singleton? If it is, this need not be static
//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)
{
Expand Down Expand Up @@ -54,52 +70,6 @@ public static string GetIssueKey(IssueEventArgs args)
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 AfterAssign(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;
StringBuilder buffer = new StringBuilder();
var usersCache = GeminiApp.Cache().Users;
foreach (var userId in args.Entity.GetResources())
{
var user = usersCache.Find(u=> u.Id == userId);
if (user != null)
{
buffer.Append(user.Fullname);
buffer.Append(", ");
}
}

if(buffer.Length > 0)
{
buffer.Remove(buffer.Length-2,2);
}
QuickSlack.Send(data.Value.SlackAPIEndpoint, channel, string.Format("{0} assigned <{1}|{2} - {3}> to {4}"
, args.User.Fullname, args.BuildIssueUrl(args.Entity), GetIssueKey(args), args.Entity.Title, buffer));

base.AfterAssign(args);
}

public override void AfterCreate(IssueEventArgs args)
{
var data = GetConfig(args.Context);
Expand All @@ -117,50 +87,12 @@ public override void AfterCreate(IssueEventArgs args)
base.AfterCreate(args);
}

public override void AfterResolutionChange(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} updated resolution on <{1}|{2} - {3}>"
, args.User.Fullname, args.BuildIssueUrl(args.Entity), GetIssueKey(args), args.Entity.Title),
"resolution changed",
"good",
new[] { new
{
title = "Resolution Change",
value = args.Context.Meta.ResolutionGet(args.Previous.ResolutionId).Label + " -> " + args.Context.Meta.ResolutionGet(args.Entity.ResolutionId).Label,
_short = true
} });

base.AfterResolutionChange(args);
}

public override void AfterStatusChange(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} updated status on <{1}|{2} - {3}>"
, args.User.Fullname, args.BuildIssueUrl(args.Entity), GetIssueKey(args), args.Entity.Title),
"status changed",
"good",
new[] { new
{
title = "Status Change",
value = args.Context.Meta.StatusGet(args.Previous.StatusId).Label + " -> " + args.Context.Meta.StatusGet(args.Entity.StatusId).Label,
_short = true
} });

base.AfterStatusChange(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);
Expand All @@ -169,26 +101,56 @@ public override void AfterUpdateFull(IssueDtoEventArgs args)
string channel = GetProjectChannel(args.Issue.Entity.ProjectId, data.Value.ProjectChannels);
if (channel == null || channel.Trim().Length == 0) return;

var issueManager = GeminiApp.GetManager<IssueManager>(args.User);
var userManager = GeminiApp.GetManager<UserManager>(args.User);
var userDto = userManager.Convert(args.User);
var changelog = issueManager.GetChangeLog(args.Issue, userDto, userDto, args.Issue.Entity.Revised.AddSeconds(-30));
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",
"warning",
fields.ToArray());
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(30),
//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,
() => { _executorDictionary.Remove(key); });
}
else
{
//we found a pending executor, just update the timeout to be later
ex.Timeout = DateTime.Now.AddSeconds(30);
}
}

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));
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}> {4} seconds ago"
, args.User.Fullname, args.BuildIssueUrl(args.Issue), args.Issue.IssueKey, args.Issue.Title, (int)((DateTime.Now-createDate).TotalSeconds)),
"details attached",
"good", //todo colors here based on something
fields.ToArray());
}

public static string StripHTML(string htmlString)
{
Expand Down
1 change: 1 addition & 0 deletions TSJ.Gemini.Slack.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="IdleTimeoutExecutor.cs" />
<Compile Include="QuickSlack.cs" />
<Compile Include="SlackConfigController.cs" />
<Compile Include="SlackConfigData.cs" />
Expand Down

0 comments on commit d304453

Please sign in to comment.