Skip to content
This repository was archived by the owner on Jan 28, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions app/controllers/api/v1/webhooks/init_controller.rb
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions app/models/integrations/init.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Integrations::Init < Integration
# @return [Boolean]
def enable
end

# @return [Boolean]
def disable
end
end
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
158 changes: 158 additions & 0 deletions spec/fixtures/init-ai/webhook-inbound.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
]
}
}
}
}
}
}