From 4feca1d88dedd02d22d49f8aa207c6d056c34199 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 7 Apr 2020 10:19:19 +0530 Subject: [PATCH 01/57] Feature: Business logo API for web widget (#674) Co-authored-by: Pranav Raj Sreepuram --- .rubocop.yml | 10 +++++ app/actions/contact_merge_action.rb | 2 +- app/controllers/api/base_controller.rb | 2 +- .../api/v1/accounts/inboxes_controller.rb | 2 +- .../concerns/access_token_auth_helper.rb | 3 +- app/models/account.rb | 2 +- app/models/inbox.rb | 1 + .../v1/accounts/inboxes_controller_spec.rb | 13 +++++++ swagger/paths/inboxes/update.yml | 10 +++-- swagger/paths/index.yml | 26 ++++++------- swagger/swagger.json | 37 +++++++++++-------- 11 files changed, 71 insertions(+), 37 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index ff4a0160a..48d234ba0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,6 +4,10 @@ require: - rubocop-rspec inherit_from: .rubocop_todo.yml +Lint/RaiseException: + Enabled: true +Lint/StructNewOverride: + Enabled: true Layout/LineLength: Max: 150 Metrics/ClassLength: @@ -16,6 +20,12 @@ Style/FrozenStringLiteralComment: Enabled: false Style/SymbolArray: Enabled: false +Style/HashEachMethods: + Enabled: true +Style/HashTransformKeys: + Enabled: true +Style/HashTransformValues: + Enabled: true Style/GlobalVars: Exclude: - 'config/initializers/redis.rb' diff --git a/app/actions/contact_merge_action.rb b/app/actions/contact_merge_action.rb index 8261a51ad..343e78032 100644 --- a/app/actions/contact_merge_action.rb +++ b/app/actions/contact_merge_action.rb @@ -17,7 +17,7 @@ class ContactMergeAction def validate_contacts return if belongs_to_account?(@base_contact) && belongs_to_account?(@mergee_contact) - raise Exception, 'contact does not belong to the account' + raise StandardError, 'contact does not belong to the account' end def belongs_to_account?(contact) diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index d512eeabf..8db37ced7 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -8,7 +8,7 @@ class Api::BaseController < ApplicationController private def authenticate_by_access_token? - request.headers[:api_access_token].present? + request.headers[:api_access_token].present? || request.headers[:HTTP_API_ACCESS_TOKEN].present? end def set_conversation diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 630f93e0d..3345a5883 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -26,6 +26,6 @@ class Api::V1::Accounts::InboxesController < Api::BaseController end def inbox_update_params - params.require(:inbox).permit(:enable_auto_assignment) + params.require(:inbox).permit(:enable_auto_assignment, :avatar) end end diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index 3e3875333..c5173e422 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -5,7 +5,8 @@ module AccessTokenAuthHelper }.freeze def authenticate_access_token! - access_token = AccessToken.find_by(token: request.headers[:api_access_token]) + token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN] + access_token = AccessToken.find_by(token: token) render_unauthorized('Invalid Access Token') && return unless access_token token_owner = access_token.owner diff --git a/app/models/account.rb b/app/models/account.rb index 883ed28aa..35cada140 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -3,7 +3,7 @@ # Table name: accounts # # id :integer not null, primary key -# locale :integer default("eng") +# locale :integer default("en") # name :string not null # created_at :datetime not null # updated_at :datetime not null diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 867559ddc..118733399 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -20,6 +20,7 @@ class Inbox < ApplicationRecord include Reportable + include Avatarable validates :account_id, presence: true diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index f03fc2a29..6aa6286a4 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -113,6 +113,19 @@ RSpec.describe 'Inboxes API', type: :request do expect(inbox.reload.enable_auto_assignment).to be_falsey end + it 'updates avatar' do + # no avatar before upload + expect(inbox.avatar.attached?).to eq(false) + file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png') + patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}", + params: { inbox: { avatar: file } }, + headers: admin.create_new_auth_token + + expect(response).to have_http_status(:success) + inbox.reload + expect(inbox.avatar.attached?).to eq(true) + end + it 'will not update inbox for agent' do agent = create(:user, account: account, role: :agent) diff --git a/swagger/paths/inboxes/update.yml b/swagger/paths/inboxes/update.yml index bfa79c27c..35c843469 100644 --- a/swagger/paths/inboxes/update.yml +++ b/swagger/paths/inboxes/update.yml @@ -1,9 +1,9 @@ patch: tags: - Inbox - operationId: disableAutoAssignment - summary: Disable auto assignment - description: Disable Auto Assignment for an inbox + operationId: updateInbox + summary: Update Inbox + description: Add avatar and disable auto assignment for an inbox parameters: - name: id in: path @@ -20,6 +20,10 @@ patch: type: boolean required: true description: 'Enable Auto Assignment' + avatar: + type: file + required: false + description: 'Image file for avatar' responses: 200: description: Success diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml index dbbd8ce89..f82ed9673 100644 --- a/swagger/paths/index.yml +++ b/swagger/paths/index.yml @@ -1,37 +1,37 @@ # Widget -/account/{account_id}/widget/inboxes: +/accounts/{account_id}/widget/inboxes: $ref: ./widget/inboxes/create.yml -/account/{account_id}/widget/inboxes/${id}: +/accounts/{account_id}/widget/inboxes/${id}: $ref: ./widget/inboxes/update.yml # Inboxes -/account/{account_id}/inboxes: +/accounts/{account_id}/inboxes: $ref: ./inboxes/index.yml -/account/{account_id}/inboxes/{id}: +/accounts/{account_id}/inboxes/{id}: $ref: ./inboxes/update.yml # Conversations -/account/{account_id}/conversations: +/accounts/{account_id}/conversations: $ref: ./conversation/list.yml -/account/{account_id}/conversations/{id}: +/accounts/{account_id}/conversations/{id}: $ref: ./conversation/crud.yml -/account/{account_id}/conversations/{id}/toggle_status: +/accounts/{account_id}/conversations/{id}/toggle_status: $ref: ./conversation/toggle_status.yml # Messages -/account/{account_id}/conversations/{id}/messages: +/accounts/{account_id}/conversations/{id}/messages: $ref: ./conversation/messages/index_create.yml -/account/{account_id}/conversations/{id}/labels: +/accounts/{account_id}/conversations/{id}/labels: $ref: ./conversation/labels.yml -/account/{account_id}/conversations/{id}/assignments: +/accounts/{account_id}/conversations/{id}/assignments: $ref: ./conversation/assignments.yml # Contacts -/account/{account_id}/contacts: +/accounts/{account_id}/contacts: $ref: ./contact/list_create.yml -/account/{account_id}/contacts/{id}: +/accounts/{account_id}/contacts/{id}: $ref: ./contact/crud.yml -/account/{account_id}/contacts/{id}/conversations: +/accounts/{account_id}/contacts/{id}/conversations: $ref: ./contact/conversations.yml diff --git a/swagger/swagger.json b/swagger/swagger.json index 049a1b21e..609c5bb93 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -25,7 +25,7 @@ "application/json; charset=utf-8" ], "paths": { - "/account/{account_id}/widget/inboxes": { + "/accounts/{account_id}/widget/inboxes": { "post": { "tags": [ "Widget" @@ -78,7 +78,7 @@ } } }, - "/account/{account_id}/widget/inboxes/${id}": { + "/accounts/{account_id}/widget/inboxes/${id}": { "patch": { "tags": [ "Widget" @@ -123,7 +123,7 @@ } } }, - "/account/{account_id}/inboxes": { + "/accounts/{account_id}/inboxes": { "get": { "tags": [ "Inbox" @@ -147,14 +147,14 @@ } } }, - "/account/{account_id}/inboxes/{id}": { + "/accounts/{account_id}/inboxes/{id}": { "patch": { "tags": [ "Inbox" ], - "operationId": "disableAutoAssignment", - "summary": "Disable auto assignment", - "description": "Disable Auto Assignment for an inbox", + "operationId": "updateInbox", + "summary": "Update Inbox", + "description": "Add avatar and disable auto assignment for an inbox", "parameters": [ { "name": "id", @@ -174,6 +174,11 @@ "type": "boolean", "required": true, "description": "Enable Auto Assignment" + }, + "avatar": { + "type": "file", + "required": false, + "description": "Image file for avatar" } } } @@ -195,7 +200,7 @@ } } }, - "/account/{account_id}/conversations": { + "/accounts/{account_id}/conversations": { "get": { "tags": [ "Conversation" @@ -262,7 +267,7 @@ } } }, - "/account/{account_id}/conversations/{id}": { + "/accounts/{account_id}/conversations/{id}": { "get": { "tags": [ "Conversation" @@ -295,7 +300,7 @@ } } }, - "/account/{account_id}/conversations/{id}/toggle_status": { + "/accounts/{account_id}/conversations/{id}/toggle_status": { "post": { "tags": [ "Conversation" @@ -347,7 +352,7 @@ } } }, - "/account/{account_id}/conversations/{id}/messages": { + "/accounts/{account_id}/conversations/{id}/messages": { "get": { "tags": [ "Messages" @@ -434,7 +439,7 @@ } } }, - "/account/{account_id}/conversations/{id}/labels": { + "/accounts/{account_id}/conversations/{id}/labels": { "get": { "tags": [ "ConversationLabels" @@ -515,7 +520,7 @@ } } }, - "/account/{account_id}/conversations/{id}/assignments": { + "/accounts/{account_id}/conversations/{id}/assignments": { "post": { "tags": [ "ConversationAssignment" @@ -561,7 +566,7 @@ } } }, - "/account/{account_id}/contacts": { + "/accounts/{account_id}/contacts": { "get": { "tags": [ "Contact" @@ -623,7 +628,7 @@ } } }, - "/account/{account_id}/contacts/{id}": { + "/accounts/{account_id}/contacts/{id}": { "get": { "tags": [ "Contact" @@ -693,7 +698,7 @@ } } }, - "/account/{account_id}/contacts/{id}/conversations": { + "/accounts/{account_id}/contacts/{id}/conversations": { "get": { "tags": [ "Contact" From 1cfa756d49bdabe8d81c59cdb96b6351d357e97a Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 7 Apr 2020 10:41:18 +0530 Subject: [PATCH 02/57] Chore: APIs for agent bots (#676) Co-authored-by: Pranav Raj Sreepuram --- .../api/v1/accounts/inboxes_controller.rb | 18 +++- .../api/v1/agent_bots_controller.rb | 8 ++ app/models/inbox.rb | 1 + app/policies/inbox_policy.rb | 4 + config/routes.rb | 6 +- .../v1/accounts/inboxes_controller_spec.rb | 58 +++++++++++ .../api/v1/agent_bots_controller_spec.rb | 17 ++++ spec/policies/inbox_policy_spec.rb | 2 +- swagger/definitions/index.yml | 2 + swagger/definitions/resource/agent_bot.yml | 14 +++ swagger/paths/agent_bots/index.yml | 17 ++++ swagger/paths/inboxes/set_agent_bot.yml | 29 ++++++ swagger/paths/index.yml | 6 ++ swagger/swagger.json | 98 +++++++++++++++++++ 14 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 app/controllers/api/v1/agent_bots_controller.rb create mode 100644 spec/controllers/api/v1/agent_bots_controller_spec.rb create mode 100644 swagger/definitions/resource/agent_bot.yml create mode 100644 swagger/paths/agent_bots/index.yml create mode 100644 swagger/paths/inboxes/set_agent_bot.yml diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 3345a5883..6a3f9d5cc 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -1,6 +1,7 @@ class Api::V1::Accounts::InboxesController < Api::BaseController before_action :check_authorization - before_action :fetch_inbox, only: [:destroy, :update] + before_action :fetch_inbox, except: [:index] + before_action :fetch_agent_bot, only: [:set_agent_bot] def index @inboxes = policy_scope(current_account.inboxes) @@ -10,6 +11,17 @@ class Api::V1::Accounts::InboxesController < Api::BaseController @inbox.update(inbox_update_params) end + def set_agent_bot + if @agent_bot + agent_bot_inbox = @inbox.agent_bot_inbox || AgentBotInbox.new(inbox: @inbox) + agent_bot_inbox.agent_bot = @agent_bot + agent_bot_inbox.save! + elsif @inbox.agent_bot_inbox.present? + @inbox.agent_bot_inbox.destroy! + end + head :ok + end + def destroy @inbox.destroy head :ok @@ -21,6 +33,10 @@ class Api::V1::Accounts::InboxesController < Api::BaseController @inbox = current_account.inboxes.find(params[:id]) end + def fetch_agent_bot + @agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot] + end + def check_authorization authorize(Inbox) end diff --git a/app/controllers/api/v1/agent_bots_controller.rb b/app/controllers/api/v1/agent_bots_controller.rb new file mode 100644 index 000000000..4c17fd4f4 --- /dev/null +++ b/app/controllers/api/v1/agent_bots_controller.rb @@ -0,0 +1,8 @@ +class Api::V1::AgentBotsController < Api::BaseController + skip_before_action :authenticate_user! + skip_before_action :check_subscription + + def index + render json: AgentBot.all + end +end diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 118733399..0ce3b251f 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -38,6 +38,7 @@ class Inbox < ApplicationRecord has_many :messages, through: :conversations has_one :agent_bot_inbox, dependent: :destroy + has_one :agent_bot, through: :agent_bot_inbox has_many :webhooks, dependent: :destroy after_create :subscribe_webhook, if: :facebook? diff --git a/app/policies/inbox_policy.rb b/app/policies/inbox_policy.rb index f491eea70..282ab7e49 100644 --- a/app/policies/inbox_policy.rb +++ b/app/policies/inbox_policy.rb @@ -31,4 +31,8 @@ class InboxPolicy < ApplicationPolicy def destroy? @user.administrator? end + + def set_agent_bot? + @user.administrator? + end end diff --git a/config/routes.rb b/config/routes.rb index 42e83ea41..be63d95e9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -65,7 +65,9 @@ Rails.application.routes.draw do end end - resources :inboxes, only: [:index, :destroy, :update] + resources :inboxes, only: [:index, :destroy, :update] do + post :set_agent_bot, on: :member + end resources :inbox_members, only: [:create, :show], param: :inbox_id resources :labels, only: [:index] do collection do @@ -104,6 +106,8 @@ Rails.application.routes.draw do resource :profile, only: [:show, :update] + resources :agent_bots, only: [:index] + namespace :widget do resource :contact, only: [:update] resources :inbox_members, only: [:index] diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index 6aa6286a4..03f1daf16 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -138,4 +138,62 @@ RSpec.describe 'Inboxes API', type: :request do end end end + + describe 'POST /api/v1/accounts/{account.id}/inboxes/:id/set_agent_bot' do + let(:inbox) { create(:inbox, account: account) } + let(:agent_bot) { create(:agent_bot) } + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/set_agent_bot" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:admin) { create(:user, account: account, role: :administrator) } + let(:valid_params) { { agent_bot: agent_bot.id } } + + it 'sets the agent bot' do + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/set_agent_bot", + headers: admin.create_new_auth_token, + params: valid_params, + as: :json + + expect(response).to have_http_status(:success) + expect(inbox.reload.agent_bot.id).to eq agent_bot.id + end + + it 'throw error when invalid agent bot id' do + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/set_agent_bot", + headers: admin.create_new_auth_token, + params: { agent_bot: 0 }, + as: :json + + expect(response).to have_http_status(:not_found) + end + + it 'disconnects the agent bot' do + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/set_agent_bot", + headers: admin.create_new_auth_token, + params: { agent_bot: nil }, + as: :json + + expect(response).to have_http_status(:success) + expect(inbox.reload.agent_bot).to be_falsey + end + + it 'will not update agent bot when its an agent' do + agent = create(:user, account: account, role: :agent) + + post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/set_agent_bot", + headers: agent.create_new_auth_token, + params: valid_params, + as: :json + + expect(response).to have_http_status(:unauthorized) + end + end + end end diff --git a/spec/controllers/api/v1/agent_bots_controller_spec.rb b/spec/controllers/api/v1/agent_bots_controller_spec.rb new file mode 100644 index 000000000..258fad5c3 --- /dev/null +++ b/spec/controllers/api/v1/agent_bots_controller_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +RSpec.describe 'Profile API', type: :request do + let!(:agent_bot1) { create(:agent_bot) } + let!(:agent_bot2) { create(:agent_bot) } + + describe 'GET /api/v1/agent_bots' do + it 'returns all the agent bots in the system' do + get '/api/v1/agent_bots', + as: :json + + expect(response).to have_http_status(:success) + expect(response.body).to include(agent_bot1.name) + expect(response.body).to include(agent_bot2.name) + end + end +end diff --git a/spec/policies/inbox_policy_spec.rb b/spec/policies/inbox_policy_spec.rb index 5e4de0538..298c6a154 100644 --- a/spec/policies/inbox_policy_spec.rb +++ b/spec/policies/inbox_policy_spec.rb @@ -11,7 +11,7 @@ RSpec.describe InboxPolicy, type: :policy do let(:agent) { create(:user, account: account) } let(:inbox) { create(:inbox) } - permissions :create?, :destroy? do + permissions :create?, :destroy?, :update?, :set_agent_bot? do context 'when administrator' do it { expect(inbox_policy).to permit(administrator, inbox) } end diff --git a/swagger/definitions/index.yml b/swagger/definitions/index.yml index f88892efd..b822c9415 100644 --- a/swagger/definitions/index.yml +++ b/swagger/definitions/index.yml @@ -17,6 +17,8 @@ user: $ref: ./resource/user.yml inbox: $ref: ./resource/inbox.yml +agent_bot: + $ref: ./resource/agent_bot.yml # RESPONSE ## contact diff --git a/swagger/definitions/resource/agent_bot.yml b/swagger/definitions/resource/agent_bot.yml new file mode 100644 index 000000000..d0f0efb3e --- /dev/null +++ b/swagger/definitions/resource/agent_bot.yml @@ -0,0 +1,14 @@ +type: object +properties: + id: + type: number + description: ID of the agent bot + description: + type: string + description: The description about the agent bot + name: + type: string + description: The name of the agent bot + outgoing_url: + type: string + description: The webhook URL for the bot diff --git a/swagger/paths/agent_bots/index.yml b/swagger/paths/agent_bots/index.yml new file mode 100644 index 000000000..2fef38ff5 --- /dev/null +++ b/swagger/paths/agent_bots/index.yml @@ -0,0 +1,17 @@ +get: + tags: + - AgentBot + operationId: listAgentBots + summary: List all agentbots + description: List all available agentbots for the current installation + responses: + 200: + description: Success + schema: + type: Array + description: 'List of agent bots' + $ref: '#/definitions/agent_bot' + 404: + description: Inbox not found, Agent bot not found + 403: + description: Access denied diff --git a/swagger/paths/inboxes/set_agent_bot.yml b/swagger/paths/inboxes/set_agent_bot.yml new file mode 100644 index 000000000..c5f0ef792 --- /dev/null +++ b/swagger/paths/inboxes/set_agent_bot.yml @@ -0,0 +1,29 @@ +post: + tags: + - Inbox + operationId: updateAgentBot + summary: Add or remove agent bot + description: To add an agent bot pass agent_bot id, to remove agent bot from an inbox pass null + parameters: + - name: id + in: path + type: number + description: ID of the inbox + required: true + - name: data + in: body + required: true + schema: + type: object + properties: + agent_bot: + type: number + required: true + description: 'Agent bot ID' + responses: + 204: + description: Success + 404: + description: Inbox not found, Agent bot not found + 403: + description: Access denied diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml index f82ed9673..67069bdc9 100644 --- a/swagger/paths/index.yml +++ b/swagger/paths/index.yml @@ -9,6 +9,12 @@ $ref: ./inboxes/index.yml /accounts/{account_id}/inboxes/{id}: $ref: ./inboxes/update.yml +/accounts/{account_id}/inboxes/{id}/set_agent_bot: + $ref: ./inboxes/update.yml + +/agent_bots: + $ref: ./agent_bots/index.yml + # Conversations /accounts/{account_id}/conversations: diff --git a/swagger/swagger.json b/swagger/swagger.json index 609c5bb93..67e3302c2 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -200,6 +200,83 @@ } } }, + "/accounts/{account_id}/inboxes/{id}/set_agent_bot": { + "patch": { + "tags": [ + "Inbox" + ], + "operationId": "updateInbox", + "summary": "Update Inbox", + "description": "Add avatar and disable auto assignment for an inbox", + "parameters": [ + { + "name": "id", + "in": "path", + "type": "number", + "description": "ID of the inbox", + "required": true + }, + { + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "enable_auto_assignment": { + "type": "boolean", + "required": true, + "description": "Enable Auto Assignment" + }, + "avatar": { + "type": "file", + "required": false, + "description": "Image file for avatar" + } + } + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/inbox" + } + }, + "404": { + "description": "Inbox not found" + }, + "403": { + "description": "Access denied" + } + } + } + }, + "/agent_bots": { + "get": { + "tags": [ + "AgentBot" + ], + "operationId": "listAgentBots", + "summary": "List all agentbots", + "description": "List all available agentbots for the current installation", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/agent_bot" + } + }, + "404": { + "description": "Inbox not found, Agent bot not found" + }, + "403": { + "description": "Access denied" + } + } + } + }, "/accounts/{account_id}/conversations": { "get": { "tags": [ @@ -942,6 +1019,27 @@ } } }, + "agent_bot": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "ID of the agent bot" + }, + "description": { + "type": "string", + "description": "The description about the agent bot" + }, + "name": { + "type": "string", + "description": "The name of the agent bot" + }, + "outgoing_url": { + "type": "string", + "description": "The webhook URL for the bot" + } + } + }, "extended_contact": { "allOf": [ { From 48f603798b2545427dc511f015dfbb6843776909 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Wed, 8 Apr 2020 22:46:58 +0530 Subject: [PATCH 03/57] Chore: Fix URLs in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index db4559b45..e71b8c6d1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Woot-logo + Woot-logo

A simple and elegant live chat software
An opensource alternative to Intercom, Zendesk, Drift, Crisp etc.
@@ -23,7 +23,7 @@ ___ Chat on Discord

-![ChatUI progess](https://storage.googleapis.com/chatwoot-assets/dashboard-screen.png) +![ChatUI progess](https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/chatwoot-dashboard-assets.png) ## Background From b0950d6880664af2ba56994847f2b19b0ad0ec92 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 10 Apr 2020 16:42:37 +0530 Subject: [PATCH 04/57] Feature: Rich Message Types (#610) Co-authored-by: Pranav Raj S Co-authored-by: Nithin David Thomas --- .../messages/outgoing/normal_builder.rb | 8 +- .../v1/accounts/conversations_controller.rb | 18 +++ .../api/v1/widget/base_controller.rb | 4 +- .../api/v1/widget/events_controller.rb | 16 ++ .../api/v1/widget/messages_controller.rb | 12 +- .../concerns/access_token_auth_helper.rb | 2 +- app/javascript/dashboard/api/inbox/message.js | 2 +- app/javascript/dashboard/i18n/de.js | 4 + app/javascript/dashboard/i18n/en.js | 3 + app/javascript/packs/sdk.js | 5 +- app/javascript/sdk/IFrameHelper.js | 3 + .../shared/components/CardButton.vue | 13 +- app/javascript/shared/components/ChatCard.vue | 1 - app/javascript/shared/components/ChatForm.vue | 114 +++++++++++++ .../shared/components/ChatOption.vue | 4 +- .../shared/components/ChatOptions.vue | 15 +- .../shared/mixins/messageFormatterMixin.js | 7 + app/javascript/widget/App.vue | 2 + app/javascript/widget/api/endPoints.js | 4 +- app/javascript/widget/api/events.js | 7 + app/javascript/widget/api/message.js | 5 +- .../widget/assets/scss/_buttons.scss | 4 + .../widget/components/AgentMessage.vue | 151 +++++++++++------- .../widget/components/AgentMessageBubble.vue | 97 +++++++++-- .../widget/components/UserMessageBubble.vue | 4 +- .../widget/components/template/Article.vue | 62 +++++++ .../widget/components/template/EmailInput.vue | 5 +- app/javascript/widget/helpers/actionCable.js | 3 + app/javascript/widget/i18n/en.js | 3 + app/javascript/widget/store/index.js | 4 +- .../widget/store/modules/conversation.js | 5 +- app/javascript/widget/store/modules/events.js | 19 +++ .../widget/store/modules/message.js | 19 ++- app/listeners/agent_bot_listener.rb | 24 +++ app/listeners/webhook_listener.rb | 24 +++ app/models/channel/web_widget.rb | 3 +- .../concerns/content_attribute_validator.rb | 52 ++++++ app/models/contact_inbox.rb | 15 ++ app/models/message.rb | 22 ++- .../conversations/create.json.jbuilder | 1 + .../messages/create.json.jbuilder | 4 +- .../partials/_conversation.json.jbuilder | 1 + .../v1/widget/messages/update.json.jbuilder | 2 +- config/routes.rb | 5 +- db/schema.rb | 2 + docs/webhooks/add-webhooks-to-chatwoot.md | 71 +++++++- lib/events/types.rb | 2 + .../conversations/messages_controller_spec.rb | 42 ++++- .../api/v1/widget/events_controller_spec.rb | 37 +++++ .../api/v1/widget/messages_controller_spec.rb | 10 +- .../factories/bot_message/bot_message_card.rb | 22 +++ .../bot_message/bot_message_select.rb | 10 ++ .../request/conversation/create_message.yml | 8 +- swagger/definitions/resource/conversation.yml | 3 + swagger/definitions/resource/message.yml | 7 + .../{list.yml => index_or_create.yml} | 36 +++++ swagger/paths/index.yml | 5 +- swagger/swagger.json | 110 ++++++++++--- 58 files changed, 997 insertions(+), 146 deletions(-) create mode 100644 app/controllers/api/v1/widget/events_controller.rb create mode 100644 app/javascript/shared/components/ChatForm.vue create mode 100644 app/javascript/widget/api/events.js create mode 100644 app/javascript/widget/components/template/Article.vue create mode 100644 app/javascript/widget/store/modules/events.js create mode 100644 app/models/concerns/content_attribute_validator.rb create mode 100644 app/views/api/v1/accounts/conversations/create.json.jbuilder create mode 100644 spec/controllers/api/v1/widget/events_controller_spec.rb create mode 100644 spec/factories/bot_message/bot_message_card.rb create mode 100644 spec/factories/bot_message/bot_message_select.rb rename swagger/paths/conversation/{list.yml => index_or_create.yml} (51%) diff --git a/app/builders/messages/outgoing/normal_builder.rb b/app/builders/messages/outgoing/normal_builder.rb index 668022e64..c420b2a41 100644 --- a/app/builders/messages/outgoing/normal_builder.rb +++ b/app/builders/messages/outgoing/normal_builder.rb @@ -3,11 +3,13 @@ class Messages::Outgoing::NormalBuilder attr_reader :message def initialize(user, conversation, params) - @content = params[:message] + @content = params[:content] @private = params[:private] || false @conversation = conversation @user = user @fb_id = params[:fb_id] + @content_type = params[:content_type] + @items = params.to_unsafe_h&.dig(:content_attributes, :items) @attachment = params[:attachment] end @@ -34,7 +36,9 @@ class Messages::Outgoing::NormalBuilder content: @content, private: @private, user_id: @user&.id, - source_id: @fb_id + source_id: @fb_id, + content_type: @content_type, + items: @items } end end diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 895091cca..3050c8ab7 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -1,5 +1,6 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController before_action :conversation, except: [:index] + before_action :contact_inbox, only: [:create] def index result = conversation_finder.perform @@ -7,6 +8,10 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController @conversations_count = result[:count] end + def create + @conversation = ::Conversation.create!(conversation_params) + end + def show; end def toggle_status @@ -29,6 +34,19 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController @conversation ||= current_account.conversations.find_by(display_id: params[:id]) end + def contact_inbox + @contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id]) + end + + def conversation_params + { + account_id: current_account.id, + inbox_id: @contact_inbox.inbox_id, + contact_id: @contact_inbox.contact_id, + contact_inbox_id: @contact_inbox.id + } + end + def conversation_finder @conversation_finder ||= ConversationFinder.new(current_user, params) end diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb index eb19e2bdd..9ef4a04f2 100644 --- a/app/controllers/api/v1/widget/base_controller.rb +++ b/app/controllers/api/v1/widget/base_controller.rb @@ -2,9 +2,9 @@ class Api::V1::Widget::BaseController < ApplicationController private def conversation - @conversation ||= @contact_inbox.conversations.find_by( + @conversation ||= @contact_inbox.conversations.where( inbox_id: auth_token_params[:inbox_id] - ) + ).last end def auth_token_params diff --git a/app/controllers/api/v1/widget/events_controller.rb b/app/controllers/api/v1/widget/events_controller.rb new file mode 100644 index 000000000..faa44994c --- /dev/null +++ b/app/controllers/api/v1/widget/events_controller.rb @@ -0,0 +1,16 @@ +class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController + include Events::Types + before_action :set_web_widget + before_action :set_contact + + def create + Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox) + head :no_content + end + + private + + def permitted_params + params.permit(:name, :website_token) + end +end diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb index 7d16f7641..7e0c446ea 100644 --- a/app/controllers/api/v1/widget/messages_controller.rb +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -15,8 +15,12 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController end def update - @message.update!(input_submitted_email: contact_email) - update_contact(contact_email) + if @message.content_type == 'input_email' + @message.update!(submitted_email: contact_email) + update_contact(contact_email) + else + @message.update!(message_update_params[:message]) + end rescue StandardError => e render json: { error: @contact.errors, message: e.message }.to_json, status: 500 end @@ -116,6 +120,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController contact_email.split('@')[0] end + def message_update_params + params.permit(message: [submitted_values: [:name, :title, :value]]) + end + def permitted_params params.permit(:id, :before, :website_token, contact: [:email], message: [:content, :referer_url, :timestamp]) end diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index c5173e422..e7af9e116 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -1,6 +1,6 @@ module AccessTokenAuthHelper BOT_ACCESSIBLE_ENDPOINTS = { - 'api/v1/accounts/conversations' => ['toggle_status'], + 'api/v1/accounts/conversations' => %w[toggle_status create], 'api/v1/accounts/conversations/messages' => ['create'] }.freeze diff --git a/app/javascript/dashboard/api/inbox/message.js b/app/javascript/dashboard/api/inbox/message.js index 579d97a10..4ceac84d0 100644 --- a/app/javascript/dashboard/api/inbox/message.js +++ b/app/javascript/dashboard/api/inbox/message.js @@ -9,7 +9,7 @@ class MessageApi extends ApiClient { create({ conversationId, message, private: isPrivate }) { return axios.post(`${this.url}/${conversationId}/messages`, { - message, + content: message, private: isPrivate, }); } diff --git a/app/javascript/dashboard/i18n/de.js b/app/javascript/dashboard/i18n/de.js index 55cb77a8c..6988a5d95 100644 --- a/app/javascript/dashboard/i18n/de.js +++ b/app/javascript/dashboard/i18n/de.js @@ -15,6 +15,10 @@ export default { DOWNLOAD: 'Herunterladen', UPLOADING: 'Hochladen...', }, + + FORM_BUBBLE: { + SUBMIT: 'Einreichen', + }, }, CONFIRM_EMAIL: 'Überprüfen...', SETTINGS: { diff --git a/app/javascript/dashboard/i18n/en.js b/app/javascript/dashboard/i18n/en.js index ae0e144d6..61029e9af 100644 --- a/app/javascript/dashboard/i18n/en.js +++ b/app/javascript/dashboard/i18n/en.js @@ -15,6 +15,9 @@ export default { DOWNLOAD: 'Download', UPLOADING: 'Uploading...', }, + FORM_BUBBLE: { + SUBMIT: 'Submit', + }, }, CONFIRM_EMAIL: 'Verifying...', SETTINGS: { diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js index 459c2bc4b..5a9826a7c 100755 --- a/app/javascript/packs/sdk.js +++ b/app/javascript/packs/sdk.js @@ -1,6 +1,5 @@ import Cookies from 'js-cookie'; import { IFrameHelper } from '../sdk/IFrameHelper'; -import { onBubbleClick } from '../sdk/bubbleHelpers'; const runSDK = ({ baseUrl, websiteToken }) => { const chatwootSettings = window.chatwootSettings || {}; @@ -13,7 +12,7 @@ const runSDK = ({ baseUrl, websiteToken }) => { websiteToken, toggle() { - onBubbleClick(); + IFrameHelper.events.toggleBubble(); }, setUser(identifier, user) { @@ -39,7 +38,7 @@ const runSDK = ({ baseUrl, websiteToken }) => { reset() { if (window.$chatwoot.isOpen) { - onBubbleClick(); + IFrameHelper.events.toggleBubble(); } Cookies.remove('cw_conversation'); diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index ec55c731d..ed85d7121 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -88,6 +88,9 @@ export const IFrameHelper = { toggleBubble: () => { onBubbleClick(); + if (window.$chatwoot.isOpen) { + IFrameHelper.pushEvent('webwidget.triggered'); + } }, }, onLoad: ({ widget_color: widgetColor }) => { diff --git a/app/javascript/shared/components/CardButton.vue b/app/javascript/shared/components/CardButton.vue index eea0b1a5a..d6d116ade 100644 --- a/app/javascript/shared/components/CardButton.vue +++ b/app/javascript/shared/components/CardButton.vue @@ -4,6 +4,8 @@ :key="action.uri" class="action-button button" :href="action.uri" + target="_blank" + rel="noopener nofollow noreferrer" > {{ action.text }} @@ -44,11 +46,14 @@ export default { @import '~dashboard/assets/scss/mixins.scss'; .action-button { - width: 100%; - padding: 0; - max-height: 34px; - margin-top: $space-smaller; + align-items: center; border-radius: $space-micro; + display: flex; font-weight: $font-weight-medium; + justify-content: center; + margin-top: $space-smaller; + max-height: 34px; + padding: 0; + width: 100%; } diff --git a/app/javascript/shared/components/ChatCard.vue b/app/javascript/shared/components/ChatCard.vue index 8accb6d8c..4e856bb6b 100644 --- a/app/javascript/shared/components/ChatCard.vue +++ b/app/javascript/shared/components/ChatCard.vue @@ -52,7 +52,6 @@ export default { @import '~dashboard/assets/scss/mixins.scss'; .card-message { - @include border-normal; background: white; max-width: 220px; padding: $space-small; diff --git a/app/javascript/shared/components/ChatForm.vue b/app/javascript/shared/components/ChatForm.vue new file mode 100644 index 000000000..f694db407 --- /dev/null +++ b/app/javascript/shared/components/ChatForm.vue @@ -0,0 +1,114 @@ +