Просмотр исходного кода

Implemented issue #2023 - Twitter Account Activity API support.

Martin Edenhofer 6 лет назад
Родитель
Сommit
6e061ab894

+ 0 - 13
.gitlab-ci.yml

@@ -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

+ 1 - 1
Gemfile

@@ -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'

+ 20 - 15
Gemfile.lock

@@ -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

+ 4 - 2
app/assets/javascripts/app/controllers/_channel/twitter.coffee

@@ -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

+ 1 - 1
app/assets/javascripts/app/views/twitter/account_edit.jst.eco

@@ -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>

+ 24 - 0
app/assets/javascripts/app/views/twitter/app_config.jst.eco

@@ -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 - 0
app/assets/javascripts/app/views/twitter/list.jst.eco

@@ -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>

+ 62 - 1
app/controllers/channels_twitter_controller.rb

@@ -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 @@ class ChannelsTwitterController < ApplicationController
     render json: {
       assets: assets,
       channel_ids: channel_ids,
+      external_credential_ids: external_credential_ids,
       callback_url: ExternalCredential.callback_url('twitter'),
     }
   end

+ 2 - 3
app/controllers/external_credentials_controller.rb

@@ -24,8 +24,7 @@ class ExternalCredentialsController < ApplicationController
   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 @@ class ExternalCredentialsController < ApplicationController
 
   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

+ 2 - 1
app/models/channel.rb

@@ -58,6 +58,7 @@ fetch one account
       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 @@ fetch one account
       self.last_log_in = error
       preferences[:last_fetch] = Time.zone.now
       save!
+      return false
     end
-
   end
 
 =begin

Некоторые файлы не были показаны из-за большого количества измененных файлов