Skip to content
Open
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A place where I put some integration scripts for the popular Slack messaging pla

#Rally Bot

We use this to query our Rally instance for defects, tasks and user stories. I have it configured to respond to a /rallyme slash command in Slack.
Pushes notifications from Rally whenever a comment is added to a ticket, and queries our Rally instance for defects, tasks and user stories. I have it configured to respond to a /rallyme slash command in Slack.

E.g.:
/rallyme DE12345
Expand Down
4 changes: 4 additions & 0 deletions scripts/config/rallycron.conf.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
$CRON_INTERVAL = 61; //seconds between cron runs; pad for script run time and latency
$RALLY_PROJECT_ID = REPLACE_ME;
$SLACK_CHANNEL_FOR_RALLY_PROJECT = 'REPLACE ME'; //do not include hash symbol
21 changes: 11 additions & 10 deletions scripts/include/rally.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?
//rally commands

$RALLY_URL = 'https://rally1.rallydev.com/';
$RALLY_TIMESTAMP_FORMAT = 'Y-m-d\TH:i:s.u\Z';

function HandleItem($slackCommand, $rallyFormattedId)
{
Expand Down Expand Up @@ -53,11 +54,11 @@ function HandleStory($id, $channel_name)
$payload = GetRequirementPayload($ref);

$result = postit($channel_name, $payload->text, $payload->attachments);

if($result=='Invalid channel specified'){
die("Sorry, the rallyme command can't post messages to your private chat.\n");
}

if($result!="ok"){
print_r($result."\n");
print_r(json_encode($payload));
Expand All @@ -71,11 +72,11 @@ function postit($channel_name, $payload, $attachments){
global $config, $slackCommand;

return slack_incoming_hook_post_with_attachments(
$config['slack']['hook'],
$config['rally']['botname'],
$slackCommand->ChannelName,
$config['rally']['boticon'],
$payload,
$config['slack']['hook'],
$config['rally']['botname'],
$slackCommand->ChannelName,
$config['rally']['boticon'],
$payload,
$attachments);
}

Expand Down Expand Up @@ -169,7 +170,7 @@ function GetDefectPayload($ref)
array_push($fields,$firstattachment);

global $slackCommand;

$userlink = BuildUserLink($slackCommand->UserName);
$user_message = "Ok, {$userlink}, here's the defect you requested.";

Expand Down Expand Up @@ -265,7 +266,7 @@ function GetRequirementPayload($ref)

if($blocked)
array_push($fields, MakeField("blocked",$blockedreason,true));

array_push($fields, MakeField("description",$short_description,false));

if($firstattachment!=null)
Expand Down
95 changes: 95 additions & 0 deletions scripts/include/rallycron.inc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
require('curl.php');
require('slack.config.php');
require('slack.php');
require('rallyme.config.php');
require('rally.php');

function FetchLatestRallyComments($since)
{
global $RALLY_URL, $RALLY_PROJECT_ID;

$api_url = $RALLY_URL . 'slm/webservice/v2.0/';
$query_url = $api_url . 'conversationpost?query=((Artifact.Project.ObjectID+%3D+' . $RALLY_PROJECT_ID . ')AND(CreationDate+>+' . $since . '))&fetch=Artifact,Text,User&order=CreationDate+asc';

$results = CallAPI($query_url);
$results = $results->QueryResult->Results;

$project_url = $RALLY_URL . '#/' . $RALLY_PROJECT_ID;

$items = array();
foreach ($results as $Result) {
switch ($type = $Result->Artifact->_type) {
case 'Defect':
$path = '/detail/defect/';
break;
case 'HierarchicalRequirement':
$type = 'User Story';
$path = '/detail/userstory/';
break;
default:
continue 2; //don't display comments attached to other artifact types
}

$items[] = array(
'type' => $type,
'title' => $Result->Artifact->_refObjectName,
'url' => $project_url . $path . basename($Result->Artifact->_ref) . '/discussion',
'user' => $Result->User->_refObjectName,
'text' => $Result->Text
);
}

return $items;
}

function SendRallyCommentNotifications($items)
{
global $SLACK_CHANNEL_FOR_RALLY_PROJECT;
$success = TRUE;

foreach ($items as $item) {
$item['title'] = SanitizeText($item['title']);
$item['title'] = TruncateText($item['title'], 300);
$slug = $item['type'] . ' ' . l($item['title'], $item['url']);

$item['text'] = SanitizeText($item['text']);
$item['text'] = TruncateText($item['text'], 300);

//display a preview of the comment as a message attachment
$pretext = em('New comment added to ' . $slug);
$text = '';
$color = '#CEC7B8'; //dove gray
$fields = array(MakeField($item['user'], $item['text']));
$fallback = $item['user'] . ' commented on ' . $slug;

$message = MakeAttachment($pretext, $text, $color, $fields, $fallback);
$success = SendIncomingWebHookMessage($SLACK_CHANNEL_FOR_RALLY_PROJECT, '', $message) && $success;
}

return $success;
}

function SendIncomingWebHookMessage($channel, $payload, $attachments)
{
global $config;

//allow bot to display formatted attachment text
$attachments->mrkdwn_in = ['pretext', 'text', 'title', 'fields'];

$reply = slack_incoming_hook_post_with_attachments(
$config['slack']['hook'],
$config['rally']['botname'],
$channel,
$config['rally']['boticon'],
$payload,
$attachments
);

$success = ($reply == 'ok');
if (!$success) {
trigger_error('Unable to send Incoming WebHook message: ' . $reply);
}
return $success;
}

34 changes: 28 additions & 6 deletions scripts/include/slack.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,31 @@ function BuildSlashCommand($request)
}


//text-formatting functions

function SanitizeText($text)
{
$text = strtr($text, array('<br />' => '\n', '<div>' => '\n', '<p>' => '\n'));
return html_entity_decode(strip_tags($text), ENT_HTML401 | ENT_COMPAT, 'UTF-8');
}

function l($text, $url)
{
return '<' . $url . '|' . $text . '>';
}

function em($text)
{
return '_' . $text . '_';
}


//posting functions

function slack_incoming_hook_post($uri, $user, $channel, $icon, $emoji, $payload){

$data = array(
"text" => $payload,
"text" => $payload,
"channel" => "#".$channel,
"username"=>$user
);
Expand All @@ -53,18 +74,19 @@ function slack_incoming_hook_post($uri, $user, $channel, $icon, $emoji, $payload
return curl_post($uri, $data_string);
}



function slack_incoming_hook_post_with_attachments($uri, $user, $channel, $icon, $payload, $attachments){

$data = array(
"text" => $payload,
"text" => $payload,
"channel" => "#".$channel,
"username"=>$user,
"icon_url"=>$icon,
"attachments"=>array($attachments));
"attachments" => array($attachments),
'link_names' => 1 //allow bot to linkify at-mentions in attachments
);

$data_string = "payload=" . json_encode($data, JSON_HEX_AMP|JSON_HEX_APOS|JSON_NUMERIC_CHECK|JSON_PRETTY_PRINT);
$data_string = strtr($data_string, array('\\\\n' => '\n')); //unescape slashes in newline characters
mylog('sent.txt',$data_string);
return curl_post($uri, $data_string);
}
Expand Down
9 changes: 9 additions & 0 deletions scripts/rallycron.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
require('config/rallycron.conf.php');
require('include/rallycron.inc.php');

date_default_timezone_set('UTC');
$since = date($RALLY_TIMESTAMP_FORMAT, time() - $CRON_INTERVAL);

$items = FetchLatestRallyComments($since);
$result = SendRallyCommentNotifications($items);