Permalink
Browse files

Implemented issue #2023 - Twitter Account Activity API support.

  • Loading branch information...
znuny-robo committed Dec 3, 2018
1 parent 6f4d584 commit 6e061ab8946621897284a65a40b8288ae8faf26a
Showing with 10,023 additions and 2,335 deletions.
  1. +0 −13 .gitlab-ci.yml
  2. +1 −1 Gemfile
  3. +20 −15 Gemfile.lock
  4. +4 −2 app/assets/javascripts/app/controllers/_channel/twitter.coffee
  5. +1 −1 app/assets/javascripts/app/views/twitter/account_edit.jst.eco
  6. +24 −0 app/assets/javascripts/app/views/twitter/app_config.jst.eco
  7. +9 −0 app/assets/javascripts/app/views/twitter/list.jst.eco
  8. +62 −1 app/controllers/channels_twitter_controller.rb
  9. +2 −3 app/controllers/external_credentials_controller.rb
  10. +2 −1 app/models/channel.rb
  11. +26 −238 app/models/channel/driver/twitter.rb
  12. +34 −15 app/models/observer/ticket/article/communicate_twitter/background_job.rb
  13. +8 −5 config/routes/channel_twitter.rb
  14. +17 −11 lib/external_credential/facebook.rb
  15. +142 −20 lib/external_credential/twitter.rb
  16. +0 −6 lib/http/uri.rb
  17. +4 −2 lib/sessions/event.rb
  18. +0 −463 lib/tweet_base.rb
  19. +0 −24 lib/tweet_rest.rb
  20. +0 −29 lib/tweet_stream.rb
  21. +969 −0 lib/twitter_sync.rb
  22. +1 −1 spec/lib/core_ext/string_spec.rb
  23. +394 −0 spec/models/channel/driver/twitter_spec.rb
  24. +111 −19 spec/requests/external_credential_spec.rb
  25. +282 −0 spec/requests/integration/twitter_webhook_spec.rb
  26. +9 −1 spec/support/vcr.rb
  27. +65 −0 test/data/twitter/webhook1_direct_message.json
  28. +162 −0 test/data/twitter/webhook1_tweet.json
  29. +113 −0 test/data/twitter/webhook2_direct_message.json
  30. +110 −0 test/data/twitter/webhook2_tweet.json
  31. +57 −0 test/data/twitter/webhook3_direct_message.json
  32. +87 −0 test/data/vcr_cassettes/models/channel/driver/twitter/article_to_tweet.yml
  33. +87 −0 test/data/vcr_cassettes/models/channel/driver/twitter/article_to_tweet_channel_replace.yml
  34. +51 −0 test/data/vcr_cassettes/models/channel/driver/twitter/fetch_channel_invalid.yml
  35. +6,566 −0 test/data/vcr_cassettes/models/channel/driver/twitter/fetch_channel_valid.yml
  36. +58 −0 test/data/vcr_cassettes/request/external_credentials/facebook/app_verify_facebook.yml
  37. +58 −0 ...r_cassettes/request/external_credentials/facebook/app_verify_invalid_credentials_with_created.yml
  38. +58 −0 ...ssettes/request/external_credentials/facebook/app_verify_invalid_credentials_with_not_created.yml
  39. +56 −0 test/data/vcr_cassettes/request/external_credentials/facebook/callback_invalid_credentials.yml
  40. +58 −0 ...data/vcr_cassettes/request/external_credentials/facebook/link_account_with_invalid_credential.yml
  41. +76 −0 ...cr_cassettes/request/external_credentials/twitter/app_verify_invalid_credentials_with_created.yml
  42. +76 −0 ...assettes/request/external_credentials/twitter/app_verify_invalid_credentials_with_not_created.yml
  43. +76 −0 test/data/vcr_cassettes/request/external_credentials/twitter/app_verify_twitter.yml
  44. +76 −0 .../data/vcr_cassettes/request/external_credentials/twitter/link_account_with_invalid_credential.yml
  45. +11 −366 test/integration/twitter_browser_test.rb
  46. +0 −904 test/integration/twitter_test.rb
  47. +0 −194 test/unit/ticket_article_twitter_test.rb
@@ -126,19 +126,6 @@ test:integration:email_helper_deliver:
- ruby -I test/ test/integration/email_keep_on_server_test.rb
- rake db:drop

test:integration:twitter:
<<: *artifacts_error
stage: test
variables:
RAILS_ENV: "test"
tags:
- core-twitter
script:
- rake zammad:db:init
- ruby -I test/ test/integration/twitter_test.rb
- rake db:drop
allow_failure: true

test:integration:facebook:
<<: *artifacts_error
stage: test
@@ -74,7 +74,7 @@ gem 'omniauth-weibo-oauth2'
# channels
gem 'koala'
gem 'telegramAPI'
gem 'twitter'
gem 'twitter', git: 'https://github.com/sferik/twitter.git'

