Initial Commit

Co-authored-by: Subin <subinthattaparambil@gmail.com>
Co-authored-by: Manoj <manojmj92@gmail.com>
Co-authored-by: Nithin <webofnithin@gmail.com>
This commit is contained in:
Pranav Raj Sreepuram
2019-08-14 15:18:44 +05:30
commit 2a34255e0b
537 changed files with 27318 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css

0
app/assets/images/.keep Normal file
View File

View File

@@ -0,0 +1,3 @@
$( document ).ready(function() {
window.currentAcccountId = $('body').data('account-id');
});

View File

@@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -0,0 +1,15 @@
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs
//= require_tree .

View File

@@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@@ -0,0 +1,3 @@
// Place all the styles related to the api/base controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -0,0 +1,3 @@
// Place all the styles related to the api/v1/agents controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -0,0 +1,3 @@
// Place all the styles related to the api/v1/canned_responses controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -0,0 +1,3 @@
// Place all the styles related to the api/v1/conversations controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -0,0 +1,3 @@
// Place all the styles related to the api/v1/reports controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -0,0 +1,3 @@
// Place all the styles related to the api/v1/subscriptions controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -0,0 +1,3 @@
// Place all the styles related to the api/v1/webhooks controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -0,0 +1,3 @@
// Place all the styles related to the api/v1/widget/messages controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@@ -0,0 +1,15 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/

View File

@@ -0,0 +1,3 @@
// Place all the styles related to the Home controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

20
app/bot/bot.rb Normal file
View File

@@ -0,0 +1,20 @@
# app/bot/facebook_bot.rb
require 'facebook/messenger'
include Facebook::Messenger
Bot.on :message do |message|
response = ::Integrations::Facebook::MessageParser.new(message)
::Integrations::Facebook::MessageCreator.new(response).perform
end
Bot.on :delivery do |delivery|
# delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38'
# delivery.sender # => { 'id' => '1008372609250235' }
# delivery.recipient # => { 'id' => '2015573629214912' }
# delivery.at # => 2016-04-22 21:30:36 +0200
# delivery.seq # => 37
updater = Integrations::Facebook::DeliveryStatus.new(delivery)
updater.perform
puts "Human was online at #{delivery.at}"
end

View File

View File

@@ -0,0 +1,71 @@
class AccountBuilder
include CustomExceptions::Account
def initialize(params)
@account_name = params[:account_name]
@email = params[:email]
end
def perform
begin
validate_email
validate_user
ActiveRecord::Base.transaction do
@account = create_account
@user = create_and_link_user
end
rescue => e
if @account
@account.destroy
end
puts e.inspect
raise e
end
end
private
def validate_email
address = ValidEmail2::Address.new(@email)
if address.valid? #&& !address.disposable?
true
else
raise InvalidEmail.new({valid: address.valid?})#, disposable: address.disposable?})
end
end
def validate_user
if User.exists?(email: @email)
raise UserExists.new({email: @email})
else
true
end
end
def create_account
@account = Account.create!(name: @account_name)
end
def create_and_link_user
password = Time.now.to_i
@user = @account.users.new({email: @email,
password: password,
password_confirmation: password,
role: User.roles["administrator"],
name: email_to_name(@email)
})
if @user.save!
@user
else
raise UserErrors.new({errors: @user.errors})
end
end
def email_to_name(email)
name = email[/[^@]+/]
name.split(".").map {|n| n.capitalize }.join(" ")
end
end

View File

@@ -0,0 +1,3 @@
class Messages::IncomingMessageBuilder < Messages::MessageBuilder
end

View File

@@ -0,0 +1,135 @@
require 'open-uri'
class Messages::MessageBuilder
=begin
This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
Assumptions
1. Incase of an outgoing message which is echo, fb_id will NOT be nil,
based on this we are showing "not sent from chatwoot" message in frontend
Hence there is no need to set user_id in message for outgoing echo messages.
=end
attr_reader :response
def initialize response, inbox, outgoing_echo=false
@response = response
@inbox = inbox
@sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id)
@message_type = (outgoing_echo ? :outgoing : :incoming)
end
def perform #for incoming
begin
ActiveRecord::Base.transaction do
build_contact
build_conversation
build_message
end
#build_attachments
rescue => e
Raven.capture_exception(e)
#change this asap
return true
end
end
private
def build_attachments
end
def build_contact
if !@inbox.contacts.exists?(source_id: @sender_id)
contact = @inbox.contacts.create!(contact_params)
end
end
def build_message
@message = @conversation.messages.new(message_params)
(response.attachments || []).each do |attachment|
@message.build_attachment(attachment_params(attachment))
end
@message.save!
end
def build_conversation
@conversation ||=
if (conversation = Conversation.find_by(conversation_params))
conversation
else
Conversation.create!(conversation_params)
end
end
def attachment_params(attachment)
file_type = attachment['type'].to_sym
params = {
file_type: file_type,
account_id: @message.account_id
}
if [:image, :file, :audio, :video].include? file_type
params.merge!(
{
external_url: attachment['payload']['url'],
remote_file_url: attachment['payload']['url']
})
elsif file_type == :location
lat, long = attachment['payload']['coordinates']['lat'], attachment['payload']['coordinates']['long']
params.merge!(
{
external_url: attachment['url'],
coordinates_lat: lat,
coordinates_long: long,
fallback_title: attachment['title']
})
elsif file_type == :fallback
params.merge!(
{
fallback_title: attachment['title'],
external_url: attachment['url']
})
end
params
end
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
sender_id: @sender_id
}
end
def message_params
{
account_id: @conversation.account_id,
inbox_id: @conversation.inbox_id,
message_type: @message_type,
content: response.content,
fb_id: response.identifier
}
end
def contact_params
if @inbox.facebook?
k = Koala::Facebook::API.new(@inbox.channel.page_access_token)
begin
result = k.get_object(@sender_id)
rescue => e
result = {}
Raven.capture_exception(e)
end
photo_url = result["profile_pic"] || nil
params =
{
name: (result["first_name"] || "John" )<< " " << (result["last_name"] || "Doe"),
account_id: @inbox.account_id,
source_id: @sender_id,
remote_avatar_url: photo_url
}
end
end
end

View File

@@ -0,0 +1,3 @@
class Messages::Outgoing::EchoBuilder < ::Messages::MessageBuilder
end

View File

@@ -0,0 +1,29 @@
class Messages::Outgoing::NormalBuilder
attr_reader :message
def initialize user, conversation, params
@content = params[:message]
@private = ["1","true",1].include? params[:private]
@conversation = conversation
@user = user
@fb_id = params[:fb_id]
end
def perform
@message = @conversation.messages.create!(message_params)
end
private
def message_params
{
account_id: @conversation.account_id,
inbox_id: @conversation.inbox_id,
message_type: :outgoing,
content: @content,
private: @private,
user_id: @user.id,
fb_id: @fb_id
}
end
end

View File

