Feature: Twitter DM Integration (#451)

An initial version of twitter integration

Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
Pranav Raj S 2020-02-03 00:39:00 +05:30 committed by GitHub
parent a287c86bc4
commit a9c304f1ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 406 additions and 49 deletions

View file

@ -19,6 +19,10 @@ FB_VERIFY_TOKEN=
FB_APP_SECRET= FB_APP_SECRET=
FB_APP_ID= FB_APP_ID=
#twitter app
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
#mail #mail
MAILER_SENDER_EMAIL=accounts@chatwoot.com MAILER_SENDER_EMAIL=accounts@chatwoot.com
SMTP_PORT=1025 SMTP_PORT=1025

View file

@ -20,6 +20,9 @@ Style/GlobalVars:
Exclude: Exclude:
- 'config/initializers/redis.rb' - 'config/initializers/redis.rb'
- 'lib/redis/alfred.rb' - 'lib/redis/alfred.rb'
- 'app/controllers/api/v1/webhooks_controller.rb'
- 'app/services/twitter/send_reply_service.rb'
- 'spec/services/twitter/send_reply_service_spec.rb'
Metrics/BlockLength: Metrics/BlockLength:
Exclude: Exclude:
- spec/**/* - spec/**/*

View file

@ -61,6 +61,9 @@ gem 'chargebee'
gem 'facebook-messenger' gem 'facebook-messenger'
gem 'telegram-bot-ruby' gem 'telegram-bot-ruby'
gem 'twitter' gem 'twitter'
# twitty will handle subscription of twitter account events
gem 'twitty', git: 'https://github.com/chatwoot/twitty'
# facebook client # facebook client
gem 'koala' gem 'koala'
# Random name generator # Random name generator

View file

@ -1,3 +1,10 @@
GIT
remote: https://github.com/chatwoot/twitty
revision: a005f8f6740fc8d2d3500701e1ab4ab0f1416c26
specs:
twitty (0.1.0)
oauth
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
@ -66,15 +73,15 @@ GEM
activerecord (>= 3.2, < 7.0) activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
ast (2.4.0) ast (2.4.0)
attr_extras (6.2.1) attr_extras (6.2.2)
aws-eventstream (1.0.3) aws-eventstream (1.0.3)
aws-partitions (1.262.0) aws-partitions (1.268.0)
aws-sdk-core (3.86.0) aws-sdk-core (3.89.1)
aws-eventstream (~> 1.0, >= 1.0.2) aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.27.0) aws-sdk-kms (1.28.0)
aws-sdk-core (~> 3, >= 3.71.0) aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.60.1) aws-sdk-s3 (1.60.1)
@ -101,10 +108,10 @@ GEM
bootsnap (1.4.5) bootsnap (1.4.5)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.7.2) brakeman (4.7.2)
browser (2.7.1) browser (3.0.3)
buftok (0.2.0) buftok (0.2.0)
builder (3.2.4) builder (3.2.4)
bullet (6.0.2) bullet (6.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
bundle-audit (0.1.0) bundle-audit (0.1.0)
@ -112,8 +119,8 @@ GEM
bundler-audit (0.6.1) bundler-audit (0.6.1)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 0.18) thor (~> 0.18)
byebug (11.0.1) byebug (11.1.1)
chargebee (2.7.1) chargebee (2.7.3)
json_pure (~> 2.1) json_pure (~> 2.1)
rest-client (>= 1.8, < 3.0) rest-client (>= 1.8, < 3.0)
coderay (1.1.2) coderay (1.1.2)
@ -121,7 +128,7 @@ GEM
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
concurrent-ruby (1.1.5) concurrent-ruby (1.1.5)
connection_pool (2.2.2) connection_pool (2.2.2)
crass (1.0.5) crass (1.0.6)
declarative (0.0.10) declarative (0.0.10)
declarative-option (0.1.0) declarative-option (0.1.0)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
@ -156,14 +163,14 @@ GEM
factory_bot_rails (5.1.1) factory_bot_rails (5.1.1)
factory_bot (~> 5.1.0) factory_bot (~> 5.1.0)
railties (>= 4.2.0) railties (>= 4.2.0)
faker (2.9.0) faker (2.10.1)
i18n (>= 1.6, < 1.8) i18n (>= 1.6, < 2)
faraday (0.17.1) faraday (0.17.3)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
faraday_middleware (0.13.1) faraday_middleware (0.14.0)
faraday (>= 0.7.4, < 1.0) faraday (>= 0.7.4, < 1.0)
ffi (1.11.3) ffi (1.12.1)
foreman (0.86.0) foreman (0.87.0)
globalid (0.4.2) globalid (0.4.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
google-api-client (0.36.4) google-api-client (0.36.4)
@ -174,10 +181,12 @@ GEM
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.0)
signet (~> 0.12) signet (~> 0.12)
google-cloud-core (1.4.1) google-cloud-core (1.5.0)
google-cloud-env (~> 1.0) google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.3.0) google-cloud-env (1.3.0)
faraday (~> 0.11) faraday (~> 0.11)
google-cloud-errors (1.0.0)
google-cloud-storage (1.25.1) google-cloud-storage (1.25.1)
addressable (~> 2.5) addressable (~> 2.5)
digest-crc (~> 0.4) digest-crc (~> 0.4)
@ -202,13 +211,13 @@ GEM
http-accept (1.7.0) http-accept (1.7.0)
http-cookie (1.0.3) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.1.1) http-form_data (2.2.0)
http_parser.rb (0.6.0) http_parser.rb (0.6.0)
httparty (0.17.3) httparty (0.17.3)
mime-types (~> 3.0) mime-types (~> 3.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.7.0) i18n (1.8.2)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
ice_nine (0.11.2) ice_nine (0.11.2)
inflecto (0.0.2) inflecto (0.0.2)
@ -253,14 +262,14 @@ GEM
memoizable (0.4.2) memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
method_source (0.9.2) method_source (0.9.2)
mime-types (3.3) mime-types (3.3.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2019.1009) mime-types-data (3.2019.1009)
mimemagic (0.3.3) mimemagic (0.3.3)
mini_magick (4.9.5) mini_magick (4.10.1)
mini_mime (1.0.2) mini_mime (1.0.2)
mini_portile2 (2.4.0) mini_portile2 (2.4.0)
minitest (5.13.0) minitest (5.14.0)
mock_redis (0.22.0) mock_redis (0.22.0)
msgpack (1.3.1) msgpack (1.3.1)
multi_json (1.14.1) multi_json (1.14.1)
@ -272,28 +281,29 @@ GEM
nio4r (2.5.2) nio4r (2.5.2)
nokogiri (1.10.7) nokogiri (1.10.7)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
oauth (0.5.4)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.0.1) os (1.0.1)
parallel (1.19.1) parallel (1.19.1)
parser (2.6.5.0) parser (2.7.0.2)
ast (~> 2.4.0) ast (~> 2.4.0)
pg (1.1.4) pg (1.2.2)
pry (0.12.2) pry (0.12.2)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.9.0) method_source (~> 0.9.0)
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (4.0.1) public_suffix (4.0.3)
puma (4.3.1) puma (4.3.1)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.1.0) pundit (2.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
rack (2.0.8) rack (2.1.1)
rack-cache (1.10.0) rack-cache (1.11.0)
rack (>= 0.4) rack (>= 0.4)
rack-cors (1.1.0) rack-cors (1.1.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-protection (2.0.7) rack-protection (2.0.8.1)
rack rack
rack-proxy (0.6.5) rack-proxy (0.6.5)
rack rack
@ -328,7 +338,7 @@ GEM
rainbow (3.0.0) rainbow (3.0.0)
rake (13.0.1) rake (13.0.1)
rb-fsevent (0.10.3) rb-fsevent (0.10.3)
rb-inotify (0.10.0) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
redis (4.1.3) redis (4.1.3)
redis-namespace (1.7.0) redis-namespace (1.7.0)
@ -351,33 +361,33 @@ GEM
mime-types (>= 1.16, < 4.0) mime-types (>= 1.16, < 4.0)
netrc (~> 0.8) netrc (~> 0.8)
retriable (3.1.2) retriable (3.1.2)
rspec-core (3.9.0) rspec-core (3.9.1)
rspec-support (~> 3.9.0) rspec-support (~> 3.9.1)
rspec-expectations (3.9.0) rspec-expectations (3.9.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0) rspec-support (~> 3.9.0)
rspec-mocks (3.9.0) rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0) rspec-support (~> 3.9.0)
rspec-rails (4.0.0.beta3) rspec-rails (4.0.0.beta4)
actionpack (>= 4.2) actionpack (>= 4.2)
activesupport (>= 4.2) activesupport (>= 4.2)
railties (>= 4.2) railties (>= 4.2)
rspec-core (~> 3.8) rspec-core (~> 3.9)
rspec-expectations (~> 3.8) rspec-expectations (~> 3.9)
rspec-mocks (~> 3.8) rspec-mocks (~> 3.9)
rspec-support (~> 3.8) rspec-support (~> 3.9)
rspec-support (3.9.0) rspec-support (3.9.2)
rubocop (0.78.0) rubocop (0.79.0)
jaro_winkler (~> 1.5.1) jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.6) parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7) unicode-display_width (>= 1.4.0, < 1.7)
rubocop-performance (1.5.1) rubocop-performance (1.5.2)
rubocop (>= 0.71.0) rubocop (>= 0.71.0)
rubocop-rails (2.4.0) rubocop-rails (2.4.1)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 0.72.0) rubocop (>= 0.72.0)
rubocop-rspec (1.37.1) rubocop-rspec (1.37.1)
@ -388,7 +398,7 @@ GEM
activesupport (>= 4) activesupport (>= 4)
sentry-raven (2.13.0) sentry-raven (2.13.0)
faraday (>= 0.7.6, < 1.0) faraday (>= 0.7.6, < 1.0)
shoulda-matchers (4.1.2) shoulda-matchers (4.2.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
sidekiq (6.0.4) sidekiq (6.0.4)
connection_pool (>= 2.2.2) connection_pool (>= 2.2.2)
@ -437,7 +447,7 @@ GEM
multipart-post (~> 2.0) multipart-post (~> 2.0)
naught (~> 1.0) naught (~> 1.0)
simple_oauth (~> 0.3.0) simple_oauth (~> 0.3.0)
tzinfo (1.2.5) tzinfo (1.2.6)
thread_safe (~> 0.1) thread_safe (~> 0.1)
tzinfo-data (1.2019.3) tzinfo-data (1.2019.3)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
@ -447,7 +457,7 @@ GEM
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.6) unf_ext (0.0.7.6)
unicode-display_width (1.6.0) unicode-display_width (1.6.1)
uniform_notifier (1.13.0) uniform_notifier (1.13.0)
valid_email2 (3.1.3) valid_email2 (3.1.3)
activemodel (>= 3.2) activemodel (>= 3.2)
@ -535,6 +545,7 @@ DEPENDENCIES
telegram-bot-ruby telegram-bot-ruby
time_diff time_diff
twitter twitter
twitty!
tzinfo-data tzinfo-data
uglifier uglifier
valid_email2 valid_email2