# channels - email additions
gem 'htmlentities'
@@ -1,3 +1,19 @@
GIT
remote: https://github.com/sferik/twitter.git
revision: 844818cad07ce490ccb9d8542ebb6b4fc7a61cb4
specs:
twitter (6.2.0)
addressable (~> 2.3)
buftok (~> 0.2.0)
equalizer (~> 0.0.11)
http (~> 3.0)
http-form_data (~> 2.0)
http_parser.rb (~> 0.6.0)
memoizable (~> 0.4.0)
multipart-post (~> 2.0)
naught (~> 1.0)
simple_oauth (~> 0.3.0)

GIT
remote: https://github.com/wimm/rubyntlm
revision: 53969639b87b9e5d5fef560f19cf0d977259591c
@@ -189,14 +205,14 @@ GEM
hashdiff (0.3.7)
hashie (3.5.6)
htmlentities (4.3.4)
http (3.0.0)
http (3.3.0)
addressable (~> 2.3)
http-cookie (~> 1.0)
http-form_data (>= 2.0.0.pre.pre2, < 3)
http-form_data (~> 2.0)
http_parser.rb (~> 0.6.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
http-form_data (2.0.0)
http-form_data (2.1.1)
http_parser.rb (0.6.0)
httpclient (2.8.3)
i18n (1.1.1)
@@ -453,17 +469,6 @@ GEM
faraday (~> 0.9)
jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0)
twitter (6.2.0)
addressable (~> 2.3)
buftok (~> 0.2.0)
equalizer (~> 0.0.11)
http (~> 3.0)
http-form_data (~> 2.0)
http_parser.rb (~> 0.6.0)
memoizable (~> 0.4.0)
multipart-post (~> 2.0)
naught (~> 1.0)
simple_oauth (~> 0.3.0)
tzinfo (1.2.5)
thread_safe (~> 0.1)
uglifier (3.2.0)
@@ -584,7 +589,7 @@ DEPENDENCIES
test-unit
therubyracer
twilio-ruby
twitter
twitter!
uglifier
unicorn
valid_email2
@@ -31,7 +31,8 @@ class Index extends App.ControllerSubContent
render: (data) =>

# if no twitter app is registered, show intro
if !App.ExternalCredential.findByAttribute('name', 'twitter')
external_credential = App.ExternalCredential.findByAttribute('name', 'twitter')
if !external_credential
@html App.view('twitter/index')()
return

@@ -60,6 +61,7 @@ class Index extends App.ControllerSubContent
channels.push channel
@html App.view('twitter/list')(
channels: channels
external_credential: external_credential
)

