Skip to content

Commit

Permalink
GCF: Add Slack sample + clean up imports (#2394)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Ace Nassri authored Mar 21, 2020
1 parent 5275013 commit 1e5e99a
Show file tree
Hide file tree
Showing 6 changed files with 380 additions and 32 deletions.
8 changes: 5 additions & 3 deletions .kokoro/tests/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@ if [[ "$SCRIPT_DEBUG" != "true" ]]; then
source "${KOKORO_GFILE_DIR}/aws-secrets.sh"
# shellcheck source=src/storage-hmac-credentials.sh
source "${KOKORO_GFILE_DIR}/storage-hmac-credentials.sh"
# shellcheck source=src/dlp_secrets.sh
# shellcheck source=src/dlp_secrets.txt
source "${KOKORO_GFILE_DIR}/dlp_secrets.txt"
# shellcheck source=src/bigtable_secrets.sh
# shellcheck source=src/bigtable_secrets.txt
source "${KOKORO_GFILE_DIR}/bigtable_secrets.txt"
# shellcheck source=src/automl_secrets.sh
# shellcheck source=src/automl_secrets.txt
source "${KOKORO_GFILE_DIR}/automl_secrets.txt"
# shellcheck source=src/functions_secrets.txt
source "${KOKORO_GFILE_DIR}/functions_secrets.txt"
# Activate service account
gcloud auth activate-service-account \
--key-file="$GOOGLE_APPLICATION_CREDENTIALS" \
Expand Down
8 changes: 0 additions & 8 deletions functions/snippets/.gcloudignore

This file was deleted.

44 changes: 41 additions & 3 deletions functions/snippets/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,32 @@
</properties>

<dependencies>
<!-- Required to for com.example.functions.ParseContentType -->
<!-- Required for com.example.functions.ParseContentType -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>

<!-- Required to for com.example.functions.RetrieveLogs -->
<!-- Required for com.example.functions.RetrieveLogs -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-logging</artifactId>
<version>1.100.0</version>
</dependency>

<!-- Required for com.example.functions.SlackSlashCommand -->
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-kgsearch</artifactId>
<version>v1-rev253-1.25.0</version>
</dependency>
<dependency>
<groupId>com.github.seratch</groupId>
<artifactId>jslack</artifactId>
<version>3.4.1</version>
</dependency>

<!-- The following dependencies are only required for testing -->
<dependency>
<groupId>junit</groupId>
Expand Down Expand Up @@ -102,10 +114,36 @@
</dependency>
</dependencies>

<!-- Required for Java 8 (Alpha) functions in the inline editor -->
<!-- Disable tests during GCF builds -->
<!-- You can remove this profile to run tests -->
<!-- when deploying, but we recommend creating -->
<!-- a CI/CD pipeline via Cloud Build instead -->
<profiles>
<profile>
<activation>
<property>
<name>env.NEW_BUILD</name>
</property>
</activation>
<properties>
<skipTests>true</skipTests>
</properties>
</profile>
</profiles>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M3</version>
<configuration>
<skipTests>${skipTests}</skipTests>
<reportNameSuffix>sponge_log</reportNameSuffix>
<trimStackTrace>false</trimStackTrace>
</configuration>
</plugin>
<plugin> <!-- Required for Java 8 (Alpha) functions in the inline editor -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.functions;

import com.github.seratch.jslack.app_backend.SlackSignature;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.kgsearch.v1.Kgsearch;
import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.io.BufferedWriter;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;

public class SlackSlashCommand implements HttpFunction {

private Kgsearch kgClient;
private static final String API_KEY = System.getenv("KG_API_KEY");
private static final String SLACK_SECRET = System.getenv("SLACK_SECRET");
private static final Logger LOGGER = Logger.getLogger(SlackSlashCommand.class.getName());
private SlackSignature.Verifier verifier;
private Gson gson = new Gson();

public SlackSlashCommand() throws IOException, GeneralSecurityException {
kgClient = new Kgsearch.Builder(
GoogleNetHttpTransport.newTrustedTransport(), new JacksonFactory(), null).build();

verifier = new SlackSignature.Verifier(new SlackSignature.Generator(SLACK_SECRET));
}

boolean isValidSlackWebhook(HttpRequest request, String requestBody) throws IOException {

// Check for headers
HashMap<String, List<String>> headers = new HashMap(request.getHeaders());
if (!headers.containsKey("X-Slack-Request-Timestamp")
|| !headers.containsKey("X-Slack-Signature")) {
return false;
}
return verifier.isValid(
headers.get("X-Slack-Request-Timestamp").get(0),
requestBody,
headers.get("X-Slack-Signature").get(0),
1L);
}

void addPropertyIfPresent(
JsonObject target, String targetName, JsonObject source, String sourceName) {
if (source.has(sourceName)) {
target.addProperty(targetName, source.get(sourceName).getAsString());
}
}

String formatSlackMessage(JsonObject kgResponse, String query) {
JsonObject attachmentJson = new JsonObject();
JsonArray attachments = new JsonArray();

JsonObject responseJson = new JsonObject();
responseJson.addProperty("response_type", "in_channel");
responseJson.addProperty("text", String.format("Query: %s", query));

JsonArray entityList = kgResponse.getAsJsonArray("itemListElement");

// Extract the first entity from the result list, if any
if (entityList.size() == 0) {
attachmentJson.addProperty("text","No results match your query...");

attachments.add(attachmentJson);
responseJson.add("attachments", attachmentJson);

return gson.toJson(responseJson);
}

JsonObject entity = entityList.get(0).getAsJsonObject().getAsJsonObject("result");

// Construct Knowledge Graph response attachment
String title = entity.get("name").getAsString();
if (entity.has("description")) {
title = String.format("%s: %s", title, entity.get("description").getAsString());
}
attachmentJson.addProperty("title", title);

if (entity.has("detailedDescription")) {
JsonObject detailedDescJson = entity.getAsJsonObject("detailedDescription");
addPropertyIfPresent(attachmentJson, "title_link", detailedDescJson, "url");
addPropertyIfPresent(attachmentJson, "text", detailedDescJson, "articleBody");
}

if (entity.has("image")) {
JsonObject imageJson = entity.getAsJsonObject("image");
addPropertyIfPresent(attachmentJson, "image_url", imageJson, "contentUrl");
}

// Construct top level response
attachments.add(attachmentJson);
responseJson.add("attachments", attachmentJson);

return gson.toJson(responseJson);
}

JsonObject searchKnowledgeGraph(String query) throws IOException {
Kgsearch.Entities.Search kgRequest = kgClient.entities().search();
kgRequest.setQuery(query);
kgRequest.setKey(API_KEY);

return gson.fromJson(kgRequest.execute().toString(), JsonObject.class);
}

@Override
public void service(HttpRequest request, HttpResponse response) throws IOException {

// Validate request
if (request.getMethod() != "POST") {
response.setStatusCode(HttpURLConnection.HTTP_BAD_METHOD);
return;
}

// reader can only be read once per request, so we preserve its contents
String bodyString = request.getReader().lines().collect(Collectors.joining());
JsonObject body = (new Gson()).fromJson(bodyString, JsonObject.class);

if (body == null || !body.has("text")) {
response.setStatusCode(HttpURLConnection.HTTP_BAD_REQUEST);
return;
}

if (!isValidSlackWebhook(request, bodyString)) {
response.setStatusCode(HttpURLConnection.HTTP_UNAUTHORIZED);
return;
}

String query = body.get("text").getAsString();

// Call knowledge graph API
JsonObject kgResponse = searchKnowledgeGraph(query);

// Format response to Slack
BufferedWriter writer = response.getWriter();
writer.write(formatSlackMessage(kgResponse, query));
}
}
Loading

0 comments on commit 1e5e99a

Please sign in to comment.