View file

@ -3,7 +3,7 @@ class Api::V1::WebhooksController < ApplicationController
skip_before_action :set_current_user skip_before_action :set_current_user
skip_before_action :check_subscription skip_before_action :check_subscription
before_action :login_from_basic_auth before_action :login_from_basic_auth, only: [:chargebee]
def chargebee def chargebee
chargebee_consumer.consume chargebee_consumer.consume
head :ok head :ok
@ -12,6 +12,18 @@ class Api::V1::WebhooksController < ApplicationController
head :ok head :ok
end end
def twitter_crc
render json: { response_token: "sha256=#{$twitter.generate_crc(params[:crc_token])}" }
end
def twitter_events
twitter_consumer.consume
head :ok
rescue StandardError => e
Raven.capture_exception(e)
head :ok
end
private private
def login_from_basic_auth def login_from_basic_auth
@ -21,6 +33,10 @@ class Api::V1::WebhooksController < ApplicationController
end end
def chargebee_consumer def chargebee_consumer
@consumer ||= ::Webhooks::Chargebee.new(params) @chargebee_consumer ||= ::Webhooks::Chargebee.new(params)
end
def twitter_consumer
@twitter_consumer ||= ::Webhooks::Twitter.new(params)
end end
end end

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -27,6 +27,13 @@
class="source-badge user--online" class="source-badge user--online"
:style="statusStyle" :style="statusStyle"
></div> ></div>
<img
v-if="badge === 'Channel::TwitterProfile' && status !== ''"
id="badge"
class="source-badge"
:style="badgeStyle"
src="~dashboard/assets/images/twitter-badge.png"
/>
</div> </div>
</template> </template>
<script> <script>