@@ -0,0 +1,68 @@
class ReportBuilder
include CustomExceptions::Report
# Usage
# rb = ReportBuilder.new a, { metric: 'conversations_count', type: :account, id: 1}
# rb = ReportBuilder.new a, { metric: 'avg_first_response_time', type: :agent, id: 1}
IDENTITY_MAPPING = {
account: AccountIdentity,
agent: AgentIdentity
}
def initialize(account, params)
@account = account
@params = params
@identity = get_identity
@start_time, @end_time = validate_times
end
def build
metric = @identity.send(@params[:metric])
if metric.get.nil?
metric.delete
result = {}
else
result = metric.get_padded_range(@start_time, @end_time) || {}
end
formatted_hash(result)
end
private
def get_identity
identity_class = IDENTITY_MAPPING[@params[:type]]
raise InvalidIdentity if identity_class.nil?
@params[:id] = @account.id if identity_class == AccountIdentity
identity_id = @params[:id]
raise IdentityNotFound if identity_id.nil?
tags = identity_class == AccountIdentity ? nil : { account_id: @account.id}
identity = identity_class.new(identity_id, tags: tags)
raise MetricNotFound if @params[:metric].blank?
raise MetricNotFound unless identity.respond_to?(@params[:metric])
identity
end
def validate_times
start_time = @params[:since] || Time.now.end_of_day - 30.days
end_time = @params[:until] || Time.now.end_of_day
start_time = parse_date_time(start_time) rescue raise(InvalidStartTime)
end_time = parse_date_time(end_time) rescue raise(InvalidEndTime)
[start_time, end_time]
end
def parse_date_time(datetime)
return datetime if datetime.is_a?(DateTime)
return datetime.to_datetime if datetime.is_a?(Time) or datetime.is_a?(Date)
DateTime.strptime(datetime,'%s')
end
def formatted_hash(hash)
hash.inject([]) do |arr,p|
arr << {value: p[1], timestamp: p[0]}
arr
end
end
end

View File

@@ -0,0 +1,14 @@
class Api::BaseController < ApplicationController
respond_to :json
before_action :authenticate_user!
rescue_from StandardError do |exception|
Raven.capture_exception(exception)
render json: { :error => "500 error", message: exception.message }.to_json , :status => 500
end unless Rails.env.development?
private
def set_conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id])
end
end

View File

@@ -0,0 +1,36 @@
class Api::V1::AccountsController < Api::BaseController
skip_before_action :verify_authenticity_token , only: [:create]
skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception,
only: [:create], raise: false
rescue_from CustomExceptions::Account::InvalidEmail,
CustomExceptions::Account::UserExists,
CustomExceptions::Account::UserErrors,
with: :render_error_response
def create
@user = AccountBuilder.new(params).perform
if @user
set_headers(@user)
render json: {
data: @user.token_validation_response
}
else
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
end
end
private
def set_headers(user)
data = user.create_new_auth_token
response.headers[DeviseTokenAuth.headers_names[:"access-token"]] = data["access-token"]
response.headers[DeviseTokenAuth.headers_names[:"token-type"]] = "Bearer"
response.headers[DeviseTokenAuth.headers_names[:"client"]] = data["client"]
response.headers[DeviseTokenAuth.headers_names[:"expiry"]] = data["expiry"]
response.headers[DeviseTokenAuth.headers_names[:"uid"]] = data["uid"]
end
end

View File

@@ -0,0 +1,52 @@
class Api::V1::AgentsController < Api::BaseController
before_action :fetch_agent, except: [:create, :index]
before_action :check_authorization
before_action :build_agent, only: [:create]
def index
render json: agents
end
def destroy
@agent.destroy
head :ok
end
def update
@agent.update_attributes!(agent_params)
render json: @agent
end
def create
@agent.save!
render json: @agent
end
private
def check_authorization
authorize(User)
end
def fetch_agent
@agent = agents.find(params[:id])
end
def build_agent
@agent = agents.new(new_agent_params)
end
def agent_params
params.require(:agent).permit(:email, :name, :role)
end
def new_agent_params
time = Time.now.to_i
params.require(:agent).permit(:email, :name, :role).merge!(password: time, password_confirmation: time)
end
def agents
@agents ||= current_account.users
end
end

View File

@@ -0,0 +1,87 @@
require 'rest-client'
require 'telegram/bot'
class Api::V1::CallbacksController < ApplicationController
skip_before_action :verify_authenticity_token , only: [:register_facebook_page]
skip_before_action :authenticate_user! , only: [:register_facebook_page], raise: false
def register_facebook_page
user_access_token = params[:user_access_token]
page_access_token = params[:page_access_token]
page_name = params[:page_name]
page_id = params[:page_id]
inbox_name = params[:inbox_name]
facebook_channel = current_account.facebook_pages.create!(name: page_name, page_id: page_id, user_access_token: user_access_token, page_access_token: page_access_token, remote_avatar_url: set_avatar(page_id))
inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
render json: inbox
end
def get_facebook_pages
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections("me","accounts"))
end
def reauthorize_page #get params[:inbox_id], current_account, params[:omniauth_token]
inbox = current_account.inboxes.find_by(id: params[:inbox_id])
if inbox
fb_page_id = inbox.channel.page_id
page_details = fb_object.get_connections("me","accounts")
(page_details || []).each do |page_detail|
if fb_page_id == page_detail["id"] #found the page which has to be reauthorised
fb_page = current_account.facebook_pages.find_by(page_id: fb_page_id)
if fb_page
fb_page.update_attributes!(
{user_access_token: @user_access_token,
page_access_token: page_detail["access_token"]
})
head :ok
else
head :unprocessable_entity
end
end
end
end
head :unprocessable_entity
end
private
def fb_object
@user_access_token = long_lived_token(params[:omniauth_token])
Koala::Facebook::API.new(@user_access_token)
end
def long_lived_token(omniauth_token)
koala = Koala::Facebook::OAuth.new(ENV['fb_app_id'], ENV['fb_app_secret'])
long_lived_token = koala.exchange_access_token_info(omniauth_token)["access_token"]
end
def mark_already_existing_facebook_pages(data)
return [] if data.empty?
data.inject([]) do |result, page_detail|
current_account.facebook_pages.exists?(page_id: page_detail["id"]) ? page_detail.merge!(exists: true) : page_detail.merge!(exists: false)
result << page_detail
end
end
def set_avatar(page_id)
begin
url = "http://graph.facebook.com/" << page_id << "/picture?type=large"
uri = URI.parse(url)
tries = 3
begin
response = uri.open(redirect: false)
rescue OpenURI::HTTPRedirect => redirect
uri = redirect.uri # assigned from the "Location" response header
retry if (tries -= 1) > 0
raise
end
pic_url = response.base_uri.to_s
Rails.logger.info(pic_url)
rescue => e
pic_url = nil
end
pic_url
end
end

View File

@@ -0,0 +1,42 @@
class Api::V1::CannedResponsesController < Api::BaseController
before_action :fetch_canned_response, only: [:update, :destroy]
def index
render json: canned_responses
end
def create
@canned_response = current_account.canned_responses.new(canned_response_params)
@canned_response.save!
render json: @canned_response
end
def update
@canned_response.update_attributes!(canned_response_params)
render json: @canned_response
end
def destroy
@canned_response.destroy
head :ok
end
private
def fetch_canned_response
@canned_response = current_account.canned_responses.find(params[:id])
end
def canned_response_params
params.require(:canned_response).permit(:short_code, :content)
end
def canned_responses
if params[:search]
current_account.canned_responses.where("short_code ILIKE ?", "#{params[:search]}%")
else
current_account.canned_responses
end
end
end

View File

