Feature: Twitter DM Integration (#451)
An initial version of twitter integration Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
parent
a287c86bc4
commit
a9c304f1ef
20 changed files with 406 additions and 49 deletions
|
@ -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
|
||||||
|
|
|
@ -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/**/*
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -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
|
||||||
|
|
101
Gemfile.lock
101
Gemfile.lock
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
BIN
app/javascript/dashboard/assets/images/Mask.png
Normal file
BIN
app/javascript/dashboard/assets/images/Mask.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
app/javascript/dashboard/assets/images/twitter-badge.png
Normal file
BIN
app/javascript/dashboard/assets/images/twitter-badge.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
|
@ -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>
|
||||||
|
|
49
app/models/channel/twitter_profile.rb
Normal file
49
app/models/channel/twitter_profile.rb
Normal 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
|
|
@ -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
|
||||||
|
|
24
app/services/twitter/send_reply_service.rb
Normal file
24
app/services/twitter/send_reply_service.rb
Normal 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
|
8
config/initializers/twitter.rb
Normal file
8
config/initializers/twitter.rb
Normal 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
|
|
@ -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
|
||||||
|
|
12
db/migrate/20200124190208_create_channel_twitter_profiles.rb
Normal file
12
db/migrate/20200124190208_create_channel_twitter_profiles.rb
Normal 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
|
14
db/schema.rb
14
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_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
85
lib/webhooks/twitter.rb
Normal 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
|
11
spec/factories/channel/twitter_profiles.rb
Normal file
11
spec/factories/channel/twitter_profiles.rb
Normal 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
|
37
spec/factories/twitter/twitter_message_create_event.rb
Normal file
37
spec/factories/twitter/twitter_message_create_event.rb
Normal 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
|
23
spec/lib/webhooks/twitter_spec.rb
Normal file
23
spec/lib/webhooks/twitter_spec.rb
Normal 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
|
44
spec/services/twitter/send_reply_service_spec.rb
Normal file
44
spec/services/twitter/send_reply_service_spec.rb
Normal 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
|
Loading…
Reference in a new issue