Skip to content

Commit 1e5e99a

Browse files
author
Ace Nassri
authored
GCF: Add Slack sample + clean up imports (#2394)
* Add Slack sample + clean up imports * Address comments * Remove excess gcloudignore + actually disable tests * Simplify tests + run them on Kokoro. ALSO bugfix unused shellchecks. * Remove extra file * HACK: resolve surefire issue via file presence * HACK take 2: use a different filepath * HACK take 3: use env var not used by local Cloud Build * Remove gitignore now that config.json isnt used * DBG: print defined env vars * DBG take 2 * DBG take 3 * DBG take 4 * DBG take 5 * DBG take 6 * DBG take 7 * Fix tests...? * Revert dbg commits + fix tests
1 parent 5275013 commit 1e5e99a

File tree

6 files changed

+380
-32
lines changed

6 files changed

+380
-32
lines changed

.kokoro/tests/run_tests.sh

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,14 @@ if [[ "$SCRIPT_DEBUG" != "true" ]]; then
6565
source "${KOKORO_GFILE_DIR}/aws-secrets.sh"
6666
# shellcheck source=src/storage-hmac-credentials.sh
6767
source "${KOKORO_GFILE_DIR}/storage-hmac-credentials.sh"
68-
# shellcheck source=src/dlp_secrets.sh
68+
# shellcheck source=src/dlp_secrets.txt
6969
source "${KOKORO_GFILE_DIR}/dlp_secrets.txt"
70-
# shellcheck source=src/bigtable_secrets.sh
70+
# shellcheck source=src/bigtable_secrets.txt
7171
source "${KOKORO_GFILE_DIR}/bigtable_secrets.txt"
72-
# shellcheck source=src/automl_secrets.sh
72+
# shellcheck source=src/automl_secrets.txt
7373
source "${KOKORO_GFILE_DIR}/automl_secrets.txt"
74+
# shellcheck source=src/functions_secrets.txt
75+
source "${KOKORO_GFILE_DIR}/functions_secrets.txt"
7476
# Activate service account
7577
gcloud auth activate-service-account \
7678
--key-file="$GOOGLE_APPLICATION_CREDENTIALS" \

functions/snippets/.gcloudignore

Lines changed: 0 additions & 8 deletions
This file was deleted.

functions/snippets/pom.xml

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,32 @@
2626
</properties>
2727

2828
<dependencies>
29-
<!-- Required to for com.example.functions.ParseContentType -->
29+
<!-- Required for com.example.functions.ParseContentType -->
3030
<dependency>
3131
<groupId>com.google.code.gson</groupId>
3232
<artifactId>gson</artifactId>
3333
<version>2.8.6</version>
3434
</dependency>
3535

36-
<!-- Required to for com.example.functions.RetrieveLogs -->
36+
<!-- Required for com.example.functions.RetrieveLogs -->
3737
<dependency>
3838
<groupId>com.google.cloud</groupId>
3939
<artifactId>google-cloud-logging</artifactId>
4040
<version>1.100.0</version>
4141
</dependency>
4242

43+
<!-- Required for com.example.functions.SlackSlashCommand -->
44+
<dependency>
45+
<groupId>com.google.apis</groupId>
46+
<artifactId>google-api-services-kgsearch</artifactId>
47+
<version>v1-rev253-1.25.0</version>
48+
</dependency>
49+
<dependency>
50+
<groupId>com.github.seratch</groupId>
51+
<artifactId>jslack</artifactId>
52+
<version>3.4.1</version>
53+
</dependency>
54+
4355
<!-- The following dependencies are only required for testing -->
4456
<dependency>
4557
<groupId>junit</groupId>
@@ -102,10 +114,36 @@
102114
</dependency>
103115
</dependencies>
104116

105-
<!-- Required for Java 8 (Alpha) functions in the inline editor -->
117+
<!-- Disable tests during GCF builds -->
118+
<!-- You can remove this profile to run tests -->
119+
<!-- when deploying, but we recommend creating -->
120+
<!-- a CI/CD pipeline via Cloud Build instead -->
121+
<profiles>
122+
<profile>
123+
<activation>
124+
<property>
125+
<name>env.NEW_BUILD</name>
126+
</property>
127+
</activation>
128+
<properties>
129+
<skipTests>true</skipTests>
130+
</properties>
131+
</profile>
132+
</profiles>
133+
106134
<build>
107135
<plugins>
108136
<plugin>
137+
<groupId>org.apache.maven.plugins</groupId>
138+
<artifactId>maven-surefire-plugin</artifactId>
139+
<version>3.0.0-M3</version>
140+
<configuration>
141+
<skipTests>${skipTests}</skipTests>
142+
<reportNameSuffix>sponge_log</reportNameSuffix>
143+
<trimStackTrace>false</trimStackTrace>
144+
</configuration>
145+
</plugin>
146+
<plugin> <!-- Required for Java 8 (Alpha) functions in the inline editor -->
109147
<groupId>org.apache.maven.plugins</groupId>
110148
<artifactId>maven-compiler-plugin</artifactId>
111149
<configuration>
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.functions;
18+
19+
import com.github.seratch.jslack.app_backend.SlackSignature;
20+
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
21+
import com.google.api.client.json.jackson2.JacksonFactory;
22+
import com.google.api.services.kgsearch.v1.Kgsearch;
23+
import com.google.cloud.functions.HttpFunction;
24+
import com.google.cloud.functions.HttpRequest;
25+
import com.google.cloud.functions.HttpResponse;
26+
import com.google.gson.Gson;
27+
import com.google.gson.JsonArray;
28+
import com.google.gson.JsonObject;
29+
import java.io.BufferedWriter;
30+
import java.io.IOException;
31+
import java.net.HttpURLConnection;
32+
import java.security.GeneralSecurityException;
33+
import java.util.HashMap;
34+
import java.util.List;
35+
import java.util.logging.Logger;
36+
import java.util.stream.Collectors;
37+
38+
public class SlackSlashCommand implements HttpFunction {
39+
40+
private Kgsearch kgClient;
41+
private static final String API_KEY = System.getenv("KG_API_KEY");
42+
private static final String SLACK_SECRET = System.getenv("SLACK_SECRET");
43+
private static final Logger LOGGER = Logger.getLogger(SlackSlashCommand.class.getName());
44+
private SlackSignature.Verifier verifier;
45+
private Gson gson = new Gson();
46+
47+
public SlackSlashCommand() throws IOException, GeneralSecurityException {
48+
kgClient = new Kgsearch.Builder(
49+
GoogleNetHttpTransport.newTrustedTransport(), new JacksonFactory(), null).build();
50+
51+
verifier = new SlackSignature.Verifier(new SlackSignature.Generator(SLACK_SECRET));
52+
}
53+
54+
boolean isValidSlackWebhook(HttpRequest request, String requestBody) throws IOException {
55+
56+
// Check for headers
57+
HashMap<String, List<String>> headers = new HashMap(request.getHeaders());
58+
if (!headers.containsKey("X-Slack-Request-Timestamp")
59+
|| !headers.containsKey("X-Slack-Signature")) {
60+
return false;
61+
}
62+
return verifier.isValid(
63+
headers.get("X-Slack-Request-Timestamp").get(0),
64+
requestBody,
65+
headers.get("X-Slack-Signature").get(0),
66+
1L);
67+
}
68+
69+
void addPropertyIfPresent(
70+
JsonObject target, String targetName, JsonObject source, String sourceName) {
71+
if (source.has(sourceName)) {
72+
target.addProperty(targetName, source.get(sourceName).getAsString());
73+
}
74+
}
75+
76+
String formatSlackMessage(JsonObject kgResponse, String query) {
77+
JsonObject attachmentJson = new JsonObject();
78+
JsonArray attachments = new JsonArray();
79+
80+
JsonObject responseJson = new JsonObject();
81+
responseJson.addProperty("response_type", "in_channel");
82+
responseJson.addProperty("text", String.format("Query: %s", query));
83+
84+
JsonArray entityList = kgResponse.getAsJsonArray("itemListElement");
85+
86+
// Extract the first entity from the result list, if any
87+
if (entityList.size() == 0) {
88+
attachmentJson.addProperty("text","No results match your query...");
89+
90+
attachments.add(attachmentJson);
91+
responseJson.add("attachments", attachmentJson);
92+
93+
return gson.toJson(responseJson);
94+
}
95+
96+
JsonObject entity = entityList.get(0).getAsJsonObject().getAsJsonObject("result");
97+
98+
// Construct Knowledge Graph response attachment
99+
String title = entity.get("name").getAsString();
100+
if (entity.has("description")) {
101+
title = String.format("%s: %s", title, entity.get("description").getAsString());
102+
}
103+
attachmentJson.addProperty("title", title);
104+
105+
if (entity.has("detailedDescription")) {
106+
JsonObject detailedDescJson = entity.getAsJsonObject("detailedDescription");
107+
addPropertyIfPresent(attachmentJson, "title_link", detailedDescJson, "url");
108+
addPropertyIfPresent(attachmentJson, "text", detailedDescJson, "articleBody");
109+
}
110+
111+
if (entity.has("image")) {
112+
JsonObject imageJson = entity.getAsJsonObject("image");
113+
addPropertyIfPresent(attachmentJson, "image_url", imageJson, "contentUrl");
114+
}
115+
116+
// Construct top level response
117+
attachments.add(attachmentJson);
118+
responseJson.add("attachments", attachmentJson);
119+
120+
return gson.toJson(responseJson);
121+
}
122+
123+
JsonObject searchKnowledgeGraph(String query) throws IOException {
124+
Kgsearch.Entities.Search kgRequest = kgClient.entities().search();
125+
kgRequest.setQuery(query);
126+
kgRequest.setKey(API_KEY);
127+
128+
return gson.fromJson(kgRequest.execute().toString(), JsonObject.class);
129+
}
130+
131+
@Override
132+
public void service(HttpRequest request, HttpResponse response) throws IOException {
133+
134+
// Validate request
135+
if (request.getMethod() != "POST") {
136+
response.setStatusCode(HttpURLConnection.HTTP_BAD_METHOD);
137+
return;
138+
}
139+
140+
// reader can only be read once per request, so we preserve its contents
141+
String bodyString = request.getReader().lines().collect(Collectors.joining());
142+
JsonObject body = (new Gson()).fromJson(bodyString, JsonObject.class);
143+
144+
if (body == null || !body.has("text")) {
145+
response.setStatusCode(HttpURLConnection.HTTP_BAD_REQUEST);
146+
return;
147+
}
148+
149+
if (!isValidSlackWebhook(request, bodyString)) {
150+
response.setStatusCode(HttpURLConnection.HTTP_UNAUTHORIZED);
151+
return;
152+
}
153+
154+
String query = body.get("text").getAsString();
155+
156+
// Call knowledge graph API
157+
JsonObject kgResponse = searchKnowledgeGraph(query);
158+
159+
// Format response to Slack
160+
BufferedWriter writer = response.getWriter();
161+
writer.write(formatSlackMessage(kgResponse, query));
162+
}
163+
}

0 commit comments

Comments
 (0)