diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb index 0095b9e1b..8566dc4b0 100644 --- a/app/builders/messages/facebook/message_builder.rb +++ b/app/builders/messages/facebook/message_builder.rb @@ -50,6 +50,8 @@ class Messages::Facebook::MessageBuilder def attach_file(attachment, file_url) file_resource = LocalResource.new(file_url) attachment.file.attach(io: file_resource.file, filename: file_resource.tmp_filename, content_type: file_resource.encoding) + rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED => e + Rails.logger.info "invalid url #{file_url} : #{e.message}" end def conversation diff --git a/app/controllers/api/v1/accounts/callbacks_controller.rb b/app/controllers/api/v1/accounts/callbacks_controller.rb index 77e46d669..812646bfd 100644 --- a/app/controllers/api/v1/accounts/callbacks_controller.rb +++ b/app/controllers/api/v1/accounts/callbacks_controller.rb @@ -81,6 +81,8 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController avatar_resource = LocalResource.new(uri) facebook_inbox.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding) + rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED => e + Rails.logger.info "invalid url #{file_url} : #{e.message}" end def get_avatar_url(page_id) diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 51b6aa079..c652c3634 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::V1::Accounts::BaseController include Events::Types + before_action :conversation, except: [:index] before_action :contact_inbox, only: [:create] diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index c24b633aa..b3a2d8139 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -9,7 +9,11 @@ class AsyncDispatcher < BaseDispatcher end def listeners - listeners = [EventListener.instance, WebhookListener.instance, HookListener.instance] + listeners = [ + EventListener.instance, + WebhookListener.instance, + InstallationWebhookListener.instance, HookListener.instance + ] listeners end end diff --git a/app/jobs/contact_avatar_job.rb b/app/jobs/contact_avatar_job.rb index a99daca3e..0cd88027e 100644 --- a/app/jobs/contact_avatar_job.rb +++ b/app/jobs/contact_avatar_job.rb @@ -4,5 +4,7 @@ class ContactAvatarJob < ApplicationJob def perform(contact, avatar_url) avatar_resource = LocalResource.new(avatar_url) contact.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding) + rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED => e + Rails.logger.info "invalid url #{file_url} : #{e.message}" end end diff --git a/app/listeners/installation_webhook_listener.rb b/app/listeners/installation_webhook_listener.rb new file mode 100644 index 000000000..d45a3ef3b --- /dev/null +++ b/app/listeners/installation_webhook_listener.rb @@ -0,0 +1,19 @@ +class InstallationWebhookListener < BaseListener + def account_created(event) + payload = event.data[:account].webhook_data.merge(event: __method__.to_s) + deliver_webhook_payloads(payload) + end + + def account_destroyed(event) + payload = event.data[:account].webhook_data.merge(event: __method__.to_s) + deliver_webhook_payloads(payload) + end + + private + + def deliver_webhook_payloads(payload) + # Deliver the installation event + webhook_url = InstallationConfig.find_by(name: 'INSTALLATION_EVENTS_WEBHOOK_URL')&.value + WebhookJob.perform_later(webhook_url, payload) if webhook_url + end +end diff --git a/app/models/account.rb b/app/models/account.rb index f5d6b29f9..3c4415e08 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -16,8 +16,6 @@ class Account < ApplicationRecord # used for single column multi flags include FlagShihTzu - - include Events::Types include Reportable include Featurable @@ -54,7 +52,7 @@ class Account < ApplicationRecord enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h - after_create :notify_creation + after_create_commit :notify_creation after_destroy :notify_deletion def agents diff --git a/app/models/account_user.rb b/app/models/account_user.rb index 8c482eb3c..e337670d4 100644 --- a/app/models/account_user.rb +++ b/app/models/account_user.rb @@ -24,8 +24,6 @@ # class AccountUser < ApplicationRecord - include Events::Types - belongs_to :account belongs_to :user belongs_to :inviter, class_name: 'User', optional: true @@ -33,7 +31,7 @@ class AccountUser < ApplicationRecord enum role: { agent: 0, administrator: 1 } accepts_nested_attributes_for :account - after_create :notify_creation, :create_notification_setting + after_create_commit :notify_creation, :create_notification_setting after_destroy :notify_deletion, :destroy_notification_setting validates :user_id, uniqueness: { scope: :account_id } diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 62d06bd88..69f6efc5d 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,7 +1,11 @@ class ApplicationRecord < ActiveRecord::Base + include Events::Types self.abstract_class = true + + # the models that exposed in email templates through liquid DROPPABLES = %w[Account Channel Conversation Inbox User].freeze + # ModelDrop class should exist in app/drops def to_drop return unless DROPPABLES.include?(self.class.name) diff --git a/app/models/contact.rb b/app/models/contact.rb index 4ee5045e9..d9f27a6e4 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -26,7 +26,6 @@ class Contact < ApplicationRecord include Pubsubable include Avatarable include AvailabilityStatusable - include Events::Types validates :account_id, presence: true validates :email, allow_blank: true, uniqueness: { scope: [:account_id], case_sensitive: false } diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 773fe76b6..90b804a1d 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -31,8 +31,6 @@ # class Conversation < ApplicationRecord - include Events::Types - validates :account_id, presence: true validates :inbox_id, presence: true diff --git a/app/models/message.rb b/app/models/message.rb index 3249498fc..1e3bb8dd7 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -28,8 +28,6 @@ # class Message < ApplicationRecord - include Events::Types - NUMBER_OF_PERMITTED_ATTACHMENTS = 15 validates :account_id, presence: true @@ -105,6 +103,7 @@ class Message < ApplicationRecord created_at: created_at, message_type: message_type, content_type: content_type, + private: private, content_attributes: content_attributes, source_id: source_id, sender: sender.try(:webhook_data), diff --git a/app/models/plan.rb b/app/models/plan.rb deleted file mode 100644 index 59556a46b..000000000 --- a/app/models/plan.rb +++ /dev/null @@ -1,97 +0,0 @@ -class Plan - attr_accessor :key, :attributes - - def initialize(key, attributes = {}) - @key = key.to_sym - @attributes = attributes - end - - def name - attributes[:name] - end - - def id - attributes[:id] - end - - def price - attributes[:price] - end - - def active - attributes[:active] - end - - def version - attributes[:version] - end - - class << self - def config - Hashie::Mash.new(PLAN_CONFIG) - end - - def default_trial_period - (config['trial_period'] || 14).days - end - - def default_pricing_version - config['default_pricing_version'] - end - - def default_plans - load_active_plans + load_inactive_plans - end - - def all_plans - default_plans - end - - def active_plans - all_plans.select(&:active) - end - - def paid_plan - active_plans.first - end - - def inactive_plans - all_plans.reject(&:active) - end - - def trial_plan - all_plans.detect { |plan| plan.key == :trial } - end - - def plans_of_version(version) - all_plans.select { |plan| plan.version == version } - end - - def find_by_key(key) - key = key.to_sym - all_plans.detect { |plan| plan.key == key }.dup - end - - # #helpers - - def load_active_plans - result = [] - Plan.config.active.each_pair do |version, plans| - plans.each_pair do |key, attributes| - result << Plan.new(key, attributes.merge(active: true, version: version)) - end - end - result - end - - def load_inactive_plans - result = [] - Plan.config.inactive.each_pair do |version, plans| - plans.each_pair do |key, attributes| - result << Plan.new(key, attributes.merge(active: false, version: version)) - end - end - result - end - end -end diff --git a/app/models/user.rb b/app/models/user.rb index 133b7d660..5498ee45e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -41,7 +41,6 @@ class User < ApplicationRecord include Avatarable # Include default devise modules. include DeviseTokenAuth::Concerns::User - include Events::Types include Pubsubable include Rails.application.routes.url_helpers include Reportable @@ -78,7 +77,7 @@ class User < ApplicationRecord before_validation :set_password_and_uid, on: :create - after_create :create_access_token + after_create_commit :create_access_token after_save :update_presence_in_redis, if: :saved_change_to_availability? scope :order_by_full_name, -> { order('lower(name) ASC') } diff --git a/app/services/twilio/incoming_message_service.rb b/app/services/twilio/incoming_message_service.rb index c50016971..0f51eca0a 100644 --- a/app/services/twilio/incoming_message_service.rb +++ b/app/services/twilio/incoming_message_service.rb @@ -107,5 +107,7 @@ class Twilio::IncomingMessageService ) @message.save! + rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED => e + Rails.logger.info "invalid url #{file_url} : #{e.message}" end end diff --git a/config/installation_config.yml b/config/installation_config.yml index b48d3a9d3..41c27b6c5 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -22,3 +22,5 @@ value: false - name: BRAND_NAME value: 'Chatwoot' +- name: 'INSTALLATION_EVENTS_WEBHOOK_URL' + value: diff --git a/lib/events/types.rb b/lib/events/types.rb index a4ef24b7f..6416cce1f 100644 --- a/lib/events/types.rb +++ b/lib/events/types.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true module Events::Types + ### Installation Events ### # account events ACCOUNT_CREATED = 'account.created' ACCOUNT_DESTROYED = 'account.destroyed' + #### Account Events ### # channel events WEBWIDGET_TRIGGERED = 'webwidget.triggered' @@ -28,10 +30,7 @@ module Events::Types CONTACT_CREATED = 'contact.created' CONTACT_UPDATED = 'contact.updated' - # subscription events + # agent events AGENT_ADDED = 'agent.added' AGENT_REMOVED = 'agent.removed' - SUBSCRIPTION_CREATED = 'subscription.created' - SUBSCRIPTION_REACTIVATED = 'subscription.reactivated' - SUBSCRIPTION_DEACTIVATED = 'subscription.deactivated' end diff --git a/lib/webhooks/trigger.rb b/lib/webhooks/trigger.rb index efa555845..f3fe055dc 100644 --- a/lib/webhooks/trigger.rb +++ b/lib/webhooks/trigger.rb @@ -1,6 +1,8 @@ class Webhooks::Trigger def self.execute(url, payload) RestClient.post(url, payload.to_json, { content_type: :json, accept: :json }) + rescue RestClient::NotFound => e + Rails.logger.info "invalid url #{url} : #{e.message}" rescue StandardError => e Raven.capture_exception(e) end diff --git a/spec/factories/installation_config.rb b/spec/factories/installation_config.rb new file mode 100644 index 000000000..d78aead4d --- /dev/null +++ b/spec/factories/installation_config.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :installation_config do + name { 'xyc' } + value { 1.5 } + end +end diff --git a/spec/listeners/installation_webhook_listener_spec.rb b/spec/listeners/installation_webhook_listener_spec.rb new file mode 100644 index 000000000..534553c28 --- /dev/null +++ b/spec/listeners/installation_webhook_listener_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' +describe InstallationWebhookListener do + let(:listener) { described_class.instance } + let!(:account) { create(:account) } + let!(:event) { Events::Base.new(event_name, Time.zone.now, account: account) } + + describe '#account_created' do + let(:event_name) { :'account.created' } + + context 'when installation config is not configured' do + it 'does not trigger webhook' do + expect(WebhookJob).to receive(:perform_later).exactly(0).times + listener.account_created(event) + end + end + + context 'when installation config is configured' do + it 'triggers webhook' do + create(:installation_config, name: 'INSTALLATION_EVENTS_WEBHOOK_URL', value: 'https://test.com') + expect(WebhookJob).to receive(:perform_later).with('https://test.com', account.webhook_data.merge(event: 'account_created')).once + listener.account_created(event) + end + end + end + + describe '#account_destroyed' do + let(:event_name) { :'account.destroyed' } + + context 'when installation config is not configured' do + it 'does not trigger webhook' do + expect(WebhookJob).to receive(:perform_later).exactly(0).times + listener.account_destroyed(event) + end + end + + context 'when installation config is configured' do + it 'triggers webhook' do + create(:installation_config, name: 'INSTALLATION_EVENTS_WEBHOOK_URL', value: 'https://test.com') + expect(WebhookJob).to receive(:perform_later).with('https://test.com', account.webhook_data.merge(event: 'account_destroyed')).once + listener.account_destroyed(event) + end + end + end +end diff --git a/spec/listeners/webhook_listener_spec.rb b/spec/listeners/webhook_listener_spec.rb index 826f22458..ed581dc6f 100644 --- a/spec/listeners/webhook_listener_spec.rb +++ b/spec/listeners/webhook_listener_spec.rb @@ -17,7 +17,7 @@ describe WebhookListener do context 'when webhook is not configured' do it 'does not trigger webhook' do - expect(RestClient).to receive(:post).exactly(0).times + expect(WebhookJob).to receive(:perform_later).exactly(0).times listener.message_created(event) end end diff --git a/swagger/definitions/request/conversation/create_message.yml b/swagger/definitions/request/conversation/create_message.yml index faecf278f..da3b27a2d 100644 --- a/swagger/definitions/request/conversation/create_message.yml +++ b/swagger/definitions/request/conversation/create_message.yml @@ -12,7 +12,10 @@ properties: description: Flag to identify if it is a private note content_type: type: string - enum: ['input_select', 'form', 'cards'] + enum: ['input_email', 'cards', 'input_select', 'form' , 'article'] + example: 'cards' + description: 'if you want to create custom message types' content_attributes: type: object - description: options/form object + description: attributes based on your content type + diff --git a/swagger/swagger.json b/swagger/swagger.json index cc8b31482..de008e55e 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -1641,14 +1641,18 @@ "content_type": { "type": "string", "enum": [ + "input_email", + "cards", "input_select", "form", - "cards" - ] + "article" + ], + "example": "cards", + "description": "if you want to create custom message types" }, "content_attributes": { "type": "object", - "description": "options/form object" + "description": "attributes based on your content type" } } }