if @channel_id
@@ -177,7 +179,7 @@ class AppConfig extends App.ControllerModal
if data.attributes
if !@external_credential
@external_credential = new App.ExternalCredential
@external_credential.load(name: 'twitter', credentials: @formParams())
@external_credential.load(name: 'twitter', credentials: data.attributes)
@external_credential.save(
done: =>
@isChanged = true
@@ -34,5 +34,5 @@
<h3><%- @T('Retweets') %></h3>
<p class="description"><%- @T('Choose if retweets should also be converted to tickets.') %></p>
<input name="track_retweets" type="checkbox" id="setting-chat" value="true" <% if @channel.options.sync.track_retweets: %>checked<% end %>> <%- @T('Track retweets') %>

<input name="webhook_id" type="hidden" value="<%- @channel.options.sync.webhook_id %>">
</fieldset>
@@ -20,6 +20,30 @@
<input id="consumer_secret" type="text" name="consumer_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.consumer_secret %><% end %>" class="form-control" required autocomplete="off" >
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="oauth_token">Twitter Access Token <span>*</span></label>
</div>
<div class="controls">
<input id="oauth_token" type="text" name="oauth_token" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.oauth_token %><% end %>" class="form-control" required autocomplete="off" >
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="oauth_token_secret">Twitter Access Token Secret <span>*</span></label>
</div>
<div class="controls">
<input id="oauth_token_secret" type="text" name="oauth_token_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.oauth_token_secret %><% end %>" class="form-control" required autocomplete="off" >
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="env">Twitter Dev environment label <span>*</span></label>
</div>
<div class="controls">
<input id="env" type="text" name="env" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.env %><% end %>" class="form-control" required autocomplete="off" >
</div>
</div>
<h2><%- @T('Your callback URL') %></h2>
<div class="input form-group">
<div class="controls">
@@ -9,12 +9,21 @@
</div>
</div>

<% if @external_credential && @external_credential.credentials && !@external_credential.credentials.webhook_id: %>
<div class="alert alert--warning" role="alert"><%- @T('Your Twitter-App is not using the Twitter Account Activity API yet and is therefore limited to search terms only. Please refer to the documentation %l on how to update your account.', 'https://docs.zammad.org/en/latest/channel-twitter.html') %></div>
<% end %>

<div class="page-content">
<% for channel in @channels: %>
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%= channel.id %>">
<div class="action-block action-row">
<h2><%- @Icon('status', 'supergood-color inline') %> <%= channel.options.user.name %> <span class="text-muted">@<%= channel.options.user.screen_name %></span></h2>
</div>

<% if @external_credential && @external_credential.credentials && @external_credential.credentials.webhook_id && channel.options && channel.options.subscribed_to_webhook_id isnt @external_credential.credentials.webhook_id: %>
<div class="alert alert--warning" role="alert"><%- @T('Your Twitter-Account is not using the Twitter Account Activity API yet and is therefore limited to search terms only. Please add/update the account again via "add account".') %></div>
<% end %>

<div class="action-flow action-flow--row">
<div class="action-block">
<h3><%- @T('Search Terms') %></h3>
@@ -1,12 +1,72 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
require_dependency 'channel/driver/twitter'

class ChannelsTwitterController < ApplicationController
prepend_before_action { authentication_check(permission: 'admin.channel_twitter') }
prepend_before_action -> { authentication_check(permission: 'admin.channel_twitter') }, except: %i[webhook_incoming webhook_verify]
skip_before_action :verify_csrf_token, only: %i[webhook_incoming webhook_verify]

before_action :validate_webhook_signature!, only: :webhook_incoming

def webhook_incoming
::Channel::Driver::Twitter.new.process(params.permit!.to_h, @channel)
render json: {}
end

def validate_webhook_signature!
header_name = 'x-twitter-webhooks-signature'
given_signature = request.headers[header_name]
raise Exceptions::UnprocessableEntity, "Missing '#{header_name}' header" if given_signature.blank?

calculated_signature = hmac_signature_by_app(request.raw_post)
raise Exceptions::NotAuthorized if calculated_signature != given_signature
raise Exceptions::UnprocessableEntity, "Missing 'for_user_id' in payload!" if params[:for_user_id].blank?

@channel = nil
Channel.where(area: 'Twitter::Account', active: true).each do |channel|
next if channel.options[:user].blank?
next if channel.options[:user][:id].to_s != params[:for_user_id].to_s

@channel = channel
end

raise Exceptions::UnprocessableEntity, "No such channel for user id '#{params[:for_user_id]}'!" if !@channel

true
end

def hmac_signature_by_app(content)
external_credential = ExternalCredential.find_by(name: 'twitter')
raise Exceptions::UnprocessableEntity, 'No such external_credential \'twitter\'!' if !external_credential

hmac_signature_gen(external_credential.credentials[:consumer_secret], content)
end

def hmac_signature_gen(consumer_secret, content)
hashed = OpenSSL::HMAC.digest('sha256', consumer_secret, content)
hashed = Base64.strict_encode64(hashed)
"sha256=#{hashed}"
end

def webhook_verify
external_credential = Cache.get('external_credential_twitter')
if !external_credential && ExternalCredential.exists?(name: 'twitter')
external_credential = ExternalCredential.find_by(name: 'twitter').credentials
end
raise Exceptions::UnprocessableEntity, 'No external_credential in cache!' if external_credential.blank?
raise Exceptions::UnprocessableEntity, 'No external_credential[:consumer_secret] in cache!' if external_credential[:consumer_secret].blank?
raise Exceptions::UnprocessableEntity, 'No crc_token in verify payload from twitter!' if params['crc_token'].blank?

render json: {
response_token: hmac_signature_gen(external_credential[:consumer_secret], params['crc_token'])
}
end

def index
assets = {}
external_credential_ids = []
ExternalCredential.where(name: 'twitter').each do |external_credential|
assets = external_credential.assets(assets)
external_credential_ids.push external_credential.id
end
channel_ids = []
Channel.where(area: 'Twitter::Account').order(:id).each do |channel|
@@ -16,6 +76,7 @@ def index
render json: {
assets: assets,
channel_ids: channel_ids,
external_credential_ids: external_credential_ids,
callback_url: ExternalCredential.callback_url('twitter'),
}
end
@@ -24,8 +24,7 @@ def destroy
end

def app_verify
attributes = ExternalCredential.app_verify(params)
render json: { attributes: attributes }, status: :ok
render json: { attributes: ExternalCredential.app_verify(params.permit!.to_h) }, status: :ok
rescue => e
render json: { error: e.message }, status: :ok
end
@@ -39,7 +38,7 @@ def link_account

def callback
provider = params[:provider].downcase
channel = ExternalCredential.link_account(provider, session[:request_token], params)
channel = ExternalCredential.link_account(provider, session[:request_token], params.permit!.to_h)
session[:request_token] = nil
redirect_to app_url(provider, channel.id)
end
@@ -58,6 +58,7 @@ def fetch(force = false)
self.last_log_in = result[:notice]
preferences[:last_fetch] = Time.zone.now
save!
return true
rescue => e
error = "Can't use Channel::Driver::#{adapter.to_classname}: #{e.inspect}"
logger.error error
@@ -66,8 +67,8 @@ def fetch(force = false)
self.last_log_in = error
preferences[:last_fetch] = Time.zone.now
save!
return false
end

end

=begin
Oops, something went wrong.

0 comments on commit 6e061ab

Please sign in to comment.