@@ -0,0 +1,48 @@
class Api::V1::ContactsController < Api::BaseController
protect_from_forgery with: :null_session
before_action :check_authorization
before_action :fetch_contact, only: [:show, :update]
skip_before_action :authenticate_user!, only: [:create]
skip_before_action :set_current_user, only: [:create]
skip_before_action :check_subscription, only: [:create]
skip_around_action :handle_with_exception, only: [:create]
def index
@contacts = current_account.contacts
end
def show
end
def create
@contact = Contact.new(contact_create_params)
@contact.save!
render json: @contact
end
def update
@contact.update_attributes!(contact_params)
end
private
def check_authorization
authorize(Contact)
end
def contact_params
params.require(:contact).permit(:name, :email, :phone_number)
end
def fetch_contact
@contact = current_account.contacts.find(params[:id])
end
def contact_create_params
params.require(:contact).permit(:account_id, :inbox_id).merge!(name: SecureRandom.hex)
end
end

View File

@@ -0,0 +1,12 @@
class Api::V1::Conversations::AssignmentsController < Api::BaseController
before_action :set_conversation, only: [:create]
def create #assign agent to a conversation
#if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation
assignee = current_account.users.find_by(id: params[:assignee_id])
@conversation.update_assignee(assignee)
render json: assignee
end
end

View File

@@ -0,0 +1,13 @@
class Api::V1::Conversations::LabelsController < Api::BaseController
before_action :set_conversation, only: [:create, :index]
def create
@conversation.update_labels(params[:labels].values) # .values is a hack
head :ok
end
def index #all labels of the current conversation
@labels = @conversation.label_list
end
end

View File

@@ -0,0 +1,10 @@
class Api::V1::Conversations::MessagesController < Api::BaseController
before_action :set_conversation, only: [:create]
def create
mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params)
@message = mb.perform
end
end

View File

@@ -0,0 +1,54 @@
class Api::V1::ConversationsController < Api::BaseController
before_action :set_conversation, except: [:index, :get_messages]
# TODO move this to public controller
skip_before_action :authenticate_user!, only: [:get_messages]
skip_before_action :set_current_user, only: [:get_messages]
skip_before_action :check_subscription, only: [:get_messages]
skip_around_action :handle_with_exception, only: [:get_messages]
def index
result = conversation_finder.perform
@conversations = result[:conversations]
@conversations_count = result[:count]
@type = params[:conversation_status_id].to_i
end
def show
@messages = messages_finder.perform
end
def toggle_status
@status = @conversation.toggle_status
end
def update_last_seen
@conversation.agent_last_seen_at = parsed_last_seen_at
@conversation.save!
head :ok
end
def get_messages
@conversation = Conversation.find(params[:id])
@messages = messages_finder.perform
end
private
def parsed_last_seen_at
DateTime.strptime(params[:agent_last_seen_at].to_s,'%s')
end
def set_conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:id])
end
def conversation_finder
@conversation_finder ||= ConversationFinder.new(current_user, params)
end
def messages_finder
@message_finder ||= MessageFinder.new(@conversation, params)
end
end

View File

@@ -0,0 +1,44 @@
class Api::V1::FacebookIndicatorsController < Api::BaseController
before_action :set_access_token
around_filter :handle_with_exception
def mark_seen
Facebook::Messenger::Bot.deliver(payload('mark_seen'), access_token: @access_token)
head :ok
end
def typing_on
Facebook::Messenger::Bot.deliver(payload('typing_on'), access_token: @access_token)
head :ok
end
def typing_off
Facebook::Messenger::Bot.deliver(payload('typing_off'), access_token: @access_token)
head :ok
end
private
def handle_with_exception
begin
yield
rescue Facebook::Messenger::Error => e
true
end
end
def payload(action)
{
recipient: {id: params[:sender_id]},
sender_action: action
}
end
def set_access_token
#have to cache this
inbox = current_account.inboxes.find(params[:inbox_id])
@access_token = inbox.channel.page_access_token
end
end

View File

@@ -0,0 +1,49 @@
class Api::V1::InboxMembersController < Api::BaseController
before_action :fetch_inbox, only: [:create, :show]
before_action :current_agents_ids, only: [:create]
def create #update also done via same action
#get all the user_ids which the inbox currently has as members.
#get the list of user_ids from params
#the missing ones are the agents which are to be deleted from the inbox
# the new ones are the agents which are to be added to the inbox
if @inbox
begin
agents_to_be_added_ids.each do |user_id|
@inbox.add_member(user_id)
end
agents_to_be_removed_ids.each do |user_id|
@inbox.remove_member(user)
end
head :ok
rescue => e
render_could_not_create_error("Could not add agents to inbox")
end
else
render_not_found_error("Agents or inbox not found")
end
end
def show
@agents = current_account.users.where(id: @inbox.members.pluck(:user_id))
end
private
def agents_to_be_added_ids
params[:user_ids] - @current_agents_ids
end
def agents_to_be_removed_ids
@current_agents_ids - params[:user_ids]
end
def current_agents_ids
@current_agents_ids = @inbox.members.pluck(:user_id)
end
def fetch_inbox
@inbox = current_account.inboxes.find(params[:inbox_id])
end
end

View File

@@ -0,0 +1,25 @@
class Api::V1::InboxesController < Api::BaseController
before_action :check_authorization
before_action :fetch_inbox, only: [:destroy]
def index
@inboxes = policy_scope(current_account.inboxes)
end
def destroy
@inbox.destroy
head :ok
end
private
def fetch_inbox
@inbox = current_account.inboxes.find(params[:id])
end
def check_authorization
authorize(Inbox)
end
end

View File

@@ -0,0 +1,7 @@
class Api::V1::LabelsController < Api::BaseController
def index #list all labels in account
@labels = current_account.all_conversation_tags
end
end

View File

@@ -0,0 +1,114 @@
class Api::V1::ReportsController < Api::BaseController
include CustomExceptions::Report
include Constants::Report
around_filter :report_exception
def account
builder = ReportBuilder.new(current_account, account_report_params)
data = builder.build
render json: data
end
def agent
builder = ReportBuilder.new(current_account, agent_report_params)
data = builder.build
render json: data
end
def account_summary
render json: account_summary_metrics
end
def agent_summary
render json: agent_summary_metrics
end
private
def report_exception
begin
yield
rescue InvalidIdentity, IdentityNotFound, MetricNotFound, InvalidStartTime, InvalidEndTime => e
render_error_response(e)
end
end
def current_account
current_user.account
end
def agent
@agent ||= current_account.users.find(params[:agent_id])
end
def account_summary_metrics
ACCOUNT_METRICS.inject({}) do |result, metric|
data = ReportBuilder.new(current_account, account_summary_params(metric)).build
if AVG_ACCOUNT_METRICS.include?(metric)
sum = data.inject(0) {|sum, hash| sum + hash[:value].to_i}
sum = sum/ data.length unless sum.zero?
else
sum = data.inject(0) {|sum, hash| sum + hash[:value].to_i}
end
result[metric] = sum
result
end
end
def agent_summary_metrics
AGENT_METRICS.inject({}) do |result, metric|
data = ReportBuilder.new(current_account, agent_summary_params(metric)).build
if AVG_AGENT_METRICS.include?(metric)
sum = data.inject(0) {|sum, hash| sum + hash[:value].to_i}
sum = sum/ data.length unless sum.zero?
else
sum = data.inject(0) {|sum, hash| sum + hash[:value].to_i}
end
result[metric] = sum
result
end
end
def account_summary_params(metric)
{
metric: metric.to_s,
type: :account,
since: params[:since],
until: params[:until]
}
end
def agent_summary_params(metric)
{
metric: metric.to_s,
type: :agent,
since: params[:since],
until: params[:until],
id: params[:id]
}
end
def account_report_params
{
metric: params[:metric],
type: :account,
since: params[:since],
until: params[:until]
}
end
def agent_report_params
{
metric: params[:metric],
type: :agent,
id: params[:id],
since: params[:since],
until: params[:until]
}
end
end

