From b80f629dfeb094fd214b33be8b16269f4698e11f Mon Sep 17 00:00:00 2001 From: Philippe Dionne Date: Tue, 20 Dec 2016 10:27:00 -0500 Subject: [PATCH 1/4] Create init.ai webhook controller --- .../api/v1/webhooks/init_controller.rb | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 app/controllers/api/v1/webhooks/init_controller.rb diff --git a/app/controllers/api/v1/webhooks/init_controller.rb b/app/controllers/api/v1/webhooks/init_controller.rb new file mode 100644 index 00000000..a8909b64 --- /dev/null +++ b/app/controllers/api/v1/webhooks/init_controller.rb @@ -0,0 +1,107 @@ +class Api::V1::Webhooks::InitController < Api::V1::ApplicationController + before_action :set_bot + before_action :set_integration + before_action :authorize + before_action :throttle + + # POST api.domain.tld/v1/b/1/webhooks/init/ + # @raise [UnfullfiledRequestError] + def create + @message = Api::Message.new(message_params).tap(&:sanitize!) + @conversation = Api::Conversation.new(conversation_params) + @creator = Api::Creator.new(creator_params) + + if @message.valid? && @conversation.valid? && @creator.valid? + CreateRequestJob.perform_async(@bot.id, { request_id: request.request_id, payload: params[:init].to_json, fulfilled: true }) + TrackMessageJob.perform_async(@message.attributes, @conversation.attributes, @creator.attributes, @bot.id) + else + CreateRequestJob.perform_async(@bot.id, { request_id: request.request_id, payload: params[:init].to_json }) + + error = UnfullfiledRequestError.new + logger.error(error) + Appsignal.add_exception(error) + end + + head :created + end + + + private + + # @return [Hash] + def message_params + message = webhook_params[:data][:payload][:current_conversation][:messages][0][:parts] + + { + distinct_id: webhook_params[:data][:payload][:invocation_data][:invocation_id], + provider: 'init', + platform: "TODO", + mtype: message[:content_type], + sent_at: webhook_params[:data][:payload][:invocation_data][:initiated_at], + properties: { + text: message[:content] + } + } + end + + # Init doesn't provide a conversation ID, and doesn't support multi-user + # conversations, so it is safe to use the user ID as the conversation distinct ID + # @return [Hash] + def conversation_params + { + distinct_id: webhook_params[:app_user][:id] + } + end + + # @return [Hash] + def creator_params + if webhook_params[:event_type] == 'MessageOutbound' + { + type: 'Bot', + distinct_id: @bot.id + } + elsif webhook_params[:event_type] == 'LogicInvocation' + { + type: 'Interlocutor', + distinct_id: webhook_params[:data][:payload][:users][:user_id][:platform_user_id], + first_name: webhook_params[:data][:payload][:users][:user_id][:first_name], + last_name: webhook_params[:data][:payload][:users][:user_id][:last_name] + } + else + Hash.new + end + end + + # Whitelist webhook payload + # @raise [ActionController::ParameterMissing] + # @return [ActionController::Parameters] + def webhook_params + payload = params.except(*request.path_parameters.keys) + + payload.require(:event_type) + payload.require(:application) + payload.require(:app_user) + payload.require(:data) + + payload.slice(:event_type, :application, :app_user, :data).permit! + end + + # Authorizes a Init.ai webhook request + def authorize + valid = request.headers['X-Init-Ai-Secret'].present? && + @integration.properties.try(:[], 'webhook_secret').present? && + request.headers['X-Init-Ai-Secret'] == @integration.properties['webhook_secret'] + + error('Unauthorized', 'Bad credentials', 401) unless valid + end + + # @raise [ActiveRecord::RecordNotFound] + def set_integration + @integration = @bot.integrations.find_by!(active: true, type: "Integrations::Init") + end + + # @raise [ActiveRecord::RecordNotFound] + def set_bot + @bot = Bot.friendly.find(params[:bot_id]) + end +end From f900e9961bd0207e99188e1392bbf295c3bcd906 Mon Sep 17 00:00:00 2001 From: Philippe Dionne Date: Tue, 20 Dec 2016 10:28:10 -0500 Subject: [PATCH 2/4] Add init.ai controller route --- config/routes.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/routes.rb b/config/routes.rb index 3ec2482e..ae4f7970 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,6 +11,7 @@ resources :interlocutors, only: [:index, :show, :create] namespace :webhooks do + post :initai, controller: :init, action: :create, as: :initai post :smooch, controller: :smooch, action: :create, as: :smooch end end From d5056eeba2dc873666ac4f1ea158f6934f2dbbff Mon Sep 17 00:00:00 2001 From: Philippe Dionne Date: Tue, 20 Dec 2016 10:29:28 -0500 Subject: [PATCH 3/4] Add placeholder init.ai integration model --- app/models/integrations/init.rb | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/models/integrations/init.rb diff --git a/app/models/integrations/init.rb b/app/models/integrations/init.rb new file mode 100644 index 00000000..93873a78 --- /dev/null +++ b/app/models/integrations/init.rb @@ -0,0 +1,9 @@ +class Integrations::Init < Integration + # @return [Boolean] + def enable + end + + # @return [Boolean] + def disable + end +end From 411340b9bfca4533124f4db6a1aa83fe8107d1d9 Mon Sep 17 00:00:00 2001 From: Philippe Dionne Date: Mon, 23 Jan 2017 16:50:01 -0500 Subject: [PATCH 4/4] Add init inbound webhook fixture --- spec/fixtures/init-ai/webhook-inbound.json | 158 +++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 spec/fixtures/init-ai/webhook-inbound.json diff --git a/spec/fixtures/init-ai/webhook-inbound.json b/spec/fixtures/init-ai/webhook-inbound.json new file mode 100644 index 00000000..d3f26b3e --- /dev/null +++ b/spec/fixtures/init-ai/webhook-inbound.json @@ -0,0 +1,158 @@ +{ + "event_type": "LogicInvocation", + "data": { + "version": "1", + "format": "plain_object", + "payload": { + "deprecation_warning": "WARNING: do not use the fields current_conversation.__private_temp_user_id, execution_data, current_application.external_name, scripts, or common_scripts. These may be removed at any time.", + "execution_data": { + "execution_id": "000", + "execution_mode": "", + "deprecation_warning": "WARNING: execution_data is deprecated and may be removed at any time. Use invocation_data instead.", + "initiated_at": "2016-12-20T20:50:57.959667636Z", + "auth_token": "jwt.token", + "api": { + "base_url": "https://api.init.ai" + }, + }, + "invocation_data": { + "invocation_id": "invocation_id", + "initiated_at": "2016-12-20T20:50:57.959667636Z", + "auth_token": "jwt.token", + "api": { + "base_url": "https://api.init.ai" + } + }, + "current_conversation": { + "state": {}, + "id": "", + "conversation_message_index_to_process": 0, + "messages": [ + { + "sender_role": "end-user", + "sender_relative_id": "", + "parts": [ + { + "content_type": "text", + "content": "hi", + "classification": { + "overall_confidence": 0.99914087988077, + "base_type": { + "confidence": 0, + "value": "greeting" + }, + "sub_type": { + "confidence": 0, + "value": "" + } + }, + "predicted_next_message": { + "overall_confidence": 0.79441527690397, + "direction": { + "confidence": 0, + "value": "output" + }, + "base_type": { + "confidence": 0, + "value": "welcome" + }, + "sub_type": { + "confidence": 0, + "value": "request_player_name" + }, + "predicted_response": { + "name": "welcome/request_player_name", + "auto_fill_capable": true + } + }, + "slots": {} + } + ] + } + ], + "__private_temp_user_id": "__private_temp_user_id" + }, + "users": { + "user_id": { + "app_id": "app_id", + "id": "user_id", + "platform_user_id": "platform_user_id", + "remote_id": null, + "first_name": "joel", + "last_name": "quenneville", + "metadata": null, + "minimum_token_issued_at": 0, + "created_at": "2016-11-28T14:11:10.126221Z", + "updated_at": "2016-11-28T14:11:10.126221Z", + "deleted_at": null + } + }, + "current_application": { + "internal_name": "", + "id": "app_id", + "external_name": "", + "environment": {}, + "outbound_message_templates": { + "apology/untrained": { + "variants": [ + { + "template_slots": [ + { + "plurality": "single", + "base_type": "url", + "entity": "website", + "role": "", + "count": 1 + } + ] + }, + { + "template_slots": [ + { + "plurality": "single", + "base_type": "url", + "entity": "website", + "role": "", + "count": 1 + } + ] + } + ] + }, + "request/player_stats": { + "variants": [ + {"template_slots": null}, + {"template_slots": null} + ] + }, + "request_player/team": { + "variants": [ + { + "template_slots": [ + { + "plurality": "single", + "base_type": "string", + "entity": "player_team", + "role": "", + "count": 1 + } + ] + }, + { + "template_slots": [ + { + "plurality": "single", + "base_type": "string", + "entity": "player_team", + "role": "", + "count": 1 + } + ] + } + ] + } + } + } + } + } +}