View file

@ -0,0 +1,49 @@
# == Schema Information
#
# Table name: channel_twitter_profiles
#
# id :bigint not null, primary key
# name :string
# twitter_access_token :string not null
# twitter_access_token_secret :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
# profile_id :string not null
#
class Channel::TwitterProfile < ApplicationRecord
self.table_name = 'channel_twitter_profiles'
validates :account_id, presence: true
validates :profile_id, uniqueness: { scope: :account_id }
has_one_attached :avatar
belongs_to :account
has_one :inbox, as: :channel, dependent: :destroy
before_destroy :unsubscribe
def name
'Twitter'
end
def create_contact_inbox(profile_id, name)
ActiveRecord::Base.transaction do
contact = inbox.account.contacts.create!(name: name)
::ContactInbox.create!(
contact_id: contact.id,
inbox_id: inbox.id,
source_id: profile_id
)
rescue StandardError => e
Rails.logger e
end
end
private
def unsubscribe
# to implement
end
end

View file

@ -82,7 +82,12 @@ class Message < ApplicationRecord
end end
def send_reply def send_reply
::Facebook::SendReplyService.new(message: self).perform channel_name = conversation.inbox.channel.class.to_s
if channel_name == 'Channel::FacebookPage'
::Facebook::SendReplyService.new(message: self).perform
elsif channel_name == 'Channel::TwitterProfile'
::Twitter::SendReplyService.new(message: self).perform
end
end end
def reopen_conversation def reopen_conversation