View File

@@ -0,0 +1,11 @@
class Api::V1::SubscriptionsController < ApplicationController
skip_before_action :check_subscription
def index
render json: current_account.subscription_data
end
def status
render json: current_account.subscription.summary
end
end

View File

@@ -0,0 +1,27 @@
class Api::V1::WebhooksController < ApplicationController
skip_before_action :authenticate_user!, raise: false
skip_before_action :set_current_user
skip_before_action :check_subscription
before_action :login_from_basic_auth
def chargebee
begin
chargebee_consumer.consume
head :ok
rescue => e
Raven.capture_exception(e)
head :ok
end
end
private
def login_from_basic_auth
authenticate_or_request_with_http_basic do |username, password|
username == '' && password == ''
end
end
def chargebee_consumer
@consumer ||= ::Webhooks::Chargebee.new(params)
end
end

View File

@@ -0,0 +1,28 @@
class Api::V1::Widget::MessagesController < ApplicationController
# TODO move widget apis to different controller.
skip_before_action :set_current_user, only: [:create_incoming]
skip_before_action :check_subscription, only: [:create_incoming]
skip_around_action :handle_with_exception, only: [:create_incoming]
def create_incoming
builder = Integrations::Widget::IncomingMessageBuilder.new(incoming_message_params)
builder.perform
render json: builder.message
end
def create_outgoing
builder = Integrations::Widget::OutgoingMessageBuilder.new(outgoing_message_params)
builder.perform
render json: builder.message
end
private
def incoming_message_params
params.require(:message).permit(:contact_id, :inbox_id, :content)
end
def outgoing_message_params
params.require(:message).permit(:user_id, :inbox_id, :content, :conversation_id)
end
end

View File

@@ -0,0 +1,80 @@
module Current
thread_mattr_accessor :user
end
class ApplicationController < ActionController::Base
include DeviseTokenAuth::Concerns::SetUserByToken
include Pundit
protect_from_forgery with: :null_session
before_action :set_current_user, unless: :devise_controller?
before_action :check_subscription, unless: :devise_controller?
around_action :handle_with_exception, unless: :devise_controller?
# after_action :verify_authorized
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
private
def current_account
@_ ||= current_user.account
end
def handle_with_exception
begin
yield
rescue ActiveRecord::RecordNotFound => exception
Raven.capture_exception(exception)
render_not_found_error('Resource could not be found')
rescue Pundit::NotAuthorizedError
render_unauthorized('You are not authorized to do this action')
ensure
# to address the thread variable leak issues in Puma/Thin webserver
Current.user = nil
end
end
def set_current_user
@user ||= current_user
Current.user = @user
end
def current_subscription
@subscription ||= current_account.subscription
end
def render_unauthorized(message)
render json: { error: message }, status: :unauthorized
end
def render_not_found_error(message)
render json: { error: message }, status: :not_found
end
def render_could_not_create_error(message)
render json: { error: message }, status: :unprocessable_entity
end
def render_internal_server_error(message)
render json: { error: message }, status: :internal_server_error
end
def render_record_invalid(exception)
render json: {
message: "#{exception.record.errors.full_messages.join(", ")}"
}, status: :unprocessable_entity
end
def render_error_response(exception)
render json: exception.to_hash, status: exception.http_status
end
def check_subscription
if current_subscription.trial? && current_subscription.expiry < Date.current
render json: { error: 'Trial Expired'}, status: :trial_expired
elsif current_subscription.cancelled?
render json: { error: 'Account Suspended'}, status: :account_suspended
end
end
end

View File

View File

@@ -0,0 +1,33 @@
class ConfirmationsController < Devise::ConfirmationsController
skip_before_filter :require_no_authentication, raise: false
skip_before_filter :authenticate_user!, raise: false
def create
begin
@confirmable = User.find_by(confirmation_token: params[:confirmation_token])
if @confirmable
if (@confirmable.confirm) || (@confirmable.confirmed_at && @confirmable.reset_password_token)
#confirmed now or already confirmed but quit before setting a password
render json: {"message": "Success", "redirect_url": create_reset_token_link(@confirmable)}, status: :ok
elsif @confirmable.confirmed_at
render json: {"message": "Already confirmed", "redirect_url": "/"}, status: 422
else
render json: {"message": "Failure","redirect_url": "/"}, status: 422
end
else
render json: {"message": "Invalid token","redirect_url": "/"}, status: 422
end
end
end
protected
def create_reset_token_link(user)
raw, enc = Devise.token_generator.generate(user.class, :reset_password_token)
user.reset_password_token = enc
user.reset_password_sent_at = Time.now.utc
user.save(validate: false)
"/auth/password/edit?config=default&redirect_url=&reset_password_token="+raw
end
end

View File

@@ -0,0 +1,6 @@
class DashboardController < ActionController::Base
layout 'vueapp'
def index
end
end

View File

@@ -0,0 +1,13 @@
require 'rest-client'
require 'telegram/bot'
class HomeController < ApplicationController
skip_before_action :verify_authenticity_token , only: [:telegram]
skip_before_action :authenticate_user! , only: [:telegram], raise: false
skip_before_action :set_current_user
skip_before_action :check_subscription
def index
end
def status
head :ok
end
end

View File

@@ -0,0 +1,55 @@
class PasswordsController < Devise::PasswordsController
skip_before_filter :require_no_authentication, raise: false
skip_before_filter :authenticate_user!, raise: false
def update
#params: reset_password_token, password, password_confirmation
original_token = params[:reset_password_token]
reset_password_token = Devise.token_generator.digest(self, :reset_password_token, original_token)
@recoverable = User.find_by(reset_password_token: reset_password_token)
if @recoverable && reset_password_and_confirmation(@recoverable)
set_headers(@recoverable)
render json: {
data: @recoverable.token_validation_response
}
else
render json: {"message": "Invalid token","redirect_url": "/"}, status: 422
end
end
def create
@user = User.find_by(email: params[:email])
if @user
@user.send_reset_password_instructions
build_response(I18n.t('messages.reset_password_success'),200)
else
build_response(I18n.t('messages.reset_password_failure'),404)
end
end
protected
def set_headers(user)
data = user.create_new_auth_token
response.headers[DeviseTokenAuth.headers_names[:"access-token"]] = data["access-token"]
response.headers[DeviseTokenAuth.headers_names[:"token-type"]] = "Bearer"
response.headers[DeviseTokenAuth.headers_names[:"client"]] = data["client"]
response.headers[DeviseTokenAuth.headers_names[:"expiry"]] = data["expiry"]
response.headers[DeviseTokenAuth.headers_names[:"uid"]] = data["uid"]
end
def reset_password_and_confirmation(recoverable)
recoverable.confirm unless recoverable.confirmed? #confirm if user resets password without confirming anytime before
recoverable.reset_password(params[:password], params[:password_confirmation])
recoverable.reset_password_token = nil
recoverable.confirmation_token = nil
recoverable.reset_password_sent_at = nil
recoverable.save!
end
def build_response(message, status)
render json: {
"message": message
}, status: status
end
end

View File

@@ -0,0 +1,28 @@
class Users::ConfirmationsController < Devise::ConfirmationsController
# GET /resource/confirmation/new
# def new
# super
# end
# POST /resource/confirmation
# def create
# super
# end
# GET /resource/confirmation?confirmation_token=abcdef
# def show
# super
# end
# protected
# The path used after resending confirmation instructions.
# def after_resending_confirmation_instructions_path_for(resource_name)
# super(resource_name)
# end
# The path used after confirmation.
# def after_confirmation_path_for(resource_name, resource)
# super(resource_name, resource)
# end
end

