From 4eba754126696f81f7ac64dfa0a2ea10ece6c8d9 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Mon, 29 Sep 2014 10:56:38 -0400 Subject: [PATCH 01/41] WIP! Rearchitect rallyme script to handle different output methods --- scripts/config/default.config.php | 3 +++ scripts/include/rallyme.inc.php | 19 +++++++++++++++++++ scripts/rallyme.php | 12 +++++++----- 3 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 scripts/config/default.config.php create mode 100644 scripts/include/rallyme.inc.php diff --git a/scripts/config/default.config.php b/scripts/config/default.config.php new file mode 100644 index 0000000..2a80113 --- /dev/null +++ b/scripts/config/default.config.php @@ -0,0 +1,3 @@ +Text); - -$result = HandleItem($slackCommand, $rallyFormattedId); -?> \ No newline at end of file +if (isset($_REQUEST['token']) && $_REQUEST['token'] == $SLACK_OUTGOING_HOOK_TOKEN && isset($_REQUEST['text'])) { + $payload = FetchArtifactPayload($_REQUEST['text']); + $result = isset($_REQUEST['command']) ? SendArtifactPayload($payload) : ReturnArtifactPayload($payload); +} From 69129a12e53c66aff2c97a2e2d9a47a0a3b19f1a Mon Sep 17 00:00:00 2001 From: JP Klein Date: Mon, 29 Sep 2014 17:42:12 -0400 Subject: [PATCH 02/41] Return details of Rally defects as Slack-formatted JSON in response body --- scripts/include/rally.php | 21 ++-- scripts/include/rallyme.inc.php | 181 ++++++++++++++++++++++++++++++++ scripts/include/slack.php | 23 +++- 3 files changed, 212 insertions(+), 13 deletions(-) diff --git a/scripts/include/rally.php b/scripts/include/rally.php index 9713410..40c93fb 100644 --- a/scripts/include/rally.php +++ b/scripts/include/rally.php @@ -1,6 +1,7 @@ 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)); @@ -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); } @@ -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."; @@ -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) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 516b2bd..a41889e 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -1,19 +1,200 @@ QueryResult->TotalResultCount == 0) { + die('Sorry, I couldn\'t find ' . $formatted_id); + } + + foreach ($Results->QueryResult->Results as $Result) { + if ($Result->_type == $artifact_type) { + return call_user_func($func, $Result); + } + } + die('Sorry, your search for "' . $formatted_id . '" was ambiguous.'); } +/** + * Prepares a table of fields attached to a Rally defect for display. + * + * @param object $Defect + * + * @return string[] + */ +function ParseDefectPayload($Defect) +{ + $state = $Defect->State; + if ($state == 'Closed') { + $Date = new DateTime($Defect->ClosedDate); + $state .= ' ' . $Date->format('M j'); + } + + $ret = array( + 'item_id' => $Defect->FormattedID, + 'item_url' => $Defect->_ref, + 'title' => $Defect->_refObjectName, + + 'Creator' => $Defect->SubmittedBy->_refObjectName, + 'Created' => $Defect->_CreatedAt, + 'Owner' => $Defect->Owner->_refObjectName, + 'State' => $state, + 'Priority' => $Defect->Priority, + 'Severity' => $Defect->Severity, + 'Description' => $Defect->Description, + ); + + if ($Defect->Attachments->Count > 0) { + $ret['Attachment'] = GetAttachmentLinks($Defect->Attachments->_ref); + } + + return $ret; +} + +/** + * Prepares a table of fields attached to a Rally artifact for display. + * + * @param object $Artifact + * + * @return string[] + */ +function ParseTaskPayload($Artifact) +{ + +} + +/** + * Prepares a table of fields attached to a Rally user story for display. + * + * @param object $Artifact + * + * @return string[] + */ +function ParseStoryPayload($Artifact) +{ + +} + +/** + * Returns an array of file links listed in a Rally attachment object. + * + * @param string $attachment_ref + * + * @return string[] + */ +function GetAttachmentLinks($attachment_ref) +{ + $url = $RALLY_BASE_URL . 'slm/attachment/'; + $ret = array(); + + $Attachments = CallAPI($attachment_ref); + foreach ($Attachments->QueryResult->Results as $Attachment) { + $filename = $Attachment->_refObjectName; + $link_url = $url . $Attachment->ObjectID . '/' . urlencode($filename); + $ret[$filename] = $link_url; + } + + return $ret; +} + +/** + * Posts artifact details to a Slack channel via an incoming webhook. + * + * @param string[] $payload + * + * @return mixed + */ function SendArtifactPayload($payload) { } +/** + * Returns artifact details as Slack-formatted JSON in the body of the response. + * + * @param string[] $payload + * + * @return mixed + */ function ReturnArtifactPayload($payload) { + $text = em('Details for ' . $payload['item_id'] . ' ' . l($payload['title'], $payload['item_url'])); + + foreach (array_slice($payload, 3) as $title => $value) { + switch ($title) { + + case 'Attachment': + case 'Parent': + $link_url = reset($value); + $value = l(key($value), $link_url); + break; + + case 'Block Reason': + $title = ''; + $value = SanitizeText($value); + break; + + case 'Description': + $value = TruncateText(SanitizeText($value), 300, $payload['item_url']); + $value = '\n> ' . strtr($value, ['\n' => '\n> ']); + } + + if ($title) { + $title .= ':'; + $text .= '\n`' . str_pad($title, 15) . '`\t' . $value; + } else { + $text .= '\n>' . $value; + } + } + + $data = ['text' => $text]; + $text = json_encode($data, JSON_HEX_AMP | JSON_HEX_APOS | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT); + $text = strtr($text, ['\n' => 'n', '\t' => 't']); //fix double-escaped codes + return print_r($text); } diff --git a/scripts/include/slack.php b/scripts/include/slack.php index 38639a2..16af231 100644 --- a/scripts/include/slack.php +++ b/scripts/include/slack.php @@ -29,11 +29,28 @@ function BuildSlashCommand($request) return $cmd; } +//text-formatting functions + +function SanitizeText($text) +{ + $text = strtr($text, array('
' => '\n', '
' => '\n', '