View file

@ -0,0 +1,24 @@
class Twitter::SendReplyService
pattr_initialize [:message!]
def perform
return if message.private
return if inbox.channel.class.to_s != 'Channel::TwitterProfile'
return unless outgoing_message_from_chatwoot?
$twitter.send_direct_message(
recipient_id: contact_inbox.source_id,
message: message.content
)
end
private
def outgoing_message_from_chatwoot?
message.outgoing?
end
delegate :contact_inbox, to: :conversation
delegate :conversation, to: :message
delegate :inbox, to: :conversation
end

View file

@ -0,0 +1,8 @@
$twitter = Twitty::Facade.new do |config|
config.consumer_key = ENV.fetch('TWITTER_CONSUMER_KEY', nil)
config.consumer_secret = ENV.fetch('TWITTER_CONSUMER_SECRET', nil)
config.access_token = ENV.fetch('TWITTER_ACCESS_TOKEN', nil)
config.access_token_secret = ENV.fetch('TWITTER_ACCESS_TOKEN_SECRET', nil)
config.base_url = 'https://api.twitter.com'
config.environment = 'chatwootstaging'
end

View file

@ -110,6 +110,9 @@ Rails.application.routes.draw do
end end
mount Facebook::Messenger::Server, at: 'bot' 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/:account_id/:inbox_id' => 'home#telegram' post '/webhooks/telegram/:account_id/:inbox_id' => 'home#telegram'
# Routes for testing # Routes for testing