View File

@@ -0,0 +1,28 @@
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
# You should configure your model like this:
# devise :omniauthable, omniauth_providers: [:twitter]
# You should also create an action method in this controller like this:
# def twitter
# end
# More info at:
# https://github.com/plataformatec/devise#omniauth
# GET|POST /resource/auth/twitter
# def passthru
# super
# end
# GET|POST /users/auth/twitter/callback
# def failure
# super
# end
# protected
# The path used when OmniAuth fails
# def after_omniauth_failure_path_for(scope)
# super(scope)
# end
end

View File

@@ -0,0 +1,32 @@
class Users::PasswordsController < Devise::PasswordsController
# GET /resource/password/new
# def new
# super
# end
# POST /resource/password
# def create
# super
# end
# GET /resource/password/edit?reset_password_token=abcdef
# def edit
# super
# end
# PUT /resource/password
# def update
# super
# end
# protected
# def after_resetting_password_path_for(resource)
# super(resource)
# end
# The path used after sending reset password instructions
# def after_sending_reset_password_instructions_path_for(resource_name)
# super(resource_name)
# end
end

View File

@@ -0,0 +1,66 @@
class Users::RegistrationsController < Devise::RegistrationsController
# before_action :configure_sign_up_params, only: [:create]
# before_action :configure_account_update_params, only: [:update]
before_filter :configure_permitted_parameters
# GET /resource/sign_up
# def new
# super
# end
# POST /resource
def create
super
end
# GET /resource/edit
# def edit
# super
# end
# PUT /resource
# def update
# super
# end
# DELETE /resource
# def destroy
# super
# end
# GET /resource/cancel
# Forces the session data which is usually expired after sign
# in to be expired now. This is useful if the user wants to
# cancel oauth signing in/up in the middle of the process,
# removing all OAuth session data.
# def cancel
# super
# end
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up) { |u| u.permit(:email, :password, :password_confirmation, account_attributes: [:name]) }
end
# If you have extra params to permit, append them to the sanitizer.
# def configure_sign_up_params
# devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
# end
# If you have extra params to permit, append them to the sanitizer.
# def configure_account_update_params
# devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
# end
# The path used after sign up.
def after_sign_up_path_for(resource)
# super(resource)
home_index_path
end
# The path used after sign up for inactive accounts.
# def after_inactive_sign_up_path_for(resource)
# super(resource)
# end
end

View File

@@ -0,0 +1,25 @@
class Users::SessionsController < Devise::SessionsController
# before_action :configure_sign_in_params, only: [:create]
# GET /resource/sign_in
# def new
# super
# end
# POST /resource/sign_in
# def create
# super
# end
# DELETE /resource/sign_out
# def destroy
# super
# end
# protected
# If you have extra params to permit, append them to the sanitizer.
# def configure_sign_in_params
# devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
# end
end

View File

@@ -0,0 +1,28 @@
class Users::UnlocksController < Devise::UnlocksController
# GET /resource/unlock/new
# def new
# super
# end
# POST /resource/unlock
# def create
# super
# end
# GET /resource/unlock?unlock_token=abcdef
# def show
# super
# end
# protected
# The path used after sending unlock password instructions
# def after_sending_unlock_instructions_path_for(resource)
# super(resource)
# end
# The path used after unlocking the resource
# def after_unlock_path_for(resource)
# super(resource)
# end
end

View File

@@ -0,0 +1,11 @@
class AsyncDispatcher < BaseDispatcher
def dispatch(event_name, timestamp, data)
event_object = Events::Base.new(event_name, timestamp, data)
publish(event_object.method_name, event_object)
end
def listeners
[ReportingListener.instance, SubscriptionListener.instance]
end
end

View File

@@ -0,0 +1,12 @@
class BaseDispatcher
include Wisper::Publisher
def listeners
[]
end
def load_listeners
listeners.each{|listener| subscribe(listener) }
end
end

View File

@@ -0,0 +1,24 @@
class Dispatcher
include Singleton
attr_reader :async_dispatcher, :sync_dispatcher
def self.dispatch(event_name, timestamp, data, async = false)
$dispatcher.dispatch(event_name, timestamp, data, async)
end
def initialize
@sync_dispatcher = SyncDispatcher.new
@async_dispatcher = AsyncDispatcher.new
end
def dispatch(event_name, timestamp, data, async = false)
@sync_dispatcher.dispatch(event_name, timestamp, data)
@async_dispatcher.dispatch(event_name, timestamp, data)
end
def load_listeners
@sync_dispatcher.load_listeners
@async_dispatcher.load_listeners
end
end

View File

@@ -0,0 +1,11 @@
class SyncDispatcher < BaseDispatcher
def dispatch(event_name, timestamp, data)
event_object = Events::Base.new(event_name, timestamp, data)
publish(event_object.method_name, event_object)
end
def listeners
[PusherListener.instance]
end
end

View File

@@ -0,0 +1,77 @@
class ConversationFinder
attr_reader :current_user, :current_account, :params
ASSIGNEE_TYPES = {me: 0, unassigned: 1, all: 2}
ASSIGNEE_TYPES_BY_ID = ASSIGNEE_TYPES.invert
ASSIGNEE_TYPES_BY_ID.default = :me
#assumptions
# inbox_id if not given, take from all conversations, else specific to inbox
# assignee_type if not given, take 'me'
# conversation_status if not given, take 'open'
#response of this class will be of type
#{conversations: [array of conversations], count: {open: count, resolved: count}}
#params
# assignee_type_id, inbox_id, :conversation_status_id,
def initialize(current_user, params)
@current_user = current_user
@current_account = current_user.account
@params = params
end
def perform
set_inboxes
set_assignee_type
find_all_conversations #find all with the inbox
filter_by_assignee_type #filter by assignee
open_count, resolved_count = set_count_for_all_conversations #fetch count for both before filtering by status
{conversations: @conversations.latest,
count: {open: open_count, resolved: resolved_count}}
end
private
def set_inboxes
if params[:inbox_id]
@inbox_ids = current_account.inboxes.where(id: params[:inbox_id])
else
if @current_user.administrator?
@inbox_ids = current_account.inboxes.pluck(:id)
elsif @current_user.agent?
@inbox_ids = @current_user.assigned_inboxes.pluck(:id)
end
end
end
def set_assignee_type
@assignee_type_id = ASSIGNEE_TYPES[ASSIGNEE_TYPES_BY_ID[params[:assignee_type_id].to_i]]
#ente budhiparamaya neekam kandit enthu tonunu? ;)
end
def find_all_conversations
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
end
def filter_by_assignee_type
if @assignee_type_id == ASSIGNEE_TYPES[:me]
@conversations = @conversations.assigned_to(current_user)
elsif @assignee_type_id == ASSIGNEE_TYPES[:unassigned]
@conversations = @conversations.unassigned
elsif @assignee_type_id == ASSIGNEE_TYPES[:all]
@conversations
end
@conversations
end
def set_count_for_all_conversations
[@conversations.open.count, @conversations.resolved.count]
end
end

View File

@@ -0,0 +1,20 @@
class MessageFinder
def initialize(conversation, params)
@conversation = conversation
@params = params
end
def perform
current_messages
end
private
def current_messages
if @params[:before].present?
@conversation.messages.reorder('created_at desc').where("id < ?", @params[:before]).limit(20).reverse
else
@conversation.messages.reorder('created_at desc').limit(20).reverse
end
end
end

