Skip to content

Commit 4778f77

Browse files
author
Ace Nassri
authored
Merge branch 'master' into functions-scopes
2 parents 81422bd + 62b8ad2 commit 4778f77

File tree

6 files changed

+473
-0
lines changed

6 files changed

+473
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"require": {
3+
"google/cloud-functions-framework": "^0.6",
4+
"google/apiclient": "^2.8"
5+
},
6+
"scripts": {
7+
"post-update-cmd": "Google\\Task\\Composer::cleanup"
8+
},
9+
"extra": {
10+
"google/apiclient-services": [
11+
"Kgsearch"
12+
]
13+
}
14+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
2+
/**
3+
* Copyright 2020 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
// [START functions_slack_setup]
19+
use Psr\Http\Message\ServerRequestInterface;
20+
use Psr\Http\Message\ResponseInterface;
21+
use GuzzleHttp\Psr7\Response;
22+
23+
// [END functions_slack_setup]
24+
25+
// [START functions_verify_webhook]
26+
/**
27+
* Verify that the webhook request came from Slack.
28+
*/
29+
function isValidSlackWebhook(ServerRequestInterface $request): bool
30+
{
31+
$SLACK_SECRET = getenv('SLACK_SECRET');
32+
33+
// Check for headers
34+
$timestamp = $request->getHeader('X-Slack-Request-Timestamp');
35+
$signature = $request->getHeader('X-Slack-Signature');
36+
if (!$timestamp || !$signature) {
37+
return false;
38+
} else {
39+
$timestamp = $timestamp[0];
40+
$signature = $signature[0];
41+
}
42+
43+
// Compute signature
44+
$plaintext = 'v0:' . $timestamp . ':' . (string) $request->getBody();
45+
$hash = 'v0=' . hash_hmac('sha256', $plaintext, $SLACK_SECRET);
46+
47+
return $hash === $signature;
48+
}
49+
// [END functions_verify_webhook]
50+
51+
// [START functions_slack_format]
52+
/**
53+
* Format the Knowledge Graph API response into a richly formatted Slack message.
54+
*/
55+
function formatSlackMessage(Google_Service_Kgsearch_SearchResponse $kgResponse, string $query): string
56+
{
57+
$responseJson = [
58+
'response_type' => 'in_channel',
59+
'text' => 'Query: ' . $query
60+
];
61+
62+
$entityList = $kgResponse['itemListElement'];
63+
64+
// Extract the first entity from the result list, if any
65+
if (empty($entityList)) {
66+
$attachmentJson = ['text' => 'No results match your query...'];
67+
$responseJson['attachments'] = $attachmentJson;
68+
69+
return json_encode($responseJson);
70+
}
71+
72+
$entity = $entityList[0]['result'];
73+
74+
// Construct Knowledge Graph response attachment
75+
$title = $entity['name'];
76+
if (isset($entity['description'])) {
77+
$title = $title . ' ' . $entity['description'];
78+
}
79+
$attachmentJson = ['title' => $title];
80+
81+
if (isset($entity['detailedDescription'])) {
82+
$detailedDescJson = $entity['detailedDescription'];
83+
$attachmentJson = array_merge([
84+
'title_link' => $detailedDescJson[ 'url'],
85+
'text' => $detailedDescJson['articleBody'],
86+
], $attachmentJson);
87+
}
88+
89+
if ($entity['image']) {
90+
$imageJson = $entity['image'];
91+
$attachmentJson['image_url'] = $imageJson['contentUrl'];
92+
}
93+
94+
$responseJson['attachments'] = array($attachmentJson);
95+
96+
return json_encode($responseJson);
97+
}
98+
// [END functions_slack_format]
99+
100+
// [START functions_slack_request]
101+
/**
102+
* Send the user's search query to the Knowledge Graph API.
103+
*/
104+
function searchKnowledgeGraph(string $query): Google_Service_Kgsearch_SearchResponse
105+
{
106+
$API_KEY = getenv("KG_API_KEY");
107+
108+
$apiClient = new Google\Client();
109+
$apiClient->setDeveloperKey($API_KEY);
110+
111+
$service = new Google_Service_Kgsearch($apiClient);
112+
113+
$params = ['query' => $query];
114+
115+
$kgResults = $service->entities->search($params);
116+
117+
return $kgResults;
118+
}
119+
// [END functions_slack_request]
120+
121+
// [START functions_slack_search]
122+
/**
123+
* Receive a Slash Command request from Slack.
124+
*/
125+
function receiveRequest(ServerRequestInterface $request): ResponseInterface
126+
{
127+
// Validate request
128+
if ($request->getMethod() !== 'POST') {
129+
// [] = empty headers
130+
return new Response(405);
131+
}
132+
133+
// Parse incoming URL-encoded requests from Slack
134+
// (Slack requests use the "application/x-www-form-urlencoded" format)
135+
$bodyStr = $request->getBody();
136+
parse_str($bodyStr, $bodyParams);
137+
138+
if (!isset($bodyParams['text'])) {
139+
// [] = empty headers
140+
return new Response(400);
141+
}
142+
143+
if (!isValidSlackWebhook($request, $bodyStr)) {
144+
// [] = empty headers
145+
return new Response(403);
146+
}
147+
148+
$query = $bodyParams['text'];
149+
150+
// Call knowledge graph API
151+
$kgResponse = searchKnowledgeGraph($query);
152+
153+
// Format response to Slack
154+
// See https://api.slack.com/docs/message-formatting
155+
$formatted_message = formatSlackMessage($kgResponse, $query);
156+
157+
return new Response(
158+
200,
159+
['Content-Type' => 'application/json'],
160+
$formatted_message
161+
);
162+
}
163+
// [END functions_slack_search]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
xml version="1.0" encoding="UTF-8"?>
2+
17+
<phpunit bootstrap="../../testing/bootstrap.php" convertWarningsToExceptions="false">
18+
<testsuites>
19+
<testsuite name="Cloud Functions Slack Slash Command Test Suite">
20+
<directory>testdirectory>
21+
testsuite>
22+
testsuites>
23+
<filter>
24+
<whitelist>
25+
<directory suffix=".php">.directory>
26+
<exclude>
27+
<directory>./vendordirectory>
28+
exclude>
29+
whitelist>
30+
filter>
31+
phpunit>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
2+
/**
3+
* Copyright 2020 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace Google\Cloud\Samples\Functions\SlackSlashCommand\Test;
21+
22+
use Google\Cloud\TestUtils\CloudFunctionDeploymentTrait;
23+
use PHPUnit\Framework\TestCase;
24+
25+
require_once __DIR__ . '/TestCasesTrait.php';
26+
27+
/**
28+
* Class DeployTest.
29+
*
30+
* This test is not run by the CI system.
31+
*
32+
* To skip deployment of a new function, run with "GOOGLE_SKIP_DEPLOYMENT=true".
33+
* To skip deletion of the tested function, run with "GOOGLE_KEEP_DEPLOYMENT=true".
34+
*/
35+
class DeployTest extends TestCase
36+
{
37+
use CloudFunctionDeploymentTrait;
38+
use TestCasesTrait;
39+
40+
private static $entryPoint = 'receiveRequest';
41+
42+
/**
43+
* @dataProvider cases
44+
*/
45+
public function testFunction(
46+
$label,
47+
$body,
48+
$method,
49+
$expected,
50+
$statusCode,
51+
$headers
52+
): void {
53+
$response = $this->client->request(
54+
$method,
55+
'',
56+
['headers' => $headers, 'body' => $body]
57+
);
58+
$this->assertEquals(
59+
$statusCode,
60+
$response->getStatusCode(),
61+
$label . ': status code'
62+
);
63+
64+
if ($expected !== null) {
65+
$output = (string) $response->getBody();
66+
$this->assertContains($expected, $output, $label . ': contains');
67+
}
68+
}
69+
70+
/**
71+
* Deploy the Function.
72+
*
73+
* Overrides CloudFunctionLocalTestTrait::doDeploy().
74+
*/
75+
private static function doDeploy()
76+
{
77+
// Forward required env variables to Cloud Functions.
78+
$envVars = 'SLACK_SECRET=' . self::requireEnv('SLACK_SECRET') . ',';
79+
$envVars .= 'KG_API_KEY=' . self::requireEnv('KG_API_KEY');
80+
81+
self::$fn->deploy(['--update-env-vars' => $envVars]);
82+
}
83+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
2+
/**
3+
* Copyright 2020 Google LLC.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace Google\Cloud\Samples\Functions\SlackSlashCommand\Test;
21+
22+
use PHPUnit\Framework\TestCase;
23+
use Google\Cloud\TestUtils\CloudFunctionLocalTestTrait;
24+
25+
require_once __DIR__ . '/TestCasesTrait.php';
26+
27+
/**
28+
* Class IntegrationTest.
29+
*/
30+
class IntegrationTest extends TestCase
31+
{
32+
use CloudFunctionLocalTestTrait;
33+
use TestCasesTrait;
34+
35+
private static $entryPoint = 'receiveRequest';
36+
37+
/**
38+
* Run the PHP server locally for the defined function.
39+
*
40+
* Overrides CloudFunctionLocalTestTrait::doRun().
41+
*/
42+
private static function doRun()
43+
{
44+
self::$fn->run([
45+
'SLACK_SECRET' => self::requireEnv('SLACK_SECRET'),
46+
'KG_API_KEY' => self::requireEnv('KG_API_KEY'),
47+
]);
48+
}
49+
50+
/**
51+
* @dataProvider cases
52+
*/
53+
public function testFunction(
54+
$label,
55+
$body,
56+
$method,
57+
$expected,
58+
$statusCode,
59+
$headers
60+
): void {
61+
$response = $this->client->request(
62+
$method,
63+
'/',
64+
['headers' => $headers, 'body' => $body]
65+
);
66+
$this->assertEquals(
67+
$statusCode,
68+
$response->getStatusCode(),
69+
$label . ": status code"
70+
);
71+
72+
if ($expected !== null) {
73+
$output = (string) $response->getBody();
74+
$this->assertContains($expected, $output);
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)