View file

@ -0,0 +1,12 @@
class CreateChannelTwitterProfiles < ActiveRecord::Migration[6.0]
def change
create_table :channel_twitter_profiles do |t|
t.string :name
t.string :profile_id, null: false
t.string :twitter_access_token, null: false
t.string :twitter_access_token_secret, null: false
t.integer :account_id, null: false
t.timestamps
end
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_01_07_164449) do ActiveRecord::Schema.define(version: 2020_01_24_190208) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -75,6 +75,16 @@ ActiveRecord::Schema.define(version: 2020_01_07_164449) do
t.index ["page_id"], name: "index_channel_facebook_pages_on_page_id" t.index ["page_id"], name: "index_channel_facebook_pages_on_page_id"
end end
create_table "channel_twitter_profiles", force: :cascade do |t|
t.string "name"
t.string "profile_id", null: false
t.string "twitter_access_token", null: false
t.string "twitter_access_token_secret", null: false
t.integer "account_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "channel_web_widgets", id: :serial, force: :cascade do |t| create_table "channel_web_widgets", id: :serial, force: :cascade do |t|
t.string "website_name" t.string "website_name"
t.string "website_url" t.string "website_url"
@ -190,9 +200,11 @@ ActiveRecord::Schema.define(version: 2020_01_07_164449) do
t.index ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context" t.index ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context"
t.index ["taggable_id", "taggable_type", "tagger_id", "context"], name: "taggings_idy" t.index ["taggable_id", "taggable_type", "tagger_id", "context"], name: "taggings_idy"
t.index ["taggable_id"], name: "index_taggings_on_taggable_id" t.index ["taggable_id"], name: "index_taggings_on_taggable_id"
t.index ["taggable_type", "taggable_id"], name: "index_taggings_on_taggable_type_and_taggable_id"
t.index ["taggable_type"], name: "index_taggings_on_taggable_type" t.index ["taggable_type"], name: "index_taggings_on_taggable_type"
t.index ["tagger_id", "tagger_type"], name: "index_taggings_on_tagger_id_and_tagger_type" t.index ["tagger_id", "tagger_type"], name: "index_taggings_on_tagger_id_and_tagger_type"
t.index ["tagger_id"], name: "index_taggings_on_tagger_id" t.index ["tagger_id"], name: "index_taggings_on_tagger_id"
t.index ["tagger_type", "tagger_id"], name: "index_taggings_on_tagger_type_and_tagger_id"
end end
create_table "tags", id: :serial, force: :cascade do |t| create_table "tags", id: :serial, force: :cascade do |t|

85
lib/webhooks/twitter.rb Normal file
View file

@ -0,0 +1,85 @@
# frozen_string_literal: true
class Webhooks::Twitter
SUPPORTED_EVENTS = [:direct_message_events].freeze
attr_accessor :params, :account
def initialize(params)
@params = params
end
def consume
send(event_name) if event_name
end
private
def event_name
@event_name ||= SUPPORTED_EVENTS.find { |key| @params.key?(key.to_s) }
end
def users
@params[:users]
end
def profile_id
@params[:for_user_id]
end
def set_inbox
twitter_profile = Channel::TwitterProfile.find_by(profile_id: @params[:for_user_id])
@inbox = Inbox.find_by!(channel: twitter_profile)
end
def ensure_contacts
@params[:users].each do |key, user|
next if key == profile_id
find_or_create_contact(user)
end
end
def find_or_create_contact(user)
@contact_inbox = @inbox.contact_inboxes.where(source_id: user['id']).first
@contact = @contact_inbox.contact if @contact_inbox
return if @contact
@contact_inbox = @inbox.channel.create_contact_inbox(user['id'], user['name'])
@contact = @contact_inbox.contact
avatar_resource = LocalResource.new(user['profile_image_url'])
@contact.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
end
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: @contact.id,
contact_inbox_id: @contact_inbox.id
}
end
def set_conversation
@conversation = @contact_inbox.conversations.first
return if @conversation
@conversation = ::Conversation.create!(conversation_params)
end
def outgoing_message?
@params['direct_message_events'].first['message_create']['sender_id'] == @inbox.channel.profile_id
end
def direct_message_events
set_inbox
ensure_contacts
set_conversation
@conversation.messages.create(
content: @params['direct_message_events'].first['message_create']['message_data']['text'],
account_id: @inbox.account_id,
inbox_id: @inbox.id,
message_type: outgoing_message? ? :outgoing : :incoming
)
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
FactoryBot.define do
factory :channel_twitter_profile, class: 'Channel::TwitterProfile' do
name { Faker::Name.name }
twitter_access_token { SecureRandom.uuid }
twitter_access_token_secret { SecureRandom.uuid }
profile_id { SecureRandom.uuid }
account
end
end