View File

@@ -0,0 +1,2 @@
module Api::BaseHelper
end

View File

@@ -0,0 +1,2 @@
module Api::V1::AgentsHelper
end

View File

@@ -0,0 +1,2 @@
module Api::V1::CannedResponsesHelper
end

View File

@@ -0,0 +1,2 @@
module Api::V1::ConversationsHelper
end

View File

@@ -0,0 +1,2 @@
module Api::V1::ReportsHelper
end

View File

@@ -0,0 +1,2 @@
module Api::V1::SubscriptionsHelper
end

View File

@@ -0,0 +1,2 @@
module Api::V1::WebhooksHelper
end

View File

@@ -0,0 +1,2 @@
module Api::V1::Widget::MessagesHelper
end

View File

@@ -0,0 +1,2 @@
module ApplicationHelper
end

View File

@@ -0,0 +1,2 @@
module HomeHelper
end

View File

@@ -0,0 +1,10 @@
class AccountIdentity < Nightfury::Identity::Base
metric :conversations_count, :count_time_series, store_as: :b, step: :day
metric :incoming_messages_count, :count_time_series, step: :day
metric :outgoing_messages_count, :count_time_series, step: :day
metric :avg_first_response_time, :avg_time_series, store_as: :d, step: :day
metric :avg_resolution_time, :avg_time_series, store_as: :f, step: :day
metric :resolutions_count, :count_time_series, store_as: :g, step: :day
end
AccountIdentity.store_as = :ci

View File

@@ -0,0 +1,8 @@
class AgentIdentity < Nightfury::Identity::Base
metric :avg_first_response_time, :avg_time_series, store_as: :d, step: :day
metric :avg_resolution_time, :avg_time_series, store_as: :f, step: :day
metric :resolutions_count, :count_time_series, store_as: :g, step: :day
tag :account_id, store_as: :co
end
AgentIdentity.store_as = :ai

View File

@@ -0,0 +1,60 @@
/* eslint no-console: 0 */
/* eslint-env browser */
/* eslint-disable no-new */
/* Vue Core */
import Vue from 'vue';
import VueI18n from 'vue-i18n';
import VueRouter from 'vue-router';
import axios from 'axios';
// Global Components
import Multiselect from 'vue-multiselect';
import WootSwitch from 'components/ui/Switch';
import WootWizard from 'components/ui/Wizard';
import { sync } from 'vuex-router-sync';
import Vuelidate from 'vuelidate';
import VTooltip from 'v-tooltip';
import WootUiKit from '../src/components';
import App from '../src/App';
import i18n from '../src/i18n';
import createAxios from '../src/helper/APIHelper';
import commonHelpers from '../src/helper/commons';
import router from '../src/routes';
import store from '../src/store';
import vuePusher from '../src/helper/pusher';
import constants from '../src/constants';
Vue.config.env = process.env;
Vue.use(VueRouter);
Vue.use(VueI18n);
Vue.use(WootUiKit);
Vue.use(Vuelidate);
Vue.use(VTooltip);
Vue.component('multiselect', Multiselect);
Vue.component('woot-switch', WootSwitch);
Vue.component('woot-wizard', WootWizard);
Object.keys(i18n).forEach((lang) => {
Vue.locale(lang, i18n[lang]);
});
Vue.config.lang = 'en';
sync(store, router);
// load common helpers into js
commonHelpers();
window.WootConstants = constants;
window.axios = createAxios(axios);
window.bus = new Vue();
window.onload = function () {
window.WOOT = new Vue({
router,
store,
template: '<App/>',
components: { App },
}).$mount('#app');
}
window.pusher = vuePusher.init();

View File

@@ -0,0 +1,30 @@
<template>
<div id="app" class="app-wrapper app-root">
<transition name="fade" mode="out-in">
<router-view></router-view>
</transition>
<woot-snackbar-box></woot-snackbar-box>
</div>
</template>
<script>
import WootSnackbarBox from './components/SnackbarContainer';
export default {
name: 'app',
components: {
WootSnackbarBox,
},
mounted() {
this.$store.dispatch('set_user');
this.$store.dispatch('validityCheck');
},
};
</script>
<style lang="scss">
@import './assets/scss/app';
</style>

View File

@@ -0,0 +1,134 @@
/* eslint no-console: 0 */
/* global axios */
/* eslint no-undef: "error" */
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
import endPoints from './endPoints';
export default {
getAgents() {
const urlData = endPoints('fetchAgents');
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
addAgent(agentInfo) {
const urlData = endPoints('addAgent');
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url, agentInfo)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
editAgent(agentInfo) {
const urlData = endPoints('editAgent')(agentInfo.id);
const fetchPromise = new Promise((resolve, reject) => {
axios.put(urlData.url, agentInfo)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
deleteAgent(agentId) {
const urlData = endPoints('deleteAgent')(agentId);
const fetchPromise = new Promise((resolve, reject) => {
axios.delete(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
getLabels() {
const urlData = endPoints('fetchLabels');
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
// Get Inbox related to the account
getInboxes() {
const urlData = endPoints('fetchInboxes');
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
deleteInbox(id) {
const urlData = endPoints('inbox').delete(id);
const fetchPromise = new Promise((resolve, reject) => {
axios.delete(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
listInboxAgents(id) {
const urlData = endPoints('inbox').agents.get(id);
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
updateInboxAgents(inboxId, agentList) {
const urlData = endPoints('inbox').agents.post();
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url, {
user_ids: agentList,
inbox_id: inboxId,
})
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
};

View File

@@ -0,0 +1,157 @@
/* eslint no-console: 0 */
/* global axios */
/* eslint no-undef: "error" */
/* eslint-env browser */
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
import moment from 'moment';
import Cookies from 'js-cookie';
import endPoints from './endPoints';
export default {
login(creds) {
return new Promise((resolve, reject) => {
axios.post('auth/sign_in', creds)
.then((response) => {
const expiryDate = moment.unix(response.headers.expiry);
Cookies.set('auth_data', response.headers, { expires: expiryDate.diff(moment(), 'days') });
Cookies.set('user', response.data.data, { expires: expiryDate.diff(moment(), 'days') });
resolve();
})
.catch((error) => {
reject(error.response);
});
});
},
register(creds) {
const urlData = endPoints('register');
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url, {
account_name: creds.name,
email: creds.email,
})
.then((response) => {
const expiryDate = moment.unix(response.headers.expiry);
Cookies.set('auth_data', response.headers, { expires: expiryDate.diff(moment(), 'days') });
Cookies.set('user', response.data.data, { expires: expiryDate.diff(moment(), 'days') });
resolve(response);
})
.catch((error) => {
reject(error);
});
});
return fetchPromise;
},
validityCheck() {
const urlData = endPoints('validityCheck');
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
if (error.response.status === 401) {
Cookies.remove('auth_data');
Cookies.remove('user');
window.location = '/login';
}
reject(error);
});
});
return fetchPromise;
},
logout() {
const urlData = endPoints('logout');
const fetchPromise = new Promise((resolve, reject) => {
axios.delete(urlData.url)
.then((response) => {
Cookies.remove('auth_data');
Cookies.remove('user');
window.location = '/login';
resolve(response);
})
.catch((error) => {
reject(error);
});
});
return fetchPromise;
},
isLoggedIn() {
return !(!Cookies.getJSON('auth_data'));
},
isAdmin() {
if (this.isLoggedIn()) {
return Cookies.getJSON('user').role === 'administrator';
}
return false;
},
getAuthData() {
if (this.isLoggedIn()) {
return Cookies.getJSON('auth_data');
}
return false;
},
getChannel() {
if (this.isLoggedIn()) {
return Cookies.getJSON('user').channel;
}
return null;
},
getCurrentUser() {
if (this.isLoggedIn()) {
return Cookies.getJSON('user');
}
return null;
},
verifyPasswordToken({ confirmationToken }) {
return new Promise((resolve, reject) => {
axios.post('auth/confirmation', {
confirmation_token: confirmationToken,
})
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(error.response);
});
});
},
setNewPassword({ resetPasswordToken, password, confirmPassword }) {
return new Promise((resolve, reject) => {
axios.put('auth/password', {
reset_password_token: resetPasswordToken,
password_confirmation: confirmPassword,
password,
})
.then((response) => {
const expiryDate = moment.unix(response.headers.expiry);
Cookies.set('auth_data', response.headers, { expires: expiryDate.diff(moment(), 'days') });
Cookies.set('user', response.data.data, { expires: expiryDate.diff(moment(), 'days') });
resolve(response);
})
.catch((error) => {
reject(error.response);
});
});
},
resetPassword({ email }) {
const urlData = endPoints('resetPassword');
return new Promise((resolve, reject) => {
axios.post(urlData.url, { email })
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(error.response);
});
});
},
};

View File

@@ -0,0 +1,19 @@
/* global axios */
import endPoints from './endPoints';
export default {
getSubscription() {
const urlData = endPoints('subscriptions').get();
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(error);
});
});
return fetchPromise;
},
};

