Feature: Create conversations from Tweets (#470)
* Feature: Add tweets to conversations
This commit is contained in:
parent
3465ebefd1
commit
272c481464
29 changed files with 333 additions and 68 deletions
|
@ -20,9 +20,6 @@ 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/**/*
|
||||||
|
|
|
@ -2,7 +2,7 @@ require 'open-uri'
|
||||||
|
|
||||||
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
|
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
|
||||||
# Assumptions
|
# Assumptions
|
||||||
# 1. Incase of an outgoing message which is echo, fb_id will NOT be nil,
|
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
|
||||||
# based on this we are showing "not sent from chatwoot" message in frontend
|
# 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.
|
# Hence there is no need to set user_id in message for outgoing echo messages.
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ class Messages::MessageBuilder
|
||||||
inbox_id: conversation.inbox_id,
|
inbox_id: conversation.inbox_id,
|
||||||
message_type: @message_type,
|
message_type: @message_type,
|
||||||
content: response.content,
|
content: response.content,
|
||||||
fb_id: response.identifier
|
source_id: response.identifier
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class Messages::Outgoing::NormalBuilder
|
||||||
content: @content,
|
content: @content,
|
||||||
private: @private,
|
private: @private,
|
||||||
user_id: @user.id,
|
user_id: @user.id,
|
||||||
fb_id: @fb_id
|
source_id: @fb_id
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ class Api::V1::WebhooksController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def twitter_crc
|
def twitter_crc
|
||||||
render json: { response_token: "sha256=#{$twitter.generate_crc(params[:crc_token])}" }
|
render json: { response_token: "sha256=#{twitter_client.generate_crc(params[:crc_token])}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
def twitter_events
|
def twitter_events
|
||||||
|
@ -26,6 +26,12 @@ class Api::V1::WebhooksController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def twitter_client
|
||||||
|
Twitty::Facade.new do |config|
|
||||||
|
config.consumer_secret = ENV.fetch('TWITTER_CONSUMER_SECRET', nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def login_from_basic_auth
|
def login_from_basic_auth
|
||||||
authenticate_or_request_with_http_basic do |username, password|
|
authenticate_or_request_with_http_basic do |username, password|
|
||||||
username == ENV['CHARGEBEE_WEBHOOK_USERNAME'] && password == ENV['CHARGEBEE_WEBHOOK_PASSWORD']
|
username == ENV['CHARGEBEE_WEBHOOK_USERNAME'] && password == ENV['CHARGEBEE_WEBHOOK_PASSWORD']
|
||||||
|
|
|
@ -16,19 +16,19 @@
|
||||||
:size="avatarSize"
|
:size="avatarSize"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
v-if="badge === 'Channel::FacebookPage' && status !== ''"
|
v-if="badge === 'Channel::FacebookPage'"
|
||||||
id="badge"
|
id="badge"
|
||||||
class="source-badge"
|
class="source-badge"
|
||||||
:style="badgeStyle"
|
:style="badgeStyle"
|
||||||
src="~dashboard/assets/images/fb-badge.png"
|
src="~dashboard/assets/images/fb-badge.png"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else-if="status === 'online'"
|
v-else-if="badge === 'Channel::WebWidget' && status === 'online'"
|
||||||
class="source-badge user--online"
|
class="source-badge user--online"
|
||||||
:style="statusStyle"
|
:style="statusStyle"
|
||||||
></div>
|
></div>
|
||||||
<img
|
<img
|
||||||
v-if="badge === 'Channel::TwitterProfile' && status !== ''"
|
v-if="badge === 'Channel::TwitterProfile'"
|
||||||
id="badge"
|
id="badge"
|
||||||
class="source-badge"
|
class="source-badge"
|
||||||
:style="badgeStyle"
|
:style="badgeStyle"
|
||||||
|
|
|
@ -20,6 +20,16 @@
|
||||||
>
|
>
|
||||||
{{ contact.email }}
|
{{ contact.email }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
contact.additional_attributes &&
|
||||||
|
contact.additional_attributes.screen_name
|
||||||
|
"
|
||||||
|
class="contact--location"
|
||||||
|
>
|
||||||
|
{{ `@${contact.additional_attributes.screen_name}` }}
|
||||||
|
</div>
|
||||||
<div class="contact--location">
|
<div class="contact--location">
|
||||||
{{ contact.location }}
|
{{ contact.location }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,6 +38,15 @@
|
||||||
<div v-if="contact.bio" class="contact--bio">
|
<div v-if="contact.bio" class="contact--bio">
|
||||||
{{ contact.bio }}
|
{{ contact.bio }}
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
contact.additional_attributes &&
|
||||||
|
contact.additional_attributes.description
|
||||||
|
"
|
||||||
|
class="contact--bio"
|
||||||
|
>
|
||||||
|
{{ contact.additional_attributes.description }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="browser" class="conversation--details">
|
<div v-if="browser" class="conversation--details">
|
||||||
<contact-details-item
|
<contact-details-item
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default [
|
||||||
private: false,
|
private: false,
|
||||||
user_id: 1,
|
user_id: 1,
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
fb_id: null,
|
source_id: null,
|
||||||
content_type: 'text',
|
content_type: 'text',
|
||||||
content_attributes: {},
|
content_attributes: {},
|
||||||
sender: {
|
sender: {
|
||||||
|
@ -63,7 +63,7 @@ export default [
|
||||||
private: false,
|
private: false,
|
||||||
user_id: 2,
|
user_id: 2,
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
fb_id: null,
|
source_id: null,
|
||||||
content_type: 'text',
|
content_type: 'text',
|
||||||
content_attributes: {},
|
content_attributes: {},
|
||||||
sender: {
|
sender: {
|
||||||
|
|
|
@ -28,9 +28,9 @@ class Channel::TwitterProfile < ApplicationRecord
|
||||||
'Twitter'
|
'Twitter'
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_contact_inbox(profile_id, name)
|
def create_contact_inbox(profile_id, name, additional_attributes)
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
contact = inbox.account.contacts.create!(name: name)
|
contact = inbox.account.contacts.create!(additional_attributes: additional_attributes, name: name)
|
||||||
::ContactInbox.create!(
|
::ContactInbox.create!(
|
||||||
contact_id: contact.id,
|
contact_id: contact.id,
|
||||||
inbox_id: inbox.id,
|
inbox_id: inbox.id,
|
||||||
|
|
|
@ -2,14 +2,15 @@
|
||||||
#
|
#
|
||||||
# Table name: contacts
|
# Table name: contacts
|
||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
# email :string
|
# additional_attributes :jsonb
|
||||||
# name :string
|
# email :string
|
||||||
# phone_number :string
|
# name :string
|
||||||
# pubsub_token :string
|
# phone_number :string
|
||||||
# created_at :datetime not null
|
# pubsub_token :string
|
||||||
# updated_at :datetime not null
|
# created_at :datetime not null
|
||||||
# account_id :integer not null
|
# updated_at :datetime not null
|
||||||
|
# account_id :integer not null
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|
|
@ -12,14 +12,21 @@
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# account_id :integer not null
|
# account_id :integer not null
|
||||||
|
# contact_id :bigint
|
||||||
# conversation_id :integer not null
|
# conversation_id :integer not null
|
||||||
# fb_id :string
|
|
||||||
# inbox_id :integer not null
|
# inbox_id :integer not null
|
||||||
|
# source_id :string
|
||||||
# user_id :integer
|
# user_id :integer
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
# index_messages_on_contact_id (contact_id)
|
||||||
# index_messages_on_conversation_id (conversation_id)
|
# index_messages_on_conversation_id (conversation_id)
|
||||||
|
# index_messages_on_source_id (source_id)
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (contact_id => contacts.id)
|
||||||
#
|
#
|
||||||
|
|
||||||
class Message < ApplicationRecord
|
class Message < ApplicationRecord
|
||||||
|
|
|
@ -22,8 +22,8 @@ class Facebook::SendReplyService
|
||||||
end
|
end
|
||||||
|
|
||||||
def outgoing_message_from_chatwoot?
|
def outgoing_message_from_chatwoot?
|
||||||
# messages sent directly from chatwoot won't have fb_id.
|
# messages sent directly from chatwoot won't have source_id.
|
||||||
message.outgoing? && !message.fb_id
|
message.outgoing? && !message.source_id
|
||||||
end
|
end
|
||||||
|
|
||||||
# def reopen_lock
|
# def reopen_lock
|
||||||
|
|
|
@ -3,22 +3,60 @@ class Twitter::SendReplyService
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
return if message.private
|
return if message.private
|
||||||
|
return if message.source_id
|
||||||
return if inbox.channel.class.to_s != 'Channel::TwitterProfile'
|
return if inbox.channel.class.to_s != 'Channel::TwitterProfile'
|
||||||
return unless outgoing_message_from_chatwoot?
|
return unless outgoing_message_from_chatwoot?
|
||||||
|
|
||||||
$twitter.send_direct_message(
|
send_reply
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def twitter_client
|
||||||
|
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 = channel.twitter_access_token
|
||||||
|
config.access_token_secret = channel.twitter_access_token_secret
|
||||||
|
config.base_url = 'https://api.twitter.com'
|
||||||
|
config.environment = ENV.fetch('TWITTER_ENVIRONMENT', '')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversation_type
|
||||||
|
conversation.additional_attributes['type']
|
||||||
|
end
|
||||||
|
|
||||||
|
def screen_name
|
||||||
|
"@#{additional_attributes ? additional_attributes['screen_name'] : ''} "
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_direct_message
|
||||||
|
twitter_client.send_direct_message(
|
||||||
recipient_id: contact_inbox.source_id,
|
recipient_id: contact_inbox.source_id,
|
||||||
message: message.content
|
message: message.content
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def send_tweet_reply
|
||||||
|
twitter_client.send_tweet_reply(
|
||||||
|
reply_to_tweet_id: conversation.additional_attributes['tweet_id'],
|
||||||
|
tweet: screen_name + message.content
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_reply
|
||||||
|
conversation_type == 'tweet' ? send_tweet_reply : send_direct_message
|
||||||
|
end
|
||||||
|
|
||||||
def outgoing_message_from_chatwoot?
|
def outgoing_message_from_chatwoot?
|
||||||
message.outgoing?
|
message.outgoing?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
delegate :additional_attributes, to: :contact
|
||||||
|
delegate :contact, to: :conversation
|
||||||
delegate :contact_inbox, to: :conversation
|
delegate :contact_inbox, to: :conversation
|
||||||
delegate :conversation, to: :message
|
delegate :conversation, to: :message
|
||||||
delegate :inbox, to: :conversation
|
delegate :inbox, to: :conversation
|
||||||
|
delegate :channel, to: :inbox
|
||||||
end
|
end
|
||||||
|
|
69
app/services/twitter/tweet_parser_service.rb
Normal file
69
app/services/twitter/tweet_parser_service.rb
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
class Twitter::TweetParserService < Twitter::WebhooksBaseService
|
||||||
|
pattr_initialize [:payload]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
set_inbox
|
||||||
|
find_or_create_contact(user)
|
||||||
|
set_conversation
|
||||||
|
@conversation.messages.create(
|
||||||
|
account_id: @inbox.account_id,
|
||||||
|
contact_id: @contact.id,
|
||||||
|
content: tweet_text,
|
||||||
|
inbox_id: @inbox.id,
|
||||||
|
message_type: message_type,
|
||||||
|
source_id: tweet_data['id'].to_s
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def message_type
|
||||||
|
user['id'] == profile_id ? :outgoing : :incoming
|
||||||
|
end
|
||||||
|
|
||||||
|
def tweet_text
|
||||||
|
tweet_data['truncated'] ? tweet_data['extended_tweet']['full_text'] : tweet_data['text']
|
||||||
|
end
|
||||||
|
|
||||||
|
def tweet_create_events_params
|
||||||
|
payload['tweet_create_events']
|
||||||
|
end
|
||||||
|
|
||||||
|
def tweet_data
|
||||||
|
tweet_create_events_params.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def user
|
||||||
|
tweet_data['user']
|
||||||
|
end
|
||||||
|
|
||||||
|
def parent_tweet_id
|
||||||
|
tweet_data['in_reply_to_status_id_str'].nil? ? tweet_data['id'].to_s : tweet_data['in_reply_to_status_id_str']
|
||||||
|
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: {
|
||||||
|
type: 'tweet',
|
||||||
|
tweet_id: parent_tweet_id,
|
||||||
|
tweet_source: tweet_data['source']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_conversation
|
||||||
|
tweet_conversations = @contact_inbox.conversations.where("additional_attributes ->> 'tweet_id' = ?", parent_tweet_id)
|
||||||
|
@conversation = tweet_conversations.first
|
||||||
|
return if @conversation
|
||||||
|
|
||||||
|
tweet_message = @inbox.messages.find_by(source_id: parent_tweet_id)
|
||||||
|
@conversation = tweet_message.conversation if tweet_message
|
||||||
|
return if @conversation
|
||||||
|
|
||||||
|
@conversation = ::Conversation.create!(conversation_params)
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,6 +5,17 @@ class Twitter::WebhooksBaseService
|
||||||
payload[:for_user_id]
|
payload[:for_user_id]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def additional_contact_attributes(user)
|
||||||
|
{
|
||||||
|
screen_name: user['screen_name'],
|
||||||
|
location: user['location'],
|
||||||
|
url: user['url'],
|
||||||
|
description: user['description'],
|
||||||
|
followers_count: user['followers_count'],
|
||||||
|
friends_count: user['friends_count']
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def set_inbox
|
def set_inbox
|
||||||
twitter_profile = ::Channel::TwitterProfile.find_by(profile_id: profile_id)
|
twitter_profile = ::Channel::TwitterProfile.find_by(profile_id: profile_id)
|
||||||
@inbox = ::Inbox.find_by!(channel: twitter_profile)
|
@inbox = ::Inbox.find_by!(channel: twitter_profile)
|
||||||
|
@ -15,7 +26,9 @@ class Twitter::WebhooksBaseService
|
||||||
@contact = @contact_inbox.contact if @contact_inbox
|
@contact = @contact_inbox.contact if @contact_inbox
|
||||||
return if @contact
|
return if @contact
|
||||||
|
|
||||||
@contact_inbox = @inbox.channel.create_contact_inbox(user['id'], user['name'])
|
@contact_inbox = @inbox.channel.create_contact_inbox(
|
||||||
|
user['id'], user['name'], additional_contact_attributes(user)
|
||||||
|
)
|
||||||
@contact = @contact_inbox.contact
|
@contact = @contact_inbox.contact
|
||||||
avatar_resource = LocalResource.new(user['profile_image_url'])
|
avatar_resource = LocalResource.new(user['profile_image_url'])
|
||||||
@contact.avatar.attach(
|
@contact.avatar.attach(
|
||||||
|
|
|
@ -5,5 +5,6 @@ json.payload do
|
||||||
json.email contact.email
|
json.email contact.email
|
||||||
json.phone_number contact.phone_number
|
json.phone_number contact.phone_number
|
||||||
json.thumbnail contact.avatar.profile_thumb.url
|
json.thumbnail contact.avatar.profile_thumb.url
|
||||||
|
json.additional_attributes contact.additional_attributes
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,4 +5,5 @@ json.payload do
|
||||||
json.name @contact.name
|
json.name @contact.name
|
||||||
json.phone_number @contact.phone_number
|
json.phone_number @contact.phone_number
|
||||||
json.thumbnail @contact.avatar_url
|
json.thumbnail @contact.avatar_url
|
||||||
|
json.additional_attributes @contact.additional_attributes
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,4 +4,5 @@ json.payload do
|
||||||
json.email @contact.email
|
json.email @contact.email
|
||||||
json.phone_number @contact.phone_number
|
json.phone_number @contact.phone_number
|
||||||
json.thumbnail @contact.avatar.thumb.url
|
json.thumbnail @contact.avatar.thumb.url
|
||||||
|
json.additional_attributes @contact.additional_attributes
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,8 +7,9 @@ json.payload do
|
||||||
json.message_type message.message_type_before_type_cast
|
json.message_type message.message_type_before_type_cast
|
||||||
json.created_at message.created_at.to_i
|
json.created_at message.created_at.to_i
|
||||||
json.private message.private
|
json.private message.private
|
||||||
json.fb_id message.fb_id
|
json.source_id message.source_id
|
||||||
json.attachment message.attachment.push_event_data if message.attachment
|
json.attachment message.attachment.push_event_data if message.attachment
|
||||||
json.sender message.user.push_event_data if message.user
|
json.sender message.user.push_event_data if message.user
|
||||||
|
json.contact message.contact
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ json.payload do
|
||||||
json.message_type message.message_type_before_type_cast
|
json.message_type message.message_type_before_type_cast
|
||||||
json.created_at message.created_at.to_i
|
json.created_at message.created_at.to_i
|
||||||
json.private message.private
|
json.private message.private
|
||||||
json.fb_id message.fb_id
|
json.source_id message.source_id
|
||||||
json.attachment message.attachment.push_event_data if message.attachment
|
json.attachment message.attachment.push_event_data if message.attachment
|
||||||
json.sender message.user.push_event_data if message.user
|
json.sender message.user.push_event_data if message.user
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
$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 = ENV.fetch('TWITTER_ENVIRONMENT', '')
|
|
||||||
end
|
|
12
db/migrate/20200206180408_add_contact_to_message.rb
Normal file
12
db/migrate/20200206180408_add_contact_to_message.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
class AddContactToMessage < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
add_reference(:messages, :contact, foreign_key: true, index: true)
|
||||||
|
|
||||||
|
::Message.all.each do |message|
|
||||||
|
conversation = message.conversation
|
||||||
|
next if conversation.contact.nil?
|
||||||
|
|
||||||
|
message.update!(contact_id: conversation.contact.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
7
db/migrate/20200206180927_rename_fb_id_to_source_id.rb
Normal file
7
db/migrate/20200206180927_rename_fb_id_to_source_id.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class RenameFbIdToSourceId < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
rename_column :messages, :fb_id, :source_id
|
||||||
|
|
||||||
|
add_index(:messages, :source_id)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddAdditionalAttributesToContact < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
add_column :contacts, :additional_attributes, :jsonb
|
||||||
|
end
|
||||||
|
end
|
11
db/schema.rb
11
db/schema.rb
|
@ -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_24_190208) do
|
ActiveRecord::Schema.define(version: 2020_02_06_182948) 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"
|
||||||
|
@ -116,6 +116,7 @@ ActiveRecord::Schema.define(version: 2020_01_24_190208) do
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "pubsub_token"
|
t.string "pubsub_token"
|
||||||
|
t.jsonb "additional_attributes"
|
||||||
t.index ["account_id"], name: "index_contacts_on_account_id"
|
t.index ["account_id"], name: "index_contacts_on_account_id"
|
||||||
t.index ["pubsub_token"], name: "index_contacts_on_pubsub_token", unique: true
|
t.index ["pubsub_token"], name: "index_contacts_on_pubsub_token", unique: true
|
||||||
end
|
end
|
||||||
|
@ -168,10 +169,13 @@ ActiveRecord::Schema.define(version: 2020_01_24_190208) do
|
||||||
t.boolean "private", default: false
|
t.boolean "private", default: false
|
||||||
t.integer "user_id"
|
t.integer "user_id"
|
||||||
t.integer "status", default: 0
|
t.integer "status", default: 0
|
||||||
t.string "fb_id"
|
t.string "source_id"
|
||||||
t.integer "content_type", default: 0
|
t.integer "content_type", default: 0
|
||||||
t.json "content_attributes", default: {}
|
t.json "content_attributes", default: {}
|
||||||
|
t.bigint "contact_id"
|
||||||
|
t.index ["contact_id"], name: "index_messages_on_contact_id"
|
||||||
t.index ["conversation_id"], name: "index_messages_on_conversation_id"
|
t.index ["conversation_id"], name: "index_messages_on_conversation_id"
|
||||||
|
t.index ["source_id"], name: "index_messages_on_source_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "subscriptions", id: :serial, force: :cascade do |t|
|
create_table "subscriptions", id: :serial, force: :cascade do |t|
|
||||||
|
@ -200,11 +204,9 @@ ActiveRecord::Schema.define(version: 2020_01_24_190208) 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|
|
||||||
|
@ -258,5 +260,6 @@ ActiveRecord::Schema.define(version: 2020_01_24_190208) do
|
||||||
add_foreign_key "contact_inboxes", "contacts"
|
add_foreign_key "contact_inboxes", "contacts"
|
||||||
add_foreign_key "contact_inboxes", "inboxes"
|
add_foreign_key "contact_inboxes", "inboxes"
|
||||||
add_foreign_key "conversations", "contact_inboxes"
|
add_foreign_key "conversations", "contact_inboxes"
|
||||||
|
add_foreign_key "messages", "contacts"
|
||||||
add_foreign_key "users", "users", column: "inviter_id", on_delete: :nullify
|
add_foreign_key "users", "users", column: "inviter_id", on_delete: :nullify
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Webhooks::Twitter
|
class Webhooks::Twitter
|
||||||
SUPPORTED_EVENTS = [:direct_message_events].freeze
|
SUPPORTED_EVENTS = [:direct_message_events, :tweet_create_events].freeze
|
||||||
|
|
||||||
attr_accessor :params, :account
|
attr_accessor :params, :account
|
||||||
|
|
||||||
|
@ -22,4 +22,8 @@ class Webhooks::Twitter
|
||||||
def direct_message_events
|
def direct_message_events
|
||||||
::Twitter::DirectMessageParserService.new(payload: @params).perform
|
::Twitter::DirectMessageParserService.new(payload: @params).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def tweet_create_events
|
||||||
|
::Twitter::TweetParserService.new(payload: @params).perform
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
47
spec/factories/twitter/tweet_create_event.rb
Normal file
47
spec/factories/twitter/tweet_create_event.rb
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :tweet_create_event, class: Hash do
|
||||||
|
for_user_id { '1' }
|
||||||
|
tweet_create_events do
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'created_at' => 'Wed Feb 05 08:39:31 +0000 2020',
|
||||||
|
'id' => 1,
|
||||||
|
'id_str' => '1',
|
||||||
|
'text' => '@chatwootapp Testing a sample tweet with Twitter client',
|
||||||
|
'source' => '<a href="http://twitter.com/download/iphone" rel="nofollow">Twitter for iPhone</a>',
|
||||||
|
'truncated' => false,
|
||||||
|
'in_reply_to_status_id' => nil,
|
||||||
|
'in_reply_to_status_id_str' => nil,
|
||||||
|
'in_reply_to_user_id' => 1,
|
||||||
|
'in_reply_to_user_id_str' => '1',
|
||||||
|
'in_reply_to_screen_name' => 'chatwootapp',
|
||||||
|
'user' => {
|
||||||
|
'id' => 2,
|
||||||
|
'name' => 'SurveyJoy',
|
||||||
|
'screen_name' => 'surveyjoyHQ',
|
||||||
|
'location' => 'Bangalore',
|
||||||
|
'url' => 'https://surveyjoy.co?utm_source=twitter',
|
||||||
|
'description' => 'Delightful in-product customer satisfaction surveys',
|
||||||
|
'followers_count' => 21,
|
||||||
|
'friends_count' => 13,
|
||||||
|
'profile_image_url' => 'http://pbs.twimg.com/profile_images/1114792399597985792/iHc5Gmez_normal.png',
|
||||||
|
'profile_image_url_https' => 'https://pbs.twimg.com/profile_images/1114792399597985792/iHc5Gmez_normal.png',
|
||||||
|
'profile_banner_url' => 'https://pbs.twimg.com/profile_banners/1109349707783041024/1554622013'
|
||||||
|
},
|
||||||
|
'geo' => nil,
|
||||||
|
'coordinates' => nil,
|
||||||
|
'place' => nil,
|
||||||
|
'contributors' => nil,
|
||||||
|
'is_quote_status' => false,
|
||||||
|
'quote_count' => 0,
|
||||||
|
'reply_count' => 0,
|
||||||
|
'retweet_count' => 0,
|
||||||
|
'favorite_count' => 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
initialize_with { attributes }
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,18 +2,28 @@ require 'rails_helper'
|
||||||
require 'webhooks/twitter'
|
require 'webhooks/twitter'
|
||||||
|
|
||||||
describe Webhooks::Twitter do
|
describe Webhooks::Twitter do
|
||||||
subject(:process_twitter_event) { described_class.new(params).consume }
|
subject(:twitter_webhook) { described_class }
|
||||||
|
|
||||||
let!(:account) { create(:account) }
|
let!(:account) { create(:account) }
|
||||||
# FIX ME: recipient id is set to 1 inside event factories
|
# 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_channel) { create(:channel_twitter_profile, account: account, profile_id: '1') }
|
||||||
let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account) }
|
let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account) }
|
||||||
let!(:params) { build(:twitter_message_create_event).with_indifferent_access }
|
let!(:dm_params) { build(:twitter_message_create_event).with_indifferent_access }
|
||||||
|
let!(:tweet_params) { build(:tweet_create_event).with_indifferent_access }
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
context 'with correct params' do
|
context 'with direct_message params' do
|
||||||
it 'creates incoming message in the twitter inbox' do
|
it 'creates incoming message in the twitter inbox' do
|
||||||
process_twitter_event
|
twitter_webhook.new(dm_params).consume
|
||||||
|
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
|
||||||
|
|
||||||
|
context 'with tweet_params params' do
|
||||||
|
it 'creates incoming message in the twitter inbox' do
|
||||||
|
twitter_webhook.new(tweet_params).consume
|
||||||
expect(twitter_inbox.contacts.count).to be 1
|
expect(twitter_inbox.contacts.count).to be 1
|
||||||
expect(twitter_inbox.conversations.count).to be 1
|
expect(twitter_inbox.conversations.count).to be 1
|
||||||
expect(twitter_inbox.messages.count).to be 1
|
expect(twitter_inbox.messages.count).to be 1
|
||||||
|
|
|
@ -35,7 +35,7 @@ describe Facebook::SendReplyService do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'if message has an FB ID' do
|
it 'if message has an FB ID' do
|
||||||
create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, fb_id: SecureRandom.uuid)
|
create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, source_id: SecureRandom.uuid)
|
||||||
expect(bot).not_to have_received(:deliver)
|
expect(bot).not_to have_received(:deliver)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,41 +3,72 @@ require 'rails_helper'
|
||||||
describe Twitter::SendReplyService do
|
describe Twitter::SendReplyService do
|
||||||
subject(:send_reply_service) { described_class.new(message: message) }
|
subject(:send_reply_service) { described_class.new(message: message) }
|
||||||
|
|
||||||
before do
|
let(:twitter_client) { instance_double(::Twitty::Facade) }
|
||||||
allow($twitter).to receive(:send_direct_message).and_return(true)
|
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(:dm_conversation) do
|
||||||
|
create(
|
||||||
|
:conversation,
|
||||||
|
contact: contact,
|
||||||
|
inbox: twitter_inbox,
|
||||||
|
contact_inbox: contact_inbox,
|
||||||
|
additional_attributes: { type: 'direct_message' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
let(:tweet_conversation) do
|
||||||
|
create(
|
||||||
|
:conversation,
|
||||||
|
contact: contact,
|
||||||
|
inbox: twitter_inbox,
|
||||||
|
contact_inbox: contact_inbox,
|
||||||
|
additional_attributes: { type: 'tweet', tweet_id: '1234' }
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
let!(:account) { create(:account) }
|
before do
|
||||||
let!(:widget_inbox) { create(:inbox, account: account) }
|
allow(::Twitty::Facade).to receive(:new).and_return(twitter_client)
|
||||||
let!(:twitter_channel) { create(:channel_twitter_profile, account: account) }
|
allow(twitter_client).to receive(:send_direct_message).and_return(true)
|
||||||
let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account) }
|
allow(twitter_client).to receive(:send_tweet_reply).and_return(true)
|
||||||
let!(:contact) { create(:contact, account: account) }
|
end
|
||||||
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
|
describe '#perform' do
|
||||||
context 'without reply' 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
|
it 'if inbox channel is not twitter profile' do
|
||||||
create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account)
|
create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account)
|
||||||
expect($twitter).not_to have_received(:send_direct_message)
|
expect(twitter_client).not_to have_received(:send_direct_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'if message is private' do
|
||||||
|
create(:message, message_type: 'outgoing', private: true, inbox: twitter_inbox, account: account)
|
||||||
|
expect(twitter_client).not_to have_received(:send_direct_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'if message has source_id' do
|
||||||
|
create(:message, message_type: 'outgoing', source_id: '123', inbox: widget_inbox, account: account)
|
||||||
|
expect(twitter_client).not_to have_received(:send_direct_message)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'if message is not outgoing' do
|
it 'if message is not outgoing' do
|
||||||
create(:message, message_type: 'incoming', inbox: twitter_inbox, account: account)
|
create(:message, message_type: 'incoming', inbox: twitter_inbox, account: account)
|
||||||
expect($twitter).not_to have_received(:send_direct_message)
|
expect(twitter_client).not_to have_received(:send_direct_message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with reply' do
|
context 'with reply' do
|
||||||
it 'if message is sent from chatwoot and is outgoing' do
|
it 'if conversation is a direct message' do
|
||||||
create(:message, message_type: :incoming, inbox: twitter_inbox, account: account, conversation: conversation)
|
create(:message, message_type: :incoming, inbox: twitter_inbox, account: account, conversation: dm_conversation)
|
||||||
create(:message, message_type: 'outgoing', inbox: twitter_inbox, account: account, conversation: conversation)
|
create(:message, message_type: 'outgoing', inbox: twitter_inbox, account: account, conversation: dm_conversation)
|
||||||
expect($twitter).to have_received(:send_direct_message)
|
expect(twitter_client).to have_received(:send_direct_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'if conversation is a tweet' do
|
||||||
|
create(:message, message_type: :incoming, inbox: twitter_inbox, account: account, conversation: tweet_conversation)
|
||||||
|
create(:message, message_type: 'outgoing', inbox: twitter_inbox, account: account, conversation: tweet_conversation)
|
||||||
|
expect(twitter_client).to have_received(:send_tweet_reply)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue