functions/slack/SlackKnowledgeGraphSearch/Function.cs (109 lines of code) (raw):
// 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
//
// https://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.
using Google.Apis.Kgsearch.v1;
using Google.Apis.Kgsearch.v1.Data;
using Google.Cloud.Functions.Framework;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace SlackKnowledgeGraphSearch;
public class Function : IHttpFunction
{
// [START functions_slack_search]
private readonly ILogger _logger;
private readonly KgsearchService _kgService;
private readonly SlackRequestVerifier _verifier;
public Function(ILogger<Function> logger, KgsearchService kgService, SlackRequestVerifier verifier) =>
(_logger, _kgService, _verifier) = (logger, kgService, verifier);
public async Task HandleAsync(HttpContext context)
{
var request = context.Request;
var response = context.Response;
var cancellationToken = context.RequestAborted;
// Validate request
if (request.Method != "POST")
{
_logger.LogWarning("Unexpected request method '{method}'", request.Method);
response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
return;
}
if (!request.HasFormContentType)
{
_logger.LogWarning("Unexpected content type '{contentType}'", request.ContentType);
response.StatusCode = (int) HttpStatusCode.BadRequest;
return;
}
// We need to read the request body twice: once to validate the signature,
// and once to read the form content. We copy it into a memory stream,
// so that we can rewind it after reading.
var bodyCopy = new MemoryStream();
await request.Body.CopyToAsync(bodyCopy, cancellationToken);
request.Body = bodyCopy;
bodyCopy.Position = 0;
if (!_verifier.VerifyRequest(request, bodyCopy.ToArray()))
{
_logger.LogWarning("Slack request verification failed");
response.StatusCode = (int) HttpStatusCode.Unauthorized;
return;
}
var form = await request.ReadFormAsync();
if (!form.TryGetValue("text", out var query))
{
_logger.LogWarning("Slack request form did not contain a text element");
response.StatusCode = (int) HttpStatusCode.BadRequest;
return;
}
var kgResponse = await SearchKnowledgeGraphAsync(query, cancellationToken);
string formattedResponse = FormatSlackMessage(kgResponse, query);
response.ContentType = "application/json";
await response.WriteAsync(formattedResponse);
}
// [END functions_slack_search]
// [START functions_slack_request]
private async Task<SearchResponse> SearchKnowledgeGraphAsync(string query, CancellationToken cancellationToken)
{
_logger.LogInformation("Performing Knowledge Graph search for '{query}'", query);
var request = _kgService.Entities.Search();
request.Limit = 1;
request.Query = query;
return await request.ExecuteAsync(cancellationToken);
}
// [END functions_slack_request]
// [START functions_slack_format]
private string FormatSlackMessage(SearchResponse kgResponse, string query)
{
JObject attachment = new JObject();
JObject response = new JObject();
response["response_type"] = "in_channel";
response["text"] = $"Query: {query}";
var element = kgResponse.ItemListElement?.FirstOrDefault() as JObject;
if (element is object && element.TryGetValue("result", out var entityToken) &&
entityToken is JObject entity)
{
string title = (string) entity["name"];
if (entity.TryGetValue("description", out var description))
{
title = $"{title}: {description}";
}
attachment["title"] = title;
if (entity.TryGetValue("detailedDescription", out var detailedDescriptionToken) &&
detailedDescriptionToken is JObject detailedDescription)
{
AddPropertyIfPresent(detailedDescription, "url", "title_link");
AddPropertyIfPresent(detailedDescription, "articleBody", "text");
}
if (entity.TryGetValue("image", out var imageToken) &&
imageToken is JObject image)
{
AddPropertyIfPresent(image, "contentUrl", "image_url");
}
}
else
{
attachment["text"] = "No results match your query...";
}
response["attachments"] = new JArray { attachment };
return response.ToString();
void AddPropertyIfPresent(JObject parent, string sourceProperty, string targetProperty)
{
if (parent.TryGetValue(sourceProperty, out var propertyValue))
{
attachment[targetProperty] = propertyValue;
}
}
}
// [END functions_slack_format]
}