Skip to content

Commit

Permalink
Implemented issue #2023 - Twitter Account Activity API support.
Browse files Browse the repository at this point in the history
  • Loading branch information
znuny-robo committed Dec 3, 2018
1 parent 6f4d584 commit 6e061ab
Show file tree
Hide file tree
Showing 47 changed files with 10,023 additions and 2,335 deletions.
13 changes: 0 additions & 13 deletions .gitlab-ci.yml
Expand Up @@ -126,19 +126,6 @@ test:integration:email_helper_deliver:
- ruby -I test/ test/integration/email_keep_on_server_test.rb - ruby -I test/ test/integration/email_keep_on_server_test.rb
- rake db:drop - 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: test:integration:facebook:
<<: *artifacts_error <<: *artifacts_error
stage: test stage: test
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Expand Up @@ -74,7 +74,7 @@ gem 'omniauth-weibo-oauth2'
# channels # channels
gem 'koala' gem 'koala'
gem 'telegramAPI' gem 'telegramAPI'
gem 'twitter' gem 'twitter', git: 'https://github.com/sferik/twitter.git'


# channels - email additions # channels - email additions
gem 'htmlentities' gem 'htmlentities'
Expand Down
35 changes: 20 additions & 15 deletions 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 GIT
remote: https://github.com/wimm/rubyntlm remote: https://github.com/wimm/rubyntlm
revision: 53969639b87b9e5d5fef560f19cf0d977259591c revision: 53969639b87b9e5d5fef560f19cf0d977259591c
Expand Down Expand Up @@ -189,14 +205,14 @@ GEM
hashdiff (0.3.7) hashdiff (0.3.7)
hashie (3.5.6) hashie (3.5.6)
htmlentities (4.3.4) htmlentities (4.3.4)
http (3.0.0) http (3.3.0)
addressable (~> 2.3) addressable (~> 2.3)
http-cookie (~> 1.0) 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_parser.rb (~> 0.6.0)
http-cookie (1.0.3) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.0.0) http-form_data (2.1.1)
http_parser.rb (0.6.0) http_parser.rb (0.6.0)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.1.1) i18n (1.1.1)
Expand Down Expand Up @@ -453,17 +469,6 @@ GEM
faraday (~> 0.9) faraday (~> 0.9)
jwt (>= 1.5, <= 2.5) jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0) 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) tzinfo (1.2.5)
thread_safe (~> 0.1) thread_safe (~> 0.1)
uglifier (3.2.0) uglifier (3.2.0)
Expand Down Expand Up @@ -584,7 +589,7 @@ DEPENDENCIES
test-unit test-unit
therubyracer therubyracer
twilio-ruby twilio-ruby
twitter twitter!
uglifier uglifier
unicorn unicorn
valid_email2 valid_email2
Expand Down
Expand Up @@ -31,7 +31,8 @@ class Index extends App.ControllerSubContent
render: (data) => render: (data) =>


# if no twitter app is registered, show intro # 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')() @html App.view('twitter/index')()
return return


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


if @channel_id if @channel_id
Expand Down Expand Up @@ -177,7 +179,7 @@ class AppConfig extends App.ControllerModal
if data.attributes if data.attributes
if !@external_credential if !@external_credential
@external_credential = new App.ExternalCredential @external_credential = new App.ExternalCredential
@external_credential.load(name: 'twitter', credentials: @formParams()) @external_credential.load(name: 'twitter', credentials: data.attributes)
@external_credential.save( @external_credential.save(
done: => done: =>
@isChanged = true @isChanged = true
Expand Down
Expand Up @@ -34,5 +34,5 @@
<h3><%- @T('Retweets') %></h3> <h3><%- @T('Retweets') %></h3>
<p class="description"><%- @T('Choose if retweets should also be converted to tickets.') %></p> <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="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> </fieldset>
24 changes: 24 additions & 0 deletions app/assets/javascripts/app/views/twitter/app_config.jst.eco
Expand Up @@ -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" > <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> </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> <h2><%- @T('Your callback URL') %></h2>
<div class="input form-group"> <div class="input form-group">
<div class="controls"> <div class="controls">
Expand Down
9 changes: 9 additions & 0 deletions app/assets/javascripts/app/views/twitter/list.jst.eco
Expand Up @@ -9,12 +9,21 @@
</div> </div>
</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"> <div class="page-content">
<% for channel in @channels: %> <% for channel in @channels: %>
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%= channel.id %>"> <div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%= channel.id %>">
<div class="action-block action-row"> <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> <h2><%- @Icon('status', 'supergood-color inline') %> <%= channel.options.user.name %> <span class="text-muted">@<%= channel.options.user.screen_name %></span></h2>
</div> </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-flow action-flow--row">
<div class="action-block"> <div class="action-block">
<h3><%- @T('Search Terms') %></h3> <h3><%- @T('Search Terms') %></h3>
Expand Down
63 changes: 62 additions & 1 deletion app/controllers/channels_twitter_controller.rb
@@ -1,12 +1,72 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
require_dependency 'channel/driver/twitter'


class ChannelsTwitterController < ApplicationController 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 def index
assets = {} assets = {}
external_credential_ids = []
ExternalCredential.where(name: 'twitter').each do |external_credential| ExternalCredential.where(name: 'twitter').each do |external_credential|
assets = external_credential.assets(assets) assets = external_credential.assets(assets)
external_credential_ids.push external_credential.id
end end
channel_ids = [] channel_ids = []
Channel.where(area: 'Twitter::Account').order(:id).each do |channel| Channel.where(area: 'Twitter::Account').order(:id).each do |channel|
Expand All @@ -16,6 +76,7 @@ def index
render json: { render json: {
assets: assets, assets: assets,
channel_ids: channel_ids, channel_ids: channel_ids,
external_credential_ids: external_credential_ids,
callback_url: ExternalCredential.callback_url('twitter'), callback_url: ExternalCredential.callback_url('twitter'),
} }
end end
Expand Down
5 changes: 2 additions & 3 deletions app/controllers/external_credentials_controller.rb
Expand Up @@ -24,8 +24,7 @@ def destroy
end end


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


def callback def callback
provider = params[:provider].downcase 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 session[:request_token] = nil
redirect_to app_url(provider, channel.id) redirect_to app_url(provider, channel.id)
end end
Expand Down
3 changes: 2 additions & 1 deletion app/models/channel.rb
Expand Up @@ -58,6 +58,7 @@ def fetch(force = false)
self.last_log_in = result[:notice] self.last_log_in = result[:notice]
preferences[:last_fetch] = Time.zone.now preferences[:last_fetch] = Time.zone.now
save! save!
return true
rescue => e rescue => e
error = "Can't use Channel::Driver::#{adapter.to_classname}: #{e.inspect}" error = "Can't use Channel::Driver::#{adapter.to_classname}: #{e.inspect}"
logger.error error logger.error error
Expand All @@ -66,8 +67,8 @@ def fetch(force = false)
self.last_log_in = error self.last_log_in = error
preferences[:last_fetch] = Time.zone.now preferences[:last_fetch] = Time.zone.now
save! save!
return false
end end

end end


=begin =begin
Expand Down

0 comments on commit 6e061ab

Please sign in to comment.