' => '\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 . '_'; +} function slack_incoming_hook_post($uri, $user, $channel, $icon, $emoji, $payload){ - + $data = array( - "text" => $payload, + "text" => $payload, "channel" => "#".$channel, "username"=>$user ); @@ -58,7 +75,7 @@ function slack_incoming_hook_post($uri, $user, $channel, $icon, $emoji, $payload 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, From fe3994df074b98bc0ce456f5bbccfcc280688ab2 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Mon, 29 Sep 2014 18:28:29 -0400 Subject: [PATCH 03/41] Fix public link to defect --- scripts/include/rallyme.inc.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index a41889e..6125df8 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -69,6 +69,8 @@ function FetchArtifactPayload($command_text) */ function ParseDefectPayload($Defect) { + global $RALLY_BASE_URL; + $state = $Defect->State; if ($state == 'Closed') { $Date = new DateTime($Defect->ClosedDate); @@ -77,7 +79,7 @@ function ParseDefectPayload($Defect) $ret = array( 'item_id' => $Defect->FormattedID, - 'item_url' => $Defect->_ref, + 'item_url' => $RALLY_BASE_URL . '#/' . basename($Defect->Project->_ref) . '/detail/defect/' . $Defect->ObjectID, 'title' => $Defect->_refObjectName, 'Creator' => $Defect->SubmittedBy->_refObjectName, @@ -129,6 +131,7 @@ function ParseStoryPayload($Artifact) */ function GetAttachmentLinks($attachment_ref) { + global $RALLY_BASE_URL; $url = $RALLY_BASE_URL . 'slm/attachment/'; $ret = array(); From 1600e92825c3c446db3146fcfe8105a2e2f807d3 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Mon, 29 Sep 2014 19:19:08 -0400 Subject: [PATCH 04/41] Move existing GetDefectPayload to rallyme include file --- scripts/include/rally.php | 84 --------------------------------- scripts/include/rallyme.inc.php | 83 ++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 84 deletions(-) diff --git a/scripts/include/rally.php b/scripts/include/rally.php index 40c93fb..834409f 100644 --- a/scripts/include/rally.php +++ b/scripts/include/rally.php @@ -96,90 +96,6 @@ function GetRallyAttachmentLink($attachmentRef) return $linktxt; } -function GetDefectPayload($ref) -{ - global $show,$requesting_user_name; - - $object = CallAPI($ref); - - $defect = $object->Defect; - - $projecturi = $defect->Project->_ref; - - $title = $defect->_refObjectName; - $description = $defect->Description; - $owner = $defect->Owner->_refObjectName; - $submitter = $defect->SubmittedBy->_refObjectName; - $project = $object->Project->_refObjectName; - $created = $defect->_CreatedAt; - $state = $defect->State; - $priority = $defect->Priority; - $severity = $defect->Severity; - $frequency = $defect->c_Frequency; - $foundinbuild = $defect->FoundInBuild; - - $short_description = TruncateText(strip_tags($description), 200); - - $ProjectFull = CallAPI($projecturi); - $projectid = $ProjectFull->Project->ObjectID; - $defectid = $defect->ObjectID; - $projectName = $defect->Project->_refObjectName; - $itemid = $defect->FormattedID; - - $attachmentcount = $defect->Attachments->Count; - - $firstattachment = null; - if($attachmentcount>0) - { - $linktxt = GetRallyAttachmentLink($defect->Attachments->_ref); - $firstattachment = MakeField("attachment",$linktxt,false); - } - - $defecturi = "https://rally1.rallydev.com/#/{$projectid}d/detail/defect/{$defectid}"; - - $enctitle = urlencode($title); - $linktext = "<{$defecturi}|{$enctitle}>"; - - $color = "bad"; - - $clean_description = html_entity_decode(strip_tags($description), ENT_HTML401|ENT_COMPAT, 'UTF-8'); - $short_description = TruncateText($clean_description, 300); - - $fields = array( - MakeField("link",$linktext,false), - - MakeField("id",$itemid,true), - MakeField("owner",$owner,true), - - MakeField("project",$projectName,true), - MakeField("created",$created,true), - - MakeField("submitter",$submitter,true), - MakeField("state",$state,true), - - MakeField("priority",$priority,true), - MakeField("severity",$severity,true), - - MakeField("frequency",$frequency,true), - MakeField("found in",$foundinbuild,true), - - MakeField("description",$short_description,false) - ); - - if($firstattachment!=null) - array_push($fields,$firstattachment); - - global $slackCommand; - - $userlink = BuildUserLink($slackCommand->UserName); - $user_message = "Ok, {$userlink}, here's the defect you requested."; - - $obj = new stdClass; - $obj->text = ""; - $obj->attachments = MakeAttachment($user_message, "", $color, $fields, $storyuri); - return $obj; -} - function GetRequirementPayload($ref) { $object = CallAPI($ref); diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 6125df8..ea229a2 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -98,6 +98,89 @@ function ParseDefectPayload($Defect) return $ret; } +function GetDefectPayload($ref) +{ + global $show, $requesting_user_name; + + $object = CallAPI($ref); + + $defect = $object->Defect; + + $projecturi = $defect->Project->_ref; + + $title = $defect->_refObjectName; + $description = $defect->Description; + $owner = $defect->Owner->_refObjectName; + $submitter = $defect->SubmittedBy->_refObjectName; + $project = $object->Project->_refObjectName; + $created = $defect->_CreatedAt; + $state = $defect->State; + $priority = $defect->Priority; + $severity = $defect->Severity; + $frequency = $defect->c_Frequency; + $foundinbuild = $defect->FoundInBuild; + + $short_description = TruncateText(strip_tags($description), 200); + + $ProjectFull = CallAPI($projecturi); + $projectid = $ProjectFull->Project->ObjectID; + $defectid = $defect->ObjectID; + $projectName = $defect->Project->_refObjectName; + $itemid = $defect->FormattedID; + + $attachmentcount = $defect->Attachments->Count; + + $firstattachment = null; + if ($attachmentcount > 0) { + $linktxt = GetRallyAttachmentLink($defect->Attachments->_ref); + $firstattachment = MakeField("attachment", $linktxt, false); + } + + $defecturi = "https://rally1.rallydev.com/#/{$projectid}d/detail/defect/{$defectid}"; + + $enctitle = urlencode($title); + $linktext = "<{$defecturi}|{$enctitle}>"; + + $color = "bad"; + + $clean_description = html_entity_decode(strip_tags($description), ENT_HTML401 | ENT_COMPAT, 'UTF-8'); + $short_description = TruncateText($clean_description, 300); + + $fields = array( + MakeField("link", $linktext, false), + + MakeField("id", $itemid, true), + MakeField("owner", $owner, true), + + MakeField("project", $projectName, true), + MakeField("created", $created, true), + + MakeField("submitter", $submitter, true), + MakeField("state", $state, true), + + MakeField("priority", $priority, true), + MakeField("severity", $severity, true), + + MakeField("frequency", $frequency, true), + MakeField("found in", $foundinbuild, true), + + MakeField("description", $short_description, false) + ); + + if ($firstattachment != null) + array_push($fields, $firstattachment); + + global $slackCommand; + + $userlink = BuildUserLink($slackCommand->UserName); + $user_message = "Ok, {$userlink}, here's the defect you requested."; + + $obj = new stdClass; + $obj->text = ""; + $obj->attachments = MakeAttachment($user_message, "", $color, $fields, $storyuri); + return $obj; +} + /** * Prepares a table of fields attached to a Rally artifact for display. * From 233f2b0e72182da3d6a0410eff4310d13f7bbb8a Mon Sep 17 00:00:00 2001 From: JP Klein Date: Mon, 29 Sep 2014 19:24:57 -0400 Subject: [PATCH 05/41] Re-engineer GetDefectPayload interface for Fetch routine --- scripts/include/rallyme.inc.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index ea229a2..9d82283 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -98,14 +98,10 @@ function ParseDefectPayload($Defect) return $ret; } -function GetDefectPayload($ref) +function GetDefectPayload($defect) { global $show, $requesting_user_name; - $object = CallAPI($ref); - - $defect = $object->Defect; - $projecturi = $defect->Project->_ref; $title = $defect->_refObjectName; From f185ecd23cabe654917ed140086a75f6a035eb7f Mon Sep 17 00:00:00 2001 From: JP Klein Date: Mon, 29 Sep 2014 19:42:13 -0400 Subject: [PATCH 06/41] Remove unused & single-use variables in GetDefectPayload --- scripts/include/rallyme.inc.php | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 9d82283..10ef0f1 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -100,11 +100,6 @@ function ParseDefectPayload($Defect) function GetDefectPayload($defect) { - global $show, $requesting_user_name; - - $projecturi = $defect->Project->_ref; - - $title = $defect->_refObjectName; $description = $defect->Description; $owner = $defect->Owner->_refObjectName; $submitter = $defect->SubmittedBy->_refObjectName; @@ -116,25 +111,21 @@ function GetDefectPayload($defect) $frequency = $defect->c_Frequency; $foundinbuild = $defect->FoundInBuild; - $short_description = TruncateText(strip_tags($description), 200); - - $ProjectFull = CallAPI($projecturi); + $ProjectFull = CallAPI($defect->Project->_ref); $projectid = $ProjectFull->Project->ObjectID; $defectid = $defect->ObjectID; $projectName = $defect->Project->_refObjectName; $itemid = $defect->FormattedID; - $attachmentcount = $defect->Attachments->Count; - $firstattachment = null; - if ($attachmentcount > 0) { + if ($defect->Attachments->Count > 0) { $linktxt = GetRallyAttachmentLink($defect->Attachments->_ref); $firstattachment = MakeField("attachment", $linktxt, false); } $defecturi = "https://rally1.rallydev.com/#/{$projectid}d/detail/defect/{$defectid}"; - $enctitle = urlencode($title); + $enctitle = urlencode($defect->_refObjectName); $linktext = "<{$defecturi}|{$enctitle}>"; $color = "bad"; @@ -163,8 +154,9 @@ function GetDefectPayload($defect) MakeField("description", $short_description, false) ); - if ($firstattachment != null) + if ($firstattachment != null) { array_push($fields, $firstattachment); + } global $slackCommand; From 6d2a966dbb49753200aebf4fc91f7d90af83aaf8 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 01:45:33 -0400 Subject: [PATCH 07/41] Refactor defect link creation in GetDefectPayload --- scripts/include/rallyme.inc.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 10ef0f1..e5d0b09 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -111,9 +111,6 @@ function GetDefectPayload($defect) $frequency = $defect->c_Frequency; $foundinbuild = $defect->FoundInBuild; - $ProjectFull = CallAPI($defect->Project->_ref); - $projectid = $ProjectFull->Project->ObjectID; - $defectid = $defect->ObjectID; $projectName = $defect->Project->_refObjectName; $itemid = $defect->FormattedID; @@ -123,10 +120,11 @@ function GetDefectPayload($defect) $firstattachment = MakeField("attachment", $linktxt, false); } - $defecturi = "https://rally1.rallydev.com/#/{$projectid}d/detail/defect/{$defectid}"; - $enctitle = urlencode($defect->_refObjectName); - $linktext = "<{$defecturi}|{$enctitle}>"; + $projectid = basename($defect->Project->_ref); + $defectid = $defect->ObjectID; + $defecturl = $RALLY_BASE_URL . '#/' . $projectid . '/detail/defect/' . $defectid; + $linktext = l($enctitle, $defecturl); $color = "bad"; From 124b016cd4365f20293fcf3a483344fb642fcacf Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 01:57:59 -0400 Subject: [PATCH 08/41] Rearrange field definitions & delete unused vars in GetDefectPayload --- scripts/include/rallyme.inc.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index e5d0b09..2ba0886 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -100,10 +100,7 @@ function ParseDefectPayload($Defect) function GetDefectPayload($defect) { - $description = $defect->Description; - $owner = $defect->Owner->_refObjectName; $submitter = $defect->SubmittedBy->_refObjectName; - $project = $object->Project->_refObjectName; $created = $defect->_CreatedAt; $state = $defect->State; $priority = $defect->Priority; @@ -111,23 +108,25 @@ function GetDefectPayload($defect) $frequency = $defect->c_Frequency; $foundinbuild = $defect->FoundInBuild; - $projectName = $defect->Project->_refObjectName; - $itemid = $defect->FormattedID; - $firstattachment = null; if ($defect->Attachments->Count > 0) { $linktxt = GetRallyAttachmentLink($defect->Attachments->_ref); $firstattachment = MakeField("attachment", $linktxt, false); } + global $RALLY_API_URL; + $enctitle = urlencode($defect->_refObjectName); $projectid = basename($defect->Project->_ref); $defectid = $defect->ObjectID; $defecturl = $RALLY_BASE_URL . '#/' . $projectid . '/detail/defect/' . $defectid; $linktext = l($enctitle, $defecturl); - $color = "bad"; + $itemid = $defect->FormattedID; + $owner = $defect->Owner->_refObjectName; + $projectName = $defect->Project->_refObjectName; + $description = $defect->Description; $clean_description = html_entity_decode(strip_tags($description), ENT_HTML401 | ENT_COMPAT, 'UTF-8'); $short_description = TruncateText($clean_description, 300); @@ -161,6 +160,8 @@ function GetDefectPayload($defect) $userlink = BuildUserLink($slackCommand->UserName); $user_message = "Ok, {$userlink}, here's the defect you requested."; + $color = "bad"; + $obj = new stdClass; $obj->text = ""; $obj->attachments = MakeAttachment($user_message, "", $color, $fields, $storyuri); From 63865483f6f69e06c798abc35ed7cf087c2350b5 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 02:13:49 -0400 Subject: [PATCH 09/41] Rearrange code in GetDefectPayload for readability --- scripts/include/rallyme.inc.php | 39 +++++++++++++++------------------ 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 2ba0886..961f3df 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -100,21 +100,12 @@ function ParseDefectPayload($Defect) function GetDefectPayload($defect) { - $submitter = $defect->SubmittedBy->_refObjectName; - $created = $defect->_CreatedAt; - $state = $defect->State; - $priority = $defect->Priority; - $severity = $defect->Severity; - $frequency = $defect->c_Frequency; - $foundinbuild = $defect->FoundInBuild; + global $slackCommand, $RALLY_BASE_URL; - $firstattachment = null; - if ($defect->Attachments->Count > 0) { - $linktxt = GetRallyAttachmentLink($defect->Attachments->_ref); - $firstattachment = MakeField("attachment", $linktxt, false); - } + $userlink = BuildUserLink($slackCommand->UserName); + $user_message = "Ok, {$userlink}, here's the defect you requested."; - global $RALLY_API_URL; + $color = "bad"; $enctitle = urlencode($defect->_refObjectName); $projectid = basename($defect->Project->_ref); @@ -125,6 +116,13 @@ function GetDefectPayload($defect) $itemid = $defect->FormattedID; $owner = $defect->Owner->_refObjectName; $projectName = $defect->Project->_refObjectName; + $created = $defect->_CreatedAt; + $submitter = $defect->SubmittedBy->_refObjectName; + $state = $defect->State; + $priority = $defect->Priority; + $severity = $defect->Severity; + $frequency = $defect->c_Frequency; + $foundinbuild = $defect->FoundInBuild; $description = $defect->Description; $clean_description = html_entity_decode(strip_tags($description), ENT_HTML401 | ENT_COMPAT, 'UTF-8'); @@ -151,20 +149,19 @@ function GetDefectPayload($defect) MakeField("description", $short_description, false) ); + $firstattachment = null; + if ($defect->Attachments->Count > 0) { + $linktxt = GetRallyAttachmentLink($defect->Attachments->_ref); + $firstattachment = MakeField("attachment", $linktxt, false); + } + if ($firstattachment != null) { array_push($fields, $firstattachment); } - global $slackCommand; - - $userlink = BuildUserLink($slackCommand->UserName); - $user_message = "Ok, {$userlink}, here's the defect you requested."; - - $color = "bad"; - $obj = new stdClass; $obj->text = ""; - $obj->attachments = MakeAttachment($user_message, "", $color, $fields, $storyuri); + $obj->attachments = MakeAttachment($user_message, "", $color, $fields, $defecturl); return $obj; } From ce2adc919bdc6cb16c863d3c7bf9bc0c0792b8b5 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 02:46:04 -0400 Subject: [PATCH 10/41] Remove unnecessary variable definitions in GetDefectPayload --- scripts/include/rallyme.inc.php | 45 +++++++++++++-------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 961f3df..b3bf6f0 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -103,9 +103,9 @@ function GetDefectPayload($defect) global $slackCommand, $RALLY_BASE_URL; $userlink = BuildUserLink($slackCommand->UserName); - $user_message = "Ok, {$userlink}, here's the defect you requested."; + $user_message = 'Ok, ' . $userlink . ', here's the defect you requested.'; - $color = "bad"; + $color = 'bad'; $enctitle = urlencode($defect->_refObjectName); $projectid = basename($defect->Project->_ref); @@ -113,46 +113,35 @@ function GetDefectPayload($defect) $defecturl = $RALLY_BASE_URL . '#/' . $projectid . '/detail/defect/' . $defectid; $linktext = l($enctitle, $defecturl); - $itemid = $defect->FormattedID; - $owner = $defect->Owner->_refObjectName; - $projectName = $defect->Project->_refObjectName; - $created = $defect->_CreatedAt; - $submitter = $defect->SubmittedBy->_refObjectName; - $state = $defect->State; - $priority = $defect->Priority; - $severity = $defect->Severity; - $frequency = $defect->c_Frequency; - $foundinbuild = $defect->FoundInBuild; - $description = $defect->Description; $clean_description = html_entity_decode(strip_tags($description), ENT_HTML401 | ENT_COMPAT, 'UTF-8'); $short_description = TruncateText($clean_description, 300); $fields = array( - MakeField("link", $linktext, false), + MakeField('link', $linktext, false), - MakeField("id", $itemid, true), - MakeField("owner", $owner, true), + MakeField('id', $defect->FormattedID, true), + MakeField('owner', $defect->Owner->_refObjectName, true), - MakeField("project", $projectName, true), - MakeField("created", $created, true), + MakeField('project', $defect->Project->_refObjectName, true), + MakeField('created', $defect->_CreatedAt, true), - MakeField("submitter", $submitter, true), - MakeField("state", $state, true), + MakeField('submitter', $defect->SubmittedBy->_refObjectName, true), + MakeField('state', $defect->State, true), - MakeField("priority", $priority, true), - MakeField("severity", $severity, true), + MakeField('priority', $defect->Priority, true), + MakeField('severity', $defect->Severity, true), - MakeField("frequency", $frequency, true), - MakeField("found in", $foundinbuild, true), + MakeField('frequency', $defect->c_Frequency, true), + MakeField('found in', $defect->FoundInBuild, true), - MakeField("description", $short_description, false) + MakeField('description', $short_description, false) ); $firstattachment = null; if ($defect->Attachments->Count > 0) { $linktxt = GetRallyAttachmentLink($defect->Attachments->_ref); - $firstattachment = MakeField("attachment", $linktxt, false); + $firstattachment = MakeField('attachment', $linktxt, false); } if ($firstattachment != null) { @@ -160,8 +149,8 @@ function GetDefectPayload($defect) } $obj = new stdClass; - $obj->text = ""; - $obj->attachments = MakeAttachment($user_message, "", $color, $fields, $defecturl); + $obj->text = ''; + $obj->attachments = MakeAttachment($user_message, '', $color, $fields, $defecturl); return $obj; } From 8d3386ad66065bfdb61d4ac71b7801d5b2bd4ed8 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 03:01:31 -0400 Subject: [PATCH 11/41] Micro-optimize object declaration to piss off ircmaxell --- scripts/include/rallyme.inc.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index b3bf6f0..1c34244 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -148,10 +148,8 @@ function GetDefectPayload($defect) array_push($fields, $firstattachment); } - $obj = new stdClass; - $obj->text = ''; - $obj->attachments = MakeAttachment($user_message, '', $color, $fields, $defecturl); - return $obj; + $payload = array('text' => '', 'attachments' => MakeAttachment($user_message, '', $color, $fields, $defecturl)); + return (object) $payload; } /** From 39bb4795b527b8a53690858e74dc182ce6ef961c Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 03:07:05 -0400 Subject: [PATCH 12/41] Update rallyme.inc.php --- scripts/include/rallyme.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 1c34244..05655f0 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -103,7 +103,7 @@ function GetDefectPayload($defect) global $slackCommand, $RALLY_BASE_URL; $userlink = BuildUserLink($slackCommand->UserName); - $user_message = 'Ok, ' . $userlink . ', here's the defect you requested.'; + $user_message = 'Ok, ' . $userlink . ', here\'s the defect you requested.'; $color = 'bad'; From 7263d8c18a388fe3667ec081c2d36b80c3ff7b20 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 10:08:24 -0400 Subject: [PATCH 13/41] Move BuildUserLink to Slack library file --- scripts/include/rally.php | 6 ------ scripts/include/slack.php | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/include/rally.php b/scripts/include/rally.php index 834409f..7219916 100644 --- a/scripts/include/rally.php +++ b/scripts/include/rally.php @@ -249,12 +249,6 @@ function FindRequirement($id) return GetFirstObjectFromSearchResult("HierarchicalRequirement", $searchresult); } -function BuildUserLink($username) -{ - $userlink = ""; - return $userlink; -} - function GetArtifactQueryUri($id) { global $config; diff --git a/scripts/include/slack.php b/scripts/include/slack.php index 16af231..efc4974 100644 --- a/scripts/include/slack.php +++ b/scripts/include/slack.php @@ -31,6 +31,12 @@ function BuildSlashCommand($request) //text-formatting functions +function BuildUserLink($username) +{ + $userlink = ""; + return $userlink; +} + function SanitizeText($text) { $text = strtr($text, array('
' => '\n', '

' => '\n', '

' => '\n')); From 4fc5427d0dcd4bc09f541dc66c71902279f5073d Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 10:12:16 -0400 Subject: [PATCH 14/41] Generalize Slack's BuildUserLink --- scripts/config/default.config.php | 1 + scripts/include/slack.php | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/config/default.config.php b/scripts/config/default.config.php index 2a80113..955b245 100644 --- a/scripts/config/default.config.php +++ b/scripts/config/default.config.php @@ -1,3 +1,4 @@ "; - return $userlink; + global $SLACK_SUBDOMAIN; + + $userlink = ''; + return $userlink; } function SanitizeText($text) From 47fd3fded1e6b66ab7f2918daa69d4dec53bba33 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 14:15:12 -0400 Subject: [PATCH 15/41] Remove deprecated functions --- scripts/include/rally.php | 68 ------------------------------ scripts/include/rallyme.config.php | 1 - 2 files changed, 69 deletions(-) diff --git a/scripts/include/rally.php b/scripts/include/rally.php index 7219916..4ee6eeb 100644 --- a/scripts/include/rally.php +++ b/scripts/include/rally.php @@ -3,28 +3,6 @@ $RALLY_BASE_URL = 'https://rally1.rallydev.com/'; $RALLY_API_URL = $RALLY_BASE_URL . 'slm/webservice/v2.0/'; -function HandleItem($slackCommand, $rallyFormattedId) -{ - $rallyItemType = substr($rallyFormattedId,0,2); - - switch($rallyItemType){ - - case "DE": - return HandleDefect($rallyFormattedId, $slackCommand->ChannelName); - die; - break; - case "US": - case "TA": - return HandleStory($rallyFormattedId, $slackCommand->ChannelName); - die; - break; - default: - print_r("Sorry, I don't know what kind of rally object {$rallyFormattedId} is. If you need rallyme to work with these, buy a :beer:. I hear he likes IPAs."); - die; - break; - } -} - function HandleDefect($id, $channel_name) { $defectref = FindDefect($id); @@ -255,52 +233,6 @@ function GetArtifactQueryUri($id) return str_replace("[[ID]]", $id, $config['rally']['artifactquery']); } -function GetDefectQueryUri($id) -{ - global $config; - return str_replace("[[ID]]", $id, $config['rally']['defectquery']); -} - -function FindDefect($id) -{ - $query = GetDefectQueryUri($id); - $searchresult = CallAPI($query); - - $count = GetCount($searchresult); - if($count == 0) - NotFound($id); - - return GetFirstObjectFromSearchResult("Defect", $searchresult); -} - -function GetCount($searchresult) -{ - return $searchresult->QueryResult->TotalResultCount; -} - -function NotFound($id) -{ - global $slackCommand; - $userlink = BuildUserLink($slackCommand->UserName); - print_r("Sorry {$userlink}, I couldn't find {$id}");die; -} - - -function GetFirstObjectFromSearchResult($objectName, $result) -{ - foreach ($result->QueryResult->Results as $result) - { - if($result->_type == $objectName) - return $result->_ref; - } - global $slackCommand; - $userlink = BuildUserLink($slackCommand->UserName); - print_r("Sorry @{$userlink}, your search for '{$slackCommand->Text}' was ambiguous.:\n"); - print_r("Here's what Rally told me:\n"); - print_r($result); - die; -} - function TruncateText($text, $len) { if(strlen($text) <= $len) diff --git a/scripts/include/rallyme.config.php b/scripts/include/rallyme.config.php index 9e0f85d..2170aff 100644 --- a/scripts/include/rallyme.config.php +++ b/scripts/include/rallyme.config.php @@ -1,7 +1,6 @@ Date: Tue, 30 Sep 2014 16:59:48 -0400 Subject: [PATCH 16/41] Report user errors either via incoming webhook or response body --- scripts/include/rallyme.inc.php | 63 +++++++++++++++++------- scripts/include/slack.php | 85 ++++++++++++++++++++++----------- 2 files changed, 105 insertions(+), 43 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 05655f0..ae333e6 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -6,6 +6,8 @@ require_once('slack.php'); require_once('rally.php'); +set_error_handler('_HandleRallyMeErrors', E_USER_ERROR); + /** * Parses artifact ID from request, queries Rally, and selects handler to gather * field values. @@ -21,43 +23,76 @@ function FetchArtifactPayload($command_text) list($formatted_id) = explode(' ', trim($command_text)); $formatted_id = strtoupper($formatted_id); - $artifact_query = $RALLY_API_URL; + //handle item + $query_url = $RALLY_API_URL; switch (substr($formatted_id, 0, 2)) { - case 'DE': - $artifact_query .= 'defect'; + case 'DE': //find defect + $query_url .= 'defect'; $artifact_type = 'Defect'; $func = 'ParseDefectPayload'; break; case 'TA': - $artifact_query .= 'artifact'; + $query_url .= 'artifact'; $artifact_type = 'Task'; $func = 'ParseTaskPayload'; break; case 'US': - $artifact_query .= 'artifact'; + $query_url .= 'artifact'; $artifact_type = 'HierarchicalRequirement'; $func = 'ParseStoryPayload'; break; default: - die('Sorry, I don\'t know how to handle "' . $command_text . '". You can look up user stories, defects, and tasks by ID, like "DE1234".'); + trigger_error('Sorry, @user, I don\'t know how to handle "' . $command_text . '". You can look up user stories, defects, and tasks by ID, like "DE1234".', E_USER_ERROR); } - $artifact_query .= '?query=(FormattedID+%3D+' . $formatted_id . ')&fetch=true'; + $query_url .= '?query=(FormattedID+%3D+' . $formatted_id . ')&fetch=true'; - $Results = CallAPI($artifact_query); - if ($Results->QueryResult->TotalResultCount == 0) { - die('Sorry, I couldn\'t find ' . $formatted_id); + $Results = CallAPI($query_url); + if ($Results->QueryResult->TotalResultCount == 0) { //get count + trigger_error('Sorry, @user, I couldn\'t find ' . $formatted_id, E_USER_ERROR); //not found } + //get first object from search result foreach ($Results->QueryResult->Results as $Result) { if ($Result->_type == $artifact_type) { return call_user_func($func, $Result); } } - die('Sorry, your search for "' . $formatted_id . '" was ambiguous.'); + trigger_error('Sorry, @user, your search for "' . $formatted_id . '" was ambiguous.', E_USER_ERROR); +} + +/** + * Notifies Slack users of errors either via an incoming webhook or in the body + * of the HTTP response. + * + * @param int $errno + * @param string $errstr + * + * @return void + */ +function _HandleRallyMeErrors($errno, $errstr) +{ + //assume at-mentions are linkified over either transmission channel + $user = '@' . $_REQUEST['user_name']; + $errstr = strtr($errstr, array('@user' => $user)); + + if (isSlashCommand()) { + //use an incoming webhook to report error + return slack_incoming_hook_post( + $config['slack']['hook'], + $config['rally']['botname'], + $_REQUEST['channel_name'], + $config['rally']['boticon'], + NULL, + $errstr + ); + } else { + //otherwise return Slack-formatted JSON in the response body + return PrintJsonResponse($errstr); + } } /** @@ -249,9 +284,5 @@ function ReturnArtifactPayload($payload) } } - $data = ['text' => $text]; - $text = json_encode($data, JSON_HEX_AMP | JSON_HEX_APOS | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT); - $text = strtr($text, ['\n' => 'n', '\t' => 't']); //fix double-escaped codes - - return print_r($text); + return PrintJsonResponse($text); } diff --git a/scripts/include/slack.php b/scripts/include/slack.php index 2ba5284..39251b4 100644 --- a/scripts/include/slack.php +++ b/scripts/include/slack.php @@ -29,6 +29,16 @@ function BuildSlashCommand($request) return $cmd; } +/** + * Determine if the incoming request was made via a slash command. + * + * @return boolean + */ +function isSlashCommand() +{ + return isset($_REQUEST['command']) ? $_REQUEST['command'] : FALSE; +} + //text-formatting functions function BuildUserLink($username) @@ -55,46 +65,67 @@ function em($text) return '_' . $text . '_'; } -function slack_incoming_hook_post($uri, $user, $channel, $icon, $emoji, $payload){ - +function slack_incoming_hook_post($url, $user, $channel, $icon, $emoji, $payload) +{ $data = array( - "text" => $payload, - "channel" => "#".$channel, - "username"=>$user - ); + 'text' => $payload, + 'channel' => '#' . $channel, + 'username' => $user, + 'link_names' => 1 + ); - if($icon!=null) - { + if ($icon != null) { $data['icon_url'] = $icon; - } - elseif($emoji!=null) - { + } elseif ($emoji != null) { $data['icon_emoji'] = $emoji; } - $data_string = "payload=" . json_encode($data, JSON_HEX_AMP|JSON_HEX_APOS|JSON_NUMERIC_CHECK|JSON_PRETTY_PRINT); - - mylog('sent.txt',$data_string); - return curl_post($uri, $data_string); + return _incoming_hook_post($url, $data); } - - -function slack_incoming_hook_post_with_attachments($uri, $user, $channel, $icon, $payload, $attachments){ +function slack_incoming_hook_post_with_attachments($url, $user, $channel, $icon, $payload, $attachments) +{ + //allow bot to display formatted attachment text + $attachments->mrkdwn_in = array('pretext', 'text', 'title', 'fields'); $data = array( - "text" => $payload, - "channel" => "#".$channel, - "username"=>$user, - "icon_url"=>$icon, - "attachments"=>array($attachments)); - - $data_string = "payload=" . json_encode($data, JSON_HEX_AMP|JSON_HEX_APOS|JSON_NUMERIC_CHECK|JSON_PRETTY_PRINT); - mylog('sent.txt',$data_string); - return curl_post($uri, $data_string); + 'text' => $payload, + 'channel' => '#' . $channel, + 'username' => $user, + 'icon_url' => $icon, + 'attachments' => array($attachments), + 'link_names' => 1 //allow bot to linkify at-mentions in attachments + ); + + return _incoming_hook_post($url, $data); +} + +function _incoming_hook_post($url, $data) +{ + $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 + + $result = curl_post($url, $data_string); + switch ($result) { + case 'ok': + mylog('sent.txt', $data_string); + return $result; + case 'Invalid channel specified': + exit('Unable to post messages to a private chat'); + default: + exit('Unable to send Incoming WebHook message: ' . $reply); + } } +function PrintJsonResponse($payload) +{ + $data = array('text' => $payload); + + $data_string = json_encode($data, JSON_HEX_AMP | JSON_HEX_APOS | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT); + $data_string = strtr($data_string, ['\n' => 'n', '\t' => 't']); //fix double-escaped codes + return print_r($data_string); +} /* slack attachment format From 639b35842ea01919d8aae422c9564130f18f738c Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 17:11:23 -0400 Subject: [PATCH 17/41] Exit on user error --- scripts/include/rallyme.inc.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index ae333e6..e3f3062 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -81,7 +81,7 @@ function _HandleRallyMeErrors($errno, $errstr) if (isSlashCommand()) { //use an incoming webhook to report error - return slack_incoming_hook_post( + slack_incoming_hook_post( $config['slack']['hook'], $config['rally']['botname'], $_REQUEST['channel_name'], @@ -91,8 +91,10 @@ function _HandleRallyMeErrors($errno, $errstr) ); } else { //otherwise return Slack-formatted JSON in the response body - return PrintJsonResponse($errstr); + PrintJsonResponse($errstr); } + + exit(); } /** From 837360f5fdc5f50d0074895ff50d40d1d6c81b9e Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 17:24:50 -0400 Subject: [PATCH 18/41] Fix error in rallyme error-handler --- scripts/include/rallyme.inc.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index e3f3062..41c5f07 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -75,6 +75,8 @@ function FetchArtifactPayload($command_text) */ function _HandleRallyMeErrors($errno, $errstr) { + global $config; + //assume at-mentions are linkified over either transmission channel $user = '@' . $_REQUEST['user_name']; $errstr = strtr($errstr, array('@user' => $user)); From e0aa70f13fa1095759e88d25b8b142a1d6bb111e Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 20:42:42 -0400 Subject: [PATCH 19/41] Enable posting two versions of defect info via incoming webhook --- scripts/config/default.config.php | 2 + scripts/include/rallyme.inc.php | 149 +++++++++++++++++++++++------- 2 files changed, 116 insertions(+), 35 deletions(-) diff --git a/scripts/config/default.config.php b/scripts/config/default.config.php index 955b245..80f23db 100644 --- a/scripts/config/default.config.php +++ b/scripts/config/default.config.php @@ -2,3 +2,5 @@ $SLACK_OUTGOING_HOOK_TOKEN = 'REPLACE ME'; //used to validate requests to fetch details about Rally artifacts $SLACK_SUBDOMAIN = 'REPLACE ME'; //subdomain used to identify your team's instance, like 'cim' + +$RALLYME_DISPLAY_VERSION = 2; //alters the fields that are displayed for fetched artifacts diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 41c5f07..31f0bc1 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -108,33 +108,62 @@ function _HandleRallyMeErrors($errno, $errstr) */ function ParseDefectPayload($Defect) { - global $RALLY_BASE_URL; - - $state = $Defect->State; - if ($state == 'Closed') { - $Date = new DateTime($Defect->ClosedDate); - $state .= ' ' . $Date->format('M j'); - } + global $RALLYME_DISPLAY_VERSION, $RALLY_BASE_URL; + + $title = $Defect->_refObjectName; + $header = array('title' => $title); + $item_url = $RALLY_BASE_URL . '#/' . basename($Defect->Project->_ref) . '/detail/defect/' . $Defect->ObjectID; + + switch ($RALLYME_DISPLAY_VERSION) { + + case 2: + $header['item_id'] = $Defect->FormattedID; + $header['item_url'] = $item_url; + + $state = $Defect->State; + if ($state == 'Closed') { + $Date = new DateTime($Defect->ClosedDate); + $state .= ' ' . $Date->format('M j'); + } + + $fields = array( + 'Creator' => $Defect->SubmittedBy->_refObjectName, + 'Created' => $Defect->_CreatedAt, + 'Owner' => $Defect->Owner->_refObjectName, + 'State' => $state, + 'Priority' => $Defect->Priority, + 'Severity' => $Defect->Severity, + 'Description' => $Defect->Description, + ); + if ($Defect->Attachments->Count > 0) { + $fields['Attachment'] = GetAttachmentLinks($Defect->Attachments->_ref); + } + break; - $ret = array( - 'item_id' => $Defect->FormattedID, - 'item_url' => $RALLY_BASE_URL . '#/' . basename($Defect->Project->_ref) . '/detail/defect/' . $Defect->ObjectID, - 'title' => $Defect->_refObjectName, - - 'Creator' => $Defect->SubmittedBy->_refObjectName, - 'Created' => $Defect->_CreatedAt, - 'Owner' => $Defect->Owner->_refObjectName, - 'State' => $state, - 'Priority' => $Defect->Priority, - 'Severity' => $Defect->Severity, - 'Description' => $Defect->Description, - ); + default: + $header['type'] = 'defect'; + + $fields = array( + 'link' => array($title => $item_url), + 'id' => $Defect->FormattedID, + 'owner' => $Defect->Owner->_refObjectName, + 'project' => $Defect->Project->_refObjectName, + 'created' => $Defect->_CreatedAt, + 'submitter' => $Defect->SubmittedBy->_refObjectName, + 'state' => $Defect->State, + 'priority' => $Defect->Priority, + 'severity' => $Defect->Severity, + 'frequency' => $Defect->c_Frequency, + 'found in' => $Defect->FoundInBuild, + 'description' => $Defect->Description, + ); + if ($Defect->Attachments->Count > 0) { + $fields['attachment'] = GetAttachmentLinks($Defect->Attachments->_ref); + } + break; - if ($Defect->Attachments->Count > 0) { - $ret['Attachment'] = GetAttachmentLinks($Defect->Attachments->_ref); } - - return $ret; + return array('header' => $header, 'fields' => $fields); } function GetDefectPayload($defect) @@ -226,16 +255,17 @@ function GetAttachmentLinks($attachment_ref) { global $RALLY_BASE_URL; $url = $RALLY_BASE_URL . 'slm/attachment/'; - $ret = array(); + $links = array(); $Attachments = CallAPI($attachment_ref); + foreach ($Attachments->QueryResult->Results as $Attachment) { $filename = $Attachment->_refObjectName; $link_url = $url . $Attachment->ObjectID . '/' . urlencode($filename); - $ret[$filename] = $link_url; + $links[$filename] = $link_url; } - return $ret; + return $links; } /** @@ -247,7 +277,44 @@ function GetAttachmentLinks($attachment_ref) */ function SendArtifactPayload($payload) { + global $config; + + $prextext = ArtifactPretext($payload['header']); + $color = 'bad'; + $fields = array(); + foreach ($payload['fields'] as $label => $value) { + $short = TRUE; + switch ($label) { + + case 'Parent': + case 'Attachment': + case 'link': + case 'attachment': + $link_url = reset($value); + $value = l(urlencode(key($value)), $link_url); + $short = FALSE; + break; + + case 'Description': + case 'description': + $value = TruncateText(SanitizeText($value), 300, $payload['header']['item_url']); + $short = FALSE; + break; + } + $fields[] = MakeField($label, $value, $short); + } + + $attachment = MakeAttachment($prextext, '', $color, $fields, $payload['header']['item_url']); + + return slack_incoming_hook_post_with_attachments( + $config['slack']['hook'], + $config['rally']['botname'], + $_REQUEST['channel_name'], + $config['rally']['boticon'], + '', + $attachment + ); } /** @@ -259,10 +326,10 @@ function SendArtifactPayload($payload) */ function ReturnArtifactPayload($payload) { - $text = em('Details for ' . $payload['item_id'] . ' ' . l($payload['title'], $payload['item_url'])); + $text = ArtifactPretext($payload['header']); - foreach (array_slice($payload, 3) as $title => $value) { - switch ($title) { + foreach ($payload['fields'] as $label => $value) { + switch ($label) { case 'Attachment': case 'Parent': @@ -271,18 +338,18 @@ function ReturnArtifactPayload($payload) break; case 'Block Reason': - $title = ''; + $label = ''; $value = SanitizeText($value); break; case 'Description': - $value = TruncateText(SanitizeText($value), 300, $payload['item_url']); + $value = TruncateText(SanitizeText($value), 300, $payload['header']['item_url']); $value = '\n> ' . strtr($value, ['\n' => '\n> ']); } - if ($title) { - $title .= ':'; - $text .= '\n`' . str_pad($title, 15) . '`\t' . $value; + if ($label) { + $label .= ':'; + $text .= '\n`' . str_pad($label, 15) . '`\t' . $value; } else { $text .= '\n>' . $value; } @@ -290,3 +357,15 @@ function ReturnArtifactPayload($payload) return PrintJsonResponse($text); } + +function ArtifactPretext($info) +{ + global $RALLYME_DISPLAY_VERSION; + + switch ($RALLYME_DISPLAY_VERSION) { + case 2: + return em('Details for ' . $info['item_id'] . ' ' . l($info['title'], $info['item_url'])); + default: + return 'Ok, @' . $_REQUEST['user_name'] . ', here\'s the ' . $info['type'] . ' you requested.'; + } +} From 2837b9b2679d1bfa04e1bf53aa8adf1f2ceddc93 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 20:49:12 -0400 Subject: [PATCH 20/41] Remove deprecated functions --- scripts/include/rally.php | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/scripts/include/rally.php b/scripts/include/rally.php index 4ee6eeb..e8f20fb 100644 --- a/scripts/include/rally.php +++ b/scripts/include/rally.php @@ -3,27 +3,6 @@ $RALLY_BASE_URL = 'https://rally1.rallydev.com/'; $RALLY_API_URL = $RALLY_BASE_URL . 'slm/webservice/v2.0/'; -function HandleDefect($id, $channel_name) -{ - $defectref = FindDefect($id); - - $payload = GetDefectPayload($defectref); - - $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)); - print_r("\n"); - die("Apparently the Rallyme script is having a problem. Ask about it. :frowning:"); - } - return $result; -} - function HandleStory($id, $channel_name) { @@ -190,11 +169,6 @@ function MakeField($title, $value, $short=false) return $attachmentfield; } -function getProjectPayload($projectRefUri) -{ - $project = CallAPI($projectRefUri); -} - function CallAPI($uri) { global $config; @@ -205,14 +179,6 @@ function CallAPI($uri) return $object; } - -function GetProjectID($projectref) -{ - $ProjectFull = CallAPI($projectref); - $projectid = $ProjectFull->Project->ObjectID; - return $projectid; -} - function FindRequirement($id) { $query = GetArtifactQueryUri($id); From 0ec67dae8993742f060edc96e7a38a5ff4afb8f4 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 20:56:02 -0400 Subject: [PATCH 21/41] Remove deprecated functions --- scripts/include/rallyme.inc.php | 54 --------------------------------- 1 file changed, 54 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 31f0bc1..768b3dc 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -166,60 +166,6 @@ function ParseDefectPayload($Defect) return array('header' => $header, 'fields' => $fields); } -function GetDefectPayload($defect) -{ - global $slackCommand, $RALLY_BASE_URL; - - $userlink = BuildUserLink($slackCommand->UserName); - $user_message = 'Ok, ' . $userlink . ', here\'s the defect you requested.'; - - $color = 'bad'; - - $enctitle = urlencode($defect->_refObjectName); - $projectid = basename($defect->Project->_ref); - $defectid = $defect->ObjectID; - $defecturl = $RALLY_BASE_URL . '#/' . $projectid . '/detail/defect/' . $defectid; - $linktext = l($enctitle, $defecturl); - - $description = $defect->Description; - $clean_description = html_entity_decode(strip_tags($description), ENT_HTML401 | ENT_COMPAT, 'UTF-8'); - $short_description = TruncateText($clean_description, 300); - - $fields = array( - MakeField('link', $linktext, false), - - MakeField('id', $defect->FormattedID, true), - MakeField('owner', $defect->Owner->_refObjectName, true), - - MakeField('project', $defect->Project->_refObjectName, true), - MakeField('created', $defect->_CreatedAt, true), - - MakeField('submitter', $defect->SubmittedBy->_refObjectName, true), - MakeField('state', $defect->State, true), - - MakeField('priority', $defect->Priority, true), - MakeField('severity', $defect->Severity, true), - - MakeField('frequency', $defect->c_Frequency, true), - MakeField('found in', $defect->FoundInBuild, true), - - MakeField('description', $short_description, false) - ); - - $firstattachment = null; - if ($defect->Attachments->Count > 0) { - $linktxt = GetRallyAttachmentLink($defect->Attachments->_ref); - $firstattachment = MakeField('attachment', $linktxt, false); - } - - if ($firstattachment != null) { - array_push($fields, $firstattachment); - } - - $payload = array('text' => '', 'attachments' => MakeAttachment($user_message, '', $color, $fields, $defecturl)); - return (object) $payload; -} - /** * Prepares a table of fields attached to a Rally artifact for display. * From 269a8d20d78131f1ffa51d0c47f7532b2da8c2b1 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 21:17:45 -0400 Subject: [PATCH 22/41] Add abstractions related to script execution for reuse/elaboration --- scripts/include/slack.php | 12 ++++++++++++ scripts/rallyme.php | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/include/slack.php b/scripts/include/slack.php index 39251b4..26e4d37 100644 --- a/scripts/include/slack.php +++ b/scripts/include/slack.php @@ -29,6 +29,18 @@ function BuildSlashCommand($request) return $cmd; } +/** + * Determine if the incoming request is using the correct token. + * + * @return boolean + */ +function isValidOutgoingHookRequest() +{ + global $SLACK_OUTGOING_HOOK_TOKEN; + + return isset($_REQUEST['token']) && $_REQUEST['token'] == $SLACK_OUTGOING_HOOK_TOKEN; +} + /** * Determine if the incoming request was made via a slash command. * diff --git a/scripts/rallyme.php b/scripts/rallyme.php index a0aced7..5935b23 100644 --- a/scripts/rallyme.php +++ b/scripts/rallyme.php @@ -9,7 +9,7 @@ $result = NULL; -if (isset($_REQUEST['token']) && $_REQUEST['token'] == $SLACK_OUTGOING_HOOK_TOKEN && isset($_REQUEST['text'])) { +if (isValidOutgoingHookRequest() && isset($_REQUEST['text'])) { $payload = FetchArtifactPayload($_REQUEST['text']); - $result = isset($_REQUEST['command']) ? SendArtifactPayload($payload) : ReturnArtifactPayload($payload); + $result = isSlashCommand() ? SendArtifactPayload($payload) : ReturnArtifactPayload($payload); } From d98c212c92b63f0563160da96c13baa1cefe7c9c Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 21:34:18 -0400 Subject: [PATCH 23/41] Remove short-form array declaration for back-compatibility --- scripts/include/rallyme.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 768b3dc..5d3a7f3 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -290,7 +290,7 @@ function ReturnArtifactPayload($payload) case 'Description': $value = TruncateText(SanitizeText($value), 300, $payload['header']['item_url']); - $value = '\n> ' . strtr($value, ['\n' => '\n> ']); + $value = '\n> ' . strtr($value, array('\n' => '\n> ')); } if ($label) { From 8b0c1d862d6f070d75eac715d1750f0bb2499b67 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 21:36:24 -0400 Subject: [PATCH 24/41] Remove short-form array declaration for back-compatibility --- scripts/include/slack.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/include/slack.php b/scripts/include/slack.php index 26e4d37..12bbd2a 100644 --- a/scripts/include/slack.php +++ b/scripts/include/slack.php @@ -134,7 +134,7 @@ function PrintJsonResponse($payload) $data = array('text' => $payload); $data_string = json_encode($data, JSON_HEX_AMP | JSON_HEX_APOS | JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT); - $data_string = strtr($data_string, ['\n' => 'n', '\t' => 't']); //fix double-escaped codes + $data_string = strtr($data_string, array('\n' => 'n', '\t' => 't')); //fix double-escaped codes return print_r($data_string); } From 388e597d6692cabf159558798d71958ad54d67f5 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 21:51:34 -0400 Subject: [PATCH 25/41] Move GetRequirementPayload to rallyme include file for re-engineering --- scripts/include/rally.php | 106 -------------------------------- scripts/include/rallyme.inc.php | 100 ++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 106 deletions(-) diff --git a/scripts/include/rally.php b/scripts/include/rally.php index e8f20fb..438d5f2 100644 --- a/scripts/include/rally.php +++ b/scripts/include/rally.php @@ -53,112 +53,6 @@ function GetRallyAttachmentLink($attachmentRef) return $linktxt; } -function GetRequirementPayload($ref) -{ - $object = CallAPI($ref); - - $requirement = null; - - if($object->HierarchicalRequirement) - { - $requirement = $object->HierarchicalRequirement; - } - elseif($object->Task) - { - $requirement = $object->Task; - } - else - { - $class = get_class($object); - global $slackCommand; - $userlink = BuildUserLink($slackCommand->UserName); - print_r("Sorry {$userlink}, I can't handle a {$class} yet. I'll let @tdm know about it."); - die; - } - - $projecturi = $requirement->Project->_ref; - - $title = $requirement->_refObjectName; - - - $ProjectFull = CallAPI($projecturi); - $projectid = $ProjectFull->Project->ObjectID; - $storyid = $requirement->ObjectID; - $description = $requirement->Description; - $owner = $requirement->Owner->_refObjectName; - $projectName = $requirement->Project->_refObjectName; - $itemid = $requirement->FormattedID; - $created = $requirement->_CreatedAt; - $estimate = $requirement->PlanEstimate; - $hasparent = $requirement->HasParent; - $childcount = $requirement->DirectChildrenCount; - $state = $requirement->ScheduleState; - $blocked = $requirement->Blocked; - $blockedreason = $requirement->BlockedReason; - $ready = $requirement->Ready; - - $attachmentcount = $requirement->Attachments->Count; - - $firstattachment = null; - if($attachmentcount>0) - { - $linktxt = GetRallyAttachmentLink($requirement->Attachments->_ref); - $firstattachment = MakeField("attachment",$linktxt,false); - } - - $parent = null; - if($hasparent) - $parent = $requirement->Parent->_refObjectName; - - $clean_description = html_entity_decode(strip_tags($description), ENT_HTML401|ENT_COMPAT, 'UTF-8'); - $short_description = TruncateText($clean_description, 300); - - $storyuri = "https://rally1.rallydev.com/#/{$projectid}d/detail/userstory/{$storyid}"; - $enctitle = urlencode($title); - $linktext = "<{$storyuri}|{$enctitle}>"; - - $dovegray = "#CEC7B8"; - - - - $fields = array( - MakeField("link",$linktext,false), - MakeField("parent",$parent,false), - - MakeField("id",$itemid,true), - MakeField("owner",$owner,true), - - MakeField("project",$projectName,true), - MakeField("created",$created,true), - - MakeField("estimate",$estimate,true), - MakeField("state",$state,true)); - - if($childcount>0) - array_push($fields,MakeField("children",$childcount,true)); - - if($blocked) - array_push($fields, MakeField("blocked",$blockedreason,true)); - - array_push($fields, MakeField("description",$short_description,false)); - - if($firstattachment!=null) - array_push($fields,$firstattachment); - - - global $slackCommand; - $userlink = BuildUserLink($slackCommand->UserName); - $user_message = "Ok {$userlink}, here's the story you requested."; - - $obj = new stdClass; - $obj->text = ""; - $obj->attachments = MakeAttachment($user_message, "", $dovegray, $fields, $storyuri); -// print_r(json_encode($obj));die; - - return $obj; -} - - function MakeField($title, $value, $short=false) { $attachmentfield = array( diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 5d3a7f3..e44f6c0 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -190,6 +190,106 @@ function ParseStoryPayload($Artifact) } +function GetRequirementPayload($ref) +{ + $object = CallAPI($ref); + + $requirement = null; + + if ($object->HierarchicalRequirement) { + $requirement = $object->HierarchicalRequirement; + } elseif ($object->Task) { + $requirement = $object->Task; + } else { + $class = get_class($object); + global $slackCommand; + $userlink = BuildUserLink($slackCommand->UserName); + print_r("Sorry {$userlink}, I can't handle a {$class} yet. I'll let @tdm know about it."); + die; + } + + $projecturi = $requirement->Project->_ref; + + $title = $requirement->_refObjectName; + + $ProjectFull = CallAPI($projecturi); + $projectid = $ProjectFull->Project->ObjectID; + $storyid = $requirement->ObjectID; + $description = $requirement->Description; + $owner = $requirement->Owner->_refObjectName; + $projectName = $requirement->Project->_refObjectName; + $itemid = $requirement->FormattedID; + $created = $requirement->_CreatedAt; + $estimate = $requirement->PlanEstimate; + $hasparent = $requirement->HasParent; + $childcount = $requirement->DirectChildrenCount; + $state = $requirement->ScheduleState; + $blocked = $requirement->Blocked; + $blockedreason = $requirement->BlockedReason; + $ready = $requirement->Ready; + + $attachmentcount = $requirement->Attachments->Count; + + $firstattachment = null; + if ($attachmentcount > 0) { + $linktxt = GetRallyAttachmentLink($requirement->Attachments->_ref); + $firstattachment = MakeField("attachment", $linktxt, false); + } + + $parent = null; + if ($hasparent) { + $parent = $requirement->Parent->_refObjectName; + } + + $clean_description = html_entity_decode(strip_tags($description), ENT_HTML401 | ENT_COMPAT, 'UTF-8'); + $short_description = TruncateText($clean_description, 300); + + $storyuri = "https://rally1.rallydev.com/#/{$projectid}d/detail/userstory/{$storyid}"; + $enctitle = urlencode($title); + $linktext = "<{$storyuri}|{$enctitle}>"; + + $dovegray = "#CEC7B8"; + + $fields = array( + MakeField("link", $linktext, false), + MakeField("parent", $parent, false), + + MakeField("id", $itemid, true), + MakeField("owner", $owner, true), + + MakeField("project", $projectName, true), + MakeField("created", $created, true), + + MakeField("estimate", $estimate, true), + MakeField("state", $state, true) + ); + + if ($childcount > 0) { + array_push($fields, MakeField("children", $childcount, true)); + } + + if ($blocked) { + array_push($fields, MakeField("blocked", $blockedreason, true)); + } + + array_push($fields, MakeField("description", $short_description, false)); + + if ($firstattachment != null) { + array_push($fields, $firstattachment); + } + + global $slackCommand; + $userlink = BuildUserLink($slackCommand->UserName); + $user_message = "Ok {$userlink}, here's the story you requested."; + + $obj = new stdClass; + $obj->text = ""; + $obj->attachments = MakeAttachment($user_message, "", $dovegray, $fields, $storyuri); + // print_r(json_encode($obj));die; + + return $obj; +} + /** * Returns an array of file links listed in a Rally attachment object. * From 99113c0c9092647ec52bc08ee2dc5a1aa4f45dd0 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 22:48:08 -0400 Subject: [PATCH 26/41] Refactor GetRequirementPayload to make it easier on my poor lizard brain --- scripts/include/rallyme.inc.php | 96 +++++++++++++++++---------------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index e44f6c0..5cb32d4 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -167,7 +167,7 @@ function ParseDefectPayload($Defect) } /** - * Prepares a table of fields attached to a Rally artifact for display. + * Prepares a table of fields attached to a Rally task for display. * * @param object $Artifact * @@ -181,13 +181,35 @@ function ParseTaskPayload($Artifact) /** * Prepares a table of fields attached to a Rally user story for display. * - * @param object $Artifact + * @param object $Story * * @return string[] */ -function ParseStoryPayload($Artifact) +function ParseStoryPayload($Story) { + global $RALLYME_DISPLAY_VERSION, $RALLY_BASE_URL; + + $title = $Story->_refObjectName; + $header = array('title' => $title); + $item_url = $RALLY_BASE_URL . '#/' . basename($Story->Project->_ref) . '/detail/userstory/' . $Story->ObjectID; + switch ($RALLYME_DISPLAY_VERSION) { + + case 2: + break; + + default: + $header['type'] = 'story'; + + $fields = array( + ); + if ($Story->Attachments->Count > 0) { + $fields['attachment'] = GetAttachmentLinks($Story->Attachments->_ref); + } + break; + + } + return array('header' => $header, 'fields' => $fields); } function GetRequirementPayload($ref) @@ -209,71 +231,51 @@ function GetRequirementPayload($ref) } $projecturi = $requirement->Project->_ref; - - $title = $requirement->_refObjectName; - $ProjectFull = CallAPI($projecturi); $projectid = $ProjectFull->Project->ObjectID; $storyid = $requirement->ObjectID; - $description = $requirement->Description; - $owner = $requirement->Owner->_refObjectName; - $projectName = $requirement->Project->_refObjectName; - $itemid = $requirement->FormattedID; - $created = $requirement->_CreatedAt; - $estimate = $requirement->PlanEstimate; - $hasparent = $requirement->HasParent; - $childcount = $requirement->DirectChildrenCount; - $state = $requirement->ScheduleState; - $blocked = $requirement->Blocked; - $blockedreason = $requirement->BlockedReason; - $ready = $requirement->Ready; - - $attachmentcount = $requirement->Attachments->Count; - - $firstattachment = null; - if ($attachmentcount > 0) { - $linktxt = GetRallyAttachmentLink($requirement->Attachments->_ref); - $firstattachment = MakeField("attachment", $linktxt, false); - } - - $parent = null; - if ($hasparent) { - $parent = $requirement->Parent->_refObjectName; - } - - $clean_description = html_entity_decode(strip_tags($description), ENT_HTML401 | ENT_COMPAT, 'UTF-8'); - $short_description = TruncateText($clean_description, 300); - $storyuri = "https://rally1.rallydev.com/#/{$projectid}d/detail/userstory/{$storyid}"; + $title = $requirement->_refObjectName; $enctitle = urlencode($title); $linktext = "<{$storyuri}|{$enctitle}>"; - $dovegray = "#CEC7B8"; + $parent = null; + if ($requirement->HasParent) { + $parent = $requirement->Parent->_refObjectName; + } $fields = array( MakeField("link", $linktext, false), MakeField("parent", $parent, false), - MakeField("id", $itemid, true), - MakeField("owner", $owner, true), + MakeField("id", $requirement->FormattedID, true), + MakeField("owner", $requirement->Owner->_refObjectName, true), - MakeField("project", $projectName, true), - MakeField("created", $created, true), + MakeField("project", $requirement->Project->_refObjectName, true), + MakeField("created", $requirement->_CreatedAt, true), - MakeField("estimate", $estimate, true), - MakeField("state", $state, true) + MakeField("estimate", $requirement->PlanEstimate, true), + MakeField("state", $requirement->ScheduleState, true) ); - if ($childcount > 0) { - array_push($fields, MakeField("children", $childcount, true)); + if ($requirement->DirectChildrenCount > 0) { + array_push($fields, MakeField("children", $requirement->DirectChildrenCount, true)); } - if ($blocked) { - array_push($fields, MakeField("blocked", $blockedreason, true)); + if ($requirement->Blocked) { + array_push($fields, MakeField("blocked", $requirement->BlockedReason, true)); } + $description = $requirement->Description; + $clean_description = html_entity_decode(strip_tags($description), ENT_HTML401 | ENT_COMPAT, 'UTF-8'); + $short_description = TruncateText($clean_description, 300); array_push($fields, MakeField("description", $short_description, false)); + $firstattachment = null; + if ($requirement->Attachments->Count > 0) { + $linktxt = GetRallyAttachmentLink($requirement->Attachments->_ref); + $firstattachment = MakeField("attachment", $linktxt, false); + } if ($firstattachment != null) { array_push($fields, $firstattachment); } @@ -282,6 +284,8 @@ function GetRequirementPayload($ref) $userlink = BuildUserLink($slackCommand->UserName); $user_message = "Ok {$userlink}, here's the story you requested."; + $dovegray = "#CEC7B8"; + $obj = new stdClass; $obj->text = ""; $obj->attachments = MakeAttachment($user_message, "", $dovegray, $fields, $storyuri); From f8a5e59d269b36fc1fed40861d04d4c2a501d836 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Tue, 30 Sep 2014 23:58:55 -0400 Subject: [PATCH 27/41] Handle sending user story details via incoming webhook --- scripts/include/rallyme.inc.php | 49 +++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 5cb32d4..d032a36 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -111,7 +111,7 @@ function ParseDefectPayload($Defect) global $RALLYME_DISPLAY_VERSION, $RALLY_BASE_URL; $title = $Defect->_refObjectName; - $header = array('title' => $title); + $header = array('title' => $title, 'type' => 'defect'); $item_url = $RALLY_BASE_URL . '#/' . basename($Defect->Project->_ref) . '/detail/defect/' . $Defect->ObjectID; switch ($RALLYME_DISPLAY_VERSION) { @@ -141,8 +141,6 @@ function ParseDefectPayload($Defect) break; default: - $header['type'] = 'defect'; - $fields = array( 'link' => array($title => $item_url), 'id' => $Defect->FormattedID, @@ -161,8 +159,8 @@ function ParseDefectPayload($Defect) $fields['attachment'] = GetAttachmentLinks($Defect->Attachments->_ref); } break; - } + return array('header' => $header, 'fields' => $fields); } @@ -190,25 +188,47 @@ function ParseStoryPayload($Story) global $RALLYME_DISPLAY_VERSION, $RALLY_BASE_URL; $title = $Story->_refObjectName; - $header = array('title' => $title); + $header = array('title' => $title, 'type' => 'story'); $item_url = $RALLY_BASE_URL . '#/' . basename($Story->Project->_ref) . '/detail/userstory/' . $Story->ObjectID; switch ($RALLYME_DISPLAY_VERSION) { case 2: - break; + // break; default: - $header['type'] = 'story'; + $parent = NULL; + if ($Story->HasParent) { + /** + * @todo perform lookup of parent's project ID to make this into + * a link; we can't assume it's in the same project + */ + $parent = $Story->Parent->_refObjectName; + } $fields = array( + 'link' => array($title => $item_url), + 'parent' => $parent, + 'id' => $Story->FormattedID, + 'owner' => $Story->Owner->_refObjectName, + 'project' => $Story->Project->_refObjectName, + 'created' => $Story->_CreatedAt, + 'estimate' => $Story->PlanEstimate, + 'state' => $Story->ScheduleState, ); + if ($Story->DirectChildrenCount > 0) { + $fields['children'] = $Story->DirectChildrenCount; + } + if ($Story->Blocked) { + $fields['blocked'] = $Story->BlockedReason; + } + $fields['description'] = $Story->Description; if ($Story->Attachments->Count > 0) { $fields['attachment'] = GetAttachmentLinks($Story->Attachments->_ref); } break; - } + return array('header' => $header, 'fields' => $fields); } @@ -330,7 +350,10 @@ function SendArtifactPayload($payload) global $config; $prextext = ArtifactPretext($payload['header']); - $color = 'bad'; + $color = '#CEC7B8'; //dove gray + if ($payload['header']['type'] == 'defect') { + $color = 'bad'; //purple + } $fields = array(); foreach ($payload['fields'] as $label => $value) { @@ -338,6 +361,11 @@ function SendArtifactPayload($payload) switch ($label) { case 'Parent': + case 'parent': + if (is_string($value)) { + $short = FALSE; + break; + } case 'Attachment': case 'link': case 'attachment': @@ -395,6 +423,7 @@ function ReturnArtifactPayload($payload) case 'Description': $value = TruncateText(SanitizeText($value), 300, $payload['header']['item_url']); $value = '\n> ' . strtr($value, array('\n' => '\n> ')); + break; } if ($label) { @@ -413,8 +442,10 @@ function ArtifactPretext($info) global $RALLYME_DISPLAY_VERSION; switch ($RALLYME_DISPLAY_VERSION) { + case 2: return em('Details for ' . $info['item_id'] . ' ' . l($info['title'], $info['item_url'])); + default: return 'Ok, @' . $_REQUEST['user_name'] . ', here\'s the ' . $info['type'] . ' you requested.'; } From 56ff06f288482d4ffc46a3379657b4e88149a192 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Wed, 1 Oct 2014 00:08:16 -0400 Subject: [PATCH 28/41] Remove deprecated functions --- scripts/include/rally.php | 69 ------------------------- scripts/include/rallyme.config.php | 2 - scripts/include/rallyme.inc.php | 82 ------------------------------ 3 files changed, 153 deletions(-) diff --git a/scripts/include/rally.php b/scripts/include/rally.php index 438d5f2..dbe992a 100644 --- a/scripts/include/rally.php +++ b/scripts/include/rally.php @@ -4,55 +4,6 @@ $RALLY_API_URL = $RALLY_BASE_URL . 'slm/webservice/v2.0/'; -function HandleStory($id, $channel_name) -{ - $ref = FindRequirement($id); - - $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)); - print_r("\n"); - die("Apparently the Rallyme script is having a problem. Ask about it. :frowning:"); - } - return $result; -} - -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, - $attachments); -} - - - -function GetRallyAttachmentLink($attachmentRef) -{ - $attachments = CallAPI($attachmentRef); - $firstattachment = $attachments->QueryResult->Results[0]; - - $attachmentname = $firstattachment->_refObjectName; - $encodedattachmentname = urlencode($attachmentname); - $id = $firstattachment->ObjectID; - - $uri = "https://rally1.rallydev.com/slm/attachment/{$id}/{$encodedattachmentname}"; - $linktxt = "<{$uri}|{$attachmentname}>"; - return $linktxt; -} - function MakeField($title, $value, $short=false) { $attachmentfield = array( @@ -73,26 +24,6 @@ function CallAPI($uri) return $object; } -function FindRequirement($id) -{ - $query = GetArtifactQueryUri($id); - - $searchresult = CallAPI($query); -// print_r($searchresult);die; - - $count = GetCount($searchresult); - if($count == 0) - NotFound($id); - - return GetFirstObjectFromSearchResult("HierarchicalRequirement", $searchresult); -} - -function GetArtifactQueryUri($id) -{ - global $config; - return str_replace("[[ID]]", $id, $config['rally']['artifactquery']); -} - function TruncateText($text, $len) { if(strlen($text) <= $len) diff --git a/scripts/include/rallyme.config.php b/scripts/include/rallyme.config.php index 2170aff..3bffea8 100644 --- a/scripts/include/rallyme.config.php +++ b/scripts/include/rallyme.config.php @@ -1,8 +1,6 @@ $header, 'fields' => $fields); } -function GetRequirementPayload($ref) -{ - $object = CallAPI($ref); - - $requirement = null; - - if ($object->HierarchicalRequirement) { - $requirement = $object->HierarchicalRequirement; - } elseif ($object->Task) { - $requirement = $object->Task; - } else { - $class = get_class($object); - global $slackCommand; - $userlink = BuildUserLink($slackCommand->UserName); - print_r("Sorry {$userlink}, I can't handle a {$class} yet. I'll let @tdm know about it."); - die; - } - - $projecturi = $requirement->Project->_ref; - $ProjectFull = CallAPI($projecturi); - $projectid = $ProjectFull->Project->ObjectID; - $storyid = $requirement->ObjectID; - $storyuri = "https://rally1.rallydev.com/#/{$projectid}d/detail/userstory/{$storyid}"; - $title = $requirement->_refObjectName; - $enctitle = urlencode($title); - $linktext = "<{$storyuri}|{$enctitle}>"; - - $parent = null; - if ($requirement->HasParent) { - $parent = $requirement->Parent->_refObjectName; - } - - $fields = array( - MakeField("link", $linktext, false), - MakeField("parent", $parent, false), - - MakeField("id", $requirement->FormattedID, true), - MakeField("owner", $requirement->Owner->_refObjectName, true), - - MakeField("project", $requirement->Project->_refObjectName, true), - MakeField("created", $requirement->_CreatedAt, true), - - MakeField("estimate", $requirement->PlanEstimate, true), - MakeField("state", $requirement->ScheduleState, true) - ); - - if ($requirement->DirectChildrenCount > 0) { - array_push($fields, MakeField("children", $requirement->DirectChildrenCount, true)); - } - - if ($requirement->Blocked) { - array_push($fields, MakeField("blocked", $requirement->BlockedReason, true)); - } - - $description = $requirement->Description; - $clean_description = html_entity_decode(strip_tags($description), ENT_HTML401 | ENT_COMPAT, 'UTF-8'); - $short_description = TruncateText($clean_description, 300); - array_push($fields, MakeField("description", $short_description, false)); - - $firstattachment = null; - if ($requirement->Attachments->Count > 0) { - $linktxt = GetRallyAttachmentLink($requirement->Attachments->_ref); - $firstattachment = MakeField("attachment", $linktxt, false); - } - if ($firstattachment != null) { - array_push($fields, $firstattachment); - } - - global $slackCommand; - $userlink = BuildUserLink($slackCommand->UserName); - $user_message = "Ok {$userlink}, here's the story you requested."; - - $dovegray = "#CEC7B8"; - - $obj = new stdClass; - $obj->text = ""; - $obj->attachments = MakeAttachment($user_message, "", $dovegray, $fields, $storyuri); - // print_r(json_encode($obj));die; - - return $obj; -} - /** * Returns an array of file links listed in a Rally attachment object. * From 0dda36d77995a5f51fefe21567a3b08fe63513b6 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Wed, 1 Oct 2014 00:44:48 -0400 Subject: [PATCH 29/41] Handle sending user story or task details via incoming webhook --- scripts/include/rallyme.inc.php | 103 +++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 36 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 5d36544..4996804 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -110,8 +110,7 @@ function ParseDefectPayload($Defect) { global $RALLYME_DISPLAY_VERSION, $RALLY_BASE_URL; - $title = $Defect->_refObjectName; - $header = array('title' => $title, 'type' => 'defect'); + $header = array('title' => $Defect->_refObjectName, 'type' => 'defect'); $item_url = $RALLY_BASE_URL . '#/' . basename($Defect->Project->_ref) . '/detail/defect/' . $Defect->ObjectID; switch ($RALLYME_DISPLAY_VERSION) { @@ -142,7 +141,7 @@ function ParseDefectPayload($Defect) default: $fields = array( - 'link' => array($title => $item_url), + 'link' => array($Defect->_refObjectName => $item_url), 'id' => $Defect->FormattedID, 'owner' => $Defect->Owner->_refObjectName, 'project' => $Defect->Project->_refObjectName, @@ -167,13 +166,28 @@ function ParseDefectPayload($Defect) /** * Prepares a table of fields attached to a Rally task for display. * - * @param object $Artifact + * @param object $Task * * @return string[] */ -function ParseTaskPayload($Artifact) +function ParseTaskPayload($Task) { + global $RALLYME_DISPLAY_VERSION, $RALLY_BASE_URL; + + $header = array('title' => $Task->_refObjectName, 'type' => 'task'); + $item_url = $RALLY_BASE_URL . '#/' . basename($Task->Project->_ref) . '/detail/task/' . $Task->ObjectID; + + switch ($RALLYME_DISPLAY_VERSION) { + + case 2: + // break; + + default: + $fields = CompileRequirementFields($Task, $item_url); + break; + } + return array('header' => $header, 'fields' => $fields); } /** @@ -187,8 +201,7 @@ function ParseStoryPayload($Story) { global $RALLYME_DISPLAY_VERSION, $RALLY_BASE_URL; - $title = $Story->_refObjectName; - $header = array('title' => $title, 'type' => 'story'); + $header = array('title' => $Story->_refObjectName, 'type' => 'story'); $item_url = $RALLY_BASE_URL . '#/' . basename($Story->Project->_ref) . '/detail/userstory/' . $Story->ObjectID; switch ($RALLYME_DISPLAY_VERSION) { @@ -197,41 +210,59 @@ function ParseStoryPayload($Story) // break; default: - $parent = NULL; - if ($Story->HasParent) { - /** - * @todo perform lookup of parent's project ID to make this into - * a link; we can't assume it's in the same project - */ - $parent = $Story->Parent->_refObjectName; - } - - $fields = array( - 'link' => array($title => $item_url), - 'parent' => $parent, - 'id' => $Story->FormattedID, - 'owner' => $Story->Owner->_refObjectName, - 'project' => $Story->Project->_refObjectName, - 'created' => $Story->_CreatedAt, - 'estimate' => $Story->PlanEstimate, - 'state' => $Story->ScheduleState, - ); - if ($Story->DirectChildrenCount > 0) { - $fields['children'] = $Story->DirectChildrenCount; - } - if ($Story->Blocked) { - $fields['blocked'] = $Story->BlockedReason; - } - $fields['description'] = $Story->Description; - if ($Story->Attachments->Count > 0) { - $fields['attachment'] = GetAttachmentLinks($Story->Attachments->_ref); - } + $fields = CompileRequirementFields($Story, $item_url); break; } return array('header' => $header, 'fields' => $fields); } +/** + * Prepare a table of field values for stories and tasks. + * + * Rally lumps stories and tasks together as types of "Hierarchical Requirements" + * and so the original version of this script rendered the same fields for both. + * + * @param object $Requirement + * @param string $item_url + * + * @return string[] + */ +function CompileRequirementFields($Requirement, $item_url) +{ + $parent = NULL; + if ($Requirement->HasParent) { + /** + * @todo perform lookup of parent's project ID to make this into + * a link; we can't assume it's in the same project + */ + $parent = $Requirement->Parent->_refObjectName; + } + + $fields = array( + 'link' => array($Requirement->_refObjectName => $item_url), + 'parent' => $parent, + 'id' => $Requirement->FormattedID, + 'owner' => $Requirement->Owner->_refObjectName, + 'project' => $Requirement->Project->_refObjectName, + 'created' => $Requirement->_CreatedAt, + 'estimate' => $Requirement->PlanEstimate, + 'state' => $Requirement->ScheduleState, + ); + if ($Requirement->DirectChildrenCount > 0) { + $fields['children'] = $Requirement->DirectChildrenCount; + } + if ($Requirement->Blocked) { + $fields['blocked'] = $Requirement->BlockedReason; + } + $fields['description'] = $Requirement->Description; + if ($Requirement->Attachments->Count > 0) { + $fields['attachment'] = GetAttachmentLinks($Requirement->Attachments->_ref); + } + + return $fields; +} + /** * Returns an array of file links listed in a Rally attachment object. * From 8119d5ceb287da7857661b8f16ecd0fbb52d2088 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Wed, 1 Oct 2014 19:36:58 -0400 Subject: [PATCH 30/41] Refactor settings variables and includes --- scripts/include/slack.config.php | 2 +- scripts/rallyme.php | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/scripts/include/slack.config.php b/scripts/include/slack.config.php index e14cd95..b77a8e4 100644 --- a/scripts/include/slack.config.php +++ b/scripts/include/slack.config.php @@ -1,7 +1,7 @@ diff --git a/scripts/rallyme.php b/scripts/rallyme.php index 5935b23..847109c 100644 --- a/scripts/rallyme.php +++ b/scripts/rallyme.php @@ -1,10 +1,7 @@ Date: Wed, 1 Oct 2014 19:41:58 -0400 Subject: [PATCH 31/41] Refactor rallyme includes --- scripts/include/rallyme.inc.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 4996804..7ebc894 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -2,9 +2,9 @@ /** * Responds to queries for information about defects, tasks, and user stories. */ -require_once('curl.php'); -require_once('slack.php'); -require_once('rally.php'); +require('curl.php'); +require('slack.php'); +require('rally.php'); set_error_handler('_HandleRallyMeErrors', E_USER_ERROR); From 5448eae9c6cc922d2338a89ea3f0048097104759 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Wed, 1 Oct 2014 19:44:11 -0400 Subject: [PATCH 32/41] Specify artifact type in query URL to reduce resultset from Rally - Rally doesn't seem to look at the first two letters of the FormattedID and returns all artifacts with the same number unless the artifact type is specified in the URL :shrug: --- scripts/include/rallyme.inc.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 7ebc894..0120d74 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -28,19 +28,16 @@ function FetchArtifactPayload($command_text) switch (substr($formatted_id, 0, 2)) { case 'DE': //find defect - $query_url .= 'defect'; $artifact_type = 'Defect'; $func = 'ParseDefectPayload'; break; case 'TA': - $query_url .= 'artifact'; $artifact_type = 'Task'; $func = 'ParseTaskPayload'; break; case 'US': - $query_url .= 'artifact'; $artifact_type = 'HierarchicalRequirement'; $func = 'ParseStoryPayload'; break; @@ -48,7 +45,7 @@ function FetchArtifactPayload($command_text) default: trigger_error('Sorry, @user, I don\'t know how to handle "' . $command_text . '". You can look up user stories, defects, and tasks by ID, like "DE1234".', E_USER_ERROR); } - $query_url .= '?query=(FormattedID+%3D+' . $formatted_id . ')&fetch=true'; + $query_url .= strtolower($artifact_type) . '?query=(FormattedID+%3D+' . $formatted_id . ')&fetch=true'; $Results = CallAPI($query_url); if ($Results->QueryResult->TotalResultCount == 0) { //get count From f5394128e0a3c0079577b5caccddb307c613a685 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Wed, 1 Oct 2014 20:09:29 -0400 Subject: [PATCH 33/41] Refactor to abstract notion of artifact-payload header information --- scripts/include/rallyme.inc.php | 73 ++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 0120d74..69bafe0 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -105,17 +105,13 @@ function _HandleRallyMeErrors($errno, $errstr) */ function ParseDefectPayload($Defect) { - global $RALLYME_DISPLAY_VERSION, $RALLY_BASE_URL; + global $RALLYME_DISPLAY_VERSION; - $header = array('title' => $Defect->_refObjectName, 'type' => 'defect'); - $item_url = $RALLY_BASE_URL . '#/' . basename($Defect->Project->_ref) . '/detail/defect/' . $Defect->ObjectID; + $header = CompileArtifactHeader($Defect, 'defect'); switch ($RALLYME_DISPLAY_VERSION) { case 2: - $header['item_id'] = $Defect->FormattedID; - $header['item_url'] = $item_url; - $state = $Defect->State; if ($state == 'Closed') { $Date = new DateTime($Defect->ClosedDate); @@ -138,7 +134,7 @@ function ParseDefectPayload($Defect) default: $fields = array( - 'link' => array($Defect->_refObjectName => $item_url), + 'link' => array($header['title'] => $header['url']), 'id' => $Defect->FormattedID, 'owner' => $Defect->Owner->_refObjectName, 'project' => $Defect->Project->_refObjectName, @@ -169,10 +165,9 @@ function ParseDefectPayload($Defect) */ function ParseTaskPayload($Task) { - global $RALLYME_DISPLAY_VERSION, $RALLY_BASE_URL; + global $RALLYME_DISPLAY_VERSION; - $header = array('title' => $Task->_refObjectName, 'type' => 'task'); - $item_url = $RALLY_BASE_URL . '#/' . basename($Task->Project->_ref) . '/detail/task/' . $Task->ObjectID; + $header = CompileArtifactHeader($Task, 'task'); switch ($RALLYME_DISPLAY_VERSION) { @@ -180,7 +175,7 @@ function ParseTaskPayload($Task) // break; default: - $fields = CompileRequirementFields($Task, $item_url); + $fields = CompileRequirementFields($Task, $header); break; } @@ -196,10 +191,9 @@ function ParseTaskPayload($Task) */ function ParseStoryPayload($Story) { - global $RALLYME_DISPLAY_VERSION, $RALLY_BASE_URL; + global $RALLYME_DISPLAY_VERSION; - $header = array('title' => $Story->_refObjectName, 'type' => 'story'); - $item_url = $RALLY_BASE_URL . '#/' . basename($Story->Project->_ref) . '/detail/userstory/' . $Story->ObjectID; + $header = CompileArtifactHeader($Story, 'story'); switch ($RALLYME_DISPLAY_VERSION) { @@ -207,13 +201,36 @@ function ParseStoryPayload($Story) // break; default: - $fields = CompileRequirementFields($Story, $item_url); + $fields = CompileRequirementFields($Story, $header); break; } return array('header' => $header, 'fields' => $fields); } +/** + * Prepares an array of fields of meta-information common to all artifacts. + * + * @param object $Artifact + * @param string $type + * + * @return string[] + */ +function CompileArtifactHeader($Artifact, $type) +{ + global $RALLY_BASE_URL; + $path_map = array('defect' => 'defect', 'task' => 'task', 'story' => 'userstory'); //associate human-readable names with Rally URL paths + + $item_url = $RALLY_BASE_URL . '#/' . basename($Artifact->Project->_ref) . '/detail/' . $path_map[$type] . '/' . $Artifact->ObjectID; + + return array( + 'type' => $type, + 'id' => $Artifact->FormattedID, + 'title' => $Artifact->_refObjectName, + 'url' => $item_url + ); +} + /** * Prepare a table of field values for stories and tasks. * @@ -221,11 +238,11 @@ function ParseStoryPayload($Story) * and so the original version of this script rendered the same fields for both. * * @param object $Requirement - * @param string $item_url + * @param string[] $header * * @return string[] */ -function CompileRequirementFields($Requirement, $item_url) +function CompileRequirementFields($Requirement, $header) { $parent = NULL; if ($Requirement->HasParent) { @@ -237,7 +254,7 @@ function CompileRequirementFields($Requirement, $item_url) } $fields = array( - 'link' => array($Requirement->_refObjectName => $item_url), + 'link' => array($header['title'] => $header['url']), 'parent' => $parent, 'id' => $Requirement->FormattedID, 'owner' => $Requirement->Owner->_refObjectName, @@ -322,14 +339,14 @@ function SendArtifactPayload($payload) case 'Description': case 'description': - $value = TruncateText(SanitizeText($value), 300, $payload['header']['item_url']); + $value = TruncateText(SanitizeText($value), 300, $payload['header']['url']); $short = FALSE; break; } $fields[] = MakeField($label, $value, $short); } - $attachment = MakeAttachment($prextext, '', $color, $fields, $payload['header']['item_url']); + $attachment = MakeAttachment($prextext, '', $color, $fields, $payload['header']['url']); return slack_incoming_hook_post_with_attachments( $config['slack']['hook'], @@ -367,7 +384,7 @@ function ReturnArtifactPayload($payload) break; case 'Description': - $value = TruncateText(SanitizeText($value), 300, $payload['header']['item_url']); + $value = TruncateText(SanitizeText($value), 300, $payload['header']['url']); $value = '\n> ' . strtr($value, array('\n' => '\n> ')); break; } @@ -383,16 +400,22 @@ function ReturnArtifactPayload($payload) return PrintJsonResponse($text); } -function ArtifactPretext($info) +/** + * Compiles a short message that Rallybot uses to announce query results. + * + * @param string[] $header + * + * @return string + */ +function ArtifactPretext($header) { global $RALLYME_DISPLAY_VERSION; - switch ($RALLYME_DISPLAY_VERSION) { case 2: - return em('Details for ' . $info['item_id'] . ' ' . l($info['title'], $info['item_url'])); + return em('Details for ' . $header['id'] . ' ' . l($header['title'], $header['url'])); default: - return 'Ok, @' . $_REQUEST['user_name'] . ', here\'s the ' . $info['type'] . ' you requested.'; + return 'Ok, @' . $_REQUEST['user_name'] . ', here\'s the ' . $header['type'] . ' you requested.'; } } From bdc71ebc94243e7b728366c7f6874a5d3daf363b Mon Sep 17 00:00:00 2001 From: JP Klein Date: Wed, 1 Oct 2014 20:17:29 -0400 Subject: [PATCH 34/41] Add troubleshooting message on webhook failure --- scripts/include/slack.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/include/slack.php b/scripts/include/slack.php index 12bbd2a..6526163 100644 --- a/scripts/include/slack.php +++ b/scripts/include/slack.php @@ -124,9 +124,11 @@ function _incoming_hook_post($url, $data) return $result; case 'Invalid channel specified': exit('Unable to post messages to a private chat'); - default: - exit('Unable to send Incoming WebHook message: ' . $reply); } + if (strpos($url, 'REPLACE')) { + $result = 'Please set your Slack subdomain and incoming webhook token'; + } + exit('Unable to send Incoming WebHook message: ' . $result); } function PrintJsonResponse($payload) From c9dd514212912b2e6385e26ac20934c674d86790 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Wed, 1 Oct 2014 20:59:31 -0400 Subject: [PATCH 35/41] Update display of stories --- scripts/include/rallyme.inc.php | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 69bafe0..92c59cd 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -105,10 +105,9 @@ function _HandleRallyMeErrors($errno, $errstr) */ function ParseDefectPayload($Defect) { - global $RALLYME_DISPLAY_VERSION; - $header = CompileArtifactHeader($Defect, 'defect'); + global $RALLYME_DISPLAY_VERSION; switch ($RALLYME_DISPLAY_VERSION) { case 2: @@ -165,10 +164,9 @@ function ParseDefectPayload($Defect) */ function ParseTaskPayload($Task) { - global $RALLYME_DISPLAY_VERSION; - $header = CompileArtifactHeader($Task, 'task'); + global $RALLYME_DISPLAY_VERSION; switch ($RALLYME_DISPLAY_VERSION) { case 2: @@ -191,14 +189,31 @@ function ParseTaskPayload($Task) */ function ParseStoryPayload($Story) { - global $RALLYME_DISPLAY_VERSION; - $header = CompileArtifactHeader($Story, 'story'); + global $RALLYME_DISPLAY_VERSION; switch ($RALLYME_DISPLAY_VERSION) { case 2: - // break; + $fields = array( + 'Project' => $Story->Project->_refObjectName, + 'Created' => $Story->_CreatedAt, + 'Owner' => $Story->Owner->_refObjectName, + 'Points' => $Story->PlanEstimate, + 'State' => $Story->ScheduleState, + 'Status' => '' + ); + if ($Story->Blocked) { + $fields['Status'] = 'Blocked'; + $fields['Block Description'] = $Story->BlockedReason; + } elseif ($Story->Ready) { + $fields['Status'] = 'Ready'; + } + $fields['Description'] = $Story->Description; + if ($Story->Attachments->Count > 0) { + $ret['Attachment'] = GetAttachmentLinks($Story->Attachments->_ref); + } + break; default: $fields = CompileRequirementFields($Story, $header); From c1bf27fd3cd4b3ee9c83b2609e0966fe06055ae9 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Wed, 1 Oct 2014 21:36:45 -0400 Subject: [PATCH 36/41] Update display of tasks --- scripts/include/rallyme.inc.php | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 92c59cd..e5f6cd7 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -170,7 +170,26 @@ function ParseTaskPayload($Task) switch ($RALLYME_DISPLAY_VERSION) { case 2: - // break; + $fields = array( + 'Parent' => $Task->WorkProduct->_refObjectName, + 'Owner' => $Task->Owner->_refObjectName, + 'Created' => $Task->_CreatedAt, + 'To Do' => $Task->ToDo, + 'Actual' => $Task->Actuals, + 'State' => $Task->State, + 'Status' => '' + ); + if ($Task->Blocked) { + $fields['Status'] = 'Blocked'; + $fields['Block Description'] = $Task->BlockedReason; + } elseif ($Task->Ready) { + $fields['Status'] = 'Ready'; + } + $fields['Description'] = $Task->Description; + if ($Task->Attachments->Count > 0) { + $fields['Attachment'] = GetAttachmentLinks($Task->Attachments->_ref); + } + break; default: $fields = CompileRequirementFields($Task, $header); @@ -211,7 +230,7 @@ function ParseStoryPayload($Story) } $fields['Description'] = $Story->Description; if ($Story->Attachments->Count > 0) { - $ret['Attachment'] = GetAttachmentLinks($Story->Attachments->_ref); + $fields['Attachment'] = GetAttachmentLinks($Story->Attachments->_ref); } break; From b1b69d3715fc59ec276a363c0ca03c5b7d2cada5 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Wed, 1 Oct 2014 22:53:37 -0400 Subject: [PATCH 37/41] Filter info when field names added to command eg: "/rallyme US1234 state attachment owner" would only display those fields in the channel --- scripts/include/rallyme.inc.php | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index e5f6cd7..7334a6b 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -18,16 +18,14 @@ */ function FetchArtifactPayload($command_text) { - global $RALLY_API_URL; - - list($formatted_id) = explode(' ', trim($command_text)); - $formatted_id = strtoupper($formatted_id); + //assume any words following the formatted id are the name of fields we want to see + $field_filter = explode(' ', trim($command_text)); + $formatted_id = strtoupper(array_shift($field_filter)); - //handle item - $query_url = $RALLY_API_URL; + //determine requested artifact type switch (substr($formatted_id, 0, 2)) { - case 'DE': //find defect + case 'DE': $artifact_type = 'Defect'; $func = 'ParseDefectPayload'; break; @@ -45,17 +43,31 @@ function FetchArtifactPayload($command_text) default: trigger_error('Sorry, @user, I don\'t know how to handle "' . $command_text . '". You can look up user stories, defects, and tasks by ID, like "DE1234".', E_USER_ERROR); } - $query_url .= strtolower($artifact_type) . '?query=(FormattedID+%3D+' . $formatted_id . ')&fetch=true'; + //compile query string + global $RALLY_API_URL; + $query_url .= $RALLY_API_URL . $artifact_type . '?query=(FormattedID+%3D+' . $formatted_id . ')&fetch=true'; + + //query Rally $Results = CallAPI($query_url); if ($Results->QueryResult->TotalResultCount == 0) { //get count trigger_error('Sorry, @user, I couldn\'t find ' . $formatted_id, E_USER_ERROR); //not found } - //get first object from search result + //generate payload from first query result foreach ($Results->QueryResult->Results as $Result) { if ($Result->_type == $artifact_type) { - return call_user_func($func, $Result); + $payload = call_user_func($func, $Result); + + //filter display of artifact fields + if (!empty($field_filter)) { + if (ctype_upper(key($payload['fields'])[0])) { //match the case of payload field labels + $field_filter = array_map('ucfirst', $field_filter); + } + $payload['fields'] = array_intersect_key($payload['fields'], array_flip($field_filter)); + } + + return $payload; } } trigger_error('Sorry, @user, your search for "' . $formatted_id . '" was ambiguous.', E_USER_ERROR); From 95a14c44ebdb49bf81308166693e52ce01e7a090 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Thu, 2 Oct 2014 18:02:06 -0400 Subject: [PATCH 38/41] Move Slack library functions out of rally file --- scripts/include/rally.php | 19 ------------------- scripts/include/slack.php | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/scripts/include/rally.php b/scripts/include/rally.php index dbe992a..bfebcb3 100644 --- a/scripts/include/rally.php +++ b/scripts/include/rally.php @@ -3,17 +3,6 @@ $RALLY_BASE_URL = 'https://rally1.rallydev.com/'; $RALLY_API_URL = $RALLY_BASE_URL . 'slm/webservice/v2.0/'; - -function MakeField($title, $value, $short=false) -{ - $attachmentfield = array( - "title" => $title, - "value" => $value, - "short" => $short); - - return $attachmentfield; -} - function CallAPI($uri) { global $config; @@ -24,12 +13,4 @@ function CallAPI($uri) return $object; } -function TruncateText($text, $len) -{ - if(strlen($text) <= $len) - return $text; - - return substr($text,0,$len)."...[MORE]"; -} - ?> diff --git a/scripts/include/slack.php b/scripts/include/slack.php index 6526163..9c2e7d6 100644 --- a/scripts/include/slack.php +++ b/scripts/include/slack.php @@ -67,6 +67,15 @@ function SanitizeText($text) return html_entity_decode(strip_tags($text), ENT_HTML401 | ENT_COMPAT, 'UTF-8'); } +function TruncateText($text, $len) +{ + if (strlen($text) <= $len) { + return $text; + } + + return substr($text, 0, $len) . '...[MORE]'; +} + function l($text, $url) { return '<' . $url . '|' . $text . '>'; @@ -175,4 +184,15 @@ function MakeAttachment($pretext, $text, $color, $fields, $fallback){ return $obj; } + +function MakeField($title, $value, $short = false) +{ + $attachmentfield = array( + "title" => $title, + "value" => $value, + "short" => $short + ); + + return $attachmentfield; +} ?> From 6a1ee49e32899bf8f9d57a499db6b22095952849 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Thu, 2 Oct 2014 18:17:00 -0400 Subject: [PATCH 39/41] Allow truncated text to display read more link --- scripts/include/slack.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/include/slack.php b/scripts/include/slack.php index 9c2e7d6..71534ed 100644 --- a/scripts/include/slack.php +++ b/scripts/include/slack.php @@ -67,13 +67,14 @@ function SanitizeText($text) return html_entity_decode(strip_tags($text), ENT_HTML401 | ENT_COMPAT, 'UTF-8'); } -function TruncateText($text, $len) +function TruncateText($text, $len, $url = '') { if (strlen($text) <= $len) { return $text; } - - return substr($text, 0, $len) . '...[MORE]'; + $text = preg_replace('/\s+?(\S+)?$/', '', substr($text, 0, $len)); + $more = ($url) ? l('more', $url) : 'more'; + return $text . '... ' . em($more); } function l($text, $url) From 1d257756a00aeef477e3d649163a1f288f357d47 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Thu, 2 Oct 2014 19:00:59 -0400 Subject: [PATCH 40/41] Update README.md with rallybot usage instructions --- README.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c01006e..2fb01d1 100644 --- a/README.md +++ b/README.md @@ -6,27 +6,24 @@ 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. - -E.g.: +We use this to query our Rally instance for defects, tasks and user stories. It can be configured to respond to Slack [slash command](https://slack.zendesk.com/hc/en-us/articles/201259356-Slash-Commands) or to messages posted to a channel that start with a Rally ticket's formatted id, e.g.: +``` /rallyme DE12345 -/rallyme US23456 -/rallyme TA34567 - -Each of these returns a nicely formatted summary of the defect, story or task, including the owner/submitter, creation date, story title, project and description. For documents that have attachments, such as uploaded screenshots, the summary will include a link to the first available attachment. +/rallyme US23456 project +TA34567 owner description +``` +Entering the id of an artifact without any arguments returns a nicely formatted summary of the defect, story or task, including the owner/submitter, creation date, story title, project and description. For documents that have attachments, such as uploaded screenshots, the summary will include a link to the first available attachment. Otherwise, adding one or more field names after the id will filter the summary to just display the requested fields. #Image Bot / Gif Bot This one is simple. It uses the google image search JSON API to query for images that match a keyword. It is configured to use the Safe Search feature, since we use this tool in the workplace. Being able to add some levity to our work chats with amusing images from the internet makes life more fun. - -E.g. +``` /imageme kittens /gifme roflcopter - +``` #XKCD Bot As geeks, we are well acquainted with the internet comic XKCD. This bot allows you to post an XKCD comic to the room, including the ALT text, which will be displayed beneath the image. -E.g. -/xkcd 100 will return the "family circus" episode of XKCD. +E.g. `/xkcd 100` will return the "family circus" episode of XKCD. From 45dbe5626d5b37f0708a800463431773535b3fb8 Mon Sep 17 00:00:00 2001 From: JP Klein Date: Thu, 2 Oct 2014 20:00:03 -0400 Subject: [PATCH 41/41] Defactor global settings vars to match legacy approach --- scripts/config/default.config.php | 6 ------ scripts/include/rally.php | 2 -- scripts/include/rallyme.config.php | 5 ++++- scripts/include/rallyme.inc.php | 28 ++++++++++++++-------------- scripts/include/slack.config.php | 5 ++--- scripts/include/slack.php | 8 ++++---- scripts/include/slack.secrets.php | 3 ++- scripts/rallyme.php | 3 +-- 8 files changed, 27 insertions(+), 33 deletions(-) delete mode 100644 scripts/config/default.config.php diff --git a/scripts/config/default.config.php b/scripts/config/default.config.php deleted file mode 100644 index 80f23db..0000000 --- a/scripts/config/default.config.php +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php index 7334a6b..22d2082 100644 --- a/scripts/include/rallyme.inc.php +++ b/scripts/include/rallyme.inc.php @@ -45,8 +45,8 @@ function FetchArtifactPayload($command_text) } //compile query string - global $RALLY_API_URL; - $query_url .= $RALLY_API_URL . $artifact_type . '?query=(FormattedID+%3D+' . $formatted_id . ')&fetch=true'; + global $config; + $query_url = $config['rally']['apiurl'] . $artifact_type . '?query=(FormattedID+%3D+' . $formatted_id . ')&fetch=true'; //query Rally $Results = CallAPI($query_url); @@ -119,8 +119,8 @@ function ParseDefectPayload($Defect) { $header = CompileArtifactHeader($Defect, 'defect'); - global $RALLYME_DISPLAY_VERSION; - switch ($RALLYME_DISPLAY_VERSION) { + global $config; + switch ($config['rallyme']['version']) { case 2: $state = $Defect->State; @@ -178,8 +178,8 @@ function ParseTaskPayload($Task) { $header = CompileArtifactHeader($Task, 'task'); - global $RALLYME_DISPLAY_VERSION; - switch ($RALLYME_DISPLAY_VERSION) { + global $config; + switch ($config['rallyme']['version']) { case 2: $fields = array( @@ -222,8 +222,8 @@ function ParseStoryPayload($Story) { $header = CompileArtifactHeader($Story, 'story'); - global $RALLYME_DISPLAY_VERSION; - switch ($RALLYME_DISPLAY_VERSION) { + global $config; + switch ($config['rallyme']['version']) { case 2: $fields = array( @@ -264,10 +264,10 @@ function ParseStoryPayload($Story) */ function CompileArtifactHeader($Artifact, $type) { - global $RALLY_BASE_URL; + global $config; $path_map = array('defect' => 'defect', 'task' => 'task', 'story' => 'userstory'); //associate human-readable names with Rally URL paths - $item_url = $RALLY_BASE_URL . '#/' . basename($Artifact->Project->_ref) . '/detail/' . $path_map[$type] . '/' . $Artifact->ObjectID; + $item_url = $config['rally']['hosturl'] . '#/' . basename($Artifact->Project->_ref) . '/detail/' . $path_map[$type] . '/' . $Artifact->ObjectID; return array( 'type' => $type, @@ -332,8 +332,8 @@ function CompileRequirementFields($Requirement, $header) */ function GetAttachmentLinks($attachment_ref) { - global $RALLY_BASE_URL; - $url = $RALLY_BASE_URL . 'slm/attachment/'; + global $config; + $url = $config['rally']['hosturl'] . 'slm/attachment/'; $links = array(); $Attachments = CallAPI($attachment_ref); @@ -455,8 +455,8 @@ function ReturnArtifactPayload($payload) */ function ArtifactPretext($header) { - global $RALLYME_DISPLAY_VERSION; - switch ($RALLYME_DISPLAY_VERSION) { + global $config; + switch ($config['rallyme']['version']) { case 2: return em('Details for ' . $header['id'] . ' ' . l($header['title'], $header['url'])); diff --git a/scripts/include/slack.config.php b/scripts/include/slack.config.php index b77a8e4..81927dc 100644 --- a/scripts/include/slack.config.php +++ b/scripts/include/slack.config.php @@ -1,7 +1,6 @@ diff --git a/scripts/include/slack.php b/scripts/include/slack.php index 71534ed..e2056be 100644 --- a/scripts/include/slack.php +++ b/scripts/include/slack.php @@ -36,9 +36,9 @@ function BuildSlashCommand($request) */ function isValidOutgoingHookRequest() { - global $SLACK_OUTGOING_HOOK_TOKEN; + global $config; - return isset($_REQUEST['token']) && $_REQUEST['token'] == $SLACK_OUTGOING_HOOK_TOKEN; + return isset($_REQUEST['token']) && $_REQUEST['token'] == $config['slack']['outgoinghooktoken']; } /** @@ -55,9 +55,9 @@ function isSlashCommand() function BuildUserLink($username) { - global $SLACK_SUBDOMAIN; + global $config; - $userlink = ''; + $userlink = ''; return $userlink; } diff --git a/scripts/include/slack.secrets.php b/scripts/include/slack.secrets.php index e243c8c..a00051f 100644 --- a/scripts/include/slack.secrets.php +++ b/scripts/include/slack.secrets.php @@ -1,4 +1,5 @@ diff --git a/scripts/rallyme.php b/scripts/rallyme.php index 847109c..84657d8 100644 --- a/scripts/rallyme.php +++ b/scripts/rallyme.php @@ -1,7 +1,6 @@