functions/slack/app.rb (65 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. # [START functions_slack_setup] require "functions_framework" require "slack-ruby-client" require "google/apis/kgsearch_v1" # This block is executed during cold start, before the function begins # handling requests. This is the recommended way to create shared resources # and objects. FunctionsFramework.on_startup do # Create a global handler object, configured with the environment-provided # API key and signing secret. kg_search = KGSearch.new kg_api_key: ENV["KG_API_KEY"], signing_secret: ENV["SLACK_SECRET"] set_global :kg_search, kg_search end # The KGSearch class implements the logic of validating and responding # to requests. More methods of this class are shown below. class KGSearch def initialize kg_api_key:, signing_secret: # Create the global client for the Knowledge Graph Search Service, # configuring it with your API key. @client = Google::Apis::KgsearchV1::KgsearchService.new @client.key = kg_api_key # Save signing secret for use by the signature validation method. @signing_secret = signing_secret end # [END functions_slack_setup] # [START functions_verify_webhook] # slack-ruby-client expects a Rails-style request object with a "headers" # method, but the Functions Framework provides only a Rack request. # To avoid bringing in Rails as a dependency, we'll create a simple class # that implements the "headers" method and delegates everything else back to # the Rack request object. require "delegate" class RequestWithHeaders < SimpleDelegator def headers env.each_with_object({}) do |(key, val), result| if /^HTTP_(\w+)$/ =~ key header = Regexp.last_match(1).split("_").map(&:capitalize).join("-") result[header] = val end end end end # This is a method of the KGSearch class. # It determines whether the given request's signature is valid. def signature_valid? request # Wrap the request with our class that provides the "headers" method. request = RequestWithHeaders.new request # Validate the request signature. slack_request = Slack::Events::Request.new request, signing_secret: @signing_secret slack_request.valid? end # [END functions_verify_webhook] # [START functions_slack_request] # This is a method of the KGSearch class. # It makes an API call to the Knowledge Graph Search Service, and formats # a Slack message as a nested Hash object. def make_search_request query response = @client.search_entities query: query, limit: 1 format_slack_message query, response end # [END functions_slack_request] # [START functions_slack_format] # This is a method of the KGSearch class. # It takes a raw SearchResponse from the Knowledge Graph Search Service, # and formats a Slack message. def format_slack_message query, response result = response.item_list_element&.first&.fetch "result", nil attachment = if result name = result.fetch "name", nil description = result.fetch "description", nil details = result.fetch "detailedDescription", {} { "title" => name && description ? "#{name}: #{description}" : name, "title_link" => details.fetch("url", nil), "text" => details.fetch("articleBody", nil), "image_url" => result.fetch("image", nil)&.fetch("contentUrl", nil) } else { "text" => "No results match your query." } end { "response_type" => "in_channel", "text" => "Query: #{query}", "attachments" => [attachment.compact] } end # [END functions_slack_format] attr_accessor :client end # [START functions_slack_search] # Handler for the function endpoint. FunctionsFramework.http "kg_search" do |request| # Return early if the request is not a POST. unless request.post? return [405, {}, ["Only POST requests are accepted."]] end # Access the global Knowledge Graph Search client kg_search = global :kg_search # Verify the request signature and return early if it failed. unless kg_search.signature_valid? request return [401, {}, ["Signature validation failed."]] end # Query the Knowledge Graph and format a Slack message with the response. # This method returns a nested hash, which the Functions Framework will # convert to JSON automatically. kg_search.make_search_request request.params["text"] end # [END functions_slack_search]