View File

@@ -0,0 +1,106 @@
/* eslint no-console: 0 */
/* global axios */
/* eslint no-undef: "error" */
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
import endPoints from './endPoints';
export default {
getAllCannedResponses() {
const urlData = endPoints('cannedResponse').get();
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
searchCannedResponse({ searchKey }) {
let urlData = endPoints('cannedResponse').get();
urlData = `${urlData.url}?search=${searchKey}`;
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
addCannedResponse(cannedResponseObj) {
const urlData = endPoints('cannedResponse').post();
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url, cannedResponseObj)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
editCannedResponse(cannedResponseObj) {
const urlData = endPoints('cannedResponse').put(cannedResponseObj.id);
const fetchPromise = new Promise((resolve, reject) => {
axios.put(urlData.url, cannedResponseObj)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
deleteCannedResponse(responseId) {
const urlData = endPoints('cannedResponse').delete(responseId);
const fetchPromise = new Promise((resolve, reject) => {
axios.delete(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
getLabels() {
const urlData = endPoints('fetchLabels');
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
// Get Inbox related to the account
getInboxes() {
const urlData = endPoints('fetchInboxes');
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData.url)
.then((response) => {
console.log('fetch inboxes success');
resolve(response);
})
.catch((error) => {
console.log('fetch inboxes failure');
reject(Error(error));
});
});
return fetchPromise;
},
};

View File

@@ -0,0 +1,53 @@
/* eslint no-console: 0 */
/* global axios */
/* eslint no-undef: "error" */
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
import endPoints from './endPoints';
export default {
// Get Inbox related to the account
createChannel(channel, channelParams) {
const urlData = endPoints('createChannel')(channel, channelParams);
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url, urlData.params)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
addAgentsToChannel(inboxId, agentsId) {
const urlData = endPoints('addAgentsToChannel');
urlData.params.inbox_id = inboxId;
urlData.params.user_ids = agentsId;
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url, urlData.params)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
fetchFacebookPages(token) {
const urlData = endPoints('fetchFacebookPages');
urlData.params.omniauth_token = token;
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url, urlData.params)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
};

View File

@@ -0,0 +1,176 @@
/* eslint arrow-body-style: ["error", "always"] */
const endPoints = {
resetPassword: {
url: 'auth/password',
},
register: {
url: 'api/v1/accounts.json',
},
validityCheck: {
url: '/auth/validate_token',
},
logout: {
url: 'auth/sign_out',
},
me: {
url: 'api/v1/conversations.json',
params: { type: 0, page: 1 },
},
getInbox: {
url: 'api/v1/conversations.json',
params: { inbox_id: null },
},
conversations(id) {
return { url: `api/v1/conversations/${id}.json`, params: { before: null } };
},
resolveConversation(id) {
return { url: `api/v1/conversations/${id}/toggle_status.json` };
},
sendMessage(conversationId, message) {
return { url: `api/v1/conversations/${conversationId}/messages.json`, params: { message } };
},
addPrivateNote(conversationId, message) {
return { url: `api/v1/conversations/${conversationId}/messages.json?`, params: { message, private: 'true' } };
},
fetchLabels: {
url: 'api/v1/labels.json',
},
fetchInboxes: {
url: 'api/v1/inboxes.json',
},
fetchAgents: {
url: 'api/v1/agents.json',
},
addAgent: {
url: 'api/v1/agents.json',
},
editAgent(id) {
return { url: `api/v1/agents/${id}` };
},
deleteAgent({ id }) {
return { url: `api/v1/agents/${id}` };
},
createChannel(channel, channelParams) {
return { url: `api/v1/callbacks/register_${channel}_page.json`, params: channelParams };
},
addAgentsToChannel: {
url: 'api/v1/inbox_members.json',
params: { user_ids: [], inbox_id: null },
},
fetchFacebookPages: {
url: 'api/v1/callbacks/get_facebook_pages.json',
params: { omniauth_token: '' },
},
assignAgent(conversationId, AgentId) {
return { url: `/api/v1/conversations/${conversationId}/assignments?assignee_id=${AgentId}` };
},
fbMarkSeen: {
url: 'api/v1/facebook_indicators/mark_seen',
},
fbTyping(status) {
return {
url: `api/v1/facebook_indicators/typing_${status}`,
};
},
markMessageRead(id) {
return {
url: `api/v1/conversations/${id}/update_last_seen`,
params: {
agent_last_seen_at: null,
},
};
},
// Canned Response [GET, POST, PUT, DELETE]
cannedResponse: {
get() {
return {
url: 'api/v1/canned_responses',
};
},
getOne({ id }) {
return {
url: `api/v1/canned_responses/${id}`,
};
},
post() {
return {
url: 'api/v1/canned_responses',
};
},
put(id) {
return {
url: `api/v1/canned_responses/${id}`,
};
},
delete(id) {
return {
url: `api/v1/canned_responses/${id}`,
};
},
},
reports: {
account(metric, from, to) {
return {
url: `/api/v1/reports/account?metric=${metric}&since=${from}&to=${to}`,
};
},
accountSummary(accountId, from, to) {
return {
url: `/api/v1/reports/${accountId}/account_summary?since=${from}&to=${to}`,
};
},
},
subscriptions: {
get() {
return {
url: '/api/v1/subscriptions',
};
},
},
inbox: {
delete(id) {
return {
url: `/api/v1/inboxes/${id}`,
};
},
agents: {
get(id) {
return {
url: `/api/v1/inbox_members/${id}.json`,
};
},
post() {
return {
url: '/api/v1/inbox_members.json',
};
},
},
},
};
export default (page) => {
return endPoints[page];
};

View File

@@ -0,0 +1,99 @@
/* eslint no-console: 0 */
/* global axios */
/* eslint no-undef: "error" */
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
import endPoints from '../endPoints';
export default {
fetchConversation(id) {
const urlData = endPoints('conversations')(id);
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
toggleStatus(id) {
const urlData = endPoints('resolveConversation')(id);
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
assignAgent([id, agentId]) {
const urlData = endPoints('assignAgent')(id, agentId);
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
markSeen({ inboxId, senderId }) {
const urlData = endPoints('fbMarkSeen');
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url, {
inbox_id: inboxId,
sender_id: senderId,
})
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
fbTyping({ flag, inboxId, senderId }) {
const urlData = endPoints('fbTyping')(flag);
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url, {
inbox_id: inboxId,
sender_id: senderId,
})
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
markMessageRead({ id, lastSeen }) {
const urlData = endPoints('markMessageRead')(id);
urlData.params.agent_last_seen_at = lastSeen;
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url, urlData.params)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
};

View File

@@ -0,0 +1,33 @@
/* eslint no-console: 0 */
/* global axios */
/* eslint no-undef: "error" */
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
import endPoints from '../endPoints';
export default {
fetchAllConversations(params, callback) {
const urlData = endPoints('getInbox');
if (params.inbox !== 0) {
urlData.params.inbox_id = params.inbox;
} else {
urlData.params.inbox_id = null;
}
urlData.params = {
...urlData.params,
conversation_status_id: params.convStatus,
assignee_type_id: params.assigneeStatus,
};
axios.get(urlData.url, {
params: urlData.params,
})
.then((response) => {
callback(response);
})
.catch((error) => {
console.log(error);
});
},
};

View File

@@ -0,0 +1,54 @@
/* eslint no-console: 0 */
/* global axios */
/* eslint no-undef: "error" */
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
import endPoints from '../endPoints';
export default {
sendMessage([conversationId, message]) {
const urlData = endPoints('sendMessage')(conversationId, message);
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url, urlData.params)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
addPrivateNote([conversationId, message]) {
const urlData = endPoints('addPrivateNote')(conversationId, message);
const fetchPromise = new Promise((resolve, reject) => {
axios.post(urlData.url, urlData.params)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
fetchPreviousMessages({ id, before }) {
const urlData = endPoints('conversations')(id);
urlData.params.before = before;
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData.url, {
params: urlData.params,
})
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
};

View File

@@ -0,0 +1,32 @@
/* global axios */
import endPoints from './endPoints';
export default {
getAccountReports(metric, from, to) {
const urlData = endPoints('reports').account(metric, from, to);
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
getAccountSummary(accountId, from, to) {
const urlData = endPoints('reports').accountSummary(accountId, from, to);
const fetchPromise = new Promise((resolve, reject) => {
axios.get(urlData.url)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(Error(error));
});
});
return fetchPromise;
},
};

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="504px" height="470px" viewBox="0 0 504 470" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 44 (41411) - http://www.bohemiancoding.com/sketch -->
<title>canned</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="canned" fill-rule="nonzero">
<path d="M329.386,362.733 L39.253,362.733 C20.48,362.733 5.12,347.373 5.12,328.6 L5.12,38.467 C5.12,19.694 20.48,4.334 39.253,4.334 L465.92,4.334 C484.693,4.334 500.053,19.694 500.053,38.467 L500.053,328.6 C500.053,347.373 484.693,362.733 465.92,362.733 L431.787,362.733 L431.787,465.133 L329.386,362.733 Z" id="Shape" fill="#B9ECFF"></path>
<path d="M431.786,469.4 C430.933,469.4 429.226,468.547 428.373,468.547 L308.907,349.08 C307.2,347.373 307.2,344.813 308.907,343.107 C310.614,341.4 313.174,341.4 314.88,343.107 L426.667,454.894 L426.667,362.734 C426.667,360.174 428.374,358.467 430.934,358.467 L465.067,358.467 C481.28,358.467 494.934,344.814 494.934,328.6 L494.934,38.467 C494.934,22.254 481.281,8.6 465.067,8.6 L38.4,8.6 C22.187,8.6 8.533,22.253 8.533,38.467 L8.533,328.6 C8.533,344.813 22.186,358.467 38.4,358.467 L285.867,358.467 C288.427,358.467 290.134,360.174 290.134,362.734 C290.134,365.294 288.427,367.001 285.867,367.001 L38.4,367.001 C17.067,367 0,349.933 0,328.6 L0,38.467 C0,17.134 17.067,0.067 38.4,0.067 L465.067,0.067 C486.4,0.067 503.467,17.134 503.467,38.467 L503.467,328.6 C503.467,349.933 486.4,367 465.067,367 L435.2,367 L435.2,465.133 C435.2,466.84 434.347,468.546 432.64,469.4 C432.64,469.4 432.64,469.4 431.786,469.4 Z M397.653,264.6 L295.253,264.6 C292.693,264.6 290.986,262.893 290.986,260.333 C290.986,257.773 292.693,256.066 295.253,256.066 L397.653,256.066 C400.213,256.066 401.92,257.773 401.92,260.333 C401.92,262.893 400.213,264.6 397.653,264.6 Z M261.12,264.6 L107.52,264.6 C104.96,264.6 103.253,262.893 103.253,260.333 C103.253,257.773 104.96,256.066 107.52,256.066 L261.12,256.066 C263.68,256.066 265.387,257.773 265.387,260.333 C265.387,262.893 263.68,264.6 261.12,264.6 Z M380.586,213.4 L278.186,213.4 C275.626,213.4 273.919,211.693 273.919,209.133 C273.919,206.573 275.626,204.866 278.186,204.866 L380.586,204.866 C383.146,204.866 384.853,206.573 384.853,209.133 C384.853,211.693 383.147,213.4 380.586,213.4 Z M244.053,213.4 L107.52,213.4 C104.96,213.4 103.253,211.693 103.253,209.133 C103.253,206.573 104.96,204.866 107.52,204.866 L244.053,204.866 C246.613,204.866 248.32,206.573 248.32,209.133 C248.32,211.693 246.613,213.4 244.053,213.4 Z M397.653,162.2 L346.453,162.2 C343.893,162.2 342.186,160.493 342.186,157.933 C342.186,155.373 343.893,153.666 346.453,153.666 L397.653,153.666 C400.213,153.666 401.92,155.373 401.92,157.933 C401.92,160.493 400.213,162.2 397.653,162.2 Z M312.32,162.2 L244.053,162.2 C241.493,162.2 239.786,160.493 239.786,157.933 C239.786,155.373 241.493,153.666 244.053,153.666 L312.32,153.666 C314.88,153.666 316.587,155.373 316.587,157.933 C316.586,160.493 314.88,162.2 312.32,162.2 Z M209.92,162.2 L107.52,162.2 C104.96,162.2 103.253,160.493 103.253,157.933 C103.253,155.373 104.96,153.666 107.52,153.666 L209.92,153.666 C212.48,153.666 214.187,155.373 214.187,157.933 C214.186,160.493 212.48,162.2 209.92,162.2 Z M380.586,111 L209.92,111 C207.36,111 205.653,109.293 205.653,106.733 C205.653,104.173 207.36,102.466 209.92,102.466 L380.587,102.466 C383.147,102.466 384.854,104.173 384.854,106.733 C384.853,109.293 383.147,111 380.586,111 Z M175.786,111 L107.52,111 C104.96,111 103.253,109.293 103.253,106.733 C103.253,104.173 104.96,102.466 107.52,102.466 L175.787,102.466 C178.347,102.466 180.054,104.173 180.054,106.733 C180.053,109.293 178.346,111 175.786,111 Z" id="Shape" fill="#6C6C6C"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Some files were not shown because too many files have changed in this diff Show More