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. diff --git a/scripts/include/rally.php b/scripts/include/rally.php index 9713410..e3788da 100644 --- a/scripts/include/rally.php +++ b/scripts/include/rally.php @@ -1,305 +1,6 @@ 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); - - $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) -{ - $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 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); - - $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( - "title" => $title, - "value" => $value, - "short" => $short); - - return $attachmentfield; -} - -function getProjectPayload($projectRefUri) -{ - $project = CallAPI($projectRefUri); -} - function CallAPI($uri) { global $config; @@ -310,92 +11,4 @@ function CallAPI($uri) return $object; } - -function GetProjectID($projectref) -{ - $ProjectFull = CallAPI($projectref); - $projectid = $ProjectFull->Project->ObjectID; - return $projectid; -} - -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 BuildUserLink($username) -{ - $userlink = ""; - return $userlink; -} - -function GetArtifactQueryUri($id) -{ - global $config; - 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) - return $text; - - return substr($text,0,$len)."...[MORE]"; -} - ?> diff --git a/scripts/include/rallyme.config.php b/scripts/include/rallyme.config.php index 9e0f85d..ccf579d 100644 --- a/scripts/include/rallyme.config.php +++ b/scripts/include/rallyme.config.php @@ -1,11 +1,11 @@ diff --git a/scripts/include/rallyme.inc.php b/scripts/include/rallyme.inc.php new file mode 100644 index 0000000..22d2082 --- /dev/null +++ b/scripts/include/rallyme.inc.php @@ -0,0 +1,467 @@ +QueryResult->TotalResultCount == 0) { //get count + trigger_error('Sorry, @user, I couldn\'t find ' . $formatted_id, E_USER_ERROR); //not found + } + + //generate payload from first query result + foreach ($Results->QueryResult->Results as $Result) { + if ($Result->_type == $artifact_type) { + $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); +} + +/** + * 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) +{ + global $config; + + //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 + 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 + PrintJsonResponse($errstr); + } + + exit(); +} + +/** + * Prepares a table of fields attached to a Rally defect for display. + * + * @param object $Defect + * + * @return string[] + */ +function ParseDefectPayload($Defect) +{ + $header = CompileArtifactHeader($Defect, 'defect'); + + global $config; + switch ($config['rallyme']['version']) { + + case 2: + $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; + + default: + $fields = array( + 'link' => array($header['title'] => $header['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; + } + + return array('header' => $header, 'fields' => $fields); +} + +/** + * Prepares a table of fields attached to a Rally task for display. + * + * @param object $Task + * + * @return string[] + */ +function ParseTaskPayload($Task) +{ + $header = CompileArtifactHeader($Task, 'task'); + + global $config; + switch ($config['rallyme']['version']) { + + case 2: + $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); + break; + } + + return array('header' => $header, 'fields' => $fields); +} + +/** + * Prepares a table of fields attached to a Rally user story for display. + * + * @param object $Story + * + * @return string[] + */ +function ParseStoryPayload($Story) +{ + $header = CompileArtifactHeader($Story, 'story'); + + global $config; + switch ($config['rallyme']['version']) { + + case 2: + $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) { + $fields['Attachment'] = GetAttachmentLinks($Story->Attachments->_ref); + } + break; + + default: + $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 $config; + $path_map = array('defect' => 'defect', 'task' => 'task', 'story' => 'userstory'); //associate human-readable names with Rally URL paths + + $item_url = $config['rally']['hosturl'] . '#/' . 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. + * + * 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[] $header + * + * @return string[] + */ +function CompileRequirementFields($Requirement, $header) +{ + $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($header['title'] => $header['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. + * + * @param string $attachment_ref + * + * @return string[] + */ +function GetAttachmentLinks($attachment_ref) +{ + global $config; + $url = $config['rally']['hosturl'] . 'slm/attachment/'; + $links = array(); + + $Attachments = CallAPI($attachment_ref); + + foreach ($Attachments->QueryResult->Results as $Attachment) { + $filename = $Attachment->_refObjectName; + $link_url = $url . $Attachment->ObjectID . '/' . urlencode($filename); + $links[$filename] = $link_url; + } + + return $links; +} + +/** + * Posts artifact details to a Slack channel via an incoming webhook. + * + * @param string[] $payload + * + * @return mixed + */ +function SendArtifactPayload($payload) +{ + global $config; + + $prextext = ArtifactPretext($payload['header']); + $color = '#CEC7B8'; //dove gray + if ($payload['header']['type'] == 'defect') { + $color = 'bad'; //purple + } + + $fields = array(); + foreach ($payload['fields'] as $label => $value) { + $short = TRUE; + switch ($label) { + + case 'Parent': + case 'parent': + if (is_string($value)) { + $short = FALSE; + break; + } + 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']['url']); + $short = FALSE; + break; + } + $fields[] = MakeField($label, $value, $short); + } + + $attachment = MakeAttachment($prextext, '', $color, $fields, $payload['header']['url']); + + return slack_incoming_hook_post_with_attachments( + $config['slack']['hook'], + $config['rally']['botname'], + $_REQUEST['channel_name'], + $config['rally']['boticon'], + '', + $attachment + ); +} + +/** + * Returns artifact details as Slack-formatted JSON in the body of the response. + * + * @param string[] $payload + * + * @return mixed + */ +function ReturnArtifactPayload($payload) +{ + $text = ArtifactPretext($payload['header']); + + foreach ($payload['fields'] as $label => $value) { + switch ($label) { + + case 'Attachment': + case 'Parent': + $link_url = reset($value); + $value = l(key($value), $link_url); + break; + + case 'Block Reason': + $label = ''; + $value = SanitizeText($value); + break; + + case 'Description': + $value = TruncateText(SanitizeText($value), 300, $payload['header']['url']); + $value = '\n> ' . strtr($value, array('\n' => '\n> ')); + break; + } + + if ($label) { + $label .= ':'; + $text .= '\n`' . str_pad($label, 15) . '`\t' . $value; + } else { + $text .= '\n>' . $value; + } + } + + return PrintJsonResponse($text); +} + +/** + * Compiles a short message that Rallybot uses to announce query results. + * + * @param string[] $header + * + * @return string + */ +function ArtifactPretext($header) +{ + global $config; + switch ($config['rallyme']['version']) { + + case 2: + return em('Details for ' . $header['id'] . ' ' . l($header['title'], $header['url'])); + + default: + return 'Ok, @' . $_REQUEST['user_name'] . ', here\'s the ' . $header['type'] . ' you requested.'; + } +} diff --git a/scripts/include/slack.config.php b/scripts/include/slack.config.php index e14cd95..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 38639a2..e2056be 100644 --- a/scripts/include/slack.php +++ b/scripts/include/slack.php @@ -29,47 +29,127 @@ function BuildSlashCommand($request) return $cmd; } +/** + * Determine if the incoming request is using the correct token. + * + * @return boolean + */ +function isValidOutgoingHookRequest() +{ + global $config; -function slack_incoming_hook_post($uri, $user, $channel, $icon, $emoji, $payload){ - - $data = array( - "text" => $payload, - "channel" => "#".$channel, - "username"=>$user - ); + return isset($_REQUEST['token']) && $_REQUEST['token'] == $config['slack']['outgoinghooktoken']; +} - if($icon!=null) - { - $data['icon_url'] = $icon; - } - elseif($emoji!=null) - { - $data['icon_emoji'] = $emoji; +/** + * 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) +{ + global $config; + + $userlink = ''; + return $userlink; +} + +function SanitizeText($text) +{ + $text = strtr($text, array('
' => '\n', '
' => '\n', '

' => '\n')); + return html_entity_decode(strip_tags($text), ENT_HTML401 | ENT_COMPAT, 'UTF-8'); +} + +function TruncateText($text, $len, $url = '') +{ + if (strlen($text) <= $len) { + return $text; } + $text = preg_replace('/\s+?(\S+)?$/', '', substr($text, 0, $len)); + $more = ($url) ? l('more', $url) : 'more'; + return $text . '... ' . em($more); +} - $data_string = "payload=" . json_encode($data, JSON_HEX_AMP|JSON_HEX_APOS|JSON_NUMERIC_CHECK|JSON_PRETTY_PRINT); +function l($text, $url) +{ + return '<' . $url . '|' . $text . '>'; +} - mylog('sent.txt',$data_string); - return curl_post($uri, $data_string); +function em($text) +{ + return '_' . $text . '_'; } +function slack_incoming_hook_post($url, $user, $channel, $icon, $emoji, $payload) +{ + $data = array( + 'text' => $payload, + 'channel' => '#' . $channel, + 'username' => $user, + 'link_names' => 1 + ); + if ($icon != null) { + $data['icon_url'] = $icon; + } elseif ($emoji != null) { + $data['icon_emoji'] = $emoji; + } -function slack_incoming_hook_post_with_attachments($uri, $user, $channel, $icon, $payload, $attachments){ + return _incoming_hook_post($url, $data); +} + +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'); + } + 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) +{ + $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, array('\n' => 'n', '\t' => 't')); //fix double-escaped codes + return print_r($data_string); +} /* slack attachment format @@ -105,4 +185,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; +} ?> 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 aca567c..84657d8 100644 --- a/scripts/rallyme.php +++ b/scripts/rallyme.php @@ -1,13 +1,11 @@ Text); +$result = NULL; -$result = HandleItem($slackCommand, $rallyFormattedId); -?> \ No newline at end of file +if (isValidOutgoingHookRequest() && isset($_REQUEST['text'])) { + $payload = FetchArtifactPayload($_REQUEST['text']); + $result = isSlashCommand() ? SendArtifactPayload($payload) : ReturnArtifactPayload($payload); +}