From 671c5c931fee4882507d9e31e71aadf68fb87e9f Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 10 Sep 2021 00:00:52 +0530 Subject: [PATCH] feat: Telegram Channel (#2901) - Ability to configure telegram bots as a channel in chatwoot - Receive a message sent to the telegram bot in chatwoot - Ability to reply to telegram users from chatwoot - Receive attachment messages in chatwoot fixes: #1843 --- .rubocop.yml | 1 + Gemfile | 1 - Gemfile.lock | 20 ---- .../api/v1/accounts/inboxes_controller.rb | 72 +++++++----- .../webhooks/telegram_controller.rb | 6 + .../components/widgets/ChannelItem.vue | 2 +- .../components/widgets/Thumbnail.vue | 7 ++ .../dashboard/i18n/locale/en/inboxMgmt.json | 13 +++ .../routes/dashboard/settings/inbox/Index.vue | 3 + .../settings/inbox/channel-factory.js | 2 + .../settings/inbox/channels/Telegram.vue | 93 +++++++++++++++ app/jobs/send_reply_job.rb | 2 + app/jobs/webhooks/telegram_events_job.rb | 12 ++ app/models/account.rb | 1 + app/models/channel/api.rb | 1 + app/models/channel/email.rb | 1 + app/models/channel/telegram.rb | 89 +++++++++++++++ app/models/channel/web_widget.rb | 4 +- app/models/inbox.rb | 4 + .../telegram/incoming_message_service.rb | 106 ++++++++++++++++++ .../telegram/send_on_telegram_service.rb | 22 ++++ app/views/api/v1/models/_inbox.json.jbuilder | 2 + config/routes.rb | 1 + .../20210828124043_add_telegram_channel.rb | 10 ++ db/schema.rb | 11 +- .../v1/accounts/inboxes_controller_spec.rb | 58 ++++++++-- .../webhooks/telegram_controller_spec.rb | 12 ++ spec/factories/channel/channel_telegram.rb | 16 +++ spec/jobs/send_reply_job_spec.rb | 59 ++++++++++ .../jobs/webhooks/telegram_events_job_spec.rb | 36 ++++++ .../telegram/incoming_message_service_spec.rb | 27 +++++ .../telegram/send_on_telegram_service_spec.rb | 19 ++++ 32 files changed, 648 insertions(+), 65 deletions(-) create mode 100644 app/controllers/webhooks/telegram_controller.rb create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Telegram.vue create mode 100644 app/jobs/webhooks/telegram_events_job.rb create mode 100644 app/models/channel/telegram.rb create mode 100644 app/services/telegram/incoming_message_service.rb create mode 100644 app/services/telegram/send_on_telegram_service.rb create mode 100644 db/migrate/20210828124043_add_telegram_channel.rb create mode 100644 spec/controllers/webhooks/telegram_controller_spec.rb create mode 100644 spec/factories/channel/channel_telegram.rb create mode 100644 spec/jobs/send_reply_job_spec.rb create mode 100644 spec/jobs/webhooks/telegram_events_job_spec.rb create mode 100644 spec/services/telegram/incoming_message_service_spec.rb create mode 100644 spec/services/telegram/send_on_telegram_service_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index c05cbb9bf..76ef5fcfc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -100,6 +100,7 @@ Metrics/AbcSize: - 'app/controllers/concerns/auth_helper.rb' - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb' - 'db/migrate/20161123131628_devise_token_auth_create_users.rb' + - 'app/controllers/api/v1/accounts/inboxes_controller.rb' Metrics/CyclomaticComplexity: Max: 7 Exclude: diff --git a/Gemfile b/Gemfile index 37dba6a19..7f0933b37 100644 --- a/Gemfile +++ b/Gemfile @@ -78,7 +78,6 @@ gem 'wisper', '2.0.0' ##--- gems for channels ---## # TODO: bump up gem to 2.0 gem 'facebook-messenger' -gem 'telegram-bot-ruby' gem 'twilio-ruby', '~> 5.32.0' # twitty will handle subscription of twitter account events # gem 'twitty', git: 'https://github.com/chatwoot/twitty' diff --git a/Gemfile.lock b/Gemfile.lock index c11e09a14..31b9f7bbc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,10 +98,6 @@ GEM aws-sigv4 (~> 1.1) aws-sigv4 (1.2.4) aws-eventstream (~> 1, >= 1.0.2) - axiom-types (0.1.1) - descendants_tracker (~> 0.0.4) - ice_nine (~> 0.11.0) - thread_safe (~> 0.3, >= 0.3.1) azure-storage-blob (2.0.1) azure-storage-common (~> 2.0) nokogiri (~> 1.11.0.rc2) @@ -130,8 +126,6 @@ GEM thor (~> 1.0) byebug (11.1.3) coderay (1.1.3) - coercible (1.0.0) - descendants_tracker (~> 0.0.1) commonmarker (0.22.0) concurrent-ruby (1.1.9) connection_pool (2.2.5) @@ -152,8 +146,6 @@ GEM ffi (~> 1.0) msgpack declarative (0.0.20) - descendants_tracker (0.0.4) - thread_safe (~> 0.3, >= 0.3.1) devise (4.8.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -179,7 +171,6 @@ GEM railties (>= 3.2) down (5.2.3) addressable (~> 2.8) - dry-inflector (0.2.1) ecma-re-validator (0.3.0) regexp_parser (~> 2.0) erubi (1.10.0) @@ -292,7 +283,6 @@ GEM httpclient (2.8.3) i18n (1.8.10) concurrent-ruby (~> 1.0) - ice_nine (0.11.2) image_processing (1.12.1) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) @@ -564,13 +554,8 @@ GEM sprockets (>= 3.0.0) squasher (0.6.2) statsd-ruby (1.5.0) - telegram-bot-ruby (0.16.0) - dry-inflector - faraday - virtus (~> 2.0) telephone_number (1.4.12) thor (1.1.0) - thread_safe (0.3.6) tilt (2.0.10) time_diff (0.3.0) activesupport @@ -598,10 +583,6 @@ GEM valid_email2 (4.0.0) activemodel (>= 3.2) mail (~> 2.5) - virtus (2.0.0) - axiom-types (~> 0.1) - coercible (~> 1.0) - descendants_tracker (~> 0.0, >= 0.0.3) warden (1.2.9) rack (>= 2.0.9) web-console (4.1.0) @@ -716,7 +697,6 @@ DEPENDENCIES spring spring-watcher-listen squasher - telegram-bot-ruby telephone_number time_diff twilio-ruby (~> 5.32.0) diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 59ab107dc..3b21b70bb 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -27,22 +27,23 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController ActiveRecord::Base.transaction do channel = create_channel @inbox = Current.account.inboxes.build( - name: permitted_params[:name], - greeting_message: permitted_params[:greeting_message], - greeting_enabled: permitted_params[:greeting_enabled], - channel: channel + { + name: inbox_name(channel), + channel: channel + }.merge( + permitted_params.except(:channel) + ) ) - @inbox.avatar.attach(permitted_params[:avatar]) @inbox.save! end end def update - @inbox.update(inbox_update_params.except(:channel)) + @inbox.update(permitted_params.except(:channel)) @inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours] - return unless @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present? - @inbox.channel.update!(inbox_update_params[:channel]) + channel_attributes = get_channel_attributes(@inbox.channel_type) + @inbox.channel.update!(permitted_params(channel_attributes)[:channel]) update_channel_feature_flags end @@ -77,43 +78,52 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController @agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot] end + def inbox_name(channel) + return channel.try(:bot_name) if channel.is_a?(Channel::Telegram) + + permitted_params[:name] + end + def create_channel case permitted_params[:channel][:type] when 'web_widget' - Current.account.web_widgets.create!(permitted_params[:channel].except(:type)) + Current.account.web_widgets.create!(permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].except(:type)) when 'api' - Current.account.api_channels.create!(permitted_params[:channel].except(:type)) + Current.account.api_channels.create!(permitted_params(Channel::Api::EDITABLE_ATTRS)[:channel].except(:type)) when 'email' - Current.account.email_channels.create!(permitted_params[:channel].except(:type)) + Current.account.email_channels.create!(permitted_params(Channel::Email::EDITABLE_ATTRS)[:channel].except(:type)) + when 'telegram' + Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type)) end end def update_channel_feature_flags - return unless inbox_update_params[:channel].key? :selected_feature_flags + return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags - @inbox.channel.selected_feature_flags = inbox_update_params[:channel][:selected_feature_flags] + @inbox.channel.selected_feature_flags = permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel][:selected_feature_flags] @inbox.channel.save! end - def permitted_params - params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, :enable_email_collect, :csat_survey_enabled, channel: - [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email, :reply_time]) + def permitted_params(channel_attributes = []) + params.permit( + :name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled, + :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, + channel: [:type, *channel_attributes] + ) end - def inbox_update_params - params.permit(:enable_auto_assignment, :enable_email_collect, :name, :avatar, :greeting_message, :greeting_enabled, :csat_survey_enabled, - :working_hours_enabled, :out_of_office_message, :timezone, - channel: [ - :website_url, - :widget_color, - :welcome_title, - :welcome_tagline, - :webhook_url, - :email, - :reply_time, - :pre_chat_form_enabled, - { pre_chat_form_options: [:pre_chat_message, :require_email] }, - { selected_feature_flags: [] } - ]) + def get_channel_attributes(channel_type) + case channel_type + when 'Channel::WebWidget' + Channel::WebWidget::EDITABLE_ATTRS + when 'Channel::Api' + Channel::Api::EDITABLE_ATTRS + when 'Channel::Email' + Channel::Email::EDITABLE_ATTRS + when 'Channel::Telegram' + Channel::Telegram::EDITABLE_ATTRS + else + [] + end end end diff --git a/app/controllers/webhooks/telegram_controller.rb b/app/controllers/webhooks/telegram_controller.rb new file mode 100644 index 000000000..bc65061e7 --- /dev/null +++ b/app/controllers/webhooks/telegram_controller.rb @@ -0,0 +1,6 @@ +class Webhooks::TelegramController < ActionController::API + def process_payload + Webhooks::TelegramEventsJob.perform_later(params.to_unsafe_hash) + head :ok + end +end diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue index e612fa14b..819d912bd 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -76,7 +76,7 @@ export default { if (key === 'email') { return this.enabledFeatures.channel_email; } - return ['website', 'twilio', 'api', 'whatsapp', 'sms'].includes(key); + return ['website', 'twilio', 'api', 'whatsapp', 'sms', 'telegram'].includes(key); }, }, methods: { diff --git a/app/javascript/dashboard/components/widgets/Thumbnail.vue b/app/javascript/dashboard/components/widgets/Thumbnail.vue index 2d1c3e200..e43d48a4b 100644 --- a/app/javascript/dashboard/components/widgets/Thumbnail.vue +++ b/app/javascript/dashboard/components/widgets/Thumbnail.vue @@ -35,6 +35,13 @@ :style="badgeStyle" src="~dashboard/assets/images/channels/whatsapp.png" /> +
Email + + Telegram + {{ globalConfig.apiChannelName || 'API' }} diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js b/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js index 0cf37f148..db7fb0c43 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channel-factory.js @@ -5,6 +5,7 @@ import Api from './channels/Api'; import Email from './channels/Email'; import Sms from './channels/Sms'; import Whatsapp from './channels/Whatsapp'; +import Telegram from './channels/Telegram'; const channelViewList = { facebook: Facebook, @@ -14,6 +15,7 @@ const channelViewList = { email: Email, sms: Sms, whatsapp: Whatsapp, + telegram: Telegram, }; export default { diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Telegram.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Telegram.vue new file mode 100644 index 000000000..710f12f86 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Telegram.vue @@ -0,0 +1,93 @@ + + + diff --git a/app/jobs/send_reply_job.rb b/app/jobs/send_reply_job.rb index 6d0d171c5..77d17cc22 100644 --- a/app/jobs/send_reply_job.rb +++ b/app/jobs/send_reply_job.rb @@ -11,6 +11,8 @@ class SendReplyJob < ApplicationJob ::Twitter::SendOnTwitterService.new(message: message).perform when 'Channel::TwilioSms' ::Twilio::SendOnTwilioService.new(message: message).perform + when 'Channel::Telegram' + ::Telegram::SendOnTelegramService.new(message: message).perform end end end diff --git a/app/jobs/webhooks/telegram_events_job.rb b/app/jobs/webhooks/telegram_events_job.rb new file mode 100644 index 000000000..defdb02b0 --- /dev/null +++ b/app/jobs/webhooks/telegram_events_job.rb @@ -0,0 +1,12 @@ +class Webhooks::TelegramEventsJob < ApplicationJob + queue_as :default + + def perform(params = {}) + return unless params[:bot_token] + + channel = Channel::Telegram.find_by(bot_token: params[:bot_token]) + return unless channel + + Telegram::IncomingMessageService.new(inbox: channel.inbox, params: params['telegram'].with_indifferent_access).perform + end +end diff --git a/app/models/account.rb b/app/models/account.rb index 0cfc14293..817013373 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -51,6 +51,7 @@ class Account < ApplicationRecord has_many :web_widgets, dependent: :destroy, class_name: '::Channel::WebWidget' has_many :email_channels, dependent: :destroy, class_name: '::Channel::Email' has_many :api_channels, dependent: :destroy, class_name: '::Channel::Api' + has_many :telegram_channels, dependent: :destroy, class_name: '::Channel::Telegram' has_many :canned_responses, dependent: :destroy has_many :webhooks, dependent: :destroy has_many :labels, dependent: :destroy diff --git a/app/models/channel/api.rb b/app/models/channel/api.rb index 7899da789..f3d1a9b82 100644 --- a/app/models/channel/api.rb +++ b/app/models/channel/api.rb @@ -19,6 +19,7 @@ class Channel::Api < ApplicationRecord self.table_name = 'channel_api' + EDITABLE_ATTRS = [:webhook_url].freeze validates :account_id, presence: true belongs_to :account diff --git a/app/models/channel/email.rb b/app/models/channel/email.rb index d68136b76..806ae0f08 100644 --- a/app/models/channel/email.rb +++ b/app/models/channel/email.rb @@ -17,6 +17,7 @@ class Channel::Email < ApplicationRecord self.table_name = 'channel_email' + EDITABLE_ATTRS = [:email].freeze validates :account_id, presence: true belongs_to :account diff --git a/app/models/channel/telegram.rb b/app/models/channel/telegram.rb new file mode 100644 index 000000000..3dd66dfd3 --- /dev/null +++ b/app/models/channel/telegram.rb @@ -0,0 +1,89 @@ +# == Schema Information +# +# Table name: channel_telegram +# +# id :bigint not null, primary key +# bot_name :string +# bot_token :string not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :integer not null +# +# Indexes +# +# index_channel_telegram_on_bot_token (bot_token) UNIQUE +# + +class Channel::Telegram < ApplicationRecord + self.table_name = 'channel_telegram' + EDITABLE_ATTRS = [:bot_token].freeze + + has_one :inbox, as: :channel, dependent: :destroy + belongs_to :account + + before_validation :ensure_valid_bot_token, on: :create + validates :account_id, presence: true + validates :bot_token, presence: true, uniqueness: true + before_save :setup_telegram_webhook + + def name + 'Telegram' + end + + def has_24_hour_messaging_window? + false + end + + def telegram_api_url + "https://api.telegram.org/bot#{bot_token}" + end + + def send_message_on_telegram(message, chat_id) + response = HTTParty.post("#{telegram_api_url}/sendMessage", + body: { + chat_id: chat_id, + text: message + }) + + response.parsed_response['result']['message_id'] if response.success? + end + + def get_telegram_profile_image(user_id) + # get profile image from telegram + response = HTTParty.get("#{telegram_api_url}/getUserProfilePhotos", query: { user_id: user_id }) + return nil unless response.success? + + photos = response.parsed_response['result']['photos'] + return if photos.blank? + + get_telegram_file_path(photos.first.last['file_id']) + end + + def get_telegram_file_path(file_id) + response = HTTParty.get("#{telegram_api_url}/getFile", query: { file_id: file_id }) + return nil unless response.success? + + "https://api.telegram.org/file/bot#{bot_token}/#{response.parsed_response['result']['file_path']}" + end + + private + + def ensure_valid_bot_token + response = HTTParty.get("#{telegram_api_url}/getMe") + unless response.success? + errors.add(:bot_token, 'invalid token') + return + end + + self.bot_name = response.parsed_response['result']['username'] + end + + def setup_telegram_webhook + HTTParty.post("#{telegram_api_url}/deleteWebhook") + response = HTTParty.post("#{telegram_api_url}/setWebhook", + body: { + url: "#{ENV['FRONTEND_URL']}/webhooks/telegram/#{bot_token}" + }) + errors.add(:bot_token, 'error setting up the webook') unless response.success? + end +end diff --git a/app/models/channel/web_widget.rb b/app/models/channel/web_widget.rb index 02b85f75a..58f9b9ec3 100644 --- a/app/models/channel/web_widget.rb +++ b/app/models/channel/web_widget.rb @@ -26,8 +26,10 @@ class Channel::WebWidget < ApplicationRecord include FlagShihTzu - self.table_name = 'channel_web_widgets' + EDITABLE_ATTRS = [:website_url, :widget_color, :welcome_title, :welcome_tagline, :reply_time, :pre_chat_form_enabled, + { pre_chat_form_options: [:pre_chat_message, :require_email] }, + { selected_feature_flags: [] }].freeze validates :website_url, presence: true validates :widget_color, presence: true diff --git a/app/models/inbox.rb b/app/models/inbox.rb index ea4a1faec..e6f89ab21 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -78,6 +78,10 @@ class Inbox < ApplicationRecord channel_type == 'Channel::Api' end + def email? + channel_type == 'Channel::Email' + end + def inbox_type channel.name end diff --git a/app/services/telegram/incoming_message_service.rb b/app/services/telegram/incoming_message_service.rb new file mode 100644 index 000000000..0e47f2813 --- /dev/null +++ b/app/services/telegram/incoming_message_service.rb @@ -0,0 +1,106 @@ +class Telegram::IncomingMessageService + include ::FileTypeHelper + pattr_initialize [:inbox!, :params!] + + def perform + set_contact + update_contact_avatar + set_conversation + @message = @conversation.messages.create( + content: params[:message][:text], + account_id: @inbox.account_id, + inbox_id: @inbox.id, + message_type: :incoming, + sender: @contact, + source_id: (params[:message][:message_id]).to_s + ) + attach_files + @message.save! + end + + private + + def account + @account ||= inbox.account + end + + def set_contact + contact_inbox = ::ContactBuilder.new( + source_id: params[:message][:from][:id], + inbox: inbox, + contact_attributes: contact_attributes + ).perform + + @contact_inbox = contact_inbox + @contact = contact_inbox.contact + end + + def update_contact_avatar + return if @contact.avatar.attached? + + avatar_url = inbox.channel.get_telegram_profile_image(params[:message][:from][:id]) + ::ContactAvatarJob.perform_later(@contact, avatar_url) if avatar_url + end + + def conversation_params + { + account_id: @inbox.account_id, + inbox_id: @inbox.id, + contact_id: @contact.id, + contact_inbox_id: @contact_inbox.id, + additional_attributes: conversation_additional_attributes + } + end + + def set_conversation + @conversation = @contact_inbox.conversations.first + return if @conversation + + @conversation = ::Conversation.create!(conversation_params) + end + + def contact_attributes + { + name: "#{params[:message][:from][:first_name]} #{params[:message][:from][:last_name]}", + additional_attributes: additional_attributes + } + end + + def additional_attributes + { + username: params[:message][:from][:username], + language_code: params[:message][:from][:language_code] + } + end + + def conversation_additional_attributes + { + chat_id: params[:message][:chat][:id] + } + end + + def file_content_type + params[:message][:photo].present? ? :image : file_type(params[:message][:document][:mime_type]) + end + + def attach_files + file = params[:message][:document] + file ||= params[:message][:photo]&.last + + return unless file + + attachment_file = Down.download( + inbox.channel.get_telegram_file_path(file[:file_id]) + ) + + @message.attachments.new( + account_id: @message.account_id, + file_type: file_content_type, + file: { + io: attachment_file, + filename: attachment_file.original_filename, + content_type: attachment_file.content_type + } + ) + end +end diff --git a/app/services/telegram/send_on_telegram_service.rb b/app/services/telegram/send_on_telegram_service.rb new file mode 100644 index 000000000..e6e98ff39 --- /dev/null +++ b/app/services/telegram/send_on_telegram_service.rb @@ -0,0 +1,22 @@ +class Telegram::SendOnTelegramService < Base::SendOnChannelService + private + + def channel_class + Channel::Telegram + end + + def perform_reply + ## send reply to telegram message api + # https://core.telegram.org/bots/api#sendmessage + message_id = channel.send_message_on_telegram(message.content, conversation[:additional_attributes]['chat_id']) + message.update!(source_id: message_id) if message_id.present? + end + + def inbox + @inbox ||= message.inbox + end + + def channel + @channel ||= inbox.channel + end +end diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder index 55ea31e3c..139831b1e 100644 --- a/app/views/api/v1/models/_inbox.json.jbuilder +++ b/app/views/api/v1/models/_inbox.json.jbuilder @@ -29,4 +29,6 @@ if resource.web_widget? json.pre_chat_form_enabled resource.channel.try(:pre_chat_form_enabled) json.pre_chat_form_options resource.channel.try(:pre_chat_form_options) end +json.email resource.channel.try(:email) if resource.email? +json.webhook_url resource.channel.try(:webhook_url) if resource.api? json.inbox_identifier resource.channel.try(:identifier) if resource.api? diff --git a/config/routes.rb b/config/routes.rb index 836acee04..a3e543074 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -242,6 +242,7 @@ Rails.application.routes.draw do mount Facebook::Messenger::Server, at: 'bot' get 'webhooks/twitter', to: 'api/v1/webhooks#twitter_crc' post 'webhooks/twitter', to: 'api/v1/webhooks#twitter_events' + post 'webhooks/telegram/:bot_token', to: 'webhooks/telegram#process_payload' namespace :twitter do resource :callback, only: [:show] diff --git a/db/migrate/20210828124043_add_telegram_channel.rb b/db/migrate/20210828124043_add_telegram_channel.rb new file mode 100644 index 000000000..58466550c --- /dev/null +++ b/db/migrate/20210828124043_add_telegram_channel.rb @@ -0,0 +1,10 @@ +class AddTelegramChannel < ActiveRecord::Migration[6.1] + def change + create_table :channel_telegram do |t| + t.string :bot_name + t.integer :account_id, null: false + t.string :bot_token, null: false, index: { unique: true } + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 86bb50653..5b0e0366b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_08_27_120929) do +ActiveRecord::Schema.define(version: 2021_08_28_124043) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" @@ -185,6 +185,15 @@ ActiveRecord::Schema.define(version: 2021_08_27_120929) do t.index ["page_id"], name: "index_channel_facebook_pages_on_page_id" end + create_table "channel_telegram", force: :cascade do |t| + t.string "bot_name" + t.integer "account_id", null: false + t.string "bot_token", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["bot_token"], name: "index_channel_telegram_on_bot_token", unique: true + end + create_table "channel_twilio_sms", force: :cascade do |t| t.string "phone_number", null: false t.string "auth_token", null: false diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index d769302a2..b4ac8c70c 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -255,16 +255,6 @@ RSpec.describe 'Inboxes API', type: :request do let(:admin) { create(:user, account: account, role: :administrator) } let(:valid_params) { { name: 'test', channel: { type: 'web_widget', website_url: 'test.com' } } } - it 'creates inbox' do - post "/api/v1/accounts/#{account.id}/inboxes", - headers: admin.create_new_auth_token, - params: valid_params, - as: :json - - expect(response).to have_http_status(:success) - expect(response.body).to include('test.com') - end - it 'will not create inbox for agent' do agent = create(:user, account: account, role: :agent) @@ -275,6 +265,26 @@ RSpec.describe 'Inboxes API', type: :request do expect(response).to have_http_status(:unauthorized) end + + it 'creates a webwidget inbox when administrator' do + post "/api/v1/accounts/#{account.id}/inboxes", + headers: admin.create_new_auth_token, + params: valid_params, + as: :json + + expect(response).to have_http_status(:success) + expect(response.body).to include('test.com') + end + + it 'creates a email inbox when administrator' do + post "/api/v1/accounts/#{account.id}/inboxes", + headers: admin.create_new_auth_token, + params: { name: 'test', channel: { type: 'email', email: 'test@test.com' } }, + as: :json + + expect(response).to have_http_status(:success) + expect(response.body).to include('test@test.com') + end end end @@ -314,6 +324,34 @@ RSpec.describe 'Inboxes API', type: :request do expect(inbox.reload.enable_auto_assignment).to be_falsey end + it 'updates api inbox when administrator' do + api_channel = create(:channel_api, account: account) + api_inbox = create(:inbox, channel: api_channel, account: account) + + patch "/api/v1/accounts/#{account.id}/inboxes/#{api_inbox.id}", + headers: admin.create_new_auth_token, + params: { enable_auto_assignment: false, channel: { webhook_url: 'webhook.test' } }, + as: :json + + expect(response).to have_http_status(:success) + expect(api_inbox.reload.enable_auto_assignment).to be_falsey + expect(api_channel.reload.webhook_url).to eq('webhook.test') + end + + it 'updates email inbox when administrator' do + email_channel = create(:channel_email, account: account) + email_inbox = create(:inbox, channel: email_channel, account: account) + + patch "/api/v1/accounts/#{account.id}/inboxes/#{email_inbox.id}", + headers: admin.create_new_auth_token, + params: { enable_auto_assignment: false, channel: { email: 'emailtest@email.test' } }, + as: :json + + expect(response).to have_http_status(:success) + expect(email_inbox.reload.enable_auto_assignment).to be_falsey + expect(email_channel.reload.email).to eq('emailtest@email.test') + end + it 'updates avatar when administrator' do # no avatar before upload expect(inbox.avatar.attached?).to eq(false) diff --git a/spec/controllers/webhooks/telegram_controller_spec.rb b/spec/controllers/webhooks/telegram_controller_spec.rb new file mode 100644 index 000000000..5366b467a --- /dev/null +++ b/spec/controllers/webhooks/telegram_controller_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +RSpec.describe 'Webhooks::TelegramController', type: :request do + describe 'POST /webhooks/telegram/{:bot_token}' do + it 'call the telegram events job with the params' do + allow(Webhooks::TelegramEventsJob).to receive(:perform_later) + expect(Webhooks::TelegramEventsJob).to receive(:perform_later) + post '/webhooks/telegram/random_bot_token', params: { content: 'hello' } + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/factories/channel/channel_telegram.rb b/spec/factories/channel/channel_telegram.rb new file mode 100644 index 000000000..99f332c4d --- /dev/null +++ b/spec/factories/channel/channel_telegram.rb @@ -0,0 +1,16 @@ +FactoryBot.define do + factory :channel_telegram, class: 'Channel::Telegram' do + bot_token { '2324234324' } + account + + before(:create) do |channel_telegram| + # we are skipping some of the validation methods + channel_telegram.define_singleton_method(:ensure_valid_bot_token) { return } + channel_telegram.define_singleton_method(:setup_telegram_webhook) { return } + end + + after(:create) do |channel_telegram| + create(:inbox, channel: channel_telegram, account: channel_telegram.account) + end + end +end diff --git a/spec/jobs/send_reply_job_spec.rb b/spec/jobs/send_reply_job_spec.rb new file mode 100644 index 000000000..b7c27974b --- /dev/null +++ b/spec/jobs/send_reply_job_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +RSpec.describe SendReplyJob, type: :job do + subject(:job) { described_class.perform_later(message) } + + let(:message) { create(:message) } + + it 'enqueues the job' do + expect { job }.to have_enqueued_job(described_class) + .with(message) + .on_queue('high') + end + + context 'when the job is triggered on a new message' do + let(:process_service) { double } + + before do + allow(process_service).to receive(:perform) + end + + it 'calls Facebook::SendOnFacebookService when its facebook message' do + facebook_channel = create(:channel_facebook_page) + facebook_inbox = create(:inbox, channel: facebook_channel) + message = create(:message, conversation: create(:conversation, inbox: facebook_inbox)) + allow(Facebook::SendOnFacebookService).to receive(:new).with(message: message).and_return(process_service) + expect(Facebook::SendOnFacebookService).to receive(:new).with(message: message) + expect(process_service).to receive(:perform) + described_class.perform_now(message.id) + end + + it 'calls ::Twitter::SendOnTwitterService when its twitter message' do + twitter_channel = create(:channel_twitter_profile) + twitter_inbox = create(:inbox, channel: twitter_channel) + message = create(:message, conversation: create(:conversation, inbox: twitter_inbox)) + allow(::Twitter::SendOnTwitterService).to receive(:new).with(message: message).and_return(process_service) + expect(::Twitter::SendOnTwitterService).to receive(:new).with(message: message) + expect(process_service).to receive(:perform) + described_class.perform_now(message.id) + end + + it 'calls ::Twilio::SendOnTwilioService when its twilio message' do + twilio_channel = create(:channel_twilio_sms) + message = create(:message, conversation: create(:conversation, inbox: twilio_channel.inbox)) + allow(::Twilio::SendOnTwilioService).to receive(:new).with(message: message).and_return(process_service) + expect(::Twilio::SendOnTwilioService).to receive(:new).with(message: message) + expect(process_service).to receive(:perform) + described_class.perform_now(message.id) + end + + it 'calls ::Telegram::SendOnTelegramService when its telegram message' do + telegram_channel = create(:channel_telegram) + message = create(:message, conversation: create(:conversation, inbox: telegram_channel.inbox)) + allow(::Telegram::SendOnTelegramService).to receive(:new).with(message: message).and_return(process_service) + expect(::Telegram::SendOnTelegramService).to receive(:new).with(message: message) + expect(process_service).to receive(:perform) + described_class.perform_now(message.id) + end + end +end diff --git a/spec/jobs/webhooks/telegram_events_job_spec.rb b/spec/jobs/webhooks/telegram_events_job_spec.rb new file mode 100644 index 000000000..9f45955e6 --- /dev/null +++ b/spec/jobs/webhooks/telegram_events_job_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +RSpec.describe Webhooks::TelegramEventsJob, type: :job do + subject(:job) { described_class.perform_later(params) } + + let!(:telegram_channel) { create(:channel_telegram) } + let!(:params) { { bot_token: telegram_channel.bot_token, 'telegram' => { test: 'test' } } } + + it 'enqueues the job' do + expect { job }.to have_enqueued_job(described_class) + .with(params) + .on_queue('default') + end + + context 'when invalid params' do + it 'returns nil when no bot_token' do + expect(described_class.perform_now({})).to be_nil + end + + it 'returns nil when invalid bot_token' do + expect(described_class.perform_now({ bot_token: 'invalid' })).to be_nil + end + end + + context 'when valid params' do + it 'calls Telegram::IncomingMessageService' do + process_service = double + allow(Telegram::IncomingMessageService).to receive(:new).and_return(process_service) + allow(process_service).to receive(:perform) + expect(Telegram::IncomingMessageService).to receive(:new).with(inbox: telegram_channel.inbox, + params: params['telegram'].with_indifferent_access) + expect(process_service).to receive(:perform) + described_class.perform_now(params) + end + end +end diff --git a/spec/services/telegram/incoming_message_service_spec.rb b/spec/services/telegram/incoming_message_service_spec.rb new file mode 100644 index 000000000..79e361c79 --- /dev/null +++ b/spec/services/telegram/incoming_message_service_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +describe Telegram::IncomingMessageService do + let!(:telegram_channel) { create(:channel_telegram) } + + describe '#perform' do + context 'when valid text message params' do + it 'creates appropriate conversations, message and contacts' do + params = { + 'update_id' => 2_342_342_343_242, + 'message' => { + 'message_id' => 1, + 'from' => { + 'id' => 23, 'is_bot' => false, 'first_name' => 'Sojan', 'last_name' => 'Jose', 'username' => 'sojan', 'language_code' => 'en' + }, + 'chat' => { 'id' => 23, 'first_name' => 'Sojan', 'last_name' => 'Jose', 'username' => 'sojan', 'type' => 'private' }, + 'date' => 1_631_132_077, 'text' => 'test' + } + }.with_indifferent_access + described_class.new(inbox: telegram_channel.inbox, params: params).perform + expect(telegram_channel.inbox.conversations).not_to eq(0) + expect(Contact.all.first.name).to eq('Sojan Jose') + expect(telegram_channel.inbox.messages.first.content).to eq('test') + end + end + end +end diff --git a/spec/services/telegram/send_on_telegram_service_spec.rb b/spec/services/telegram/send_on_telegram_service_spec.rb new file mode 100644 index 000000000..6dbb3f7c6 --- /dev/null +++ b/spec/services/telegram/send_on_telegram_service_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +describe Telegram::SendOnTelegramService do + describe '#perform' do + context 'when a valid message' do + it 'calls channel.send_message_on_telegram' do + telegram_request = double + telegram_channel = create(:channel_telegram) + message = create(:message, message_type: :outgoing, content: 'test', + conversation: create(:conversation, inbox: telegram_channel.inbox, additional_attributes: { 'chat_id' => '123' })) + allow(HTTParty).to receive(:post).and_return(telegram_request) + allow(telegram_request).to receive(:success?).and_return(true) + allow(telegram_request).to receive(:parsed_response).and_return({ 'result' => { 'message_id' => 'telegram_123' } }) + described_class.new(message: message).perform + expect(message.source_id).to eq('telegram_123') + end + end + end +end