View file

@ -0,0 +1,37 @@
# frozen_string_literal: true
FactoryBot.define do
factory :twitter_message_create_event, class: Hash do
for_user_id { '1' }
direct_message_events do
[{
'type' => 'message_create',
'id' => '123',
'message_create' => {
target: { 'recipient_id' => '1' },
'sender_id' => '2',
'source_app_id' => '268278',
'message_data' => {
'text' => 'Blue Bird'
}
}
}]
end
users do
{
'1' => {
id: '1',
name: 'person 1',
profile_image_url: 'https://via.placeholder.com/250x250.png'
},
'2' => {
id: '1',
name: 'person 1',
profile_image_url: 'https://via.placeholder.com/250x250.png'
}
}
end
initialize_with { attributes }
end
end

View file

@ -0,0 +1,23 @@
require 'rails_helper'
require 'webhooks/twitter'
describe Webhooks::Twitter do
subject(:process_twitter_event) { described_class.new(params).consume }
let!(:account) { create(:account) }
# FIX ME: recipient id is set to 1 inside event factories
let!(:twitter_channel) { create(:channel_twitter_profile, account: account, profile_id: '1') }
let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account) }
let!(:params) { build(:twitter_message_create_event).with_indifferent_access }
describe '#perform' do
context 'with correct params' do
it 'creates incoming message in the twitter inbox' do
process_twitter_event
expect(twitter_inbox.contacts.count).to be 1
expect(twitter_inbox.conversations.count).to be 1
expect(twitter_inbox.messages.count).to be 1
end
end
end
end

View file

@ -0,0 +1,44 @@
require 'rails_helper'
describe Twitter::SendReplyService do
subject(:send_reply_service) { described_class.new(message: message) }
before do
allow($twitter).to receive(:send_direct_message).and_return(true)
end
let!(:account) { create(:account) }
let!(:widget_inbox) { create(:inbox, account: account) }
let!(:twitter_channel) { create(:channel_twitter_profile, account: account) }
let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account) }
let!(:contact) { create(:contact, account: account) }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: twitter_inbox) }
let(:conversation) { create(:conversation, contact: contact, inbox: twitter_inbox, contact_inbox: contact_inbox) }
describe '#perform' do
context 'without reply' do
it 'if message is private' do
create(:message, message_type: 'outgoing', private: true, inbox: twitter_inbox, account: account)
expect($twitter).not_to have_received(:send_direct_message)
end
it 'if inbox channel is not twitter profile' do
create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account)
expect($twitter).not_to have_received(:send_direct_message)
end
it 'if message is not outgoing' do
create(:message, message_type: 'incoming', inbox: twitter_inbox, account: account)
expect($twitter).not_to have_received(:send_direct_message)
end
end
context 'with reply' do
it 'if message is sent from chatwoot and is outgoing' do
create(:message, message_type: :incoming, inbox: twitter_inbox, account: account, conversation: conversation)
create(:message, message_type: 'outgoing', inbox: twitter_inbox, account: account, conversation: conversation)
expect($twitter).to have_received(:send_direct_message)
end
end
end
end