Implemented issue #2023 - Twitter Account Activity API support.
This commit is contained in:
parent
6f4d584fae
commit
6e061ab894
47 changed files with 10025 additions and 2337 deletions
|
@ -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
|
||||
|
|
2
Gemfile
2
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'
|
||||
|
|
35
Gemfile.lock
35
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
|
||||
|
|
|
@ -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 @@ class ChannelsTwitterController < ApplicationController
|
|||
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 @@ 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# Copyright (C) 2012-2015 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
require_dependency 'external_credential/twitter'
|
||||
|
||||
class Channel::Driver::Twitter
|
||||
|
||||
=begin
|
||||
|
@ -47,17 +49,15 @@ returns
|
|||
|
||||
def fetch(options, channel)
|
||||
|
||||
options = check_external_credential(options)
|
||||
options = self.class.check_external_credential(options)
|
||||
|
||||
@rest_client = TweetRest.new(options[:auth])
|
||||
@sync = options[:sync]
|
||||
@channel = channel
|
||||
@client = TwitterSync.new(options[:auth])
|
||||
@sync = options[:sync]
|
||||
@channel = channel
|
||||
|
||||
Rails.logger.debug { 'twitter fetch started' }
|
||||
|
||||
fetch_mentions
|
||||
fetch_search
|
||||
fetch_direct_messages
|
||||
|
||||
disconnect
|
||||
|
||||
|
@ -111,17 +111,16 @@ returns
|
|||
# return if we run import mode
|
||||
return if Setting.get('import_mode')
|
||||
|
||||
options = check_external_credential(options)
|
||||
options = self.class.check_external_credential(options)
|
||||
|
||||
@rest_client = TweetRest.new(options[:auth])
|
||||
tweet = @rest_client.from_article(article)
|
||||
@client = TwitterSync.new(options[:auth])
|
||||
tweet = @client.from_article(article)
|
||||
disconnect
|
||||
tweet
|
||||
end
|
||||
|
||||
def disconnect
|
||||
@stream_client&.disconnect
|
||||
@rest_client&.disconnect
|
||||
@client&.disconnect
|
||||
end
|
||||
|
||||
=begin
|
||||
|
@ -135,184 +134,29 @@ returns
|
|||
=end
|
||||
|
||||
def self.streamable?
|
||||
true
|
||||
false
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
create stream endpoint form twitter account
|
||||
|
||||
options = {
|
||||
adapter: 'twitter',
|
||||
auth: {
|
||||
consumer_key: consumer_key,
|
||||
consumer_secret: consumer_secret,
|
||||
oauth_token: armin_theo_token,
|
||||
oauth_token_secret: armin_theo_token_secret,
|
||||
},
|
||||
sync: {
|
||||
search: [
|
||||
{
|
||||
term: '#citheo42',
|
||||
group_id: 2,
|
||||
},
|
||||
{
|
||||
term: '#citheo24',
|
||||
group_id: 1,
|
||||
},
|
||||
],
|
||||
mentions: {
|
||||
group_id: 2,
|
||||
},
|
||||
direct_messages: {
|
||||
group_id: 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instance = Channel::Driver::Twitter.new
|
||||
stream_instance = instance.stream_instance(channel)
|
||||
|
||||
returns
|
||||
|
||||
instance_of_stream_handle
|
||||
Channel::Driver::Twitter.process(payload, channel)
|
||||
|
||||
=end
|
||||
|
||||
def stream_instance(channel)
|
||||
@channel = channel
|
||||
options = @channel.options
|
||||
@stream_client = TweetStream.new(options[:auth])
|
||||
def process(payload, channel)
|
||||
@client = TwitterSync.new(channel.options[:auth], payload)
|
||||
@client.process_webhook(channel)
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
stream tweets from twitter account
|
||||
|
||||
instance.stream
|
||||
|
||||
returns
|
||||
|
||||
# endless loop
|
||||
|
||||
=end
|
||||
|
||||
def stream
|
||||
sleep_on_unauthorized = 65
|
||||
2.times do |loop_count|
|
||||
begin
|
||||
stream_start
|
||||
rescue Twitter::Error::Unauthorized => e
|
||||
Rails.logger.info "Unable to stream, try #{loop_count}, error #{e.inspect}"
|
||||
if loop_count >= 2
|
||||
raise "Unable to stream, try #{loop_count}, error #{e.inspect}"
|
||||
end
|
||||
|
||||
Rails.logger.info "wait for #{sleep_on_unauthorized} sec. and try it again"
|
||||
sleep sleep_on_unauthorized
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def stream_start
|
||||
|
||||
sync = @channel.options['sync']
|
||||
raise 'Need channel.options[\'sync\'] for account, but no params found' if !sync
|
||||
|
||||
filter = {}
|
||||
if sync['search']
|
||||
hashtags = []
|
||||
sync['search'].each do |item|
|
||||
next if item['term'].blank?
|
||||
next if item['term'] == '#'
|
||||
next if item['group_id'].blank?
|
||||
|
||||
hashtags.push item['term']
|
||||
end
|
||||
filter[:track] = hashtags.join(',')
|
||||
end
|
||||
if sync['mentions'] && sync['mentions']['group_id'] != ''
|
||||
filter[:replies] = 'all'
|
||||
end
|
||||
|
||||
return if filter.blank?
|
||||
|
||||
@stream_client.client.user(filter) do |tweet|
|
||||
next if tweet.class != Twitter::Tweet && tweet.class != Twitter::DirectMessage
|
||||
|
||||
# wait until own posts are stored in local database to prevent importing own tweets
|
||||
next if @stream_client.locale_sender?(tweet) && own_tweet_already_imported?(tweet)
|
||||
|
||||
next if Ticket::Article.find_by(message_id: tweet.id)
|
||||
|
||||
# check direct message
|
||||
if tweet.class == Twitter::DirectMessage
|
||||
if sync['direct_messages'] && sync['direct_messages']['group_id'] != ''
|
||||
next if @stream_client.direct_message_limit_reached(tweet, 2)
|
||||
|
||||
@stream_client.to_group(tweet, sync['direct_messages']['group_id'], @channel)
|
||||
end
|
||||
next
|
||||
end
|
||||
|
||||
next if !track_retweets? && tweet.retweet?
|
||||
next if @stream_client.tweet_limit_reached(tweet, 2)
|
||||
|
||||
# check if it's mention
|
||||
if sync['mentions'] && sync['mentions']['group_id'].present?
|
||||
hit = false
|
||||
tweet.user_mentions&.each do |user|
|
||||
if user.id.to_s == @channel.options['user']['id'].to_s
|
||||
hit = true
|
||||
end
|
||||
end
|
||||
if hit
|
||||
@stream_client.to_group(tweet, sync['mentions']['group_id'], @channel)
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
# check hashtags
|
||||
if sync['search'] && tweet.hashtags
|
||||
hit = false
|
||||
sync['search'].each do |item|
|
||||
next if item['term'].blank?
|
||||
next if item['term'] == '#'
|
||||
next if item['group_id'].blank?
|
||||
|
||||
tweet.hashtags.each do |hashtag|
|
||||
next if item['term'] !~ /^#/
|
||||
|
||||
if item['term'].sub(/^#/, '') == hashtag.text
|
||||
hit = item
|
||||
end
|
||||
end
|
||||
end
|
||||
if hit
|
||||
@stream_client.to_group(tweet, hit['group_id'], @channel)
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
# check stings
|
||||
if sync['search']
|
||||
hit = false
|
||||
body = tweet.text
|
||||
sync['search'].each do |item|
|
||||
next if item['term'].blank?
|
||||
next if item['term'] == '#'
|
||||
next if item['group_id'].blank?
|
||||
|
||||
if body.match?(/#{item['term']}/)
|
||||
hit = item
|
||||
end
|
||||
end
|
||||
if hit
|
||||
@stream_client.to_group(tweet, hit['group_id'], @channel)
|
||||
end
|
||||
end
|
||||
def self.check_external_credential(options)
|
||||
if options[:auth] && options[:auth][:external_credential_id]
|
||||
external_credential = ExternalCredential.find_by(id: options[:auth][:external_credential_id])
|
||||
raise "No such ExternalCredential.find(#{options[:auth][:external_credential_id]})" if !external_credential
|
||||
|
||||
options[:auth][:consumer_key] = external_credential.credentials['consumer_key']
|
||||
options[:auth][:consumer_secret] = external_credential.credentials['consumer_secret']
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -329,7 +173,7 @@ returns
|
|||
Rails.logger.debug { " - searching for '#{search[:term]}'" }
|
||||
older_import = 0
|
||||
older_import_max = 20
|
||||
@rest_client.client.search(search[:term], result_type: result_type).collect do |tweet|
|
||||
@client.client.search(search[:term], result_type: result_type).collect do |tweet|
|
||||
next if !track_retweets? && tweet.retweet?
|
||||
|
||||
# ignore older messages
|
||||
|
@ -339,71 +183,15 @@ returns
|
|||
next
|
||||
end
|
||||
|
||||
next if @rest_client.locale_sender?(tweet) && own_tweet_already_imported?(tweet)
|
||||
next if @client.locale_sender?(tweet) && own_tweet_already_imported?(tweet)
|
||||
next if Ticket::Article.find_by(message_id: tweet.id)
|
||||
break if @rest_client.tweet_limit_reached(tweet)
|
||||
break if @client.tweet_limit_reached(tweet)
|
||||
|
||||
@rest_client.to_group(tweet, search[:group_id], @channel)
|
||||
@client.to_group(tweet, search[:group_id], @channel)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_mentions
|
||||
return if @sync[:mentions].blank?
|
||||
return if @sync[:mentions][:group_id].blank?
|
||||
|
||||
Rails.logger.debug { ' - searching for mentions' }
|
||||
older_import = 0
|
||||
older_import_max = 20
|
||||
@rest_client.client.mentions_timeline.each do |tweet|
|
||||
next if !track_retweets? && tweet.retweet?
|
||||
|
||||
# ignore older messages
|
||||
if (@channel.created_at - 15.days) > tweet.created_at.dup.utc || older_import >= older_import_max
|
||||
older_import += 1
|
||||
Rails.logger.debug { "tweet to old: #{tweet.id}/#{tweet.created_at}" }
|
||||
next
|
||||
end
|
||||
next if Ticket::Article.find_by(message_id: tweet.id)
|
||||
break if @rest_client.tweet_limit_reached(tweet)
|
||||
|
||||
@rest_client.to_group(tweet, @sync[:mentions][:group_id], @channel)
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_direct_messages
|
||||
return if @sync[:direct_messages].blank?
|
||||
return if @sync[:direct_messages][:group_id].blank?
|
||||
|
||||
Rails.logger.debug { ' - searching for direct_messages' }
|
||||
older_import = 0
|
||||
older_import_max = 20
|
||||
@rest_client.client.direct_messages(full_text: 'true').each do |tweet|
|
||||
|
||||
# ignore older messages
|
||||
if (@channel.created_at - 15.days) > tweet.created_at.dup.utc || older_import >= older_import_max
|
||||
older_import += 1
|
||||
Rails.logger.debug { "tweet to old: #{tweet.id}/#{tweet.created_at}" }
|
||||
next
|
||||
end
|
||||
next if Ticket::Article.find_by(message_id: tweet.id)
|
||||
break if @rest_client.direct_message_limit_reached(tweet)
|
||||
|
||||
@rest_client.to_group(tweet, @sync[:direct_messages][:group_id], @channel)
|
||||
end
|
||||
end
|
||||
|
||||
def check_external_credential(options)
|
||||
if options[:auth] && options[:auth][:external_credential_id]
|
||||
external_credential = ExternalCredential.find_by(id: options[:auth][:external_credential_id])
|
||||
raise "No such ExternalCredential.find(#{options[:auth][:external_credential_id]})" if !external_credential
|
||||
|
||||
options[:auth][:consumer_key] = external_credential.credentials['consumer_key']
|
||||
options[:auth][:consumer_secret] = external_credential.credentials['consumer_secret']
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
def track_retweets?
|
||||
@channel.options && @channel.options['sync'] && @channel.options['sync']['track_retweets']
|
||||
end
|
||||
|
|
|
@ -14,6 +14,21 @@ class Observer::Ticket::Article::CommunicateTwitter::BackgroundJob
|
|||
log_error(article, "Can't find ticket.preferences for Ticket.find(#{article.ticket_id})") if !ticket.preferences
|
||||
log_error(article, "Can't find ticket.preferences['channel_id'] for Ticket.find(#{article.ticket_id})") if !ticket.preferences['channel_id']
|
||||
channel = Channel.lookup(id: ticket.preferences['channel_id'])
|
||||
|
||||
# search for same channel channel_screen_name, in case the channel got re-added
|
||||
if !channel
|
||||
Channel.where(area: 'Twitter::Account', active: true).each do |local_channel|
|
||||
next if ticket.preferences[:channel_screen_name].blank?
|
||||
next if !local_channel.options
|
||||
next if local_channel.options[:user].blank?
|
||||
next if local_channel.options[:user][:screen_name].blank?
|
||||
next if local_channel.options[:user][:screen_name] != ticket.preferences[:channel_screen_name]
|
||||
|
||||
channel = local_channel
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
log_error(article, "No such channel id #{ticket.preferences['channel_id']}") if !channel
|
||||
log_error(article, "Channel.find(#{channel.id}) isn't a twitter channel!") if channel.options[:adapter] !~ /\Atwitter/i
|
||||
|
||||
|
@ -36,20 +51,24 @@ class Observer::Ticket::Article::CommunicateTwitter::BackgroundJob
|
|||
# fill article with tweet info
|
||||
|
||||
# direct message
|
||||
if tweet.class == Twitter::DirectMessage
|
||||
article.from = "@#{tweet.sender.screen_name}"
|
||||
article.to = "@#{tweet.recipient.screen_name}"
|
||||
tweet_id = nil
|
||||
if tweet.is_a?(Hash)
|
||||
tweet_type = 'DirectMessage'
|
||||
tweet_id = tweet[:event][:id].to_s
|
||||
if tweet[:event] && tweet[:event][:type] == 'message_create'
|
||||
#article.from = "@#{tweet.sender.screen_name}"
|
||||
#article.to = "@#{tweet.recipient.screen_name}"
|
||||
|
||||
article.preferences['twitter'] = {
|
||||
created_at: tweet.created_at,
|
||||
recipient_id: tweet.recipient.id,
|
||||
recipient_screen_name: tweet.recipient.screen_name,
|
||||
sender_id: tweet.sender.id,
|
||||
sender_screen_name: tweet.sender.screen_name,
|
||||
}
|
||||
article.preferences['twitter'] = {
|
||||
recipient_id: tweet[:event][:message_create][:target][:recipient_id],
|
||||
sender_id: tweet[:event][:message_create][:sender_id],
|
||||
}
|
||||
end
|
||||
|
||||
# regular tweet
|
||||
elsif tweet.class == Twitter::Tweet
|
||||
tweet_type = 'Tweet'
|
||||
tweet_id = tweet.id.to_s
|
||||
article.from = "@#{tweet.user.screen_name}"
|
||||
if tweet.user_mentions
|
||||
to = ''
|
||||
|
@ -62,7 +81,7 @@ class Observer::Ticket::Article::CommunicateTwitter::BackgroundJob
|
|||
mention_ids.push user.id
|
||||
end
|
||||
article.to = to
|
||||
article.preferences['twitter'] = TweetBase.preferences_cleanup(
|
||||
article.preferences['twitter'] = TwitterSync.preferences_cleanup(
|
||||
mention_ids: mention_ids,
|
||||
geo: tweet.geo,
|
||||
retweeted: tweet.retweeted?,
|
||||
|
@ -85,10 +104,10 @@ class Observer::Ticket::Article::CommunicateTwitter::BackgroundJob
|
|||
article.preferences['delivery_status'] = 'success'
|
||||
article.preferences['delivery_status_date'] = Time.zone.now
|
||||
|
||||
article.message_id = tweet.id.to_s
|
||||
article.message_id = tweet_id
|
||||
article.preferences['links'] = [
|
||||
{
|
||||
url: "https://twitter.com/statuses/#{tweet.id}",
|
||||
url: "https://twitter.com/statuses/#{tweet_id}",
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
|
@ -96,7 +115,7 @@ class Observer::Ticket::Article::CommunicateTwitter::BackgroundJob
|
|||
|
||||
article.save!
|
||||
|
||||
Rails.logger.info "Send twitter (#{tweet.class}) to: '#{article.to}' (from #{article.from})"
|
||||
Rails.logger.info "Send twitter (#{tweet_type}) to: '#{article.to}' (from #{article.from})"
|
||||
|
||||
article
|
||||
end
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
Zammad::Application.routes.draw do
|
||||
api_path = Rails.configuration.api_path
|
||||
|
||||
match api_path + '/channels_twitter', to: 'channels_twitter#index', via: :get
|
||||
match api_path + '/channels_twitter/:id', to: 'channels_twitter#update', via: :post
|
||||
match api_path + '/channels_twitter_disable', to: 'channels_twitter#disable', via: :post
|
||||
match api_path + '/channels_twitter_enable', to: 'channels_twitter#enable', via: :post
|
||||
match api_path + '/channels_twitter', to: 'channels_twitter#destroy', via: :delete
|
||||
match api_path + '/channels_twitter', to: 'channels_twitter#index', via: :get
|
||||
match api_path + '/channels_twitter/:id', to: 'channels_twitter#update', via: :post
|
||||
match api_path + '/channels_twitter_disable', to: 'channels_twitter#disable', via: :post
|
||||
match api_path + '/channels_twitter_enable', to: 'channels_twitter#enable', via: :post
|
||||
match api_path + '/channels_twitter', to: 'channels_twitter#destroy', via: :delete
|
||||
match api_path + '/channels_twitter_webhook', to: 'channels_twitter#webhook_verify', via: :get
|
||||
match api_path + '/channels_twitter_webhook', to: 'channels_twitter#webhook_incoming', via: :post
|
||||
|
||||
end
|
||||
|
|
|
@ -1,20 +1,26 @@
|
|||
class ExternalCredential::Facebook
|
||||
|
||||
def self.app_verify(params)
|
||||
request_account_to_link(params)
|
||||
request_account_to_link(params, false)
|
||||
params
|
||||
end
|
||||
|
||||
def self.request_account_to_link(credentials = {})
|
||||
def self.request_account_to_link(credentials = {}, app_required = true)
|
||||
external_credential = ExternalCredential.find_by(name: 'facebook')
|
||||
raise Exceptions::UnprocessableEntity, 'No facebook app configured!' if !external_credential
|
||||
raise Exceptions::UnprocessableEntity, 'No facebook app configured!' if !external_credential && app_required
|
||||
|
||||
if !credentials[:application_id]
|
||||
credentials[:application_id] = external_credential.credentials['application_id']
|
||||
end
|
||||
if !credentials[:application_secret]
|
||||
credentials[:application_secret] = external_credential.credentials['application_secret']
|
||||
if external_credential
|
||||
if credentials[:application_id].blank?
|
||||
credentials[:application_id] = external_credential.credentials['application_id']
|
||||
end
|
||||
if credentials[:application_secret].blank?
|
||||
credentials[:application_secret] = external_credential.credentials['application_secret']
|
||||
end
|
||||
end
|
||||
|
||||
raise Exceptions::UnprocessableEntity, 'No application_id param!' if credentials[:application_id].blank?
|
||||
raise Exceptions::UnprocessableEntity, 'No application_secret param!' if credentials[:application_secret].blank?
|
||||
|
||||
oauth = Koala::Facebook::OAuth.new(
|
||||
credentials[:application_id],
|
||||
credentials[:application_secret],
|
||||
|
@ -32,7 +38,7 @@ class ExternalCredential::Facebook
|
|||
def self.link_account(_request_token, params)
|
||||
# fail if request_token.params[:oauth_token] != params[:state]
|
||||
external_credential = ExternalCredential.find_by(name: 'facebook')
|
||||
raise 'No such account' if !external_credential
|
||||
raise Exceptions::UnprocessableEntity, 'No facebook app configured!' if !external_credential
|
||||
|
||||
oauth = Koala::Facebook::OAuth.new(
|
||||
external_credential.credentials['application_id'],
|
||||
|
@ -63,12 +69,12 @@ class ExternalCredential::Facebook
|
|||
|
||||
channel.options['auth']['access_token'] = access_token
|
||||
channel.options['pages'] = pages
|
||||
channel.save
|
||||
channel.save!
|
||||
return channel
|
||||
end
|
||||
|
||||
# create channel
|
||||
Channel.create(
|
||||
Channel.create!(
|
||||
area: 'Facebook::Account',
|
||||
options: {
|
||||
adapter: 'facebook',
|
||||
|
|
|
@ -1,27 +1,41 @@
|
|||
class ExternalCredential::Twitter
|
||||
|
||||
def self.app_verify(params)
|
||||
request_account_to_link(params)
|
||||
params
|
||||
register_webhook(params)
|
||||
end
|
||||
|
||||
def self.request_account_to_link(credentials = {})
|
||||
def self.request_account_to_link(credentials = {}, app_required = true)
|
||||
external_credential = ExternalCredential.find_by(name: 'twitter')
|
||||
raise Exceptions::UnprocessableEntity, 'No twitter app configured!' if !external_credential
|
||||
raise Exceptions::UnprocessableEntity, 'No twitter app configured!' if !external_credential && app_required
|
||||
|
||||
if !credentials[:consumer_key]
|
||||
credentials[:consumer_key] = external_credential.credentials['consumer_key']
|
||||
end
|
||||
if !credentials[:consumer_secret]
|
||||
credentials[:consumer_secret] = external_credential.credentials['consumer_secret']
|
||||
if external_credential
|
||||
if credentials[:consumer_key].blank?
|
||||
credentials[:consumer_key] = external_credential.credentials['consumer_key']
|
||||
end
|
||||
if credentials[:consumer_secret].blank?
|
||||
credentials[:consumer_secret] = external_credential.credentials['consumer_secret']
|
||||
end
|
||||
end
|
||||
|
||||
raise Exceptions::UnprocessableEntity, 'No consumer_key param!' if credentials[:consumer_key].blank?
|
||||
raise Exceptions::UnprocessableEntity, 'No consumer_secret param!' if credentials[:consumer_secret].blank?
|
||||
|
||||
consumer = OAuth::Consumer.new(
|
||||
credentials[:consumer_key],
|
||||
credentials[:consumer_secret], {
|
||||
site: 'https://api.twitter.com'
|
||||
}
|
||||
)
|
||||
request_token = consumer.get_request_token(oauth_callback: ExternalCredential.callback_url('twitter'))
|
||||
begin
|
||||
request_token = consumer.get_request_token(oauth_callback: ExternalCredential.callback_url('twitter'))
|
||||
rescue => e
|
||||
if e.message == '403 Forbidden'
|
||||
raise "#{e.message}, maybe credentials wrong or callback_url for application wrong configured."
|
||||
end
|
||||
|
||||
raise e
|
||||
end
|
||||
|
||||
{
|
||||
request_token: request_token,
|
||||
authorize_url: request_token.authorize_url,
|
||||
|
@ -29,42 +43,56 @@ class ExternalCredential::Twitter
|
|||
end
|
||||
|
||||
def self.link_account(request_token, params)
|
||||
raise if request_token.params[:oauth_token] != params[:oauth_token]
|
||||
|
||||
external_credential = ExternalCredential.find_by(name: 'twitter')
|
||||
raise Exceptions::UnprocessableEntity, 'No twitter app configured!' if !external_credential
|
||||
raise Exceptions::UnprocessableEntity, 'No request_token for session found!' if !request_token
|
||||
raise Exceptions::UnprocessableEntity, 'Invalid oauth_token given!' if request_token.params[:oauth_token] != params[:oauth_token]
|
||||
|
||||
access_token = request_token.get_access_token(oauth_verifier: params[:oauth_verifier])
|
||||
client = Twitter::REST::Client.new(
|
||||
client = TwitterSync.new(
|
||||
consumer_key: external_credential.credentials[:consumer_key],
|
||||
consumer_secret: external_credential.credentials[:consumer_secret],
|
||||
access_token: access_token.token,
|
||||
access_token_secret: access_token.secret,
|
||||
)
|
||||
user = client.user
|
||||
client_user = client.who_am_i
|
||||
client_user_id = client_user.id
|
||||
|
||||
# check if account already exists
|
||||
Channel.where(area: 'Twitter::Account').each do |channel|
|
||||
next if !channel.options
|
||||
next if !channel.options['user']
|
||||
next if !channel.options['user']['id']
|
||||
next if channel.options['user']['id'] != user['id']
|
||||
next if channel.options['user']['id'] != client_user_id && channel.options['user']['screen_name'] != client_user.screen_name
|
||||
|
||||
channel.options['user']['id'] = client_user_id
|
||||
channel.options['user']['screen_name'] = client_user.screen_name
|
||||
channel.options['user']['name'] = client_user.name
|
||||
|
||||
# update access_token
|
||||
channel.options['auth']['external_credential_id'] = external_credential.id
|
||||
channel.options['auth']['oauth_token'] = access_token.token
|
||||
channel.options['auth']['oauth_token_secret'] = access_token.secret
|
||||
channel.save
|
||||
channel.save!
|
||||
|
||||
subscribe_webhook(
|
||||
channel: channel,
|
||||
client: client,
|
||||
external_credential: external_credential,
|
||||
)
|
||||
|
||||
return channel
|
||||
end
|
||||
|
||||
# create channel
|
||||
Channel.create(
|
||||
channel = Channel.create!(
|
||||
area: 'Twitter::Account',
|
||||
options: {
|
||||
adapter: 'twitter',
|
||||
user: {
|
||||
id: user.id,
|
||||
screen_name: user.screen_name,
|
||||
name: user.name,
|
||||
id: client_user_id,
|
||||
screen_name: client_user.screen_name,
|
||||
name: client_user.name,
|
||||
},
|
||||
auth: {
|
||||
external_credential_id: external_credential.id,
|
||||
|
@ -84,6 +112,100 @@ class ExternalCredential::Twitter
|
|||
updated_by_id: 1,
|
||||
)
|
||||
|
||||
subscribe_webhook(
|
||||
channel: channel,
|
||||
client: client,
|
||||
external_credential: external_credential,
|
||||
)
|
||||
|
||||
channel
|
||||
end
|
||||
|
||||
def self.webhook_url
|
||||
"#{Setting.get('http_type')}://#{Setting.get('fqdn')}#{Rails.configuration.api_path}/channels_twitter_webhook"
|
||||
end
|
||||
|
||||
def self.register_webhook(params)
|
||||
request_account_to_link(params, false)
|
||||
|
||||
raise Exceptions::UnprocessableEntity, 'No consumer_key param!' if params[:consumer_key].blank?
|
||||
raise Exceptions::UnprocessableEntity, 'No consumer_secret param!' if params[:consumer_secret].blank?
|
||||
raise Exceptions::UnprocessableEntity, 'No oauth_token param!' if params[:oauth_token].blank?
|
||||
raise Exceptions::UnprocessableEntity, 'No oauth_token_secret param!' if params[:oauth_token_secret].blank?
|
||||
|
||||
return if params[:env].blank?
|
||||
|
||||
env_name = params[:env]
|
||||
|
||||
client = TwitterSync.new(
|
||||
consumer_key: params[:consumer_key],
|
||||
consumer_secret: params[:consumer_secret],
|
||||
access_token: params[:oauth_token],
|
||||
access_token_secret: params[:oauth_token_secret],
|
||||
)
|
||||
|
||||
# needed for verify callback
|
||||
Cache.write('external_credential_twitter', {
|
||||
consumer_key: params[:consumer_key],
|
||||
consumer_secret: params[:consumer_secret],
|
||||
access_token: params[:oauth_token],
|
||||
access_token_secret: params[:oauth_token_secret],
|
||||
})
|
||||
|
||||
# verify if webhook is already registered
|
||||
begin
|
||||
webhooks = client.webhooks_by_env_name(env_name)
|
||||
rescue => e
|
||||
begin
|
||||
webhooks = client.webhooks
|
||||
raise "Unable to get list of webooks. You use the wrong 'Dev environment label', only #{webhooks.inspect} available."
|
||||
rescue => e
|
||||
raise "Unable to get list of webooks. Maybe you do not have an Twitter developer approval right now or you use the wrong 'Dev environment label': #{e.message}"
|
||||
end
|
||||
end
|
||||
webhook_id = nil
|
||||
webhook_valid = nil
|
||||
webhooks.each do |webhook|
|
||||
next if webhook[:url] != webhook_url
|
||||
|
||||
webhook_id = webhook[:id]
|
||||
webhook_valid = webhook[:valid]
|
||||
end
|
||||
|
||||
# if webhook is already registered
|
||||
# - in case if webhook is invalid, just send a new verification request
|
||||
# - in case if webhook is valid return
|
||||
if webhook_id
|
||||
if webhook_valid == false
|
||||
client.webhook_request_verification(webhook_id, env_name, webhook_url)
|
||||
end
|
||||
params[:webhook_id] = webhook_id
|
||||
return params
|
||||
end
|
||||
|
||||
# delete already registered webhooks
|
||||
webhooks.each do |webhook|
|
||||
client.webhook_delete(webhook[:id])
|
||||
end
|
||||
|
||||
# register new webhook
|
||||
response = client.webhook_register(env_name, webhook_url)
|
||||
|
||||
params[:webhook_id] = response[:id]
|
||||
params
|
||||
end
|
||||
|
||||
def self.subscribe_webhook(channel:, client:, external_credential:)
|
||||
env_name = external_credential.credentials[:env]
|
||||
webhook_id = external_credential.credentials[:webhook_id]
|
||||
|
||||
Rails.logger.debug { "Starting Twitter subscription for webhook_id #{webhook_id} and Channel #{channel.id}" }
|
||||
client.webhook_subscribe(env_name)
|
||||
|
||||
channel.options['subscribed_to_webhook_id'] = webhook_id
|
||||
channel.save!
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
# Monkey-patch HTTP::URI
|
||||
class HTTP::URI
|
||||
def port
|
||||
443 if https?
|
||||
end
|
||||
end
|
|
@ -7,7 +7,8 @@ class Sessions::Event
|
|||
begin
|
||||
backend = load_adapter(adapter)
|
||||
rescue => e
|
||||
Rails.logger.error e
|
||||
Rails.logger.error e.inspect
|
||||
Rails.logger.error e.backtrace
|
||||
return { event: 'error', data: { error: "No such event #{params[:event]}: #{e.inspect}", payload: params[:payload] } }
|
||||
end
|
||||
|
||||
|
@ -17,7 +18,8 @@ class Sessions::Event
|
|||
instance.destroy
|
||||
result
|
||||
rescue => e
|
||||
Rails.logger.error e
|
||||
Rails.logger.error e.inspect
|
||||
Rails.logger.error e.backtrace
|
||||
return { event: 'error', data: { error: e.message, payload: params[:payload] } }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,463 +0,0 @@
|
|||
# Copyright (C) 2012-2015 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
require 'http/uri'
|
||||
|
||||
class TweetBase
|
||||
|
||||
attr_accessor :client
|
||||
|
||||
def user(tweet)
|
||||
|
||||
if tweet.class == Twitter::DirectMessage
|
||||
Rails.logger.debug { "Twitter sender for dm (#{tweet.id}): found" }
|
||||
Rails.logger.debug { tweet.sender.inspect }
|
||||
tweet.sender
|
||||
elsif tweet.class == Twitter::Tweet
|
||||
Rails.logger.debug { "Twitter sender for tweet (#{tweet.id}): found" }
|
||||
Rails.logger.debug { tweet.user.inspect }
|
||||
tweet.user
|
||||
else
|
||||
raise "Unknown tweet type '#{tweet.class}'"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def to_user(tweet)
|
||||
|
||||
Rails.logger.debug { 'Create user from tweet...' }
|
||||
Rails.logger.debug { tweet.inspect }
|
||||
|
||||
# do tweet_user lookup
|
||||
tweet_user = user(tweet)
|
||||
|
||||
auth = Authorization.find_by(uid: tweet_user.id, provider: 'twitter')
|
||||
|
||||
# create or update user
|
||||
user_data = {
|
||||
image_source: tweet_user.profile_image_url.to_s,
|
||||
}
|
||||
if auth
|
||||
user = User.find(auth.user_id)
|
||||
map = {
|
||||
note: 'description',
|
||||
web: 'website',
|
||||
address: 'location',
|
||||
}
|
||||
|
||||
# ignore if value is already set
|
||||
map.each do |target, source|
|
||||
next if user[target].present?
|
||||
|
||||
new_value = tweet_user.send(source).to_s
|
||||
next if new_value.blank?
|
||||
|
||||
user_data[target] = new_value
|
||||
end
|
||||
user.update!(user_data)
|
||||
else
|
||||
user_data[:login] = tweet_user.screen_name
|
||||
user_data[:firstname] = tweet_user.name
|
||||
user_data[:web] = tweet_user.website.to_s
|
||||
user_data[:note] = tweet_user.description
|
||||
user_data[:address] = tweet_user.location
|
||||
user_data[:active] = true
|
||||
user_data[:role_ids] = Role.signup_role_ids
|
||||
|
||||
user = User.create!(user_data)
|
||||
end
|
||||
|
||||
if user_data[:image_source]
|
||||
avatar = Avatar.add(
|
||||
object: 'User',
|
||||
o_id: user.id,
|
||||
url: user_data[:image_source],
|
||||
source: 'twitter',
|
||||
deletable: true,
|
||||
updated_by_id: user.id,
|
||||
created_by_id: user.id,
|
||||
)
|
||||
|
||||
# update user link
|
||||
if avatar && user.image != avatar.store_hash
|
||||
user.image = avatar.store_hash
|
||||
user.save
|
||||
end
|
||||
end
|
||||
|
||||
# create or update authorization
|
||||
auth_data = {
|
||||
uid: tweet_user.id,
|
||||
username: tweet_user.screen_name,
|
||||
user_id: user.id,
|
||||
provider: 'twitter'
|
||||
}
|
||||
if auth
|
||||
auth.update!(auth_data)
|
||||
else
|
||||
Authorization.create!(auth_data)
|
||||
end
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def to_ticket(tweet, user, group_id, channel)
|
||||
UserInfo.current_user_id = user.id
|
||||
|
||||
Rails.logger.debug { 'Create ticket from tweet...' }
|
||||
Rails.logger.debug { tweet.inspect }
|
||||
Rails.logger.debug { user.inspect }
|
||||
Rails.logger.debug { group_id.inspect }
|
||||
|
||||
if tweet.class == Twitter::DirectMessage
|
||||
ticket = Ticket.find_by(
|
||||
create_article_type: Ticket::Article::Type.lookup(name: 'twitter direct-message'),
|
||||
customer_id: user.id,
|
||||
state: Ticket::State.where.not(
|
||||
state_type_id: Ticket::StateType.where(
|
||||
name: %w[closed merged removed],
|
||||
)
|
||||
)
|
||||
)
|
||||
return ticket if ticket
|
||||
end
|
||||
|
||||
# prepare title
|
||||
title = tweet.text
|
||||
if title.length > 80
|
||||
title = "#{title[0, 80]}..."
|
||||
end
|
||||
|
||||
state = get_state(channel, tweet)
|
||||
|
||||
Ticket.create!(
|
||||
customer_id: user.id,
|
||||
title: title,
|
||||
group_id: group_id || Group.first.id,
|
||||
state: state,
|
||||
priority: Ticket::Priority.find_by(name: '2 normal'),
|
||||
preferences: {
|
||||
channel_id: channel.id,
|
||||
channel_screen_name: channel.options['user']['screen_name'],
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
def to_article(tweet, user, ticket, channel)
|
||||
|
||||
Rails.logger.debug { 'Create article from tweet...' }
|
||||
Rails.logger.debug { tweet.inspect }
|
||||
Rails.logger.debug { user.inspect }
|
||||
Rails.logger.debug { ticket.inspect }
|
||||
|
||||
# import tweet
|
||||
to = nil
|
||||
from = nil
|
||||
article_type = nil
|
||||
in_reply_to = nil
|
||||
twitter_preferences = {}
|
||||
if tweet.class == Twitter::DirectMessage
|
||||
article_type = 'twitter direct-message'
|
||||
to = "@#{tweet.recipient.screen_name}"
|
||||
from = "@#{tweet.sender.screen_name}"
|
||||
twitter_preferences = {
|
||||
created_at: tweet.created_at,
|
||||
recipient_id: tweet.recipient.id,
|
||||
recipient_screen_name: tweet.recipient.screen_name,
|
||||
sender_id: tweet.sender.id,
|
||||
sender_screen_name: tweet.sender.screen_name,
|
||||
}
|
||||
elsif tweet.class == Twitter::Tweet
|
||||
article_type = 'twitter status'
|
||||
from = "@#{tweet.user.screen_name}"
|
||||
mention_ids = []
|
||||
tweet.user_mentions&.each do |local_user|
|
||||
if !to
|
||||
to = ''
|
||||
else
|
||||
to += ', '
|
||||
end
|
||||
to += "@#{local_user.screen_name}"
|
||||
mention_ids.push local_user.id
|
||||
end
|
||||
in_reply_to = tweet.in_reply_to_status_id
|
||||
|
||||
twitter_preferences = {
|
||||
mention_ids: mention_ids,
|
||||
geo: tweet.geo,
|
||||
retweeted: tweet.retweeted?,
|
||||
possibly_sensitive: tweet.possibly_sensitive?,
|
||||
in_reply_to_user_id: tweet.in_reply_to_user_id,
|
||||
place: tweet.place,
|
||||
retweet_count: tweet.retweet_count,
|
||||
source: tweet.source,
|
||||
favorited: tweet.favorited?,
|
||||
truncated: tweet.truncated?,
|
||||
}
|
||||
|
||||
else
|
||||
raise "Unknown tweet type '#{tweet.class}'"
|
||||
end
|
||||
|
||||
UserInfo.current_user_id = user.id
|
||||
|
||||
# set ticket state to open if not new
|
||||
ticket_state = get_state(channel, tweet, ticket)
|
||||
if ticket_state.name != ticket.state.name
|
||||
ticket.state = ticket_state
|
||||
ticket.save!
|
||||
end
|
||||
|
||||
article_preferences = {
|
||||
twitter: self.class.preferences_cleanup(twitter_preferences),
|
||||
links: [
|
||||
{
|
||||
url: "https://twitter.com/statuses/#{tweet.id}",
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
Ticket::Article.create!(
|
||||
from: from,
|
||||
to: to,
|
||||
body: tweet.text,
|
||||
message_id: tweet.id,
|
||||
ticket_id: ticket.id,
|
||||
in_reply_to: in_reply_to,
|
||||
type_id: Ticket::Article::Type.find_by(name: article_type).id,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
internal: false,
|
||||
preferences: article_preferences,
|
||||
)
|
||||
end
|
||||
|
||||
def to_group(tweet, group_id, channel)
|
||||
|
||||
Rails.logger.debug { 'import tweet' }
|
||||
|
||||
# use transaction
|
||||
if @connection_type == 'stream'
|
||||
ActiveRecord::Base.connection.reconnect!
|
||||
end
|
||||
|
||||
ticket = nil
|
||||
Transaction.execute(reset_user_id: true) do
|
||||
|
||||
# check if parent exists
|
||||
user = to_user(tweet)
|
||||
if tweet.class == Twitter::DirectMessage
|
||||
ticket = to_ticket(tweet, user, group_id, channel)
|
||||
to_article(tweet, user, ticket, channel)
|
||||
elsif tweet.class == Twitter::Tweet
|
||||
if tweet.in_reply_to_status_id && tweet.in_reply_to_status_id.to_s != ''
|
||||
existing_article = Ticket::Article.find_by(message_id: tweet.in_reply_to_status_id)
|
||||
if existing_article
|
||||
ticket = existing_article.ticket
|
||||
else
|
||||
begin
|
||||
# in case of streaming mode, get parent tweet via REST client
|
||||
if @connection_type == 'stream'
|
||||
client = TweetRest.new(@auth)
|
||||
parent_tweet = client.status(tweet.in_reply_to_status_id)
|
||||
else
|
||||
parent_tweet = @client.status(tweet.in_reply_to_status_id)
|
||||
end
|
||||
ticket = to_group(parent_tweet, group_id, channel)
|
||||
rescue Twitter::Error::NotFound, Twitter::Error::Forbidden => e
|
||||
# just ignore if tweet has already gone
|
||||
Rails.logger.info "Can't import tweet (#{tweet.in_reply_to_status_id}), #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
if !ticket
|
||||
ticket = to_ticket(tweet, user, group_id, channel)
|
||||
end
|
||||
to_article(tweet, user, ticket, channel)
|
||||
else
|
||||
raise "Unknown tweet type '#{tweet.class}'"
|
||||
end
|
||||
end
|
||||
|
||||
if @connection_type == 'stream'
|
||||
ActiveRecord::Base.connection.close
|
||||
end
|
||||
ticket
|
||||
end
|
||||
|
||||
def from_article(article)
|
||||
|
||||
tweet = nil
|
||||
if article[:type] == 'twitter direct-message'
|
||||
|
||||
Rails.logger.debug { "Create twitter direct message from article to '#{article[:to]}'..." }
|
||||
|
||||
tweet = @client.create_direct_message(
|
||||
article[:to],
|
||||
article[:body],
|
||||
{}
|
||||
)
|
||||
elsif article[:type] == 'twitter status'
|
||||
|
||||
Rails.logger.debug { 'Create tweet from article...' }
|
||||
|
||||
tweet = @client.update(
|
||||
article[:body],
|
||||
{
|
||||
in_reply_to_status_id: article[:in_reply_to]
|
||||
}
|
||||
)
|
||||
else
|
||||
raise "Can't handle unknown twitter article type '#{article[:type]}'."
|
||||
end
|
||||
|
||||
Rails.logger.debug { tweet.inspect }
|
||||
tweet
|
||||
end
|
||||
|
||||
def get_state(channel, tweet, ticket = nil)
|
||||
|
||||
tweet_user = user(tweet)
|
||||
|
||||
# no changes in post is from page user it self
|
||||
if channel.options[:user][:id].to_s == tweet_user.id.to_s
|
||||
if !ticket
|
||||
return Ticket::State.find_by(name: 'closed') if !ticket
|
||||
end
|
||||
return ticket.state
|
||||
end
|
||||
|
||||
state = Ticket::State.find_by(default_create: true)
|
||||
return state if !ticket
|
||||
return ticket.state if ticket.state_id == state.id
|
||||
|
||||
Ticket::State.find_by(default_follow_up: true)
|
||||
end
|
||||
|
||||
def tweet_limit_reached(tweet, factor = 1)
|
||||
max_count = 120
|
||||
if @connection_type == 'stream'
|
||||
max_count = 30
|
||||
end
|
||||
max_count = max_count * factor
|
||||
type_id = Ticket::Article::Type.lookup(name: 'twitter status').id
|
||||
created_at = Time.zone.now - 15.minutes
|
||||
created_count = Ticket::Article.where('created_at > ? AND type_id = ?', created_at, type_id).count
|
||||
if created_count > max_count
|
||||
Rails.logger.info "Tweet limit of #{created_count}/#{max_count} reached, ignored tweed id (#{tweet.id})"
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def direct_message_limit_reached(tweet, factor = 1)
|
||||
max_count = 100
|
||||
if @connection_type == 'stream'
|
||||
max_count = 40
|
||||
end
|
||||
max_count = max_count * factor
|
||||
type_id = Ticket::Article::Type.lookup(name: 'twitter direct-message').id
|
||||
created_at = Time.zone.now - 15.minutes
|
||||
created_count = Ticket::Article.where('created_at > ? AND type_id = ?', created_at, type_id).count
|
||||
if created_count > max_count
|
||||
Rails.logger.info "Tweet direct message limit reached #{created_count}/#{max_count}, ignored tweed id (#{tweet.id})"
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
replace Twitter::Place and Twitter::Geo as hash and replace Twitter::NullObject with nil
|
||||
|
||||
preferences = TweetBase.preferences_cleanup(
|
||||
twitter: twitter_preferences,
|
||||
links: [
|
||||
{
|
||||
url: 'https://twitter.com/statuses/123',
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
or
|
||||
|
||||
preferences = {
|
||||
twitter: TweetBase.preferences_cleanup(twitter_preferences),
|
||||
links: [
|
||||
{
|
||||
url: 'https://twitter.com/statuses/123',
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
=end
|
||||
|
||||
def self.preferences_cleanup(preferences)
|
||||
|
||||
# replace Twitter::NullObject with nill to prevent elasticsearch index issue
|
||||
preferences.each do |key, value|
|
||||
|
||||
if value.class == Twitter::Place || value.class == Twitter::Geo
|
||||
preferences[key] = value.to_h
|
||||
next
|
||||
end
|
||||
if value.class == Twitter::NullObject
|
||||
preferences[key] = nil
|
||||
next
|
||||
end
|
||||
|
||||
next if !value.is_a?(Hash)
|
||||
|
||||
value.each do |sub_key, sub_level|
|
||||
if sub_level.class == NilClass
|
||||
value[sub_key] = nil
|
||||
next
|
||||
end
|
||||
if sub_level.class == Twitter::Place || sub_level.class == Twitter::Geo
|
||||
value[sub_key] = sub_level.to_h
|
||||
next
|
||||
end
|
||||
next if sub_level.class != Twitter::NullObject
|
||||
|
||||
value[sub_key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if preferences[:twitter]
|
||||
if preferences[:twitter][:geo].blank?
|
||||
preferences[:twitter][:geo] = {}
|
||||
end
|
||||
if preferences[:twitter][:place].blank?
|
||||
preferences[:twitter][:place] = {}
|
||||
end
|
||||
else
|
||||
if preferences[:geo].blank?
|
||||
preferences[:geo] = {}
|
||||
end
|
||||
if preferences[:place].blank?
|
||||
preferences[:place] = {}
|
||||
end
|
||||
end
|
||||
|
||||
preferences
|
||||
end
|
||||
|
||||
def locale_sender?(tweet)
|
||||
tweet_user = user(tweet)
|
||||
Channel.where(area: 'Twitter::Account').each do |local_channel|
|
||||
next if !local_channel.options
|
||||
next if !local_channel.options[:user]
|
||||
next if !local_channel.options[:user][:id]
|
||||
next if local_channel.options[:user][:id].to_s != tweet_user.id.to_s
|
||||
|
||||
Rails.logger.debug { "Tweet is sent by local account with user id #{tweet_user.id} and tweet.id #{tweet.id}" }
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
end
|
|
@ -1,24 +0,0 @@
|
|||
# Copyright (C) 2012-2015 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class TweetRest < TweetBase
|
||||
|
||||
attr_accessor :client
|
||||
|
||||
def initialize(auth)
|
||||
@connection_type = 'rest'
|
||||
@client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = auth[:consumer_key]
|
||||
config.consumer_secret = auth[:consumer_secret]
|
||||
config.access_token = auth[:oauth_token]
|
||||
config.access_token_secret = auth[:oauth_token_secret]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def disconnect
|
||||
return if !@client
|
||||
|
||||
@client = nil
|
||||
end
|
||||
|
||||
end
|
|
@ -1,29 +0,0 @@
|
|||
# Copyright (C) 2012-2015 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class TweetStream < TweetBase
|
||||
|
||||
attr_accessor :client
|
||||
|
||||
def initialize(auth)
|
||||
@connection_type = 'stream'
|
||||
@auth = auth
|
||||
@client = Twitter::Streaming::ClientCustom.new do |config|
|
||||
config.consumer_key = auth[:consumer_key]
|
||||
config.consumer_secret = auth[:consumer_secret]
|
||||
config.access_token = auth[:oauth_token]
|
||||
config.access_token_secret = auth[:oauth_token_secret]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def disconnect
|
||||
if @client&.custom_connection_handle
|
||||
@client.custom_connection_handle.close
|
||||
end
|
||||
|
||||
return if !@client
|
||||
|
||||
@client = nil
|
||||
end
|
||||
|
||||
end
|
969
lib/twitter_sync.rb
Normal file
969
lib/twitter_sync.rb
Normal file
|
@ -0,0 +1,969 @@
|
|||
# Copyright (C) 2012-2015 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
require 'http/uri'
|
||||
|
||||
class TwitterSync
|
||||
|
||||
attr_accessor :client
|
||||
|
||||
def initialize(auth, payload = nil)
|
||||
@client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = auth[:consumer_key]
|
||||
config.consumer_secret = auth[:consumer_secret]
|
||||
config.access_token = auth[:oauth_token] || auth[:access_token]
|
||||
config.access_token_secret = auth[:oauth_token_secret] || auth[:access_token_secret]
|
||||
end
|
||||
@payload = payload
|
||||
end
|
||||
|
||||
def disconnect
|
||||
return if !@client
|
||||
|
||||
@client = nil
|
||||
end
|
||||
|
||||
def user(tweet)
|
||||
raise "Unknown tweet type '#{tweet.class}'" if tweet.class != Twitter::Tweet
|
||||
|
||||
Rails.logger.debug { "Twitter sender for tweet (#{tweet.id}): found" }
|
||||
Rails.logger.debug { tweet.user.inspect }
|
||||
tweet.user
|
||||
end
|
||||
|
||||
def to_user(tweet)
|
||||
|
||||
Rails.logger.debug { 'Create user from tweet...' }
|
||||
Rails.logger.debug { tweet.inspect }
|
||||
|
||||
# do tweet_user lookup
|
||||
tweet_user = user(tweet)
|
||||
|
||||
auth = Authorization.find_by(uid: tweet_user.id, provider: 'twitter')
|
||||
|
||||
# create or update user
|
||||
user_data = {
|
||||
image_source: tweet_user.profile_image_url.to_s,
|
||||
}
|
||||
if auth
|
||||
user = User.find(auth.user_id)
|
||||
map = {
|
||||
note: 'description',
|
||||
web: 'website',
|
||||
address: 'location',
|
||||
}
|
||||
|
||||
# ignore if value is already set
|
||||
map.each do |target, source|
|
||||
next if user[target].present?
|
||||
|
||||
new_value = tweet_user.send(source).to_s
|
||||
next if new_value.blank?
|
||||
|
||||
user_data[target] = new_value
|
||||
end
|
||||
user.update!(user_data)
|
||||
else
|
||||
user_data[:login] = tweet_user.screen_name
|
||||
user_data[:firstname] = tweet_user.name
|
||||
user_data[:web] = tweet_user.website.to_s
|
||||
user_data[:note] = tweet_user.description
|
||||
user_data[:address] = tweet_user.location
|
||||
user_data[:active] = true
|
||||
user_data[:role_ids] = Role.signup_role_ids
|
||||
|
||||
user = User.create!(user_data)
|
||||
end
|
||||
|
||||
if user_data[:image_source]
|
||||
avatar = Avatar.add(
|
||||
object: 'User',
|
||||
o_id: user.id,
|
||||
url: user_data[:image_source],
|
||||
source: 'twitter',
|
||||
deletable: true,
|
||||
updated_by_id: user.id,
|
||||
created_by_id: user.id,
|
||||
)
|
||||
|
||||
# update user link
|
||||
if avatar && user.image != avatar.store_hash
|
||||
user.image = avatar.store_hash
|
||||
user.save
|
||||
end
|
||||
end
|
||||
|
||||
# create or update authorization
|
||||
auth_data = {
|
||||
uid: tweet_user.id,
|
||||
username: tweet_user.screen_name,
|
||||
user_id: user.id,
|
||||
provider: 'twitter'
|
||||
}
|
||||
if auth
|
||||
auth.update!(auth_data)
|
||||
else
|
||||
Authorization.create!(auth_data)
|
||||
end
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def to_ticket(tweet, user, group_id, channel)
|
||||
UserInfo.current_user_id = user.id
|
||||
|
||||
Rails.logger.debug { 'Create ticket from tweet...' }
|
||||
Rails.logger.debug { tweet.inspect }
|
||||
Rails.logger.debug { user.inspect }
|
||||
Rails.logger.debug { group_id.inspect }
|
||||
|
||||
# normalize message
|
||||
message = {}
|
||||
|
||||
if tweet.class == Twitter::Tweet
|
||||
message = {
|
||||
type: 'tweet',
|
||||
text: tweet.text,
|
||||
}
|
||||
state = get_state(channel, tweet)
|
||||
end
|
||||
|
||||
if tweet.is_a?(Hash) && tweet['type'] == 'message_create'
|
||||
message = {
|
||||
type: 'direct_message',
|
||||
text: tweet['message_create']['message_data']['text'],
|
||||
}
|
||||
state = get_state(channel, tweet)
|
||||
end
|
||||
|
||||
if tweet.is_a?(Hash) && tweet['text'].present?
|
||||
message = {
|
||||
type: 'tweet',
|
||||
text: tweet['text'],
|
||||
}
|
||||
state = get_state(channel, tweet)
|
||||
end
|
||||
|
||||
# process message
|
||||
if message[:type] == 'direct_message'
|
||||
ticket = Ticket.find_by(
|
||||
create_article_type: Ticket::Article::Type.lookup(name: 'twitter direct-message'),
|
||||
customer_id: user.id,
|
||||
state: Ticket::State.where.not(
|
||||
state_type_id: Ticket::StateType.where(
|
||||
name: %w[closed merged removed],
|
||||
)
|
||||
)
|
||||
)
|
||||
return ticket if ticket
|
||||
end
|
||||
|
||||
# prepare title
|
||||
title = message[:text]
|
||||
if title.length > 80
|
||||
title = "#{title[0, 80]}..."
|
||||
end
|
||||
|
||||
Ticket.create!(
|
||||
customer_id: user.id,
|
||||
title: title,
|
||||
group_id: group_id || Group.first.id,
|
||||
state: state,
|
||||
priority: Ticket::Priority.find_by(default_create: true),
|
||||
preferences: {
|
||||
channel_id: channel.id,
|
||||
channel_screen_name: channel.options['user']['screen_name'],
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
def to_article_webhook(item, user, ticket, channel)
|
||||
|
||||
Rails.logger.debug { 'Create article from tweet...' }
|
||||
Rails.logger.debug { item.inspect }
|
||||
Rails.logger.debug { user.inspect }
|
||||
Rails.logger.debug { ticket.inspect }
|
||||
|
||||
# import tweet
|
||||
to = nil
|
||||
from = nil
|
||||
text = nil
|
||||
message_id = nil
|
||||
article_type = nil
|
||||
in_reply_to = nil
|
||||
twitter_preferences = {}
|
||||
attachments = []
|
||||
|
||||
if item['type'] == 'message_create'
|
||||
message_id = item['id']
|
||||
text = item['message_create']['message_data']['text']
|
||||
if item['message_create']['message_data']['entities'] && item['message_create']['message_data']['entities']['urls'].present?
|
||||
item['message_create']['message_data']['entities']['urls'].each do |local_url|
|
||||
next if local_url['url'].blank?
|
||||
|
||||
if local_url['expanded_url'].present?
|
||||
text.gsub!(/#{Regexp.quote(local_url['url'])}/, local_url['expanded_url'])
|
||||
elsif local_url['display_url']
|
||||
text.gsub!(/#{Regexp.quote(local_url['url'])}/, local_url['display_url'])
|
||||
end
|
||||
end
|
||||
end
|
||||
app = get_app_webhook(item['message_create']['source_app_id'])
|
||||
article_type = 'twitter direct-message'
|
||||
recipient_screen_name = to_user_webhook_data(item['message_create']['target']['recipient_id'])['screen_name']
|
||||
sender_screen_name = to_user_webhook_data(item['message_create']['sender_id'])['screen_name']
|
||||
to = "@#{recipient_screen_name}"
|
||||
from = "@#{sender_screen_name}"
|
||||
twitter_preferences = {
|
||||
created_at: item['created_timestamp'],
|
||||
recipient_id: item['message_create']['target']['recipient_id'],
|
||||
recipient_screen_name: recipient_screen_name,
|
||||
sender_id: item['message_create']['sender_id'],
|
||||
sender_screen_name: sender_screen_name,
|
||||
app_id: app['app_id'],
|
||||
app_name: app['app_name'],
|
||||
}
|
||||
elsif item['text'].present?
|
||||
message_id = item['id']
|
||||
text = item['text']
|
||||
if item['extended_tweet'] && item['extended_tweet']['full_text'].present?
|
||||
text = item['extended_tweet']['full_text']
|
||||
end
|
||||
article_type = 'twitter status'
|
||||
sender_screen_name = item['user']['screen_name']
|
||||
from = "@#{sender_screen_name}"
|
||||
mention_ids = []
|
||||
if item['entities']
|
||||
|
||||
item['entities']['user_mentions']&.each do |local_user|
|
||||
if !to
|
||||
to = ''
|
||||
else
|
||||
to += ', '
|
||||
end
|
||||
to += "@#{local_user['screen_name']}"
|
||||
mention_ids.push local_user['id']
|
||||
end
|
||||
|
||||
item['entities']['media']&.each do |local_media|
|
||||
|
||||
if local_media['url'].present?
|
||||
if local_media['expanded_url'].present?
|
||||
text.gsub!(/#{Regexp.quote(local_media['url'])}/, local_media['expanded_url'])
|
||||
elsif local_media['display_url']
|
||||
text.gsub!(/#{Regexp.quote(local_media['url'])}/, local_media['display_url'])
|
||||
end
|
||||
end
|
||||
|
||||
url = local_media['media_url_https'] || local_media['media_url']
|
||||
next if url.blank?
|
||||
|
||||
result = download_file(url)
|
||||
if !result.success? || !result.body
|
||||
Rails.logger.error "Unable for download image from twitter (#{url}): #{result.code}"
|
||||
next
|
||||
end
|
||||
|
||||
attachment = {
|
||||
filename: url.sub(%r{^.*/(.+?)$}, '\1'),
|
||||
content: result.body,
|
||||
|
||||
}
|
||||
attachments.push attachment
|
||||
end
|
||||
end
|
||||
|
||||
in_reply_to = item['in_reply_to_status_id']
|
||||
|
||||
twitter_preferences = {
|
||||
mention_ids: mention_ids,
|
||||
geo: item['geo'],
|
||||
retweeted: item['retweeted'],
|
||||
possibly_sensitive: item['possibly_sensitive'],
|
||||
in_reply_to_user_id: item['in_reply_to_user_id'],
|
||||
place: item['place'],
|
||||
retweet_count: item['retweet_count'],
|
||||
source: item['source'],
|
||||
favorited: item['favorited'],
|
||||
truncated: item['truncated'],
|
||||
}
|
||||
|
||||
else
|
||||
raise "Unknown tweet type '#{item.class}'"
|
||||
end
|
||||
|
||||
UserInfo.current_user_id = user.id
|
||||
|
||||
# set ticket state to open if not new
|
||||
ticket_state = get_state(channel, item, ticket)
|
||||
if ticket_state.name != ticket.state.name
|
||||
ticket.state = ticket_state
|
||||
ticket.save!
|
||||
end
|
||||
|
||||
article_preferences = {
|
||||
twitter: self.class.preferences_cleanup(twitter_preferences),
|
||||
links: [
|
||||
{
|
||||
url: "https://twitter.com/statuses/#{item['id']}",
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
article = Ticket::Article.create!(
|
||||
from: from,
|
||||
to: to,
|
||||
body: text,
|
||||
message_id: message_id,
|
||||
ticket_id: ticket.id,
|
||||
in_reply_to: in_reply_to,
|
||||
type_id: Ticket::Article::Type.find_by(name: article_type).id,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
internal: false,
|
||||
preferences: self.class.preferences_cleanup(article_preferences),
|
||||
)
|
||||
|
||||
attachments.each do |attachment|
|
||||
Store.add(
|
||||
object: 'Ticket::Article',
|
||||
o_id: article.id,
|
||||
data: attachment[:content],
|
||||
filename: attachment[:filename],
|
||||
preferences: {},
|
||||
)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def to_article(tweet, user, ticket, channel)
|
||||
|
||||
Rails.logger.debug { 'Create article from tweet...' }
|
||||
Rails.logger.debug { tweet.inspect }
|
||||
Rails.logger.debug { user.inspect }
|
||||
Rails.logger.debug { ticket.inspect }
|
||||
|
||||
# import tweet
|
||||
to = nil
|
||||
from = nil
|
||||
article_type = nil
|
||||
in_reply_to = nil
|
||||
twitter_preferences = {}
|
||||
raise "Unknown tweet type '#{tweet.class}'" if tweet.class != Twitter::Tweet
|
||||
|
||||
article_type = 'twitter status'
|
||||
from = "@#{tweet.user.screen_name}"
|
||||
mention_ids = []
|
||||
tweet.user_mentions&.each do |local_user|
|
||||
if !to
|
||||
to = ''
|
||||
else
|
||||
to += ', '
|
||||
end
|
||||
to += "@#{local_user.screen_name}"
|
||||
mention_ids.push local_user.id
|
||||
end
|
||||
in_reply_to = tweet.in_reply_to_status_id
|
||||
|
||||
twitter_preferences = {
|
||||
mention_ids: mention_ids,
|
||||
geo: tweet.geo,
|
||||
retweeted: tweet.retweeted?,
|
||||
possibly_sensitive: tweet.possibly_sensitive?,
|
||||
in_reply_to_user_id: tweet.in_reply_to_user_id,
|
||||
place: tweet.place,
|
||||
retweet_count: tweet.retweet_count,
|
||||
source: tweet.source,
|
||||
favorited: tweet.favorited?,
|
||||
truncated: tweet.truncated?,
|
||||
}
|
||||
|
||||
UserInfo.current_user_id = user.id
|
||||
|
||||
# set ticket state to open if not new
|
||||
ticket_state = get_state(channel, tweet, ticket)
|
||||
if ticket_state.name != ticket.state.name
|
||||
ticket.state = ticket_state
|
||||
ticket.save!
|
||||
end
|
||||
|
||||
article_preferences = {
|
||||
twitter: self.class.preferences_cleanup(twitter_preferences),
|
||||
links: [
|
||||
{
|
||||
url: "https://twitter.com/statuses/#{tweet.id}",
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
Ticket::Article.create!(
|
||||
from: from,
|
||||
to: to,
|
||||
body: tweet.text,
|
||||
message_id: tweet.id,
|
||||
ticket_id: ticket.id,
|
||||
in_reply_to: in_reply_to,
|
||||
type_id: Ticket::Article::Type.find_by(name: article_type).id,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
internal: false,
|
||||
preferences: self.class.preferences_cleanup(article_preferences),
|
||||
)
|
||||
end
|
||||
|
||||
def to_group(tweet, group_id, channel)
|
||||
|
||||
Rails.logger.debug { 'import tweet' }
|
||||
|
||||
ticket = nil
|
||||
Transaction.execute(reset_user_id: true) do
|
||||
|
||||
# check if parent exists
|
||||
user = to_user(tweet)
|
||||
raise "Unknown tweet type '#{tweet.class}'" if tweet.class != Twitter::Tweet
|
||||
|
||||
if tweet.in_reply_to_status_id && tweet.in_reply_to_status_id.to_s != ''
|
||||
existing_article = Ticket::Article.find_by(message_id: tweet.in_reply_to_status_id)
|
||||
if existing_article
|
||||
ticket = existing_article.ticket
|
||||
else
|
||||
begin
|
||||
parent_tweet = @client.status(tweet.in_reply_to_status_id)
|
||||
ticket = to_group(parent_tweet, group_id, channel)
|
||||
rescue Twitter::Error::NotFound, Twitter::Error::Forbidden => e
|
||||
# just ignore if tweet has already gone
|
||||
Rails.logger.info "Can't import tweet (#{tweet.in_reply_to_status_id}), #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
if !ticket
|
||||
ticket = to_ticket(tweet, user, group_id, channel)
|
||||
end
|
||||
to_article(tweet, user, ticket, channel)
|
||||
end
|
||||
|
||||
ticket
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
create a tweet ot direct message from an article
|
||||
|
||||
=end
|
||||
|
||||
def from_article(article)
|
||||
|
||||
tweet = nil
|
||||
if article[:type] == 'twitter direct-message'
|
||||
|
||||
Rails.logger.debug { "Create twitter direct message from article to '#{article[:to]}'..." }
|
||||
|
||||
# tweet = @client.create_direct_message(
|
||||
# article[:to],
|
||||
# article[:body],
|
||||
# {}
|
||||
# )
|
||||
article[:to].delete!('@')
|
||||
authorization = Authorization.find_by(provider: 'twitter', username: article[:to])
|
||||
raise "Unable to lookup user_id for @#{article[:to]}" if !authorization
|
||||
|
||||
data = {
|
||||
event: {
|
||||
type: 'message_create',
|
||||
message_create: {
|
||||
target: {
|
||||
recipient_id: authorization.uid,
|
||||
},
|
||||
message_data: {
|
||||
text: article[:body],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tweet = Twitter::REST::Request.new(@client, :json_post, '/1.1/direct_messages/events/new.json', data).perform
|
||||
|
||||
elsif article[:type] == 'twitter status'
|
||||
|
||||
Rails.logger.debug { 'Create tweet from article...' }
|
||||
|
||||
tweet = @client.update(
|
||||
article[:body],
|
||||
{
|
||||
in_reply_to_status_id: article[:in_reply_to]
|
||||
}
|
||||
)
|
||||
else
|
||||
raise "Can't handle unknown twitter article type '#{article[:type]}'."
|
||||
end
|
||||
|
||||
Rails.logger.debug { tweet.inspect }
|
||||
tweet
|
||||
end
|
||||
|
||||
def get_state(channel, tweet, ticket = nil)
|
||||
|
||||
user_id = nil
|
||||
user_id = if tweet.is_a?(Hash)
|
||||
if tweet['user'] && tweet['user']['id']
|
||||
tweet['user']['id']
|
||||
else
|
||||
tweet['message_create']['sender_id']
|
||||
end
|
||||
else
|
||||
user(tweet).id
|
||||
end
|
||||
|
||||
# no changes in post is from page user it self
|
||||
if channel.options[:user][:id].to_s == user_id.to_s
|
||||
if !ticket
|
||||
return Ticket::State.find_by(name: 'closed') if !ticket
|
||||
end
|
||||
return ticket.state
|
||||
end
|
||||
|
||||
state = Ticket::State.find_by(default_create: true)
|
||||
return state if !ticket
|
||||
return ticket.state if ticket.state_id == state.id
|
||||
|
||||
Ticket::State.find_by(default_follow_up: true)
|
||||
end
|
||||
|
||||
def tweet_limit_reached(tweet, factor = 1)
|
||||
max_count = 120
|
||||
max_count = max_count * factor
|
||||
type_id = Ticket::Article::Type.lookup(name: 'twitter status').id
|
||||
created_at = Time.zone.now - 15.minutes
|
||||
created_count = Ticket::Article.where('created_at > ? AND type_id = ?', created_at, type_id).count
|
||||
if created_count > max_count
|
||||
Rails.logger.info "Tweet limit of #{created_count}/#{max_count} reached, ignored tweed id (#{tweet.id})"
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def direct_message_limit_reached(tweet, factor = 1)
|
||||
max_count = 100
|
||||
max_count = max_count * factor
|
||||
type_id = Ticket::Article::Type.lookup(name: 'twitter direct-message').id
|
||||
created_at = Time.zone.now - 15.minutes
|
||||
created_count = Ticket::Article.where('created_at > ? AND type_id = ?', created_at, type_id).count
|
||||
if created_count > max_count
|
||||
Rails.logger.info "Tweet direct message limit reached #{created_count}/#{max_count}, ignored tweed id (#{tweet.id})"
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
replace Twitter::Place and Twitter::Geo as hash and replace Twitter::NullObject with nil
|
||||
|
||||
preferences = TwitterSync.preferences_cleanup(
|
||||
twitter: twitter_preferences,
|
||||
links: [
|
||||
{
|
||||
url: 'https://twitter.com/statuses/123',
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
or
|
||||
|
||||
preferences = {
|
||||
twitter: TwitterSync.preferences_cleanup(twitter_preferences),
|
||||
links: [
|
||||
{
|
||||
url: 'https://twitter.com/statuses/123',
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
=end
|
||||
|
||||
def self.preferences_cleanup(preferences)
|
||||
|
||||
# replace Twitter::NullObject with nill to prevent elasticsearch index issue
|
||||
preferences.each do |key, value|
|
||||
|
||||
if value.class == Twitter::Place || value.class == Twitter::Geo
|
||||
preferences[key] = value.to_h
|
||||
next
|
||||
end
|
||||
if value.class == Twitter::NullObject
|
||||
preferences[key] = nil
|
||||
next
|
||||
end
|
||||
|
||||
next if !value.is_a?(Hash)
|
||||
|
||||
value.each do |sub_key, sub_level|
|
||||
if sub_level.class == NilClass
|
||||
value[sub_key] = nil
|
||||
next
|
||||
end
|
||||
if sub_level.class == Twitter::Place || sub_level.class == Twitter::Geo
|
||||
value[sub_key] = sub_level.to_h
|
||||
next
|
||||
end
|
||||
next if sub_level.class != Twitter::NullObject
|
||||
|
||||
value[sub_key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if preferences[:twitter]
|
||||
if preferences[:twitter][:geo].blank?
|
||||
preferences[:twitter][:geo] = {}
|
||||
end
|
||||
if preferences[:twitter][:place].blank?
|
||||
preferences[:twitter][:place] = {}
|
||||
end
|
||||
else
|
||||
if preferences[:geo].blank?
|
||||
preferences[:geo] = {}
|
||||
end
|
||||
if preferences[:place].blank?
|
||||
preferences[:place] = {}
|
||||
end
|
||||
end
|
||||
|
||||
preferences
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
check if tweet is from local sender
|
||||
|
||||
client = TwitterSync.new
|
||||
client.locale_sender?(tweet)
|
||||
|
||||
=end
|
||||
|
||||
def locale_sender?(tweet)
|
||||
tweet_user = user(tweet)
|
||||
Channel.where(area: 'Twitter::Account').each do |local_channel|
|
||||
next if !local_channel.options
|
||||
next if !local_channel.options[:user]
|
||||
next if !local_channel.options[:user][:id]
|
||||
next if local_channel.options[:user][:id].to_s != tweet_user.id.to_s
|
||||
|
||||
Rails.logger.debug { "Tweet is sent by local account with user id #{tweet_user.id} and tweet.id #{tweet.id}" }
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
process webhook messages from twitter
|
||||
|
||||
client = TwitterSync.new
|
||||
client.process_webhook(channel)
|
||||
|
||||
=end
|
||||
|
||||
def process_webhook(channel)
|
||||
Rails.logger.debug { 'import tweet' }
|
||||
ticket = nil
|
||||
if @payload['direct_message_events'].present? && channel.options[:sync][:direct_messages][:group_id].present?
|
||||
@payload['direct_message_events'].each do |item|
|
||||
next if item['type'] != 'message_create'
|
||||
|
||||
next if Ticket::Article.find_by(message_id: item['id'])
|
||||
|
||||
user = to_user_webhook(item['message_create']['sender_id'])
|
||||
ticket = to_ticket(item, user, channel.options[:sync][:direct_messages][:group_id], channel)
|
||||
to_article_webhook(item, user, ticket, channel)
|
||||
end
|
||||
end
|
||||
|
||||
if @payload['tweet_create_events'].present?
|
||||
@payload['tweet_create_events'].each do |item|
|
||||
next if Ticket::Article.find_by(message_id: item['id'])
|
||||
|
||||
# check if it's mention
|
||||
group_id = nil
|
||||
if channel.options[:sync][:mentions][:group_id].present? && item['entities']['user_mentions']
|
||||
item['entities']['user_mentions'].each do |local_user|
|
||||
next if channel.options[:user][:id].to_s != local_user['id'].to_s
|
||||
|
||||
group_id = channel.options[:sync][:mentions][:group_id]
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
# check if it's search term
|
||||
if !group_id && channel.options[:sync][:search].present?
|
||||
channel.options[:sync][:search].each do |local_search|
|
||||
next if local_search[:term].blank?
|
||||
next if local_search[:group_id].blank?
|
||||
next if item['text'] !~ /#{Regexp.quote(local_search[:term])}/i
|
||||
|
||||
group_id = local_search[:group_id]
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
next if !group_id
|
||||
|
||||
user = to_user_webhook(item['user']['id'], item['user'])
|
||||
if item['in_reply_to_status_id'].present?
|
||||
existing_article = Ticket::Article.find_by(message_id: item['in_reply_to_status_id'])
|
||||
if existing_article
|
||||
ticket = existing_article.ticket
|
||||
else
|
||||
begin
|
||||
parent_tweet = @client.status(item['in_reply_to_status_id'])
|
||||
ticket = to_group(parent_tweet, group_id, channel)
|
||||
rescue Twitter::Error::NotFound, Twitter::Error::Forbidden => e
|
||||
# just ignore if tweet has already gone
|
||||
Rails.logger.info "Can't import tweet (#{item['in_reply_to_status_id']}), #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
if !ticket
|
||||
ticket = to_ticket(item, user, group_id, channel)
|
||||
end
|
||||
to_article_webhook(item, user, ticket, channel)
|
||||
end
|
||||
end
|
||||
|
||||
ticket
|
||||
end
|
||||
|
||||
def get_app_webhook(app_id)
|
||||
return {} if !@payload['apps']
|
||||
return {} if !@payload['apps'][app_id]
|
||||
|
||||
@payload['apps'][app_id]
|
||||
end
|
||||
|
||||
def to_user_webhook_data(user_id)
|
||||
if @payload['user'] && @payload['user']['id'].to_s == user_id.to_s
|
||||
return @payload['user']
|
||||
end
|
||||
raise 'no users in payload' if !@payload['users']
|
||||
raise 'no users in payload' if !@payload['users'][user_id]
|
||||
|
||||
@payload['users'][user_id]
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
download public media file from twitter
|
||||
|
||||
client = TwitterSync.new
|
||||
result = client.download_file(url)
|
||||
|
||||
result.body
|
||||
|
||||
=end
|
||||
|
||||
def download_file(url)
|
||||
UserAgent.get(
|
||||
url,
|
||||
{},
|
||||
{
|
||||
open_timeout: 20,
|
||||
read_timeout: 40,
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
def to_user_webhook(user_id, payload_user = nil)
|
||||
user_payload = if payload_user && payload_user['id'].to_s == user_id.to_s
|
||||
payload_user
|
||||
else
|
||||
to_user_webhook_data(user_id)
|
||||
end
|
||||
|
||||
auth = Authorization.find_by(uid: user_payload['id'], provider: 'twitter')
|
||||
|
||||
# create or update user
|
||||
user_data = {
|
||||
image_source: user_payload['profile_image_url'],
|
||||
}
|
||||
if auth
|
||||
user = User.find(auth.user_id)
|
||||
map = {
|
||||
note: 'description',
|
||||
web: 'url',
|
||||
address: 'location',
|
||||
}
|
||||
|
||||
# ignore if value is already set
|
||||
map.each do |target, _source|
|
||||
next if user[target].present?
|
||||
|
||||
new_value = user_payload['source'].to_s
|
||||
next if new_value.blank?
|
||||
|
||||
user_data[target] = new_value
|
||||
end
|
||||
user.update!(user_data)
|
||||
else
|
||||
user_data[:login] = user_payload['screen_name']
|
||||
user_data[:firstname] = user_payload['name']
|
||||
user_data[:web] = user_payload['url']
|
||||
user_data[:note] = user_payload['description']
|
||||
user_data[:address] = user_payload['location']
|
||||
user_data[:active] = true
|
||||
user_data[:role_ids] = Role.signup_role_ids
|
||||
|
||||
user = User.create!(user_data)
|
||||
end
|
||||
|
||||
if user_data[:image_source].present?
|
||||
avatar = Avatar.add(
|
||||
object: 'User',
|
||||
o_id: user.id,
|
||||
url: user_data[:image_source],
|
||||
source: 'twitter',
|
||||
deletable: true,
|
||||
updated_by_id: user.id,
|
||||
created_by_id: user.id,
|
||||
)
|
||||
|
||||
# update user link
|
||||
if avatar && user.image != avatar.store_hash
|
||||
user.image = avatar.store_hash
|
||||
user.save
|
||||
end
|
||||
end
|
||||
|
||||
# create or update authorization
|
||||
auth_data = {
|
||||
uid: user_payload['id'],
|
||||
username: user_payload['screen_name'],
|
||||
user_id: user.id,
|
||||
provider: 'twitter'
|
||||
}
|
||||
if auth
|
||||
auth.update!(auth_data)
|
||||
else
|
||||
Authorization.create!(auth_data)
|
||||
end
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
get the user of current twitter client
|
||||
|
||||
client = TwitterSync.new
|
||||
user_hash = client.who_am_i
|
||||
|
||||
=end
|
||||
|
||||
def who_am_i
|
||||
@client.user
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
request a new webhook verification request from twitter
|
||||
|
||||
client = TwitterSync.new
|
||||
webhook_request_verification(webhook_id, env_name, webhook_url)
|
||||
|
||||
=end
|
||||
|
||||
def webhook_request_verification(webhook_id, env_name, webhook_url)
|
||||
|
||||
Twitter::REST::Request.new(@client, :put, "/1.1/account_activity/all/#{env_name}/webhooks/#{webhook_id}.json", {}).perform
|
||||
rescue => e
|
||||
raise "Webhook registered but not valid (#{webhook_url}). Unable to set webhook to valid: #{e.message}"
|
||||
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
get webhooks by env_name
|
||||
|
||||
client = TwitterSync.new
|
||||
webhooks = webhooks_by_env_name(env_name)
|
||||
|
||||
=end
|
||||
|
||||
def webhooks_by_env_name(env_name)
|
||||
Twitter::REST::Request.new(@client, :get, "/1.1/account_activity/all/#{env_name}/webhooks.json", {}).perform
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
get all webhooks
|
||||
|
||||
client = TwitterSync.new
|
||||
webhooks = webhooks(env_name)
|
||||
|
||||
=end
|
||||
|
||||
def webhooks
|
||||
Twitter::REST::Request.new(@client, :get, '/1.1/account_activity/all/webhooks.json', {}).perform
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
delete a webhooks
|
||||
|
||||
client = TwitterSync.new
|
||||
webhook_delete(webhook_id)
|
||||
|
||||
=end
|
||||
|
||||
def webhook_delete(webhook_id)
|
||||
Twitter::REST::Request.new(@client, :delete, "/1.1/account_activity/all/#{env_name}/webhooks/#{webhook_id}.json", {}).perform
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
register a new webhooks at twitter
|
||||
|
||||
client = TwitterSync.new
|
||||
webhook_register(env_name, webhook_url)
|
||||
|
||||
=end
|
||||
|
||||
def webhook_register(env_name, webhook_url)
|
||||
options = {
|
||||
url: webhook_url,
|
||||
}
|
||||
begin
|
||||
response = Twitter::REST::Request.new(@client, :post, "/1.1/account_activity/all/#{env_name}/webhooks.json", options).perform
|
||||
rescue => e
|
||||
message = "Unable to register webhook: #{e.message}"
|
||||
if %r{http://}.match?(webhook_url)
|
||||
message += ' Only https webhooks possible to register.'
|
||||
elsif webhooks.count.positive?
|
||||
message += " Already #{webhooks.count} webhooks registered. Maybe you need to delete one first."
|
||||
end
|
||||
raise message
|
||||
end
|
||||
response
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
subscribe a user to a webhooks at twitter
|
||||
|
||||
client = TwitterSync.new
|
||||
webhook_subscribe(env_name)
|
||||
|
||||
=end
|
||||
|
||||
def webhook_subscribe(env_name)
|
||||
|
||||
Twitter::REST::Request.new(@client, :post, "/1.1/account_activity/all/#{env_name}/subscriptions.json", {}).perform
|
||||
rescue => e
|
||||
raise "Unable to subscriptions with via webhook: #{e.message}"
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -93,7 +93,7 @@ RSpec.describe String do
|
|||
let(:input_encoding) { Encoding::ISO_8859_1 }
|
||||
|
||||
it 'detects the input encoding' do
|
||||
Timeout.timeout(9) do
|
||||
Timeout.timeout(12) do
|
||||
expect(subject.utf8_encode(from: 'utf-8')).to eq(original_string)
|
||||
end
|
||||
end
|
||||
|
|
394
spec/models/channel/driver/twitter_spec.rb
Normal file
394
spec/models/channel/driver/twitter_spec.rb
Normal file
|
@ -0,0 +1,394 @@
|
|||
require 'rails_helper'
|
||||
|
||||
require_dependency 'channel/driver/twitter'
|
||||
|
||||
RSpec.describe ::Channel::Driver::Twitter do
|
||||
|
||||
let(:channel) do
|
||||
create(
|
||||
:channel,
|
||||
area: 'Twitter::Account',
|
||||
options: {
|
||||
adapter: 'twitter',
|
||||
auth: {
|
||||
consumer_key: 'some',
|
||||
consumer_secret: 'some',
|
||||
oauth_token: 'key',
|
||||
oauth_token_secret: 'secret',
|
||||
},
|
||||
user: {
|
||||
screen_name: 'system_login',
|
||||
id: 'system_id',
|
||||
},
|
||||
sync: {
|
||||
track_retweets: true,
|
||||
search: [
|
||||
{
|
||||
term: 'zammad',
|
||||
group_id: Group.first.id,
|
||||
},
|
||||
{
|
||||
term: 'hash_tag1',
|
||||
group_id: Group.first.id,
|
||||
},
|
||||
],
|
||||
mentions: {
|
||||
group_id: Group.first.id,
|
||||
},
|
||||
direct_messages: {
|
||||
group_id: Group.first.id,
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
active: true,
|
||||
created_by_id: 1,
|
||||
updated_by_id: 1
|
||||
)
|
||||
end
|
||||
|
||||
it 'fetch channel with invalid token' do
|
||||
VCR.use_cassette('models/channel/driver/twitter/fetch_channel_invalid') do
|
||||
expect(channel.fetch(true)).to be false
|
||||
end
|
||||
|
||||
channel.reload
|
||||
expect(channel.status_in).to eq('error')
|
||||
expect(channel.last_log_in).to eq('Can\'t use Channel::Driver::Twitter: #<Twitter::Error::Unauthorized: Invalid or expired token.>')
|
||||
expect(channel.status_out).to be nil
|
||||
expect(channel.last_log_out).to be nil
|
||||
end
|
||||
|
||||
it 'fetch channel with valid token' do
|
||||
expect(Ticket.count).to eq(1)
|
||||
VCR.use_cassette('models/channel/driver/twitter/fetch_channel_valid') do
|
||||
expect(channel.fetch(true)).to be true
|
||||
end
|
||||
|
||||
expect(Ticket.count).to eq(27)
|
||||
|
||||
ticket = Ticket.last
|
||||
expect(ticket.title).to eq('Wir haben unsere DMs deaktiviert. Leider können wir dank der neuen Twitter API k...')
|
||||
expect(ticket.preferences[:channel_id]).to eq(channel.id)
|
||||
expect(ticket.preferences[:channel_screen_name]).to eq(channel.options[:user][:screen_name])
|
||||
expect(ticket.customer.firstname).to eq('Ccc')
|
||||
expect(ticket.customer.lastname).to eq('Event Logistics')
|
||||
|
||||
channel.reload
|
||||
expect(channel.status_in).to eq('ok')
|
||||
expect(channel.last_log_in).to eq('')
|
||||
expect(channel.status_out).to be nil
|
||||
expect(channel.last_log_out).to be nil
|
||||
end
|
||||
|
||||
it 'send tweet based on article - outbound' do
|
||||
user = User.find(2)
|
||||
text = 'Today the weather is really...'
|
||||
ticket = Ticket.create!(
|
||||
title: text[0, 40],
|
||||
customer_id: user.id,
|
||||
group_id: Group.first.id,
|
||||
state: Ticket::State.find_by(name: 'new'),
|
||||
priority: Ticket::Priority.find_by(name: '2 normal'),
|
||||
preferences: {
|
||||
channel_id: channel.id,
|
||||
channel_screen_name: 'system_login',
|
||||
},
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
assert(ticket, "outbound ticket created, text: #{text}")
|
||||
article = Ticket::Article.create!(
|
||||
ticket_id: ticket.id,
|
||||
body: text,
|
||||
type: Ticket::Article::Type.find_by(name: 'twitter status'),
|
||||
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
|
||||
internal: false,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
|
||||
VCR.use_cassette('models/channel/driver/twitter/article_to_tweet') do
|
||||
Scheduler.worker(true)
|
||||
end
|
||||
|
||||
ticket.reload
|
||||
expect(ticket.state.name).to eq('open')
|
||||
expect(ticket.group.name).to eq(Group.first.name)
|
||||
expect(ticket.title).to eq('Today the weather is really...')
|
||||
|
||||
article.reload
|
||||
expect(article.from).to eq('@example')
|
||||
expect(article.to).to eq('')
|
||||
expect(article.cc).to be nil
|
||||
expect(article.subject).to be nil
|
||||
expect(article.sender.name).to eq('Agent')
|
||||
expect(article.type.name).to eq('twitter status')
|
||||
expect(article.message_id).to eq('1069382411899817990')
|
||||
expect(article.content_type).to eq('text/plain')
|
||||
expect(article.body).to eq('Today the weather is really...')
|
||||
expect(article.preferences[:links][0][:url]).to eq('https://twitter.com/statuses/1069382411899817990')
|
||||
expect(article.preferences[:links][0][:target]).to eq('_blank')
|
||||
expect(article.preferences[:links][0][:name]).to eq('on Twitter')
|
||||
|
||||
channel.reload
|
||||
expect(channel.status_in).to be nil
|
||||
expect(channel.last_log_in).to be nil
|
||||
expect(channel.status_out).to eq('ok')
|
||||
expect(channel.last_log_out).to eq('')
|
||||
end
|
||||
|
||||
it 'send tweet based on article - with replaced channel' do
|
||||
user = User.find(2)
|
||||
text = 'Today and tomorrow the weather is really...'
|
||||
ticket = Ticket.create!(
|
||||
title: text[0, 40],
|
||||
customer_id: user.id,
|
||||
group_id: Group.first.id,
|
||||
state: Ticket::State.find_by(name: 'new'),
|
||||
priority: Ticket::Priority.find_by(name: '2 normal'),
|
||||
preferences: {
|
||||
channel_id: 'some_other_id',
|
||||
channel_screen_name: 'system_login',
|
||||
},
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
assert(ticket, "outbound ticket created, text: #{text}")
|
||||
article = Ticket::Article.create!(
|
||||
ticket_id: ticket.id,
|
||||
body: text,
|
||||
type: Ticket::Article::Type.find_by(name: 'twitter status'),
|
||||
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
|
||||
internal: false,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
|
||||
channel.reload
|
||||
expect(channel.options[:user][:screen_name]).not_to be ticket.preferences[:channel_screen_name]
|
||||
expect(channel.status_in).to be nil
|
||||
expect(channel.last_log_in).to be nil
|
||||
expect(channel.status_out).to be nil
|
||||
expect(channel.last_log_out).to be nil
|
||||
|
||||
VCR.use_cassette('models/channel/driver/twitter/article_to_tweet_channel_replace') do
|
||||
Scheduler.worker(true)
|
||||
end
|
||||
|
||||
ticket.reload
|
||||
expect(ticket.state.name).to eq('open')
|
||||
expect(ticket.group.name).to eq(Group.first.name)
|
||||
expect(ticket.title).to eq('Today and tomorrow the weather is really')
|
||||
|
||||
article.reload
|
||||
expect(article.from).to eq('@example')
|
||||
expect(article.to).to eq('')
|
||||
expect(article.cc).to be nil
|
||||
expect(article.subject).to be nil
|
||||
expect(article.sender.name).to eq('Agent')
|
||||
expect(article.type.name).to eq('twitter status')
|
||||
expect(article.message_id).to eq('1069382411899817991')
|
||||
expect(article.content_type).to eq('text/plain')
|
||||
expect(article.body).to eq('Today and tomorrow the weather is really...')
|
||||
expect(article.preferences[:links][0][:url]).to eq('https://twitter.com/statuses/1069382411899817991')
|
||||
expect(article.preferences[:links][0][:target]).to eq('_blank')
|
||||
expect(article.preferences[:links][0][:name]).to eq('on Twitter')
|
||||
|
||||
channel.reload
|
||||
expect(channel.status_in).to be nil
|
||||
expect(channel.last_log_in).to be nil
|
||||
expect(channel.status_out).to eq('ok')
|
||||
expect(channel.last_log_out).to eq('')
|
||||
end
|
||||
|
||||
it 'article preferences' do
|
||||
|
||||
org_community = Organization.create_if_not_exists(
|
||||
name: 'Zammad Foundation',
|
||||
)
|
||||
user_community = User.create_or_update(
|
||||
login: 'article.twitter@example.org',
|
||||
firstname: 'Article',
|
||||
lastname: 'Twitter',
|
||||
email: 'article.twitter@example.org',
|
||||
password: '',
|
||||
active: true,
|
||||
roles: [ Role.find_by(name: 'Customer') ],
|
||||
organization_id: org_community.id,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
|
||||
ticket1 = Ticket.create!(
|
||||
group_id: Group.first.id,
|
||||
customer_id: user_community.id,
|
||||
title: 'Tweet 1!',
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
twitter_preferences = {
|
||||
mention_ids: [1_234_567_890],
|
||||
geo: Twitter::NullObject.new,
|
||||
retweeted: false,
|
||||
possibly_sensitive: false,
|
||||
in_reply_to_user_id: 1_234_567_890,
|
||||
place: Twitter::NullObject.new,
|
||||
retweet_count: 0,
|
||||
source: '<a href="http://example.com/software/tweetbot/mac" rel="nofollow">Tweetbot for Mac</a>',
|
||||
favorited: false,
|
||||
truncated: false
|
||||
}
|
||||
preferences = {
|
||||
twitter: TwitterSync.preferences_cleanup(twitter_preferences),
|
||||
links: [
|
||||
{
|
||||
url: 'https://twitter.com/statuses/123',
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
}
|
||||
article1 = Ticket::Article.create!(
|
||||
ticket_id: ticket1.id,
|
||||
type_id: Ticket::Article::Type.find_by(name: 'twitter status').id,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
from: '@example',
|
||||
body: 'some tweet',
|
||||
internal: false,
|
||||
preferences: TwitterSync.preferences_cleanup(preferences),
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
expect(article1.preferences[:twitter]).to be_truthy
|
||||
expect(article1.preferences[:twitter][:mention_ids][0]).to eq(1_234_567_890)
|
||||
expect(article1.preferences[:twitter][:geo].class).to be ActiveSupport::HashWithIndifferentAccess
|
||||
expect(article1.preferences[:twitter][:geo].blank?).to be true
|
||||
expect(article1.preferences[:twitter][:place].class).to be ActiveSupport::HashWithIndifferentAccess
|
||||
expect(article1.preferences[:twitter][:place].blank?).to be true
|
||||
|
||||
twitter_preferences = {
|
||||
mention_ids: [1_234_567_890],
|
||||
geo: Twitter::NullObject.new,
|
||||
retweeted: false,
|
||||
possibly_sensitive: false,
|
||||
in_reply_to_user_id: 1_234_567_890,
|
||||
place: Twitter::NullObject.new,
|
||||
retweet_count: 0,
|
||||
source: '<a href="http://example.com/software/tweetbot/mac" rel="nofollow">Tweetbot for Mac</a>',
|
||||
favorited: false,
|
||||
truncated: false
|
||||
}
|
||||
preferences = TwitterSync.preferences_cleanup(
|
||||
twitter: twitter_preferences,
|
||||
links: [
|
||||
{
|
||||
url: 'https://twitter.com/statuses/123',
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
)
|
||||
article2 = Ticket::Article.create!(
|
||||
ticket_id: ticket1.id,
|
||||
type_id: Ticket::Article::Type.find_by(name: 'twitter status').id,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
from: '@example',
|
||||
body: 'some tweet',
|
||||
internal: false,
|
||||
preferences: TwitterSync.preferences_cleanup(preferences),
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
expect(article2.preferences[:twitter]).to be_truthy
|
||||
expect(article2.preferences[:twitter][:mention_ids][0]).to eq(1_234_567_890)
|
||||
expect(article1.preferences[:twitter][:geo].class).to be ActiveSupport::HashWithIndifferentAccess
|
||||
expect(article1.preferences[:twitter][:geo].blank?).to be true
|
||||
expect(article1.preferences[:twitter][:place].class).to be ActiveSupport::HashWithIndifferentAccess
|
||||
expect(article1.preferences[:twitter][:place].blank?).to be true
|
||||
|
||||
twitter_preferences = {
|
||||
mention_ids: [1_234_567_890],
|
||||
geo: Twitter::Geo.new(coordinates: [1, 1]),
|
||||
retweeted: false,
|
||||
possibly_sensitive: false,
|
||||
in_reply_to_user_id: 1_234_567_890,
|
||||
place: Twitter::Place.new(country: 'da', name: 'do', woeid: 1, id: 1),
|
||||
retweet_count: 0,
|
||||
source: '<a href="http://example.com/software/tweetbot/mac" rel="nofollow">Tweetbot for Mac</a>',
|
||||
favorited: false,
|
||||
truncated: false
|
||||
}
|
||||
preferences = {
|
||||
twitter: TwitterSync.preferences_cleanup(twitter_preferences),
|
||||
links: [
|
||||
{
|
||||
url: 'https://twitter.com/statuses/123',
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
article3 = Ticket::Article.create!(
|
||||
ticket_id: ticket1.id,
|
||||
type_id: Ticket::Article::Type.find_by(name: 'twitter status').id,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
from: '@example',
|
||||
body: 'some tweet',
|
||||
internal: false,
|
||||
preferences: preferences,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
|
||||
expect(article3.preferences[:twitter]).to be_truthy
|
||||
expect(article3.preferences[:twitter][:mention_ids][0]).to eq(1_234_567_890)
|
||||
expect(article3.preferences[:twitter][:geo].class).to be ActiveSupport::HashWithIndifferentAccess
|
||||
expect(article3.preferences[:twitter][:geo]).to eq({ 'coordinates' => [1, 1] })
|
||||
expect(article3.preferences[:twitter][:place].class).to be ActiveSupport::HashWithIndifferentAccess
|
||||
expect(article3.preferences[:twitter][:place]).to eq({ 'country' => 'da', 'name' => 'do', 'woeid' => 1, 'id' => 1 })
|
||||
|
||||
twitter_preferences = {
|
||||
mention_ids: [1_234_567_890],
|
||||
geo: Twitter::Geo.new(coordinates: [1, 1]),
|
||||
retweeted: false,
|
||||
possibly_sensitive: false,
|
||||
in_reply_to_user_id: 1_234_567_890,
|
||||
place: Twitter::Place.new(country: 'da', name: 'do', woeid: 1, id: 1),
|
||||
retweet_count: 0,
|
||||
source: '<a href="http://example.com/software/tweetbot/mac" rel="nofollow">Tweetbot for Mac</a>',
|
||||
favorited: false,
|
||||
truncated: false
|
||||
}
|
||||
preferences = TwitterSync.preferences_cleanup(
|
||||
twitter: twitter_preferences,
|
||||
links: [
|
||||
{
|
||||
url: 'https://twitter.com/statuses/123',
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
article4 = Ticket::Article.create!(
|
||||
ticket_id: ticket1.id,
|
||||
type_id: Ticket::Article::Type.find_by(name: 'twitter status').id,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
from: '@example',
|
||||
body: 'some tweet',
|
||||
internal: false,
|
||||
preferences: preferences,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
expect(article4.preferences[:twitter]).to be_truthy
|
||||
expect(article4.preferences[:twitter]).to be_truthy
|
||||
expect(article4.preferences[:twitter][:mention_ids][0]).to eq(1_234_567_890)
|
||||
expect(article4.preferences[:twitter][:geo].class).to be ActiveSupport::HashWithIndifferentAccess
|
||||
expect(article4.preferences[:twitter][:geo]).to eq({ 'coordinates' => [1, 1] })
|
||||
expect(article4.preferences[:twitter][:place].class).to be ActiveSupport::HashWithIndifferentAccess
|
||||
expect(article4.preferences[:twitter][:place]).to eq({ 'country' => 'da', 'name' => 'do', 'woeid' => 1, 'id' => 1 })
|
||||
end
|
||||
end
|
|
@ -51,60 +51,151 @@ RSpec.describe 'ExternalCredentials', type: :request do
|
|||
expect(json_response.count).to eq(0)
|
||||
end
|
||||
|
||||
it 'does external_credential app_verify with admin' do
|
||||
it 'does external_credential app_verify with admin - facebook' do
|
||||
authenticated_as(admin_user)
|
||||
post '/api/v1/external_credentials/facebook/app_verify', as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('No facebook app configured!')
|
||||
expect(json_response['error']).to eq('No application_id param!')
|
||||
|
||||
create(:external_credential, name: 'facebook')
|
||||
VCR.use_cassette('request/external_credentials/facebook/app_verify_invalid_credentials_with_not_created') do
|
||||
post '/api/v1/external_credentials/facebook/app_verify', params: { application_id: 123, application_secret: 123 }, as: :json
|
||||
end
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('type: OAuthException, code: 101, message: Error validating application. Cannot get application info due to a system error. [HTTP 400]')
|
||||
|
||||
post '/api/v1/external_credentials/facebook/app_verify', as: :json
|
||||
create(:external_credential, { name: 'facebook', credentials: { application_id: 123, application_secret: 123 } })
|
||||
VCR.use_cassette('request/external_credentials/facebook/app_verify_invalid_credentials_with_created') do
|
||||
post '/api/v1/external_credentials/facebook/app_verify', as: :json
|
||||
end
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('type: OAuthException, code: 101, message: Error validating application. Cannot get application info due to a system error. [HTTP 400]')
|
||||
end
|
||||
|
||||
it 'does link_account app_verify with admin' do
|
||||
it 'does external_credential app_verify with admin - twitter' do
|
||||
authenticated_as(admin_user)
|
||||
post '/api/v1/external_credentials/twitter/app_verify', as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('No consumer_key param!')
|
||||
|
||||
VCR.use_cassette('request/external_credentials/twitter/app_verify_invalid_credentials_with_not_created') do
|
||||
post '/api/v1/external_credentials/twitter/app_verify', params: { consumer_key: 123, consumer_secret: 123, oauth_token: 123, oauth_token_secret: 123 }, as: :json
|
||||
end
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('401 Authorization Required')
|
||||
|
||||
create(:external_credential, { name: 'twitter', credentials: { consumer_key: 123, consumer_secret: 123, oauth_token: 123, oauth_token_secret: 123 } })
|
||||
VCR.use_cassette('request/external_credentials/twitter/app_verify_invalid_credentials_with_created') do
|
||||
post '/api/v1/external_credentials/twitter/app_verify', as: :json
|
||||
end
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('401 Authorization Required')
|
||||
end
|
||||
|
||||
it 'does link_account app_verify with admin - facebook' do
|
||||
authenticated_as(admin_user)
|
||||
get '/api/v1/external_credentials/facebook/link_account', as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('No facebook app configured!')
|
||||
|
||||
create(:external_credential, name: 'facebook')
|
||||
get '/api/v1/external_credentials/facebook/link_account', params: { application_id: 123, application_secret: 123 }, as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('No facebook app configured!')
|
||||
|
||||
get '/api/v1/external_credentials/facebook/link_account', as: :json
|
||||
create(:external_credential, { name: 'facebook', credentials: { application_id: 123, application_secret: 123 } })
|
||||
|
||||
VCR.use_cassette('request/external_credentials/facebook/link_account_with_invalid_credential') do
|
||||
get '/api/v1/external_credentials/facebook/link_account', as: :json
|
||||
end
|
||||
expect(response).to have_http_status(500)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('type: OAuthException, code: 101, message: Error validating application. Cannot get application info due to a system error. [HTTP 400]')
|
||||
end
|
||||
|
||||
it 'does external_credential callback with admin' do
|
||||
it 'does link_account app_verify with admin - twitter' do
|
||||
authenticated_as(admin_user)
|
||||
get '/api/v1/external_credentials/facebook/callback', as: :json
|
||||
get '/api/v1/external_credentials/twitter/link_account', as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('No twitter app configured!')
|
||||
|
||||
get '/api/v1/external_credentials/twitter/link_account', params: { consumer_key: 123, consumer_secret: 123, oauth_token: 123, oauth_token_secret: 123 }, as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('No twitter app configured!')
|
||||
|
||||
create(:external_credential, { name: 'twitter', credentials: { consumer_key: 123, consumer_secret: 123, oauth_token: 123, oauth_token_secret: 123 } })
|
||||
VCR.use_cassette('request/external_credentials/twitter/link_account_with_invalid_credential') do
|
||||
get '/api/v1/external_credentials/twitter/link_account', as: :json
|
||||
end
|
||||
expect(response).to have_http_status(500)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('No such account')
|
||||
|
||||
create(:external_credential, name: 'facebook')
|
||||
expect(json_response['error']).to eq('401 Authorization Required')
|
||||
end
|
||||
|
||||
it 'does external_credential callback with admin - facebook' do
|
||||
authenticated_as(admin_user)
|
||||
get '/api/v1/external_credentials/facebook/callback', as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('No facebook app configured!')
|
||||
|
||||
get '/api/v1/external_credentials/facebook/callback', params: { application_id: 123, application_secret: 123 }, as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('No facebook app configured!')
|
||||
|
||||
create(:external_credential, { name: 'facebook', credentials: { application_id: 123, application_secret: 123 } })
|
||||
VCR.use_cassette('request/external_credentials/facebook/callback_invalid_credentials') do
|
||||
get '/api/v1/external_credentials/facebook/callback', as: :json
|
||||
end
|
||||
expect(response).to have_http_status(500)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('type: OAuthException, code: 101, message: Error validating application. Cannot get application info due to a system error. [HTTP 400]')
|
||||
end
|
||||
|
||||
it 'does external_credential callback with admin - twitter' do
|
||||
authenticated_as(admin_user)
|
||||
get '/api/v1/external_credentials/twitter/callback', as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('No twitter app configured!')
|
||||
|
||||
get '/api/v1/external_credentials/twitter/callback', params: { consumer_key: 123, consumer_secret: 123 }, as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('No twitter app configured!')
|
||||
|
||||
create(:external_credential, { name: 'twitter', credentials: { consumer_key: 123, consumer_secret: 123 } })
|
||||
get '/api/v1/external_credentials/twitter/callback', as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('No request_token for session found!')
|
||||
|
||||
#request.session[:oauth_token] = 'some_token'
|
||||
#get '/api/v1/external_credentials/twitter/callback', as: :json
|
||||
#expect(response).to have_http_status(422)
|
||||
#expect(json_response).to be_a_kind_of(Hash)
|
||||
#expect(json_response['error']).to eq('Invalid oauth_token given!')
|
||||
end
|
||||
|
||||
it 'does external_credential app_verify with admin and different permissions' do
|
||||
authenticated_as(admin_user)
|
||||
|
||||
create(:external_credential, name: 'twitter')
|
||||
|
||||
post '/api/v1/external_credentials/twitter/app_verify', as: :json
|
||||
create(:external_credential, { name: 'twitter', credentials: { consumer_key: 123, consumer_secret: 123 } })
|
||||
VCR.use_cassette('request/external_credentials/twitter/app_verify_twitter') do
|
||||
post '/api/v1/external_credentials/twitter/app_verify', as: :json
|
||||
end
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('400 Bad Request')
|
||||
expect(json_response['error']).to eq('401 Authorization Required')
|
||||
|
||||
permission = Permission.find_by(name: 'admin.channel_twitter')
|
||||
permission.active = false
|
||||
|
@ -115,9 +206,10 @@ RSpec.describe 'ExternalCredentials', type: :request do
|
|||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('Not authorized (user)!')
|
||||
|
||||
create(:external_credential, name: 'facebook')
|
||||
|
||||
post '/api/v1/external_credentials/facebook/app_verify', as: :json
|
||||
create(:external_credential, { name: 'facebook', credentials: { application_id: 123, application_secret: 123 } })
|
||||
VCR.use_cassette('request/external_credentials/facebook/app_verify_facebook') do
|
||||
post '/api/v1/external_credentials/facebook/app_verify', as: :json
|
||||
end
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_a_kind_of(Hash)
|
||||
expect(json_response['error']).to eq('type: OAuthException, code: 101, message: Error validating application. Cannot get application info due to a system error. [HTTP 400]')
|
||||
|
|
282
spec/requests/integration/twitter_webhook_spec.rb
Normal file
282
spec/requests/integration/twitter_webhook_spec.rb
Normal file
|
@ -0,0 +1,282 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Integration Twitter Webhook', type: :request do
|
||||
|
||||
let(:agent_user) do
|
||||
create(:agent_user)
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
@external_credential = ExternalCredential.create!(
|
||||
name: 'twitter',
|
||||
credentials: {
|
||||
consumer_key: 'CCC',
|
||||
consumer_secret: 'DDD',
|
||||
}
|
||||
)
|
||||
@channel = Channel.create!(
|
||||
group_id: nil,
|
||||
area: 'Twitter::Account',
|
||||
options: {
|
||||
adapter: 'twitter',
|
||||
user: {
|
||||
id: 123,
|
||||
screen_name: 'zammadhq',
|
||||
name: 'Zammad HQ',
|
||||
},
|
||||
auth: {
|
||||
external_credential_id: 1,
|
||||
oauth_token: 'AAA',
|
||||
oauth_token_secret: 'BBB',
|
||||
consumer_key: 'CCC',
|
||||
consumer_secret: 'DDD',
|
||||
},
|
||||
sync: {
|
||||
limit: 20,
|
||||
search: [{ term: '#zammad', group_id: Group.first.id.to_s }, { term: '#hello1234', group_id: Group.first.id.to_s }],
|
||||
mentions: { group_id: Group.first.id.to_s },
|
||||
direct_messages: { group_id: Group.first.id.to_s },
|
||||
track_retweets: false
|
||||
}
|
||||
},
|
||||
active: true,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
end
|
||||
|
||||
describe 'request verify' do
|
||||
|
||||
it 'does at config check' do
|
||||
Cache.write('external_credential_twitter', @external_credential.credentials)
|
||||
@external_credential.destroy
|
||||
params = {
|
||||
crc_token: 'some_random',
|
||||
nonce: 'some_nonce',
|
||||
}
|
||||
get '/api/v1/channels_twitter_webhook', params: params, headers: { 'x-twitter-webhooks-signature' => 'something' }, as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response['response_token']).to eq('sha256=VE19eUk6krbdSqWPdvH71xtFhApBAU81uPW3UT65vOs=')
|
||||
end
|
||||
|
||||
it 'does configured check' do
|
||||
Cache.delete('external_credential_twitter')
|
||||
params = {
|
||||
crc_token: 'some_random',
|
||||
nonce: 'some_nonce',
|
||||
}
|
||||
get '/api/v1/channels_twitter_webhook', params: params, headers: { 'x-twitter-webhooks-signature' => 'something' }, as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response['response_token']).to eq('sha256=VE19eUk6krbdSqWPdvH71xtFhApBAU81uPW3UT65vOs=')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'request incoming - base' do
|
||||
|
||||
it 'does without x-twitter-webhooks-signature header check' do
|
||||
params = {}
|
||||
post '/api/v1/channels_twitter_webhook', params: params, as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response['error']).to eq('Missing \'x-twitter-webhooks-signature\' header')
|
||||
end
|
||||
|
||||
it 'does no external_credential check' do
|
||||
@external_credential.destroy
|
||||
params = {}
|
||||
post '/api/v1/channels_twitter_webhook', params: params, headers: { 'x-twitter-webhooks-signature' => 'something' }, as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response['error']).to eq('No such external_credential \'twitter\'!')
|
||||
end
|
||||
|
||||
it 'does invalid token check' do
|
||||
params = {}
|
||||
post '/api/v1/channels_twitter_webhook', params: params, headers: { 'x-twitter-webhooks-signature' => 'something' }, as: :json
|
||||
expect(response).to have_http_status(401)
|
||||
expect(json_response['error']).to eq('Not authorized')
|
||||
end
|
||||
|
||||
it 'does existing for_user_id check' do
|
||||
params = { key: 'value' }
|
||||
post '/api/v1/channels_twitter_webhook', params: params, headers: { 'x-twitter-webhooks-signature' => 'sha256=EERHBy/k17v+SuT+K0OXuwhJtKnPtxi0n/Y4Wye4kVU=' }, as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response['error']).to eq('Missing \'for_user_id\' in payload!')
|
||||
end
|
||||
|
||||
it 'does invalid user check' do
|
||||
params = { for_user_id: 'not_existing', key: 'value' }
|
||||
post '/api/v1/channels_twitter_webhook', params: params, headers: { 'x-twitter-webhooks-signature' => 'sha256=QaJiQl/4WRp/GF37b+eAdF6kPgptjDCLOgAIIbB1s0I=' }, as: :json
|
||||
expect(response).to have_http_status(422)
|
||||
expect(json_response['error']).to eq('No such channel for user id \'not_existing\'!')
|
||||
end
|
||||
|
||||
it 'does valid token check' do
|
||||
params = { for_user_id: 123, key: 'value' }
|
||||
post '/api/v1/channels_twitter_webhook', params: params, headers: { 'x-twitter-webhooks-signature' => 'sha256=JjEmBe1lVKT8XldrYUKibF+D5ehp8f0jDk3PXZSHEWI=' }, as: :json
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'request incoming direct message' do
|
||||
it 'create new ticket via tweet' do
|
||||
post '/api/v1/channels_twitter_webhook', params: read_messaage('webhook1_direct_message'), headers: { 'x-twitter-webhooks-signature' => 'sha256=xXu7qrPhqXfo8Ot14c0si9HrdQdBNru5fkSdoMZi+Ms=' }, as: :json
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
article = Ticket::Article.find_by(message_id: '1062015437679050760')
|
||||
expect(article).to be_present
|
||||
expect(article.from).to eq('@zammadhq')
|
||||
expect(article.to).to eq('@medenhofer')
|
||||
expect(article.created_by.login).to eq('zammadhq')
|
||||
expect(article.created_by.firstname).to eq('Zammad')
|
||||
expect(article.created_by.lastname).to eq('Hq')
|
||||
expect(article.attachments.count).to eq(0)
|
||||
|
||||
ticket = article.ticket
|
||||
expect(ticket.title).to eq('Hey! Hello!')
|
||||
expect(ticket.state.name).to eq('closed')
|
||||
expect(ticket.priority.name).to eq('2 normal')
|
||||
expect(ticket.customer.login).to eq('zammadhq')
|
||||
expect(ticket.customer.firstname).to eq('Zammad')
|
||||
expect(ticket.customer.lastname).to eq('Hq')
|
||||
expect(ticket.created_by.login).to eq('zammadhq')
|
||||
expect(ticket.created_by.firstname).to eq('Zammad')
|
||||
expect(ticket.created_by.lastname).to eq('Hq')
|
||||
|
||||
post '/api/v1/channels_twitter_webhook', params: read_messaage('webhook2_direct_message'), headers: { 'x-twitter-webhooks-signature' => 'sha256=wYiCk7gfAgrnerCpj3XD58hozfVDjcQvcYPZCFH+stU=' }, as: :json
|
||||
|
||||
article = Ticket::Article.find_by(message_id: '1063077238797725700')
|
||||
expect(article).to be_present
|
||||
expect(article.to).to eq('@zammadhq')
|
||||
expect(article.from).to eq('@medenhofer')
|
||||
expect(article.body).to eq("Hello Zammad #zammad @znuny\n\nYeah! https://twitter.com/messages/media/1063077238797725700")
|
||||
expect(article.created_by.login).to eq('medenhofer')
|
||||
expect(article.created_by.firstname).to eq('Martin')
|
||||
expect(article.created_by.lastname).to eq('Edenhofer')
|
||||
expect(article.attachments.count).to eq(0)
|
||||
|
||||
ticket = article.ticket
|
||||
expect(ticket.title).to eq('Hello Zammad #zammad @znuny Yeah! https://t.co/UfaCwi9cUb')
|
||||
expect(ticket.state.name).to eq('new')
|
||||
expect(ticket.priority.name).to eq('2 normal')
|
||||
expect(ticket.customer.login).to eq('medenhofer')
|
||||
expect(ticket.customer.firstname).to eq('Martin')
|
||||
expect(ticket.customer.lastname).to eq('Edenhofer')
|
||||
|
||||
post '/api/v1/channels_twitter_webhook', params: read_messaage('webhook3_direct_message'), headers: { 'x-twitter-webhooks-signature' => 'sha256=OTguUdchBdxNal/csZsRkytKL5srrUuezZ3hp/2E404=' }, as: :json
|
||||
|
||||
article = Ticket::Article.find_by(message_id: '1063077238797725701')
|
||||
expect(article).to be_present
|
||||
expect(article.to).to eq('@zammadhq')
|
||||
expect(article.from).to eq('@medenhofer')
|
||||
expect(article.body).to eq('Hello again!')
|
||||
expect(article.created_by.login).to eq('medenhofer')
|
||||
expect(article.created_by.firstname).to eq('Martin')
|
||||
expect(article.created_by.lastname).to eq('Edenhofer')
|
||||
expect(article.ticket.id).to eq(ticket.id)
|
||||
expect(article.attachments.count).to eq(0)
|
||||
|
||||
ticket = article.ticket
|
||||
expect(ticket.title).to eq('Hello Zammad #zammad @znuny Yeah! https://t.co/UfaCwi9cUb')
|
||||
expect(ticket.state.name).to eq('new')
|
||||
expect(ticket.priority.name).to eq('2 normal')
|
||||
expect(ticket.customer.login).to eq('medenhofer')
|
||||
expect(ticket.customer.firstname).to eq('Martin')
|
||||
expect(ticket.customer.lastname).to eq('Edenhofer')
|
||||
end
|
||||
|
||||
it 'check duplicate' do
|
||||
post '/api/v1/channels_twitter_webhook', params: read_messaage('webhook1_direct_message'), headers: { 'x-twitter-webhooks-signature' => 'sha256=xXu7qrPhqXfo8Ot14c0si9HrdQdBNru5fkSdoMZi+Ms=' }, as: :json
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
post '/api/v1/channels_twitter_webhook', params: read_messaage('webhook1_direct_message'), headers: { 'x-twitter-webhooks-signature' => 'sha256=xXu7qrPhqXfo8Ot14c0si9HrdQdBNru5fkSdoMZi+Ms=' }, as: :json
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
expect(Ticket::Article.where(message_id: '1062015437679050760').count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'request incoming direct message' do
|
||||
|
||||
it 'create new ticket via tweet' do
|
||||
|
||||
stub_request(:get, 'http://pbs.twimg.com/profile_images/785412960797745152/wxdIvejo_bigger.jpg')
|
||||
.to_return(status: 200, body: 'some_content')
|
||||
|
||||
stub_request(:get, 'https://pbs.twimg.com/media/DsFKfJRWkAAFEbo.jpg')
|
||||
.to_return(status: 200, body: 'some_content')
|
||||
|
||||
post '/api/v1/channels_twitter_webhook', params: read_messaage('webhook1_tweet'), headers: { 'x-twitter-webhooks-signature' => 'sha256=DmARpz6wdgte6Vj+ePeqC+RHvEDokmwOIIqr4//utkk=' }, as: :json
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
article = Ticket::Article.find_by(message_id: '1063212927510081536')
|
||||
expect(article).to be_present
|
||||
expect(article.from).to eq('@zammadhq')
|
||||
expect(article.to).to eq('@medenhofer')
|
||||
expect(article.body).to eq('Hey @medenhofer ! #hello1234 https://twitter.com/zammadhq/status/1063212927510081536/photo/1')
|
||||
expect(article.created_by.login).to eq('zammadhq')
|
||||
expect(article.created_by.firstname).to eq('Zammad')
|
||||
expect(article.created_by.lastname).to eq('Hq')
|
||||
expect(article.attachments.count).to eq(1)
|
||||
expect(article.attachments[0].filename).to eq('DsFKfJRWkAAFEbo.jpg')
|
||||
|
||||
ticket = article.ticket
|
||||
expect(ticket.title).to eq('Hey @medenhofer ! #hello1234 https://t.co/f1kffFlwpN')
|
||||
expect(ticket.state.name).to eq('closed')
|
||||
expect(ticket.priority.name).to eq('2 normal')
|
||||
expect(ticket.customer.login).to eq('zammadhq')
|
||||
expect(ticket.customer.firstname).to eq('Zammad')
|
||||
expect(ticket.customer.lastname).to eq('Hq')
|
||||
expect(ticket.created_by.login).to eq('zammadhq')
|
||||
expect(ticket.created_by.firstname).to eq('Zammad')
|
||||
expect(ticket.created_by.lastname).to eq('Hq')
|
||||
end
|
||||
|
||||
it 'create new ticket via tweet extended_tweet' do
|
||||
|
||||
stub_request(:get, 'http://pbs.twimg.com/profile_images/794220000450150401/D-eFg44R_bigger.jpg')
|
||||
.to_return(status: 200, body: 'some_content')
|
||||
|
||||
post '/api/v1/channels_twitter_webhook', params: read_messaage('webhook2_tweet'), headers: { 'x-twitter-webhooks-signature' => 'sha256=U7bglX19JitI2xuvyONAc0d/fowIFEeUzkEgnWdGyUM=' }, as: :json
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
article = Ticket::Article.find_by(message_id: '1065035365336141825')
|
||||
expect(article).to be_present
|
||||
expect(article.from).to eq('@medenhofer')
|
||||
expect(article.to).to eq('@znuny')
|
||||
expect(article.body).to eq('@znuny Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lore')
|
||||
expect(article.created_by.login).to eq('medenhofer')
|
||||
expect(article.created_by.firstname).to eq('Martin')
|
||||
expect(article.created_by.lastname).to eq('Edenhofer')
|
||||
expect(article.attachments.count).to eq(0)
|
||||
|
||||
ticket = article.ticket
|
||||
expect(ticket.title).to eq('@znuny Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy ...')
|
||||
expect(ticket.state.name).to eq('new')
|
||||
expect(ticket.priority.name).to eq('2 normal')
|
||||
expect(ticket.customer.login).to eq('medenhofer')
|
||||
expect(ticket.customer.firstname).to eq('Martin')
|
||||
expect(ticket.customer.lastname).to eq('Edenhofer')
|
||||
expect(ticket.created_by.login).to eq('medenhofer')
|
||||
expect(ticket.created_by.firstname).to eq('Martin')
|
||||
expect(ticket.created_by.lastname).to eq('Edenhofer')
|
||||
end
|
||||
|
||||
it 'check duplicate' do
|
||||
post '/api/v1/channels_twitter_webhook', params: read_messaage('webhook1_tweet'), headers: { 'x-twitter-webhooks-signature' => 'sha256=DmARpz6wdgte6Vj+ePeqC+RHvEDokmwOIIqr4//utkk=' }, as: :json
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
post '/api/v1/channels_twitter_webhook', params: read_messaage('webhook1_tweet'), headers: { 'x-twitter-webhooks-signature' => 'sha256=DmARpz6wdgte6Vj+ePeqC+RHvEDokmwOIIqr4//utkk=' }, as: :json
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
|
||||
expect(Ticket::Article.where(message_id: '1063212927510081536').count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
def read_messaage(file)
|
||||
JSON.parse(File.read(Rails.root.join('test', 'data', 'twitter', "#{file}.json")))
|
||||
end
|
||||
end
|
|
@ -1,5 +1,13 @@
|
|||
VCR.configure do |config|
|
||||
config.cassette_library_dir = 'test/data/vcr_cassettes'
|
||||
config.hook_into :webmock
|
||||
config.allow_http_connections_when_no_cassette = true
|
||||
config.allow_http_connections_when_no_cassette = false
|
||||
config.ignore_localhost = true
|
||||
config.ignore_request do |request|
|
||||
uri = URI(request.uri)
|
||||
|
||||
['zammad.com', 'google.com'].any? do |site|
|
||||
uri.host.include?(site)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
65
test/data/twitter/webhook1_direct_message.json
Normal file
65
test/data/twitter/webhook1_direct_message.json
Normal file
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"for_user_id": "123",
|
||||
"direct_message_events": [
|
||||
{
|
||||
"type": "message_create",
|
||||
"id": "1062015437679050760",
|
||||
"created_timestamp": "1542039186292",
|
||||
"message_create": {
|
||||
"target": {
|
||||
"recipient_id": "456"
|
||||
},
|
||||
"sender_id": "123",
|
||||
"source_app_id": "268278",
|
||||
"message_data": {
|
||||
"text": "Hey! Hello!",
|
||||
"entities": {
|
||||
"hashtags": [],
|
||||
"symbols": [],
|
||||
"user_mentions": [],
|
||||
"urls": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"apps": {
|
||||
"268278": {
|
||||
"id": "268278",
|
||||
"name": "Twitter Web Client",
|
||||
"url": "http://twitter.com"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"123": {
|
||||
"id": "123",
|
||||
"created_timestamp": "1476091912921",
|
||||
"name": "Zammad HQ",
|
||||
"screen_name": "zammadhq",
|
||||
"description": "Helpdesk and Customer Support made easy. Open Source for download or to go with SaaS. #zammad",
|
||||
"url": "https://t.co/XITyrXmhTP",
|
||||
"protected": false,
|
||||
"verified": false,
|
||||
"followers_count": 426,
|
||||
"friends_count": 509,
|
||||
"statuses_count": 436,
|
||||
"profile_image_url": "http://pbs.twimg.com/profile_images/785412960797745152/wxdIvejo_normal.jpg",
|
||||
"profile_image_url_https": "https://pbs.twimg.com/profile_images/785412960797745152/wxdIvejo_normal.jpg"
|
||||
},
|
||||
"456": {
|
||||
"id": "456",
|
||||
"created_timestamp": "1290730789000",
|
||||
"name": "Martin Edenhofer",
|
||||
"screen_name": "medenhofer",
|
||||
"description": "Open Source professional and geek. Also known as #OTRS and #Zammad inventor. ;)\r\nEntrepreneur and Advisor for open source people in need.",
|
||||
"url": "https://t.co/whm4HTWdMw",
|
||||
"protected": false,
|
||||
"verified": false,
|
||||
"followers_count": 312,
|
||||
"friends_count": 314,
|
||||
"statuses_count": 222,
|
||||
"profile_image_url": "http://pbs.twimg.com/profile_images/794220000450150401/D-eFg44R_normal.jpg",
|
||||
"profile_image_url_https": "https://pbs.twimg.com/profile_images/794220000450150401/D-eFg44R_normal.jpg"
|
||||
}
|
||||
}
|
||||
}
|
162
test/data/twitter/webhook1_tweet.json
Normal file
162
test/data/twitter/webhook1_tweet.json
Normal file
|
@ -0,0 +1,162 @@
|
|||
{
|
||||
"for_user_id": "123",
|
||||
"tweet_create_events": [
|
||||
{
|
||||
"created_at": "Thu Nov 15 23:31:30 +0000 2018",
|
||||
"id": 1063212927510081536,
|
||||
"id_str": "1063212927510081536",
|
||||
"text": "Hey @medenhofer ! #hello1234 https://t.co/f1kffFlwpN",
|
||||
"display_text_range": [0, 29],
|
||||
"source": "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>",
|
||||
"truncated": false,
|
||||
"in_reply_to_status_id": null,
|
||||
"in_reply_to_status_id_str": null,
|
||||
"in_reply_to_user_id": null,
|
||||
"in_reply_to_user_id_str": null,
|
||||
"in_reply_to_screen_name": null,
|
||||
"user": {
|
||||
"id": 123,
|
||||
"id_str": "123",
|
||||
"name": "Zammad HQ",
|
||||
"screen_name": "zammadhq",
|
||||
"location": null,
|
||||
"url": "http://zammad.com",
|
||||
"description": "Helpdesk and Customer Support made easy. Open Source for download or to go with SaaS. #zammad",
|
||||
"translator_type": "none",
|
||||
"protected": false,
|
||||
"verified": false,
|
||||
"followers_count": 427,
|
||||
"friends_count": 512,
|
||||
"listed_count": 20,
|
||||
"favourites_count": 280,
|
||||
"statuses_count": 438,
|
||||
"created_at": "Mon Oct 10 09:31:52 +0000 2016",
|
||||
"utc_offset": null,
|
||||
"time_zone": null,
|
||||
"geo_enabled": false,
|
||||
"lang": "en",
|
||||
"contributors_enabled": false,
|
||||
"is_translator": false,
|
||||
"profile_background_color": "000000",
|
||||
"profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
|
||||
"profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
|
||||
"profile_background_tile": false,
|
||||
"profile_link_color": "31B068",
|
||||
"profile_sidebar_border_color": "000000",
|
||||
"profile_sidebar_fill_color": "000000",
|
||||
"profile_text_color": "000000",
|
||||
"profile_use_background_image": false,
|
||||
"profile_image_url": "http://pbs.twimg.com/profile_images/785412960797745152/wxdIvejo_normal.jpg", "profile_image_url_https": "https://pbs.twimg.com/profile_images/785412960797745152/wxdIvejo_normal.jpg",
|
||||
"profile_banner_url": "https://pbs.twimg.com/profile_banners/123/1476097853",
|
||||
"default_profile": false,
|
||||
"default_profile_image": false,
|
||||
"following": null,
|
||||
"follow_request_sent": null,
|
||||
"notifications": null
|
||||
},
|
||||
"geo": null,
|
||||
"coordinates": null,
|
||||
"place": null,
|
||||
"contributors": null,
|
||||
"is_quote_status": false,
|
||||
"quote_count": 0,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0,
|
||||
"entities": {
|
||||
"hashtags": [
|
||||
{"text": "hello1234", "indices": [19, 29]}
|
||||
],
|
||||
"urls": [],
|
||||
"user_mentions": [
|
||||
{
|
||||
"screen_name": "medenhofer",
|
||||
"name": "Martin Edenhofer",
|
||||
"id": 456,
|
||||
"id_str": "456",
|
||||
"indices": [4, 15]
|
||||
}
|
||||
],
|
||||
"symbols": [],
|
||||
"media": [
|
||||
{
|
||||
"id": 1063212885961248768,
|
||||
"id_str": "1063212885961248768",
|
||||
"indices": [30, 53],
|
||||
"media_url": "http://pbs.twimg.com/media/DsFKfJRWkAAFEbo.jpg",
|
||||
"media_url_https": "https://pbs.twimg.com/media/DsFKfJRWkAAFEbo.jpg",
|
||||
"url": "https://t.co/f1kffFlwpN",
|
||||
"display_url": "pic.twitter.com/f1kffFlwpN",
|
||||
"expanded_url": "https://twitter.com/zammadhq/status/1063212927510081536/photo/1",
|
||||
"type": "photo",
|
||||
"sizes": {
|
||||
"thumb": {
|
||||
"w": 150,
|
||||
"h": 150,
|
||||
"resize": "crop"
|
||||
},
|
||||
"large": {
|
||||
"w": 852,
|
||||
"h": 462,
|
||||
"resize": "fit"
|
||||
},
|
||||
"medium": {
|
||||
"w": 852,
|
||||
"h": 462,
|
||||
"resize": "fit"
|
||||
},
|
||||
"small": {
|
||||
"w": 680,
|
||||
"h": 369,
|
||||
"resize": "fit"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"extended_entities": {
|
||||
"media": [
|
||||
{
|
||||
"id": 1063212885961248768,
|
||||
"id_str": "1063212885961248768",
|
||||
"indices": [30, 53],
|
||||
"media_url": "http://pbs.twimg.com/media/DsFKfJRWkAAFEbo.jpg",
|
||||
"media_url_https": "https://pbs.twimg.com/media/DsFKfJRWkAAFEbo.jpg",
|
||||
"url": "https://t.co/f1kffFlwpN",
|
||||
"display_url": "pic.twitter.com/f1kffFlwpN",
|
||||
"expanded_url": "https://twitter.com/zammadhq/status/1063212927510081536/photo/1",
|
||||
"type": "photo",
|
||||
"sizes": {
|
||||
"thumb": {
|
||||
"w": 150,
|
||||
"h": 150,
|
||||
"resize": "crop"
|
||||
},
|
||||
"large": {
|
||||
"w": 852,
|
||||
"h": 462,
|
||||
"resize": "fit"
|
||||
},
|
||||
"medium": {
|
||||
"w": 852,
|
||||
"h": 462,
|
||||
"resize": "fit"
|
||||
},
|
||||
"small": {
|
||||
"w": 680,
|
||||
"h": 369,
|
||||
"resize": "fit"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"favorited": false,
|
||||
"retweeted": false,
|
||||
"possibly_sensitive": false,
|
||||
"filter_level": "low",
|
||||
"lang": "und",
|
||||
"timestamp_ms": "1542324690116"
|
||||
}
|
||||
]
|
||||
}
|
113
test/data/twitter/webhook2_direct_message.json
Normal file
113
test/data/twitter/webhook2_direct_message.json
Normal file
|
@ -0,0 +1,113 @@
|
|||
{
|
||||
"for_user_id": "123",
|
||||
"direct_message_events": [
|
||||
{
|
||||
"type": "message_create",
|
||||
"id": "1063077238797725700",
|
||||
"created_timestamp": "1542292339406",
|
||||
"message_create": {
|
||||
"target": {
|
||||
"recipient_id": "123"
|
||||
},
|
||||
"sender_id": "456",
|
||||
"message_data": {
|
||||
"text": "Hello Zammad #zammad @znuny\n\nYeah! https://t.co/UfaCwi9cUb",
|
||||
"entities": {
|
||||
"hashtags": [
|
||||
{
|
||||
"text": "zammad",
|
||||
"indices": [13,20]
|
||||
}
|
||||
],
|
||||
"symbols": [],
|
||||
"user_mentions": [
|
||||
{
|
||||
"screen_name": "znuny",
|
||||
"name": "Znuny / ES for OTRS",
|
||||
"id": 789,
|
||||
"id_str": "789",
|
||||
"indices": [21, 27]
|
||||
}
|
||||
],
|
||||
"urls": [
|
||||
{
|
||||
"url": "https://t.co/UfaCwi9cUb",
|
||||
"expanded_url": "https://twitter.com/messages/media/1063077238797725700",
|
||||
"display_url": "pic.twitter.com/UfaCwi9cUb",
|
||||
"indices": [35, 58]
|
||||
}
|
||||
]
|
||||
},
|
||||
"attachment": {
|
||||
"type": "media",
|
||||
"media": {
|
||||
"id": 1063077198536556545,
|
||||
"id_str": "1063077198536556545",
|
||||
"indices": [35, 58],
|
||||
"media_url": "https://ton.twitter.com/1.1/ton/data/dm/1063077238797725700/1063077198536556545/9FZgsMdV.jpg",
|
||||
"media_url_https": "https://ton.twitter.com/1.1/ton/data/dm/1063077238797725700/1063077198536556545/9FZgsMdV.jpg",
|
||||
"url": "https://t.co/UfaCwi9cUb",
|
||||
"display_url": "pic.twitter.com/UfaCwi9cUb",
|
||||
"expanded_url": "https://twitter.com/messages/media/1063077238797725700",
|
||||
"type": "photo",
|
||||
"sizes": {
|
||||
"thumb": {
|
||||
"w": 150,
|
||||
"h": 150,
|
||||
"resize": "crop"
|
||||
},
|
||||
"medium": {
|
||||
"w": 1200,
|
||||
"h": 313,
|
||||
"resize": "fit"
|
||||
},
|
||||
"small": {
|
||||
"w": 680,
|
||||
"h": 177,
|
||||
"resize": "fit"
|
||||
},
|
||||
"large": {
|
||||
"w": 1472,
|
||||
"h": 384,
|
||||
"resize": "fit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"users": {
|
||||
"456": {
|
||||
"id": "456",
|
||||
"created_timestamp": "1290730789000",
|
||||
"name": "Martin Edenhofer",
|
||||
"screen_name": "medenhofer",
|
||||
"description": "Open Source professional and geek. Also known as #OTRS and #Zammad inventor. ;)\r\nEntrepreneur and Advisor for open source people in need.",
|
||||
"url": "https://t.co/whm4HTWdMw",
|
||||
"protected": false,
|
||||
"verified": false,
|
||||
"followers_count": 312,
|
||||
"friends_count": 314,
|
||||
"statuses_count": 222,
|
||||
"profile_image_url": "http://pbs.twimg.com/profile_images/794220000450150401/D-eFg44R_normal.jpg",
|
||||
"profile_image_url_https": "https://pbs.twimg.com/profile_images/794220000450150401/D-eFg44R_normal.jpg"
|
||||
},
|
||||
"123": {
|
||||
"id": "123",
|
||||
"created_timestamp": "1476091912921",
|
||||
"name": "Zammad HQ",
|
||||
"screen_name": "zammadhq",
|
||||
"description": "Helpdesk and Customer Support made easy. Open Source for download or to go with SaaS. #zammad",
|
||||
"url": "https://t.co/XITyrXmhTP",
|
||||
"protected": false,
|
||||
"verified": false,
|
||||
"followers_count": 427,
|
||||
"friends_count": 512,
|
||||
"statuses_count": 437,
|
||||
"profile_image_url": "http://pbs.twimg.com/profile_images/785412960797745152/wxdIvejo_normal.jpg",
|
||||
"profile_image_url_https": "https://pbs.twimg.com/profile_images/785412960797745152/wxdIvejo_normal.jpg"
|
||||
}
|
||||
}
|
||||
}
|
110
test/data/twitter/webhook2_tweet.json
Normal file
110
test/data/twitter/webhook2_tweet.json
Normal file
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"for_user_id": "123",
|
||||
"tweet_create_events": [
|
||||
{
|
||||
"created_at": "Wed Nov 21 00:13:13 +0000 2018",
|
||||
"id": 1065035365336141825,
|
||||
"id_str": "1065035365336141825",
|
||||
"text": "@znuny Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et… https://t.co/b9woj0QXNZ",
|
||||
"source": "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>",
|
||||
"truncated": true,
|
||||
"in_reply_to_status_id": null,
|
||||
"in_reply_to_status_id_str": null,
|
||||
"in_reply_to_user_id": 123,
|
||||
"in_reply_to_user_id_str": "123",
|
||||
"in_reply_to_screen_name": "znuny",
|
||||
"user": {
|
||||
"id": 219826253,
|
||||
"id_str": "219826253",
|
||||
"name": "Martin Edenhofer",
|
||||
"screen_name": "medenhofer",
|
||||
"location": null,
|
||||
"url": "http://edenhofer.de/",
|
||||
"description": "Open Source professional and geek. Also known as #OTRS and #Zammad inventor. ;)\r\nEntrepreneur and Advisor for open source people in need.",
|
||||
"translator_type": "regular",
|
||||
"protected": false,
|
||||
"verified": false,
|
||||
"followers_count": 310,
|
||||
"friends_count": 314,
|
||||
"listed_count": 16,
|
||||
"favourites_count": 129,
|
||||
"statuses_count": 225,
|
||||
"created_at": "Fri Nov 26 00:19:49 +0000 2010",
|
||||
"utc_offset": null,
|
||||
"time_zone": null,
|
||||
"geo_enabled": false,
|
||||
"lang": "en",
|
||||
"contributors_enabled": false,
|
||||
"is_translator": false,
|
||||
"profile_background_color": "C0DEED",
|
||||
"profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
|
||||
"profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
|
||||
"profile_background_tile": true, "profile_link_color": "0084B4", "profile_sidebar_border_color": "FFFFFF",
|
||||
"profile_sidebar_fill_color": "DDEEF6",
|
||||
"profile_text_color": "333333",
|
||||
"profile_use_background_image": true,
|
||||
"profile_image_url": "http://pbs.twimg.com/profile_images/794220000450150401/D-eFg44R_normal.jpg",
|
||||
"profile_image_url_https": "https://pbs.twimg.com/profile_images/794220000450150401/D-eFg44R_normal.jpg",
|
||||
"profile_banner_url": "https://pbs.twimg.com/profile_banners/219826253/1349428277",
|
||||
"default_profile": false,
|
||||
"default_profile_image": false,
|
||||
"following": null,
|
||||
"follow_request_sent": null,
|
||||
"notifications": null
|
||||
},
|
||||
"geo": null,
|
||||
"coordinates": null,
|
||||
"place": null,
|
||||
"contributors": null,
|
||||
"is_quote_status": false,
|
||||
"extended_tweet": {
|
||||
"full_text": "@znuny Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lore",
|
||||
"display_text_range": [0, 279],
|
||||
"entities": {
|
||||
"hashtags": [],
|
||||
"urls": [],
|
||||
"user_mentions": [
|
||||
{
|
||||
"screen_name": "znuny",
|
||||
"name": "Znuny / ES for OTRS",
|
||||
"id": 123,
|
||||
"id_str": "123",
|
||||
"indices": [0, 6]
|
||||
}
|
||||
],
|
||||
"symbols": []
|
||||
}
|
||||
},
|
||||
"quote_count": 0,
|
||||
"reply_count": 0,
|
||||
"retweet_count": 0,
|
||||
"favorite_count": 0,
|
||||
"entities": {
|
||||
"hashtags": [],
|
||||
"urls": [
|
||||
{
|
||||
"url": "https://t.co/b9woj0QXNZ",
|
||||
"expanded_url": "https://twitter.com/i/web/status/1065035365336141825",
|
||||
"display_url": "twitter.com/i/web/status/1…",
|
||||
"indices": [117, 140]
|
||||
}
|
||||
],
|
||||
"user_mentions": [
|
||||
{
|
||||
"screen_name": "znuny",
|
||||
"name": "Znuny / ES for OTRS",
|
||||
"id": 123,
|
||||
"id_str": "123",
|
||||
"indices": [0, 6]
|
||||
}
|
||||
],
|
||||
"symbols": []
|
||||
},
|
||||
"favorited": false,
|
||||
"retweeted": false,
|
||||
"filter_level": "low",
|
||||
"lang": "ro",
|
||||
"timestamp_ms": "1542759193153"
|
||||
}
|
||||
]
|
||||
}
|
57
test/data/twitter/webhook3_direct_message.json
Normal file
57
test/data/twitter/webhook3_direct_message.json
Normal file
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"for_user_id": "123",
|
||||
"direct_message_events": [
|
||||
{
|
||||
"type": "message_create",
|
||||
"id": "1063077238797725701",
|
||||
"created_timestamp": "1542292339406",
|
||||
"message_create": {
|
||||
"target": {
|
||||
"recipient_id": "123"
|
||||
},
|
||||
"sender_id": "456",
|
||||
"message_data": {
|
||||
"text": "Hello again!",
|
||||
"entities": {
|
||||
"hashtags": [],
|
||||
"symbols": [],
|
||||
"user_mentions": [],
|
||||
"urls": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"users": {
|
||||
"456": {
|
||||
"id": "456",
|
||||
"created_timestamp": "1290730789000",
|
||||
"name": "Martin Edenhofer",
|
||||
"screen_name": "medenhofer",
|
||||
"description": "Open Source professional and geek. Also known as #OTRS and #Zammad inventor. ;)\r\nEntrepreneur and Advisor for open source people in need.",
|
||||
"url": "https://t.co/whm4HTWdMw",
|
||||
"protected": false,
|
||||
"verified": false,
|
||||
"followers_count": 312,
|
||||
"friends_count": 314,
|
||||
"statuses_count": 222,
|
||||
"profile_image_url": "http://pbs.twimg.com/profile_images/794220000450150401/D-eFg44R_normal.jpg",
|
||||
"profile_image_url_https": "https://pbs.twimg.com/profile_images/794220000450150401/D-eFg44R_normal.jpg"
|
||||
},
|
||||
"123": {
|
||||
"id": "123",
|
||||
"created_timestamp": "1476091912921",
|
||||
"name": "Zammad HQ",
|
||||
"screen_name": "zammadhq",
|
||||
"description": "Helpdesk and Customer Support made easy. Open Source for download or to go with SaaS. #zammad",
|
||||
"url": "https://t.co/XITyrXmhTP",
|
||||
"protected": false,
|
||||
"verified": false,
|
||||
"followers_count": 427,
|
||||
"friends_count": 512,
|
||||
"statuses_count": 437,
|
||||
"profile_image_url": "http://pbs.twimg.com/profile_images/785412960797745152/wxdIvejo_normal.jpg",
|
||||
"profile_image_url_https": "https://pbs.twimg.com/profile_images/785412960797745152/wxdIvejo_normal.jpg"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://api.twitter.com/1.1/statuses/update.json
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: in_reply_to_status_id&status=Today+the+weather+is+really...
|
||||
headers:
|
||||
User-Agent:
|
||||
- TwitterRubyGem/6.2.0
|
||||
Authorization:
|
||||
- OAuth oauth_consumer_key="some", oauth_nonce="some",
|
||||
oauth_signature="some%3D", oauth_signature_method="HMAC-SHA1",
|
||||
oauth_timestamp="1543795610", oauth_token="some",
|
||||
oauth_version="1.0"
|
||||
Connection:
|
||||
- close
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
Host:
|
||||
- api.twitter.com
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Cache-Control:
|
||||
- no-cache, no-store, must-revalidate, pre-check=0, post-check=0
|
||||
Connection:
|
||||
- close
|
||||
Content-Disposition:
|
||||
- attachment; filename=json.json
|
||||
Content-Length:
|
||||
- '2376'
|
||||
Content-Type:
|
||||
- application/json;charset=utf-8
|
||||
Date:
|
||||
- Mon, 03 Dec 2018 00:06:49 GMT
|
||||
Expires:
|
||||
- Tue, 31 Mar 1981 05:00:00 GMT
|
||||
Last-Modified:
|
||||
- Mon, 03 Dec 2018 00:06:49 GMT
|
||||
Pragma:
|
||||
- no-cache
|
||||
Server:
|
||||
- tsa_o
|
||||
Set-Cookie:
|
||||
- guest_id=v1%3A154379560980468557; Expires=Wed, 02 Dec 2020 00:06:49 GMT; Path=/;
|
||||
Domain=.twitter.com
|
||||
- lang=en; Path=/
|
||||
- personalization_id="v1_I6QHv6WAcEJj8qqGADKl+Q=="; Expires=Wed, 02 Dec 2020
|
||||
00:06:49 GMT; Path=/; Domain=.twitter.com
|
||||
Status:
|
||||
- 200 OK
|
||||
Strict-Transport-Security:
|
||||
- max-age=631138519
|
||||
X-Access-Level:
|
||||
- read-write-directmessages
|
||||
X-Connection-Hash:
|
||||
- af3c2f4e24b6e6b940f913b84f710297
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Response-Time:
|
||||
- '209'
|
||||
X-Transaction:
|
||||
- 00fb3d5400c70774
|
||||
X-Tsa-Request-Body-Time:
|
||||
- '0'
|
||||
X-Twitter-Response-Tags:
|
||||
- BouncerCompliant
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block; report=https://twitter.com/i/xss_report
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"created_at":"Mon Dec 03 00:06:49 +0000 2018","id":1069382411899817990,"id_str":"1069382411899817990","text":"Today
|
||||
the weather is really...","truncated":false,"entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[]},"source":"\u003ca
|
||||
href=\"https:\/\/edenhofer.de\" rel=\"nofollow\"\u003eMartin Edenhofer\u003c\/a\u003e","in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":219826253,"id_str":"219826253","name":"Martin
|
||||
Edenhofer","screen_name":"example","location":"","description":"Open Source
|
||||
professional and geek. Also known as #OTRS and #Zammad inventor. ;)\r\nEntrepreneur
|
||||
and Advisor for open source people in need.","url":"https:\/\/t.co\/whm4HTWdMw","entities":{"url":{"urls":[{"url":"https:\/\/t.co\/whm4HTWdMw","expanded_url":"http:\/\/edenhofer.de\/","display_url":"edenhofer.de","indices":[0,23]}]},"description":{"urls":[]}},"protected":false,"followers_count":311,"friends_count":314,"listed_count":16,"created_at":"Fri
|
||||
Nov 26 00:19:49 +0000 2010","favourites_count":129,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":227,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":true,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/794220000450150401\/D-eFg44R_normal.jpg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/794220000450150401\/D-eFg44R_normal.jpg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/219826253\/1349428277","profile_link_color":"0084B4","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"has_extended_profile":false,"default_profile":false,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false,"translator_type":"regular"},"geo":null,"coordinates":null,"place":null,"contributors":null,"is_quote_status":false,"retweet_count":0,"favorite_count":0,"favorited":false,"retweeted":false,"lang":"en"}'
|
||||
http_version:
|
||||
recorded_at: Mon, 03 Dec 2018 00:06:50 GMT
|
||||
recorded_with: VCR 4.0.0
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://api.twitter.com/1.1/statuses/update.json
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: in_reply_to_status_id&status=Today+and+tomorrow+the+weather+is+really...
|
||||
headers:
|
||||
User-Agent:
|
||||
- TwitterRubyGem/6.2.0
|
||||
Authorization:
|
||||
- OAuth oauth_consumer_key="some", oauth_nonce="some",
|
||||
oauth_signature="some%3D", oauth_signature_method="HMAC-SHA1",
|
||||
oauth_timestamp="1543795610", oauth_token="some",
|
||||
oauth_version="1.0"
|
||||
Connection:
|
||||
- close
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
Host:
|
||||
- api.twitter.com
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Cache-Control:
|
||||
- no-cache, no-store, must-revalidate, pre-check=0, post-check=0
|
||||
Connection:
|
||||
- close
|
||||
Content-Disposition:
|
||||
- attachment; filename=json.json
|
||||
Content-Length:
|
||||
- '2376'
|
||||
Content-Type:
|
||||
- application/json;charset=utf-8
|
||||
Date:
|
||||
- Mon, 03 Dec 2018 00:06:49 GMT
|
||||
Expires:
|
||||
- Tue, 31 Mar 1981 05:00:00 GMT
|
||||
Last-Modified:
|
||||
- Mon, 03 Dec 2018 00:06:49 GMT
|
||||
Pragma:
|
||||
- no-cache
|
||||
Server:
|
||||
- tsa_o
|
||||
Set-Cookie:
|
||||
- guest_id=v1%3A154379560980468557; Expires=Wed, 02 Dec 2020 00:06:49 GMT; Path=/;
|
||||
Domain=.twitter.com
|
||||
- lang=en; Path=/
|
||||
- personalization_id="v1_I6QHv6WAcEJj8qqGADKl+Q=="; Expires=Wed, 02 Dec 2020
|
||||
00:06:49 GMT; Path=/; Domain=.twitter.com
|
||||
Status:
|
||||
- 200 OK
|
||||
Strict-Transport-Security:
|
||||
- max-age=631138519
|
||||
X-Access-Level:
|
||||
- read-write-directmessages
|
||||
X-Connection-Hash:
|
||||
- af3c2f4e24b6e6b940f913b84f710297
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Response-Time:
|
||||
- '209'
|
||||
X-Transaction:
|
||||
- 00fb3d5400c70774
|
||||
X-Tsa-Request-Body-Time:
|
||||
- '0'
|
||||
X-Twitter-Response-Tags:
|
||||
- BouncerCompliant
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block; report=https://twitter.com/i/xss_report
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"created_at":"Mon Dec 03 00:06:49 +0000 2018","id":1069382411899817991,"id_str":"1069382411899817991","text":"Today and tomorrow
|
||||
the weather is really...","truncated":false,"entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[]},"source":"\u003ca
|
||||
href=\"https:\/\/edenhofer.de\" rel=\"nofollow\"\u003eMartin Edenhofer\u003c\/a\u003e","in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":219826253,"id_str":"219826253","name":"Martin
|
||||
Edenhofer","screen_name":"example","location":"","description":"Open Source
|
||||
professional and geek. Also known as #OTRS and #Zammad inventor. ;)\r\nEntrepreneur
|
||||
and Advisor for open source people in need.","url":"https:\/\/t.co\/whm4HTWdMw","entities":{"url":{"urls":[{"url":"https:\/\/t.co\/whm4HTWdMw","expanded_url":"http:\/\/edenhofer.de\/","display_url":"edenhofer.de","indices":[0,23]}]},"description":{"urls":[]}},"protected":false,"followers_count":311,"friends_count":314,"listed_count":16,"created_at":"Fri
|
||||
Nov 26 00:19:49 +0000 2010","favourites_count":129,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":227,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":true,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/794220000450150401\/D-eFg44R_normal.jpg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/794220000450150401\/D-eFg44R_normal.jpg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/219826253\/1349428277","profile_link_color":"0084B4","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"has_extended_profile":false,"default_profile":false,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false,"translator_type":"regular"},"geo":null,"coordinates":null,"place":null,"contributors":null,"is_quote_status":false,"retweet_count":0,"favorite_count":0,"favorited":false,"retweeted":false,"lang":"en"}'
|
||||
http_version:
|
||||
recorded_at: Mon, 03 Dec 2018 00:06:50 GMT
|
||||
recorded_with: VCR 4.0.0
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://api.twitter.com/1.1/search/tweets.json?count=100&q=zammad&result_type=mixed
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: ''
|
||||
headers:
|
||||
User-Agent:
|
||||
- TwitterRubyGem/6.2.0
|
||||
Authorization:
|
||||
- OAuth oauth_consumer_key="some", oauth_nonce="b5b77e1667355db2efc64e178b8a0aaa",
|
||||
oauth_signature="tybPhlz3I5fMRF5%2BE12Pwx3U5XM%3D", oauth_signature_method="HMAC-SHA1",
|
||||
oauth_timestamp="1543796201", oauth_token="key", oauth_version="1.0"
|
||||
Connection:
|
||||
- close
|
||||
Host:
|
||||
- api.twitter.com
|
||||
response:
|
||||
status:
|
||||
code: 401
|
||||
message: Unauthorized
|
||||
headers:
|
||||
Connection:
|
||||
- close
|
||||
Content-Length:
|
||||
- '62'
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Date:
|
||||
- Mon, 03 Dec 2018 00:16:41 GMT
|
||||
Server:
|
||||
- tsa_o
|
||||
Set-Cookie:
|
||||
- guest_id=v1%3A154379620109191613; Expires=Wed, 02 Dec 2020 00:16:41 GMT; Path=/;
|
||||
Domain=.twitter.com
|
||||
- personalization_id="v1_i2UDOt8QXhYvnNAv90Q8jA=="; Expires=Wed, 02 Dec 2020
|
||||
00:16:41 GMT; Path=/; Domain=.twitter.com
|
||||
Strict-Transport-Security:
|
||||
- max-age=631138519
|
||||
X-Connection-Hash:
|
||||
- 8af740bd8d5f98022086657c7172b7ee
|
||||
X-Response-Time:
|
||||
- '114'
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"errors":[{"code":89,"message":"Invalid or expired token."}]}'
|
||||
http_version:
|
||||
recorded_at: Mon, 03 Dec 2018 00:16:41 GMT
|
||||
recorded_with: VCR 4.0.0
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://graph.facebook.com/oauth/access_token
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: client_id=123&client_secret=123&grant_type=client_credentials
|
||||
headers:
|
||||
User-Agent:
|
||||
- Faraday v0.12.2
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
response:
|
||||
status:
|
||||
code: 400
|
||||
message: Bad Request
|
||||
headers:
|
||||
Www-Authenticate:
|
||||
- OAuth "Facebook Platform" "invalid_client" "Error validating application.
|
||||
Cannot get application info due to a system error."
|
||||
Content-Type:
|
||||
- application/json; charset=UTF-8
|
||||
Facebook-Api-Version:
|
||||
- v2.8
|
||||
X-Fb-Rev:
|
||||
- '4583987'
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Cache-Control:
|
||||
- no-store
|
||||
X-Fb-Trace-Id:
|
||||
- Gun7Y5LdGdV
|
||||
Expires:
|
||||
- Sat, 01 Jan 2000 00:00:00 GMT
|
||||
Strict-Transport-Security:
|
||||
- max-age=15552000; preload
|
||||
Pragma:
|
||||
- no-cache
|
||||
X-Fb-Debug:
|
||||
- 6TUcLsJ9OAIw/Pb2N6TLCham7A35JxDcZGYRF8P/KOsWeJQNr7YiKMmb+PSN2yO11B/55cBLEiTzamU4ejATvQ==
|
||||
Date:
|
||||
- Fri, 30 Nov 2018 12:50:49 GMT
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '166'
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"error":{"message":"Error validating application. Cannot get application
|
||||
info due to a system error.","type":"OAuthException","code":101,"fbtrace_id":"Gun7Y5LdGdV"}}'
|
||||
http_version:
|
||||
recorded_at: Fri, 30 Nov 2018 12:50:49 GMT
|
||||
recorded_with: VCR 4.0.0
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://graph.facebook.com/oauth/access_token
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: client_id=123&client_secret=123&grant_type=client_credentials
|
||||
headers:
|
||||
User-Agent:
|
||||
- Faraday v0.12.2
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
response:
|
||||
status:
|
||||
code: 400
|
||||
message: Bad Request
|
||||
headers:
|
||||
Www-Authenticate:
|
||||
- OAuth "Facebook Platform" "invalid_client" "Error validating application.
|
||||
Cannot get application info due to a system error."
|
||||
Content-Type:
|
||||
- application/json; charset=UTF-8
|
||||
Facebook-Api-Version:
|
||||
- v2.8
|
||||
X-Fb-Rev:
|
||||
- '4583987'
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Cache-Control:
|
||||
- no-store
|
||||
X-Fb-Trace-Id:
|
||||
- GZPegj7a6Qi
|
||||
Expires:
|
||||
- Sat, 01 Jan 2000 00:00:00 GMT
|
||||
Strict-Transport-Security:
|
||||
- max-age=15552000; preload
|
||||
Pragma:
|
||||
- no-cache
|
||||
X-Fb-Debug:
|
||||
- wTb/DgqyxUo12+6UzdZYRoTSgDxHMW+7vSlIMS5qBunqL5yvX99n99/qu4d8PnQWd39XDK/k/mW5/w3uLlZh5A==
|
||||
Date:
|
||||
- Fri, 30 Nov 2018 12:50:46 GMT
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '166'
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"error":{"message":"Error validating application. Cannot get application
|
||||
info due to a system error.","type":"OAuthException","code":101,"fbtrace_id":"GZPegj7a6Qi"}}'
|
||||
http_version:
|
||||
recorded_at: Fri, 30 Nov 2018 12:50:46 GMT
|
||||
recorded_with: VCR 4.0.0
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://graph.facebook.com/oauth/access_token
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: client_id=123&client_secret=123&grant_type=client_credentials
|
||||
headers:
|
||||
User-Agent:
|
||||
- Faraday v0.12.2
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
response:
|
||||
status:
|
||||
code: 400
|
||||
message: Bad Request
|
||||
headers:
|
||||
Www-Authenticate:
|
||||
- OAuth "Facebook Platform" "invalid_client" "Error validating application.
|
||||
Cannot get application info due to a system error."
|
||||
Content-Type:
|
||||
- application/json; charset=UTF-8
|
||||
Facebook-Api-Version:
|
||||
- v2.8
|
||||
X-Fb-Rev:
|
||||
- '4583987'
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Cache-Control:
|
||||
- no-store
|
||||
X-Fb-Trace-Id:
|
||||
- Ggs96Qoszeb
|
||||
Expires:
|
||||
- Sat, 01 Jan 2000 00:00:00 GMT
|
||||
Strict-Transport-Security:
|
||||
- max-age=15552000; preload
|
||||
Pragma:
|
||||
- no-cache
|
||||
X-Fb-Debug:
|
||||
- yM7KX2GFHeiEVA5j5YGg01LW/cHXYlryMROYhI24z7qMCd983WTNydJ0Lyy8Ve+i9HGTKoOEieWQqs576gYy1A==
|
||||
Date:
|
||||
- Fri, 30 Nov 2018 12:50:46 GMT
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '166'
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"error":{"message":"Error validating application. Cannot get application
|
||||
info due to a system error.","type":"OAuthException","code":101,"fbtrace_id":"Ggs96Qoszeb"}}'
|
||||
http_version:
|
||||
recorded_at: Fri, 30 Nov 2018 12:50:46 GMT
|
||||
recorded_with: VCR 4.0.0
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: get
|
||||
uri: https://graph.facebook.com/oauth/access_token?client_id=123&client_secret=123&code&redirect_uri=http://zammad.example.com/api/v1/external_credentials/facebook/callback
|
||||
body:
|
||||
encoding: US-ASCII
|
||||
string: ''
|
||||
headers:
|
||||
User-Agent:
|
||||
- Faraday v0.12.2
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
response:
|
||||
status:
|
||||
code: 400
|
||||
message: Bad Request
|
||||
headers:
|
||||
Www-Authenticate:
|
||||
- OAuth "Facebook Platform" "invalid_client" "Error validating application.
|
||||
Cannot get application info due to a system error."
|
||||
Content-Type:
|
||||
- application/json; charset=UTF-8
|
||||
Facebook-Api-Version:
|
||||
- v2.8
|
||||
X-Fb-Rev:
|
||||
- '4583987'
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Cache-Control:
|
||||
- no-store
|
||||
X-Fb-Trace-Id:
|
||||
- FEdFTInow6l
|
||||
Expires:
|
||||
- Sat, 01 Jan 2000 00:00:00 GMT
|
||||
Strict-Transport-Security:
|
||||
- max-age=15552000; preload
|
||||
Pragma:
|
||||
- no-cache
|
||||
X-Fb-Debug:
|
||||
- JIhX2xi6mDCBjKZi8VNZ9BEtE/qYmrtLadaqbo6Rkj941+6PJIL3Sd3cmtf/Oa5NjclmRNSuNTEEx2gjmrmgxg==
|
||||
Date:
|
||||
- Fri, 30 Nov 2018 12:50:48 GMT
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '166'
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"error":{"message":"Error validating application. Cannot get application
|
||||
info due to a system error.","type":"OAuthException","code":101,"fbtrace_id":"FEdFTInow6l"}}'
|
||||
http_version:
|
||||
recorded_at: Fri, 30 Nov 2018 12:50:48 GMT
|
||||
recorded_with: VCR 4.0.0
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://graph.facebook.com/oauth/access_token
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: client_id=123&client_secret=123&grant_type=client_credentials
|
||||
headers:
|
||||
User-Agent:
|
||||
- Faraday v0.12.2
|
||||
Content-Type:
|
||||
- application/x-www-form-urlencoded
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
response:
|
||||
status:
|
||||
code: 400
|
||||
message: Bad Request
|
||||
headers:
|
||||
Www-Authenticate:
|
||||
- OAuth "Facebook Platform" "invalid_client" "Error validating application.
|
||||
Cannot get application info due to a system error."
|
||||
Content-Type:
|
||||
- application/json; charset=UTF-8
|
||||
Facebook-Api-Version:
|
||||
- v2.8
|
||||
X-Fb-Rev:
|
||||
- '4583987'
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Cache-Control:
|
||||
- no-store
|
||||
X-Fb-Trace-Id:
|
||||
- GHQfpOGoO6+
|
||||
Expires:
|
||||
- Sat, 01 Jan 2000 00:00:00 GMT
|
||||
Strict-Transport-Security:
|
||||
- max-age=15552000; preload
|
||||
Pragma:
|
||||
- no-cache
|
||||
X-Fb-Debug:
|
||||
- m45LKcljfKLk5t2vVVgXoLkxboPq32H2Byv20O+HYluzgxL562XCEFcUiEH2dyt9UOGMqoFUpYHSYJGaEnrxRA==
|
||||
Date:
|
||||
- Fri, 30 Nov 2018 12:50:47 GMT
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '166'
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"error":{"message":"Error validating application. Cannot get application
|
||||
info due to a system error.","type":"OAuthException","code":101,"fbtrace_id":"GHQfpOGoO6+"}}'
|
||||
http_version:
|
||||
recorded_at: Fri, 30 Nov 2018 12:50:47 GMT
|
||||
recorded_with: VCR 4.0.0
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://api.twitter.com/oauth/request_token
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: ''
|
||||
headers:
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
User-Agent:
|
||||
- OAuth gem v0.5.3
|
||||
Content-Length:
|
||||
- '0'
|
||||
Authorization:
|
||||
- OAuth oauth_callback="http%3A%2F%2Fzammad.example.com%2Fapi%2Fv1%2Fexternal_credentials%2Ftwitter%2Fcallback",
|
||||
oauth_consumer_key="123", oauth_nonce="0ZvmCFseUeq6QZxGhuzQxLiyty2UErgeVcdRPOk",
|
||||
oauth_signature="Ps3iBseIQuh0ERb%2F7tEfFBERbwA%3D", oauth_signature_method="HMAC-SHA1",
|
||||
oauth_timestamp="1543582246", oauth_version="1.0"
|
||||
response:
|
||||
status:
|
||||
code: 401
|
||||
message: Authorization Required
|
||||
headers:
|
||||
Cache-Control:
|
||||
- no-cache, no-store, must-revalidate, pre-check=0, post-check=0
|
||||
Content-Disposition:
|
||||
- attachment; filename=json.json
|
||||
Content-Length:
|
||||
- '89'
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Date:
|
||||
- Fri, 30 Nov 2018 12:50:47 GMT
|
||||
Expires:
|
||||
- Tue, 31 Mar 1981 05:00:00 GMT
|
||||
Last-Modified:
|
||||
- Fri, 30 Nov 2018 12:50:47 GMT
|
||||
Pragma:
|
||||
- no-cache
|
||||
Server:
|
||||
- tsa_o
|
||||
Set-Cookie:
|
||||
- guest_id=v1%3A154358224734601031; Expires=Sun, 29 Nov 2020 12:50:47 GMT; Path=/;
|
||||
Domain=.twitter.com
|
||||
- personalization_id="v1_QoTam409bMc8TzMu10F/CA=="; Expires=Sun, 29 Nov 2020
|
||||
12:50:47 GMT; Path=/; Domain=.twitter.com
|
||||
Status:
|
||||
- 401 Unauthorized
|
||||
Strict-Transport-Security:
|
||||
- max-age=631138519
|
||||
Www-Authenticate:
|
||||
- OAuth realm="https://api.twitter.com"
|
||||
X-Connection-Hash:
|
||||
- 2ac9b707f5cdd229664772dc9d4a5a8b
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Response-Time:
|
||||
- '120'
|
||||
X-Transaction:
|
||||
- 00d9dad60073e68c
|
||||
X-Twitter-Response-Tags:
|
||||
- BouncerCompliant
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block; report=https://twitter.com/i/xss_report
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"errors":[{"code":32,"message":"Could not authenticate you."}]}'
|
||||
http_version:
|
||||
recorded_at: Fri, 30 Nov 2018 12:50:47 GMT
|
||||
recorded_with: VCR 4.0.0
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://api.twitter.com/oauth/request_token
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: ''
|
||||
headers:
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
User-Agent:
|
||||
- OAuth gem v0.5.3
|
||||
Content-Length:
|
||||
- '0'
|
||||
Authorization:
|
||||
- OAuth oauth_callback="http%3A%2F%2Fzammad.example.com%2Fapi%2Fv1%2Fexternal_credentials%2Ftwitter%2Fcallback",
|
||||
oauth_consumer_key="123", oauth_nonce="OnSYESrZ9psNGeefPXIKKdm1UmtORvReUC7L84EbI",
|
||||
oauth_signature="poSAw51WwFmwPig%2BsegGMAshh38%3D", oauth_signature_method="HMAC-SHA1",
|
||||
oauth_timestamp="1543582246", oauth_version="1.0"
|
||||
response:
|
||||
status:
|
||||
code: 401
|
||||
message: Authorization Required
|
||||
headers:
|
||||
Cache-Control:
|
||||
- no-cache, no-store, must-revalidate, pre-check=0, post-check=0
|
||||
Content-Disposition:
|
||||
- attachment; filename=json.json
|
||||
Content-Length:
|
||||
- '89'
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Date:
|
||||
- Fri, 30 Nov 2018 12:50:47 GMT
|
||||
Expires:
|
||||
- Tue, 31 Mar 1981 05:00:00 GMT
|
||||
Last-Modified:
|
||||
- Fri, 30 Nov 2018 12:50:47 GMT
|
||||
Pragma:
|
||||
- no-cache
|
||||
Server:
|
||||
- tsa_o
|
||||
Set-Cookie:
|
||||
- guest_id=v1%3A154358224702333984; Expires=Sun, 29 Nov 2020 12:50:47 GMT; Path=/;
|
||||
Domain=.twitter.com
|
||||
- personalization_id="v1_WpGcECrn1i/aClxHh3u6dg=="; Expires=Sun, 29 Nov 2020
|
||||
12:50:47 GMT; Path=/; Domain=.twitter.com
|
||||
Status:
|
||||
- 401 Unauthorized
|
||||
Strict-Transport-Security:
|
||||
- max-age=631138519
|
||||
Www-Authenticate:
|
||||
- OAuth realm="https://api.twitter.com"
|
||||
X-Connection-Hash:
|
||||
- 39c269320726b34fa0002f339f8f095a
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Response-Time:
|
||||
- '120'
|
||||
X-Transaction:
|
||||
- 00455fc500e7af18
|
||||
X-Twitter-Response-Tags:
|
||||
- BouncerCompliant
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block; report=https://twitter.com/i/xss_report
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"errors":[{"code":32,"message":"Could not authenticate you."}]}'
|
||||
http_version:
|
||||
recorded_at: Fri, 30 Nov 2018 12:50:46 GMT
|
||||
recorded_with: VCR 4.0.0
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://api.twitter.com/oauth/request_token
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: ''
|
||||
headers:
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
User-Agent:
|
||||
- OAuth gem v0.5.3
|
||||
Content-Length:
|
||||
- '0'
|
||||
Authorization:
|
||||
- OAuth oauth_callback="http%3A%2F%2Fzammad.example.com%2Fapi%2Fv1%2Fexternal_credentials%2Ftwitter%2Fcallback",
|
||||
oauth_consumer_key="123", oauth_nonce="MUJuxD5pJylV4EjZdF6Z4aOa4ersvQ7X1Yn79OmI",
|
||||
oauth_signature="fahmle9Bx8I6xsXd4PdB0QjPaog%3D", oauth_signature_method="HMAC-SHA1",
|
||||
oauth_timestamp="1543582248", oauth_version="1.0"
|
||||
response:
|
||||
status:
|
||||
code: 401
|
||||
message: Authorization Required
|
||||
headers:
|
||||
Cache-Control:
|
||||
- no-cache, no-store, must-revalidate, pre-check=0, post-check=0
|
||||
Content-Disposition:
|
||||
- attachment; filename=json.json
|
||||
Content-Length:
|
||||
- '89'
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Date:
|
||||
- Fri, 30 Nov 2018 12:50:49 GMT
|
||||
Expires:
|
||||
- Tue, 31 Mar 1981 05:00:00 GMT
|
||||
Last-Modified:
|
||||
- Fri, 30 Nov 2018 12:50:49 GMT
|
||||
Pragma:
|
||||
- no-cache
|
||||
Server:
|
||||
- tsa_o
|
||||
Set-Cookie:
|
||||
- guest_id=v1%3A154358224907677984; Max-Age=63072000; Expires=Sun, 29 Nov 2020
|
||||
12:50:49 GMT; Path=/; Domain=.twitter.com
|
||||
- personalization_id="v1_HLys+XMhL9WX47EwRLZ9ZQ=="; Max-Age=63072000; Expires=Sun,
|
||||
29 Nov 2020 12:50:49 GMT; Path=/; Domain=.twitter.com
|
||||
Status:
|
||||
- 401 Unauthorized
|
||||
Strict-Transport-Security:
|
||||
- max-age=631138519
|
||||
Www-Authenticate:
|
||||
- OAuth realm="https://api.twitter.com"
|
||||
X-Connection-Hash:
|
||||
- b8e5026ed8e6cef6e85a0e07023a10ad
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Response-Time:
|
||||
- '120'
|
||||
X-Transaction:
|
||||
- 002723f700aff7dd
|
||||
X-Twitter-Response-Tags:
|
||||
- BouncerCompliant
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block; report=https://twitter.com/i/xss_report
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"errors":[{"code":32,"message":"Could not authenticate you."}]}'
|
||||
http_version:
|
||||
recorded_at: Fri, 30 Nov 2018 12:50:49 GMT
|
||||
recorded_with: VCR 4.0.0
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://api.twitter.com/oauth/request_token
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: ''
|
||||
headers:
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
Accept:
|
||||
- "*/*"
|
||||
User-Agent:
|
||||
- OAuth gem v0.5.3
|
||||
Content-Length:
|
||||
- '0'
|
||||
Authorization:
|
||||
- OAuth oauth_callback="http%3A%2F%2Fzammad.example.com%2Fapi%2Fv1%2Fexternal_credentials%2Ftwitter%2Fcallback",
|
||||
oauth_consumer_key="123", oauth_nonce="t1hjtzQSIPDmLGGvg4Z6SkvEONEiuURXlplztO4SA",
|
||||
oauth_signature="%2BaKBQlubEInj%2Fiso8%2B24N%2FpTqNU%3D", oauth_signature_method="HMAC-SHA1",
|
||||
oauth_timestamp="1543582247", oauth_version="1.0"
|
||||
response:
|
||||
status:
|
||||
code: 401
|
||||
message: Authorization Required
|
||||
headers:
|
||||
Cache-Control:
|
||||
- no-cache, no-store, must-revalidate, pre-check=0, post-check=0
|
||||
Content-Disposition:
|
||||
- attachment; filename=json.json
|
||||
Content-Length:
|
||||
- '89'
|
||||
Content-Type:
|
||||
- application/json; charset=utf-8
|
||||
Date:
|
||||
- Fri, 30 Nov 2018 12:50:48 GMT
|
||||
Expires:
|
||||
- Tue, 31 Mar 1981 05:00:00 GMT
|
||||
Last-Modified:
|
||||
- Fri, 30 Nov 2018 12:50:48 GMT
|
||||
Pragma:
|
||||
- no-cache
|
||||
Server:
|
||||
- tsa_o
|
||||
Set-Cookie:
|
||||
- guest_id=v1%3A154358224815402336; Expires=Sun, 29 Nov 2020 12:50:48 GMT; Path=/;
|
||||
Domain=.twitter.com
|
||||
- personalization_id="v1_U3NLmMuIacImKBuAWQWA4w=="; Expires=Sun, 29 Nov 2020
|
||||
12:50:48 GMT; Path=/; Domain=.twitter.com
|
||||
Status:
|
||||
- 401 Unauthorized
|
||||
Strict-Transport-Security:
|
||||
- max-age=631138519
|
||||
Www-Authenticate:
|
||||
- OAuth realm="https://api.twitter.com"
|
||||
X-Connection-Hash:
|
||||
- b1de50b95473bb7923c67c58b9f6a226
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-Response-Time:
|
||||
- '112'
|
||||
X-Transaction:
|
||||
- '0095bdf70074b7cd'
|
||||
X-Twitter-Response-Tags:
|
||||
- BouncerCompliant
|
||||
X-Xss-Protection:
|
||||
- 1; mode=block; report=https://twitter.com/i/xss_report
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: '{"errors":[{"code":32,"message":"Could not authenticate you."}]}'
|
||||
http_version:
|
||||
recorded_at: Fri, 30 Nov 2018 12:50:48 GMT
|
||||
recorded_with: VCR 4.0.0
|
|
@ -2,9 +2,6 @@ require 'browser_test_helper'
|
|||
|
||||
class TwitterBrowserTest < TestCase
|
||||
def test_add_config
|
||||
twitter_config
|
||||
|
||||
hash = "#sweet#{hash_gen}"
|
||||
|
||||
@browser = browser_instance
|
||||
login(
|
||||
|
@ -21,392 +18,40 @@ class TwitterBrowserTest < TestCase
|
|||
sleep 2
|
||||
set(
|
||||
css: '.content.active .modal [name=consumer_key]',
|
||||
value: twitter_config[:consumer_key],
|
||||
value: 'some_key',
|
||||
)
|
||||
set(
|
||||
css: '.content.active .modal [name=consumer_secret]',
|
||||
value: 'wrong',
|
||||
value: 'some_secret',
|
||||
)
|
||||
click(css: '.content.active .modal .js-submit')
|
||||
|
||||
watch_for(
|
||||
css: '.content.active .modal .alert',
|
||||
value: 'Authorization Required',
|
||||
value: '401 Authorization Required',
|
||||
)
|
||||
|
||||
set(
|
||||
css: '.content.active .modal [name=consumer_secret]',
|
||||
value: twitter_config[:consumer_secret],
|
||||
css: '.content.active .modal [name=oauth_token]',
|
||||
value: 'some_oauth_token',
|
||||
)
|
||||
click(css: '.content.active .modal .js-submit')
|
||||
|
||||
watch_for_disappear(
|
||||
css: '.content.active .modal .alert',
|
||||
value: 'Authorization Required',
|
||||
)
|
||||
|
||||
watch_for(
|
||||
css: '.content.active .js-new',
|
||||
value: 'add account',
|
||||
)
|
||||
|
||||
click(css: '.content.active .js-configApp')
|
||||
|
||||
set(
|
||||
css: '.content.active .modal [name=consumer_secret]',
|
||||
value: 'wrong',
|
||||
css: '.content.active .modal [name=oauth_token_secret]',
|
||||
value: 'some_oauth_token_secret',
|
||||
)
|
||||
|
||||
set(
|
||||
css: '.content.active .modal [name=env]',
|
||||
value: 'some_env',
|
||||
)
|
||||
|
||||
click(css: '.content.active .modal .js-submit')
|
||||
|
||||
watch_for(
|
||||
css: '.content.active .modal .alert',
|
||||
value: 'Authorization Required',
|
||||
value: '401 Authorization Required',
|
||||
)
|
||||
|
||||
set(
|
||||
css: '.content.active .modal [name=consumer_secret]',
|
||||
value: twitter_config[:consumer_secret],
|
||||
)
|
||||
click(css: '.content.active .modal .js-submit')
|
||||
|
||||
watch_for_disappear(
|
||||
css: '.content.active .modal .alert',
|
||||
value: 'Authorization Required',
|
||||
)
|
||||
|
||||
watch_for(
|
||||
css: '.content.active .js-new',
|
||||
value: 'add account',
|
||||
)
|
||||
|
||||
click(css: '.content.active .js-new')
|
||||
|
||||
sleep 10
|
||||
|
||||
set(
|
||||
css: '#username_or_email',
|
||||
value: twitter_config[:twitter_user_login],
|
||||
no_click: true, # <label> other element would receive the click
|
||||
)
|
||||
set(
|
||||
css: '#password',
|
||||
value: twitter_config[:twitter_user_pw],
|
||||
no_click: true, # <label> other element would receive the click
|
||||
)
|
||||
click(css: '#allow')
|
||||
|
||||
#watch_for(
|
||||
# css: '.notice.callback',
|
||||
# value: 'Redirecting you back to the application',
|
||||
#)
|
||||
|
||||
watch_for(
|
||||
css: '.content.active .modal',
|
||||
value: 'Search Terms',
|
||||
)
|
||||
|
||||
# add hash tag to search
|
||||
click(css: '.content.active .modal .js-searchTermAdd')
|
||||
set(css: '.content.active .modal [name="search::term"]', value: hash)
|
||||
select(css: '.content.active .modal [name="search::group_id"]', value: 'Users')
|
||||
select(css: '.content.active .modal [name="direct_messages::group_id"]', value: 'Users')
|
||||
click(css: '.content.active .modal .js-submit')
|
||||
modal_disappear
|
||||
|
||||
watch_for(
|
||||
css: '.content.active',
|
||||
value: 'Bob Mutschler',
|
||||
)
|
||||
watch_for(
|
||||
css: '.content.active',
|
||||
value: "@#{twitter_config[:twitter_user_login]}",
|
||||
)
|
||||
exists(
|
||||
css: '.content.active .main .action:nth-child(1)'
|
||||
)
|
||||
exists_not(
|
||||
css: '.content.active .main .action:nth-child(2)'
|
||||
)
|
||||
|
||||
# add account again
|
||||
click(css: '.content.active .js-new')
|
||||
|
||||
sleep 10
|
||||
|
||||
click(css: '#allow')
|
||||
|
||||
watch_for(
|
||||
css: '.content.active .modal',
|
||||
value: 'Search Terms',
|
||||
)
|
||||
|
||||
click(css: '.content.active .modal .js-close')
|
||||
|
||||
watch_for(
|
||||
css: '.content.active',
|
||||
value: 'Bob Mutschler',
|
||||
)
|
||||
watch_for(
|
||||
css: '.content.active',
|
||||
value: "@#{twitter_config[:twitter_user_login]}",
|
||||
)
|
||||
exists(
|
||||
css: '.content.active .main .action:nth-child(1)'
|
||||
)
|
||||
exists_not(
|
||||
css: '.content.active .main .action:nth-child(2)'
|
||||
)
|
||||
|
||||
# wait till new streaming of channel is active
|
||||
sleep 80
|
||||
|
||||
# start tweet from customer
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = twitter_config[:consumer_key]
|
||||
config.consumer_secret = twitter_config[:consumer_secret]
|
||||
config.access_token = twitter_config[:twitter_customer_token]
|
||||
config.access_token_secret = twitter_config[:twitter_customer_token_secret]
|
||||
end
|
||||
|
||||
text = "Today #{rand_word}... #{hash} #{hash_gen}"
|
||||
tweet = client.update(
|
||||
text,
|
||||
)
|
||||
|
||||
# watch till tweet is in app
|
||||
click(text: 'Overviews')
|
||||
|
||||
# enable full overviews
|
||||
execute(
|
||||
js: '$(".content.active .sidebar").css("display", "block")',
|
||||
)
|
||||
|
||||
click(text: 'Unassigned & Open')
|
||||
|
||||
watch_for(
|
||||
css: '.content.active',
|
||||
value: hash,
|
||||
timeout: 36,
|
||||
)
|
||||
|
||||
ticket_open_by_title(
|
||||
title: hash,
|
||||
)
|
||||
|
||||
# reply via app
|
||||
click(css: '.content.active [data-type="twitterStatusReply"]')
|
||||
|
||||
ticket_update(
|
||||
data: {
|
||||
body: '@dzucker6 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890',
|
||||
},
|
||||
do_not_submit: true,
|
||||
)
|
||||
click(
|
||||
css: '.content.active .js-submit',
|
||||
)
|
||||
sleep 10
|
||||
click(
|
||||
css: '.content.active .js-reset',
|
||||
)
|
||||
sleep 2
|
||||
|
||||
match_not(
|
||||
css: '.content.active',
|
||||
value: '1234567890',
|
||||
)
|
||||
|
||||
click(css: '.content.active [data-type="twitterStatusReply"]')
|
||||
sleep 2
|
||||
|
||||
re_hash = "#{hash}re#{rand(99_999)}"
|
||||
|
||||
ticket_update(
|
||||
data: {
|
||||
body: "@dzucker6 #{rand_word} reply #{re_hash} #{rand(999_999)}",
|
||||
},
|
||||
)
|
||||
sleep 20
|
||||
|
||||
match(
|
||||
css: '.content.active .ticket-article',
|
||||
value: re_hash,
|
||||
)
|
||||
|
||||
# watch till tweet reached customer
|
||||
sleep 10
|
||||
text = nil
|
||||
client.search(re_hash, result_type: 'mixed').collect do |local_tweet|
|
||||
text = local_tweet.text
|
||||
end
|
||||
assert(text)
|
||||
|
||||
end
|
||||
|
||||
def reply_direct_message
|
||||
twitter_config
|
||||
|
||||
@browser = browser_instance
|
||||
login(
|
||||
username: 'master@example.com',
|
||||
password: 'test',
|
||||
url: browser_url,
|
||||
auto_wizard: true,
|
||||
)
|
||||
tasks_close_all()
|
||||
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = twitter_config[:consumer_key]
|
||||
config.consumer_secret = twitter_config[:consumer_secret]
|
||||
config.access_token = twitter_config[:twitter_customer_token]
|
||||
config.access_token_secret = twitter_config[:twitter_customer_token_secret]
|
||||
end
|
||||
|
||||
text = "Today #{rand_word}... #{hash} #{hash_gen}"
|
||||
tweet = client.create_direct_message(
|
||||
"@#{twitter_config[:twitter_user_login]}",
|
||||
text,
|
||||
)
|
||||
|
||||
# watch till tweet is in app
|
||||
click(text: 'Overviews')
|
||||
|
||||
# enable full overviews
|
||||
execute(
|
||||
js: '$(".content.active .sidebar").css("display", "block")',
|
||||
)
|
||||
|
||||
click(text: 'Unassigned & Open')
|
||||
|
||||
watch_for(
|
||||
css: '.content.active',
|
||||
value: hash,
|
||||
timeout: 36,
|
||||
)
|
||||
|
||||
ticket_open_by_title(
|
||||
title: hash,
|
||||
)
|
||||
|
||||
# reply via app
|
||||
click(css: '.content.active [data-type="twitterStatusReply"]')
|
||||
|
||||
ticket_update(
|
||||
data: {
|
||||
body: '@dzucker6 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890',
|
||||
},
|
||||
do_not_submit: true,
|
||||
)
|
||||
click(
|
||||
css: '.content.active .js-submit',
|
||||
)
|
||||
sleep 10
|
||||
click(
|
||||
css: '.content.active .js-reset',
|
||||
)
|
||||
sleep 2
|
||||
|
||||
match_not(
|
||||
css: '.content.active',
|
||||
value: '1234567890',
|
||||
)
|
||||
|
||||
click(css: '.content.active [data-type="twitterStatusReply"]')
|
||||
sleep 2
|
||||
|
||||
re_hash = "#{hash}re#{rand(99_999)}"
|
||||
|
||||
ticket_update(
|
||||
data: {
|
||||
body: "@dzucker6 #{rand_word} reply #{re_hash} #{rand(999_999)}",
|
||||
},
|
||||
)
|
||||
sleep 20
|
||||
|
||||
match(
|
||||
css: '.content.active .ticket-article',
|
||||
value: re_hash,
|
||||
)
|
||||
|
||||
# watch till tweet reached customer
|
||||
sleep 10
|
||||
text = nil
|
||||
client.search(re_hash, result_type: 'mixed').collect do |local_tweet|
|
||||
text = local_tweet.text
|
||||
end
|
||||
assert(text)
|
||||
end
|
||||
|
||||
def hash_gen
|
||||
(0...10).map { ('a'..'z').to_a[rand(26)] }.join + rand(999).to_s
|
||||
end
|
||||
|
||||
def rand_word
|
||||
words = [
|
||||
'dog',
|
||||
'cat',
|
||||
'house',
|
||||
'home',
|
||||
'yesterday',
|
||||
'tomorrow',
|
||||
'new york',
|
||||
'berlin',
|
||||
'coffee script',
|
||||
'java script',
|
||||
'bob smith',
|
||||
'be open',
|
||||
'really nice',
|
||||
'stay tuned',
|
||||
'be a good boy',
|
||||
'invent new things',
|
||||
]
|
||||
words[rand(words.length)]
|
||||
end
|
||||
|
||||
def twitter_config
|
||||
# app config
|
||||
if !ENV['TWITTER_BT_CONSUMER_KEY']
|
||||
raise "ERROR: Need TWITTER_BT_CONSUMER_KEY - hint TWITTER_BT_CONSUMER_KEY='1234'"
|
||||
end
|
||||
|
||||
consumer_key = ENV['TWITTER_BT_CONSUMER_KEY']
|
||||
if !ENV['TWITTER_BT_CONSUMER_SECRET']
|
||||
raise "ERROR: Need TWITTER_BT_CONSUMER_SECRET - hint TWITTER_BT_CONSUMER_SECRET='1234'"
|
||||
end
|
||||
|
||||
consumer_secret = ENV['TWITTER_BT_CONSUMER_SECRET']
|
||||
|
||||
if !ENV['TWITTER_BT_USER_LOGIN']
|
||||
raise "ERROR: Need TWITTER_BT_USER_LOGIN - hint TWITTER_BT_USER_LOGIN='1234'"
|
||||
end
|
||||
|
||||
twitter_user_login = ENV['TWITTER_BT_USER_LOGIN']
|
||||
|
||||
if !ENV['TWITTER_BT_USER_PW']
|
||||
raise "ERROR: Need TWITTER_BT_USER_PW - hint TWITTER_BT_USER_PW='1234'"
|
||||
end
|
||||
|
||||
twitter_user_pw = ENV['TWITTER_BT_USER_PW']
|
||||
|
||||
if !ENV['TWITTER_BT_CUSTOMER_TOKEN']
|
||||
raise "ERROR: Need TWITTER_BT_CUSTOMER_TOKEN - hint TWITTER_BT_CUSTOMER_TOKEN='1234'"
|
||||
end
|
||||
|
||||
twitter_customer_token = ENV['TWITTER_BT_CUSTOMER_TOKEN']
|
||||
|
||||
if !ENV['TWITTER_BT_CUSTOMER_TOKEN_SECRET']
|
||||
raise "ERROR: Need TWITTER_BT_CUSTOMER_TOKEN_SECRET - hint TWITTER_BT_CUSTOMER_TOKEN_SECRET='1234'"
|
||||
end
|
||||
|
||||
twitter_customer_token_secret = ENV['TWITTER_BT_CUSTOMER_TOKEN_SECRET']
|
||||
|
||||
hash = {
|
||||
consumer_key: consumer_key,
|
||||
consumer_secret: consumer_secret,
|
||||
twitter_user_login: twitter_user_login,
|
||||
twitter_user_pw: twitter_user_pw,
|
||||
twitter_customer_token: twitter_customer_token,
|
||||
twitter_customer_token_secret: twitter_customer_token_secret
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,904 +0,0 @@
|
|||
require 'integration_test_helper'
|
||||
|
||||
class TwitterTest < ActiveSupport::TestCase
|
||||
self.test_order = :sorted
|
||||
self.use_transactional_tests = false
|
||||
|
||||
# set system mode to done / to activate
|
||||
Setting.set('system_init_done', true)
|
||||
|
||||
# needed to check correct behavior
|
||||
group = Group.create_if_not_exists(
|
||||
name: 'Twitter',
|
||||
note: 'All Tweets.',
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1
|
||||
)
|
||||
|
||||
{
|
||||
'TWITTER_CONSUMER_KEY' => '1234',
|
||||
'TWITTER_CONSUMER_SECRET' => '1234',
|
||||
'TWITTER_SYSTEM_LOGIN' => '@system',
|
||||
'TWITTER_SYSTEM_ID' => '1405469528',
|
||||
'TWITTER_SYSTEM_TOKEN' => '1234',
|
||||
'TWITTER_SYSTEM_TOKEN_SECRET' => '1234',
|
||||
'TWITTER_CUSTOMER_LOGIN' => '@customer',
|
||||
'TWITTER_CUSTOMER_TOKEN' => '1234',
|
||||
'TWITTER_CUSTOMER_TOKEN_SECRET' => '1234',
|
||||
}.each do |key, example_value|
|
||||
next if ENV[key]
|
||||
|
||||
raise "ERROR: Need ENV #{key} - hint: export #{key}='#{example_value}'"
|
||||
end
|
||||
|
||||
# app config
|
||||
consumer_key = ENV['TWITTER_CONSUMER_KEY']
|
||||
consumer_secret = ENV['TWITTER_CONSUMER_SECRET']
|
||||
|
||||
# armin_theo (is system and is following marion_bauer)
|
||||
system_login = ENV['TWITTER_SYSTEM_LOGIN']
|
||||
system_id = ENV['TWITTER_SYSTEM_ID']
|
||||
system_login_without_at = system_login[1, system_login.length]
|
||||
system_token = ENV['TWITTER_SYSTEM_TOKEN']
|
||||
system_token_secret = ENV['TWITTER_SYSTEM_TOKEN_SECRET']
|
||||
hash_tag1 = "#zarepl#{rand(999)}"
|
||||
hash_tag2 = "#citheo#{rand(999)}"
|
||||
|
||||
# me_bauer (is customer and is following armin_theo)
|
||||
customer_login = ENV['TWITTER_CUSTOMER_LOGIN']
|
||||
customer_login_without_at = customer_login[1, customer_login.length]
|
||||
customer_token = ENV['TWITTER_CUSTOMER_TOKEN']
|
||||
customer_token_secret = ENV['TWITTER_CUSTOMER_TOKEN_SECRET']
|
||||
|
||||
# ensure channel configuration
|
||||
Channel.where(area: 'Twitter::Account').each(&:destroy)
|
||||
|
||||
channel = Channel.create!(
|
||||
area: 'Twitter::Account',
|
||||
options: {
|
||||
adapter: 'twitter',
|
||||
auth: {
|
||||
consumer_key: consumer_key,
|
||||
consumer_secret: consumer_secret,
|
||||
oauth_token: system_token,
|
||||
oauth_token_secret: system_token_secret,
|
||||
},
|
||||
user: {
|
||||
screen_name: system_login,
|
||||
id: system_id,
|
||||
},
|
||||
sync: {
|
||||
track_retweets: true,
|
||||
search: [
|
||||
{
|
||||
term: hash_tag2,
|
||||
group_id: group.id,
|
||||
},
|
||||
{
|
||||
term: hash_tag1,
|
||||
group_id: 1,
|
||||
},
|
||||
],
|
||||
mentions: {
|
||||
group_id: group.id,
|
||||
},
|
||||
direct_messages: {
|
||||
group_id: group.id,
|
||||
}
|
||||
}
|
||||
},
|
||||
active: true,
|
||||
created_by_id: 1,
|
||||
updated_by_id: 1,
|
||||
)
|
||||
|
||||
test 'a new outbound and reply' do
|
||||
|
||||
hash = "#{hash_tag2}#{rand(999_999)}"
|
||||
user = User.find(2)
|
||||
text = "Today the weather is really #{rand_word}... #{hash}"
|
||||
ticket = Ticket.create!(
|
||||
title: text[0, 40],
|
||||
customer_id: user.id,
|
||||
group_id: group.id,
|
||||
state: Ticket::State.find_by(name: 'new'),
|
||||
priority: Ticket::Priority.find_by(name: '2 normal'),
|
||||
preferences: {
|
||||
channel_id: channel.id,
|
||||
},
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
assert(ticket, "outbound ticket created, text: #{text}")
|
||||
article = Ticket::Article.create!(
|
||||
ticket_id: ticket.id,
|
||||
body: text,
|
||||
type: Ticket::Article::Type.find_by(name: 'twitter status'),
|
||||
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
|
||||
internal: false,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
|
||||
Scheduler.worker(true)
|
||||
|
||||
article = Ticket::Article.find(article.id)
|
||||
assert(article, "outbound article created, text: #{text}")
|
||||
assert_equal(system_login, article.from, 'ticket article from')
|
||||
assert_equal('', article.to, 'ticket article to')
|
||||
|
||||
ticket = Ticket.find(article.ticket_id)
|
||||
ticket.state = Ticket::State.find_by(name: 'closed')
|
||||
ticket.save!
|
||||
|
||||
# reply by me_bauer
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = consumer_key
|
||||
config.consumer_secret = consumer_secret
|
||||
config.access_token = customer_token
|
||||
config.access_token_secret = customer_token_secret
|
||||
end
|
||||
|
||||
tweet_found = false
|
||||
client.user_timeline(system_login_without_at).each do |tweet|
|
||||
next if tweet.id.to_s != article.message_id.to_s
|
||||
|
||||
tweet_found = true
|
||||
break
|
||||
end
|
||||
assert(tweet_found, "found outbound '#{text}' tweet '#{article.message_id}'")
|
||||
|
||||
reply_text = "#{system_login} on my side the weather is nice, too! 😍😍😍 #weather#{rand(999_999)}"
|
||||
tweet = client.update(
|
||||
reply_text,
|
||||
{
|
||||
in_reply_to_status_id: article.message_id
|
||||
}
|
||||
)
|
||||
|
||||
# fetch check system account
|
||||
sleep 10
|
||||
article = nil
|
||||
2.times do
|
||||
Channel.fetch
|
||||
|
||||
# check if follow up article has been created
|
||||
article = Ticket::Article.find_by(message_id: tweet.id)
|
||||
break if article
|
||||
|
||||
sleep 10
|
||||
end
|
||||
|
||||
assert(article, "article tweet '#{tweet.id}' imported")
|
||||
assert_equal(customer_login, article.from, 'ticket article from')
|
||||
assert_equal(system_login, article.to, 'ticket article to')
|
||||
assert_equal(tweet.id.to_s, article.message_id, 'ticket article inbound message_id')
|
||||
assert_equal(2, article.ticket.articles.count, 'ticket article inbound count')
|
||||
assert_equal(reply_text.utf8_to_3bytesutf8, ticket.articles.last.body, 'ticket article inbound body')
|
||||
|
||||
assert_equal('open', ticket.reload.state.name)
|
||||
|
||||
channel = Channel.find(channel.id)
|
||||
assert_equal('', channel.last_log_out)
|
||||
assert_equal('ok', channel.status_out)
|
||||
assert_equal('', channel.last_log_in)
|
||||
assert_equal('ok', channel.status_in)
|
||||
end
|
||||
|
||||
test 'b new inbound and reply' do
|
||||
|
||||
# new tweet by me_bauer
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = consumer_key
|
||||
config.consumer_secret = consumer_secret
|
||||
config.access_token = customer_token
|
||||
config.access_token_secret = customer_token_secret
|
||||
end
|
||||
|
||||
hash = "#{hash_tag1} ##{hash_gen}"
|
||||
text = "Today #{rand_word}... #{hash}"
|
||||
tweet = client.update(
|
||||
text,
|
||||
)
|
||||
|
||||
# fetch check system account
|
||||
sleep 20
|
||||
article = nil
|
||||
2.times do
|
||||
Channel.fetch
|
||||
|
||||
# check if ticket and article has been created
|
||||
article = Ticket::Article.find_by(message_id: tweet.id)
|
||||
break if article
|
||||
|
||||
sleep 20
|
||||
end
|
||||
assert(article, "Can't find tweet id #{tweet.id}/#{text}")
|
||||
assert_equal(customer_login, article.from, 'ticket article from')
|
||||
assert_nil(article.to, 'ticket article to')
|
||||
ticket = article.ticket
|
||||
assert_equal('new', ticket.reload.state.name)
|
||||
|
||||
# send reply
|
||||
reply_text = "#{customer_login} on my side #weather#{hash_gen}"
|
||||
article = Ticket::Article.create!(
|
||||
ticket_id: ticket.id,
|
||||
body: reply_text,
|
||||
type: Ticket::Article::Type.find_by(name: 'twitter status'),
|
||||
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
|
||||
internal: false,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
|
||||
Scheduler.worker(true)
|
||||
assert_equal('open', ticket.reload.state.name)
|
||||
|
||||
article = Ticket::Article.find(article.id)
|
||||
assert(article, "outbound article created, text: #{reply_text}")
|
||||
assert_equal(system_login, article.from, 'ticket article from')
|
||||
assert_equal(customer_login, article.to, 'ticket article to')
|
||||
sleep 5
|
||||
tweet_found = false
|
||||
client.user_timeline(system_login_without_at).each do |local_tweet|
|
||||
sleep 10
|
||||
next if local_tweet.id.to_s != article.message_id.to_s
|
||||
|
||||
tweet_found = true
|
||||
break
|
||||
end
|
||||
assert(tweet_found, "found outbound '#{reply_text}' tweet '#{article.message_id}'")
|
||||
|
||||
channel = Channel.find(channel.id)
|
||||
assert_equal('', channel.last_log_out)
|
||||
assert_equal('ok', channel.status_out)
|
||||
assert_equal('', channel.last_log_in)
|
||||
assert_equal('ok', channel.status_in)
|
||||
|
||||
ticket = Ticket.find(article.ticket_id)
|
||||
ticket.state = Ticket::State.find_by(name: 'closed')
|
||||
ticket.save!
|
||||
|
||||
# reply with zammad user directly
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = consumer_key
|
||||
config.consumer_secret = consumer_secret
|
||||
config.access_token = system_token
|
||||
config.access_token_secret = system_token_secret
|
||||
end
|
||||
|
||||
hash = "#{hash_tag1} ##{hash_gen}"
|
||||
text = "Today #{system_login} #{rand_word}... #{hash}"
|
||||
tweet = client.update(
|
||||
text,
|
||||
)
|
||||
|
||||
# fetch check system account
|
||||
sleep 20
|
||||
article = nil
|
||||
2.times do
|
||||
Channel.fetch
|
||||
|
||||
# check if ticket and article has been created
|
||||
article = Ticket::Article.find_by(message_id: tweet.id)
|
||||
break if article
|
||||
|
||||
sleep 20
|
||||
end
|
||||
|
||||
assert(article, "Can't find tweet id #{tweet.id}/#{text}")
|
||||
assert_equal('closed', ticket.reload.state.name)
|
||||
end
|
||||
|
||||
test 'c new by direct message inbound' do
|
||||
|
||||
# cleanup direct messages of system
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = consumer_key
|
||||
config.consumer_secret = consumer_secret
|
||||
config.access_token = system_token
|
||||
config.access_token_secret = system_token_secret
|
||||
end
|
||||
dms = client.direct_messages(count: 100)
|
||||
dms.each do |dm|
|
||||
client.destroy_direct_message(dm.id)
|
||||
end
|
||||
client = Twitter::REST::Client.new(
|
||||
consumer_key: consumer_key,
|
||||
consumer_secret: consumer_secret,
|
||||
access_token: customer_token,
|
||||
access_token_secret: customer_token_secret
|
||||
)
|
||||
dms = client.direct_messages(count: 100)
|
||||
dms.each do |dm|
|
||||
client.destroy_direct_message(dm.id)
|
||||
end
|
||||
hash = "#citheo44 #{hash_gen}"
|
||||
text = "How about #{rand_word} the details? #{hash} - #{'Long' * 50}"
|
||||
dm = client.create_direct_message(
|
||||
system_login_without_at,
|
||||
text,
|
||||
)
|
||||
assert(dm, "dm with ##{hash} created")
|
||||
|
||||
# fetch check system account
|
||||
sleep 15
|
||||
article = nil
|
||||
1.times do
|
||||
Channel.fetch
|
||||
|
||||
# check if ticket and article has been created
|
||||
article = Ticket::Article.find_by(message_id: dm.id)
|
||||
break if article
|
||||
|
||||
sleep 10
|
||||
end
|
||||
|
||||
assert(article, "inbound article '#{text}' created")
|
||||
assert_equal(customer_login, article.from, 'ticket article from')
|
||||
assert_equal(text, article.body, 'ticket article body')
|
||||
ticket = article.ticket
|
||||
assert(ticket, 'ticket of inbound article exists')
|
||||
assert(ticket.articles, 'ticket.articles exists')
|
||||
assert_equal(1, ticket.articles.count, 'ticket article inbound count')
|
||||
assert_equal(ticket.state.name, 'new')
|
||||
|
||||
# reply via ticket
|
||||
outbound_article = Ticket::Article.create!(
|
||||
ticket_id: ticket.id,
|
||||
to: customer_login,
|
||||
body: "Will call you later #{rand_word}!",
|
||||
type: Ticket::Article::Type.find_by(name: 'twitter direct-message'),
|
||||
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
|
||||
internal: false,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
|
||||
Scheduler.worker(true)
|
||||
|
||||
outbound_article = Ticket::Article.find(outbound_article.id)
|
||||
assert(outbound_article, 'outbound article created')
|
||||
assert_equal(2, outbound_article.ticket.articles.count, 'ticket article outbound count')
|
||||
assert_equal(system_login, outbound_article.from, 'ticket article from')
|
||||
assert_equal(customer_login, outbound_article.to, 'ticket article to')
|
||||
ticket.state = Ticket::State.find_by(name: 'pending reminder')
|
||||
ticket.save
|
||||
|
||||
text = "#{rand_word}. #{hash}"
|
||||
dm = client.create_direct_message(
|
||||
system_login_without_at,
|
||||
text,
|
||||
)
|
||||
assert(dm, "second dm with ##{hash} created")
|
||||
|
||||
# fetch check system account
|
||||
sleep 15
|
||||
article = nil
|
||||
1.times do
|
||||
Channel.fetch
|
||||
|
||||
# check if ticket and article has been created
|
||||
article = Ticket::Article.find_by(message_id: dm.id)
|
||||
break if article
|
||||
|
||||
sleep 10
|
||||
end
|
||||
|
||||
assert(article, "inbound article '#{text}' created")
|
||||
assert_equal(customer_login, article.from, 'ticket article inbound from')
|
||||
assert_equal(system_login, article.to, 'ticket article inbound to')
|
||||
assert_equal(article.ticket.id, ticket.id, 'still the same ticket')
|
||||
ticket = article.ticket
|
||||
assert(ticket, 'ticket of inbound article exists')
|
||||
assert(ticket.articles, 'ticket.articles exists')
|
||||
assert_equal(3, ticket.articles.count, 'ticket article inbound count')
|
||||
assert_equal(ticket.state.name, 'open')
|
||||
|
||||
# close dm ticket, next dm should open a new
|
||||
ticket.state = Ticket::State.find_by(name: 'closed')
|
||||
ticket.save
|
||||
|
||||
text = "Thanks #{rand_word} for your call. I just have one question. #{hash}"
|
||||
dm = client.create_direct_message(
|
||||
system_login_without_at,
|
||||
text,
|
||||
)
|
||||
assert(dm, "third dm with ##{hash} created")
|
||||
|
||||
# fetch check system account
|
||||
sleep 15
|
||||
article = nil
|
||||
1.times do
|
||||
Channel.fetch
|
||||
|
||||
# check if ticket and article has been created
|
||||
article = Ticket::Article.find_by(message_id: dm.id)
|
||||
break if article
|
||||
|
||||
sleep 15
|
||||
end
|
||||
|
||||
assert(article, "inbound article '#{text}' created with dm id #{dm.id}")
|
||||
assert_equal(customer_login, article.from, 'ticket article inbound from')
|
||||
assert_equal(system_login, article.to, 'ticket article inbound to')
|
||||
ticket = article.ticket
|
||||
assert(ticket, 'ticket of inbound article exists')
|
||||
assert(ticket.articles, 'ticket.articles exists')
|
||||
assert_equal(1, ticket.articles.count, 'ticket article inbound count')
|
||||
assert_equal(ticket.state.name, 'new')
|
||||
|
||||
channel = Channel.find(channel.id)
|
||||
assert_equal('', channel.last_log_out)
|
||||
assert_equal('ok', channel.status_out)
|
||||
assert_equal('', channel.last_log_in)
|
||||
assert_equal('ok', channel.status_in)
|
||||
end
|
||||
|
||||
test 'c new by direct message outbound without required parameters' do
|
||||
|
||||
# cleanup direct messages of system
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = consumer_key
|
||||
config.consumer_secret = consumer_secret
|
||||
config.access_token = system_token
|
||||
config.access_token_secret = system_token_secret
|
||||
end
|
||||
dms = client.direct_messages(count: 100)
|
||||
dms.each do |dm|
|
||||
client.destroy_direct_message(dm.id)
|
||||
end
|
||||
client = Twitter::REST::Client.new(
|
||||
consumer_key: consumer_key,
|
||||
consumer_secret: consumer_secret,
|
||||
access_token: customer_token,
|
||||
access_token_secret: customer_token_secret
|
||||
)
|
||||
dms = client.direct_messages(count: 100)
|
||||
dms.each do |dm|
|
||||
client.destroy_direct_message(dm.id)
|
||||
end
|
||||
hash = "#citheo44 #{hash_gen}"
|
||||
text = "How about #{rand_word} the details? #{hash} - #{'Long' * 50}"
|
||||
dm = client.create_direct_message(
|
||||
system_login_without_at,
|
||||
text,
|
||||
)
|
||||
assert(dm, "dm with ##{hash} created")
|
||||
|
||||
# fetch check system account
|
||||
sleep 15
|
||||
article = nil
|
||||
1.times do
|
||||
Channel.fetch
|
||||
|
||||
# check if ticket and article has been created
|
||||
article = Ticket::Article.find_by(message_id: dm.id)
|
||||
break if article
|
||||
|
||||
sleep 10
|
||||
end
|
||||
|
||||
assert(article, "inbound article '#{text}' created")
|
||||
assert_equal(customer_login, article.from, 'ticket article from')
|
||||
assert_equal(text, article.body, 'ticket article body')
|
||||
ticket = article.ticket
|
||||
assert(ticket, 'ticket of inbound article exists')
|
||||
assert(ticket.articles, 'ticket.articles exists')
|
||||
assert_equal(1, ticket.articles.count, 'ticket article inbound count')
|
||||
assert_equal(ticket.state.name, 'closed')
|
||||
|
||||
# reply via ticket
|
||||
reply = assert_raises(Exceptions::UnprocessableEntity) do
|
||||
Ticket::Article.create!(
|
||||
ticket_id: ticket.id,
|
||||
in_reply_to: '123456789',
|
||||
body: "Will call you later #{rand_word}!",
|
||||
type: Ticket::Article::Type.find_by(name: 'twitter direct-message'),
|
||||
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
|
||||
internal: false,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
end
|
||||
|
||||
assert_equal('twitter to: parameter is missing', reply.message)
|
||||
end
|
||||
|
||||
test 'd track_retweets enabled' do
|
||||
|
||||
# enable track_retweets
|
||||
channel[:options]['sync']['track_retweets'] = true
|
||||
channel.save!
|
||||
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = consumer_key
|
||||
config.consumer_secret = consumer_secret
|
||||
config.access_token = system_token
|
||||
config.access_token_secret = system_token_secret
|
||||
end
|
||||
|
||||
hash = "#{hash_tag1} ##{hash_gen}"
|
||||
text = "Retweet me - I'm #{system_login} - #{rand_word}... #{hash}"
|
||||
tweet = client.update(text)
|
||||
|
||||
client = Twitter::REST::Client.new(
|
||||
consumer_key: consumer_key,
|
||||
consumer_secret: consumer_secret,
|
||||
access_token: customer_token,
|
||||
access_token_secret: customer_token_secret
|
||||
)
|
||||
|
||||
retweet = client.retweet(tweet).first
|
||||
|
||||
# fetch check system account
|
||||
sleep 15
|
||||
article = nil
|
||||
2.times do
|
||||
Channel.fetch
|
||||
|
||||
# check if ticket and article has been created
|
||||
article = Ticket::Article.find_by(message_id: retweet.id)
|
||||
break if article
|
||||
|
||||
sleep 10
|
||||
end
|
||||
|
||||
assert(article, "retweet article '#{text}' created")
|
||||
end
|
||||
|
||||
test 'e track_retweets disabled' do
|
||||
|
||||
# disable track_retweets
|
||||
channel[:options]['sync']['track_retweets'] = false
|
||||
channel.save!
|
||||
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = consumer_key
|
||||
config.consumer_secret = consumer_secret
|
||||
config.access_token = system_token
|
||||
config.access_token_secret = system_token_secret
|
||||
end
|
||||
|
||||
hash = "#{hash_tag1} ##{hash_gen}"
|
||||
text = "Retweet me - I'm #{system_login} - #{rand_word}... #{hash}"
|
||||
tweet = client.update(text)
|
||||
|
||||
client = Twitter::REST::Client.new(
|
||||
consumer_key: consumer_key,
|
||||
consumer_secret: consumer_secret,
|
||||
access_token: customer_token,
|
||||
access_token_secret: customer_token_secret
|
||||
)
|
||||
|
||||
retweet = client.retweet(tweet).first
|
||||
|
||||
# fetch check system account
|
||||
sleep 15
|
||||
article = nil
|
||||
2.times do
|
||||
Channel.fetch
|
||||
|
||||
# check if ticket and article has been created
|
||||
article = Ticket::Article.find_by(message_id: retweet.id)
|
||||
break if article
|
||||
|
||||
sleep 10
|
||||
end
|
||||
|
||||
assert_nil(article, "retweet article '#{text}' not created")
|
||||
end
|
||||
|
||||
test 'f streaming test' do
|
||||
thread = Thread.new do
|
||||
Channel.stream
|
||||
end
|
||||
sleep 10
|
||||
|
||||
# new tweet I - by me_bauer
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = consumer_key
|
||||
config.consumer_secret = consumer_secret
|
||||
config.access_token = customer_token
|
||||
config.access_token_secret = customer_token_secret
|
||||
end
|
||||
|
||||
hash = "#{hash_tag1} ##{hash_gen}"
|
||||
text = "Today... #{rand_word} #{hash}"
|
||||
tweet = client.update(
|
||||
text,
|
||||
)
|
||||
|
||||
article = nil
|
||||
5.times do
|
||||
Scheduler.worker(true)
|
||||
article = Ticket::Article.find_by(message_id: tweet.id)
|
||||
break if article
|
||||
|
||||
ActiveRecord::Base.clear_all_connections!
|
||||
ActiveRecord::Base.connection.query_cache.clear
|
||||
sleep 10
|
||||
end
|
||||
|
||||
assert(article, "article from customer with text '#{text}' message_id '#{tweet.id}' created")
|
||||
assert_equal(customer_login, article.from, 'ticket article from')
|
||||
assert_nil(article.to, 'ticket article to')
|
||||
|
||||
# new tweet II - by me_bauer
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = consumer_key
|
||||
config.consumer_secret = consumer_secret
|
||||
config.access_token = customer_token
|
||||
config.access_token_secret = customer_token_secret
|
||||
end
|
||||
hash = "#{hash_tag1} ##{rand(999_999)}"
|
||||
text = "Today... #{rand_word} #{hash}"
|
||||
tweet = client.update(
|
||||
text,
|
||||
)
|
||||
|
||||
article = nil
|
||||
5.times do
|
||||
Scheduler.worker(true)
|
||||
article = Ticket::Article.find_by(message_id: tweet.id)
|
||||
break if article
|
||||
|
||||
ActiveRecord::Base.clear_all_connections!
|
||||
ActiveRecord::Base.connection.query_cache.clear
|
||||
sleep 10
|
||||
end
|
||||
assert(article, "article from customer with text '#{text}' message_id '#{tweet.id}' created")
|
||||
assert_equal(customer_login, article.from, 'ticket article from')
|
||||
assert_nil(article.to, 'ticket article to')
|
||||
|
||||
# send reply
|
||||
reply_text = "RE #{text}"
|
||||
article = Ticket::Article.create!(
|
||||
ticket_id: article.ticket_id,
|
||||
body: reply_text,
|
||||
type: Ticket::Article::Type.find_by(name: 'twitter status'),
|
||||
sender: Ticket::Article::Sender.find_by(name: 'Agent'),
|
||||
internal: false,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
|
||||
Scheduler.worker(true)
|
||||
|
||||
article = Ticket::Article.find(article.id)
|
||||
assert(article, "outbound article created, text: #{reply_text}")
|
||||
assert_equal(system_login, article.from, 'ticket article from')
|
||||
assert_equal('', article.to, 'ticket article to')
|
||||
sleep 5
|
||||
|
||||
tweet_found = false
|
||||
client.user_timeline(system_login_without_at).each do |local_tweet|
|
||||
sleep 10
|
||||
next if local_tweet.id.to_s != article.message_id.to_s
|
||||
|
||||
tweet_found = true
|
||||
break
|
||||
end
|
||||
assert(tweet_found, "found outbound '#{reply_text}' tweet '#{article.message_id}'")
|
||||
|
||||
count = Ticket::Article.where(message_id: article.message_id).count
|
||||
assert_equal(1, count, "tweet #{article.message_id}")
|
||||
|
||||
channel_id = article.ticket.preferences[:channel_id]
|
||||
assert(channel_id)
|
||||
channel = Channel.find(channel_id)
|
||||
assert_equal('', channel.last_log_out)
|
||||
assert_equal('ok', channel.status_out)
|
||||
|
||||
# get dm via stream
|
||||
client = Twitter::REST::Client.new(
|
||||
consumer_key: consumer_key,
|
||||
consumer_secret: consumer_secret,
|
||||
access_token: customer_token,
|
||||
access_token_secret: customer_token_secret
|
||||
)
|
||||
hash = "#citheo44#{rand(999_999)}"
|
||||
text = "How about the #{rand_word}? #{hash}"
|
||||
dm = client.create_direct_message(
|
||||
system_login_without_at,
|
||||
text,
|
||||
)
|
||||
assert(dm, "dm with ##{hash} created")
|
||||
|
||||
article = nil
|
||||
5.times do
|
||||
Scheduler.worker(true)
|
||||
article = Ticket::Article.find_by(message_id: dm.id)
|
||||
break if article
|
||||
|
||||
sleep 10
|
||||
end
|
||||
assert(article, "inbound article '#{text}' message_id '#{dm.id}' created")
|
||||
assert_equal(customer_login, article.from, 'ticket article from')
|
||||
assert_equal(system_login, article.to, 'ticket article to')
|
||||
thread.exit
|
||||
thread.join
|
||||
end
|
||||
|
||||
test 'g streaming test retweet enabled' do
|
||||
thread = Thread.new do
|
||||
# enable track_retweets in current thread
|
||||
# since Threads are not spawned in the same scope
|
||||
# as the current test is running in .....
|
||||
channel_thread = Channel.find(channel.id)
|
||||
channel_thread[:options]['sync']['track_retweets'] = true
|
||||
channel_thread.save!
|
||||
|
||||
Channel.stream
|
||||
end
|
||||
sleep 10
|
||||
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = consumer_key
|
||||
config.consumer_secret = consumer_secret
|
||||
config.access_token = system_token
|
||||
config.access_token_secret = system_token_secret
|
||||
end
|
||||
|
||||
hash = "#{hash_tag1} ##{hash_gen}"
|
||||
text = "Retweet me - I'm #{system_login} - #{rand_word}... #{hash}"
|
||||
tweet = client.update(text)
|
||||
|
||||
client = Twitter::REST::Client.new(
|
||||
consumer_key: consumer_key,
|
||||
consumer_secret: consumer_secret,
|
||||
access_token: customer_token,
|
||||
access_token_secret: customer_token_secret
|
||||
)
|
||||
|
||||
retweet = client.retweet(tweet).first
|
||||
|
||||
# fetch check system account
|
||||
sleep 15
|
||||
article = nil
|
||||
2.times do
|
||||
Channel.fetch
|
||||
|
||||
# check if ticket and article has been created
|
||||
article = Ticket::Article.find_by(message_id: retweet.id)
|
||||
break if article
|
||||
|
||||
ActiveRecord::Base.clear_all_connections!
|
||||
ActiveRecord::Base.connection.query_cache.clear
|
||||
sleep 10
|
||||
end
|
||||
|
||||
assert(article, "retweet article '#{text}' created")
|
||||
|
||||
thread.exit
|
||||
thread.join
|
||||
end
|
||||
|
||||
test 'h streaming test retweet disabled' do
|
||||
thread = Thread.new do
|
||||
# disable track_retweets in current thread
|
||||
# since Threads are not spawned in the same scope
|
||||
# as the current test is running in .....
|
||||
channel_thread = Channel.find(channel.id)
|
||||
channel_thread[:options]['sync']['track_retweets'] = false
|
||||
channel_thread.save!
|
||||
|
||||
Channel.stream
|
||||
end
|
||||
sleep 10
|
||||
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = consumer_key
|
||||
config.consumer_secret = consumer_secret
|
||||
config.access_token = system_token
|
||||
config.access_token_secret = system_token_secret
|
||||
end
|
||||
|
||||
hash = "#{hash_tag1} ##{hash_gen}"
|
||||
text = "Retweet me - I'm #{system_login} - #{rand_word}... #{hash}"
|
||||
tweet = client.update(text)
|
||||
|
||||
client = Twitter::REST::Client.new(
|
||||
consumer_key: consumer_key,
|
||||
consumer_secret: consumer_secret,
|
||||
access_token: customer_token,
|
||||
access_token_secret: customer_token_secret
|
||||
)
|
||||
|
||||
retweet = client.retweet(tweet).first
|
||||
|
||||
# fetch check system account
|
||||
article = nil
|
||||
4.times do
|
||||
# check if ticket and article has been created
|
||||
article = Ticket::Article.find_by(message_id: retweet.id)
|
||||
break if article
|
||||
|
||||
sleep 10
|
||||
end
|
||||
|
||||
assert_nil(article, "retweet article '#{text}' not created")
|
||||
|
||||
thread.exit
|
||||
thread.join
|
||||
end
|
||||
|
||||
test 'i restart stream after config of channel has changed' do
|
||||
hash = "#citheo#{rand(999)}"
|
||||
|
||||
thread = Thread.new do
|
||||
Channel.stream
|
||||
sleep 10
|
||||
item = {
|
||||
term: hash,
|
||||
group_id: group.id,
|
||||
}
|
||||
channel_thread = Channel.find(channel.id)
|
||||
channel_thread[:options]['sync']['search'].push item
|
||||
channel_thread.save!
|
||||
end
|
||||
|
||||
sleep 60
|
||||
|
||||
# new tweet - by me_bauer
|
||||
client = Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = consumer_key
|
||||
config.consumer_secret = consumer_secret
|
||||
config.access_token = customer_token
|
||||
config.access_token_secret = customer_token_secret
|
||||
end
|
||||
|
||||
hash = "#{hash_tag1} ##{hash_gen}"
|
||||
text = "Today... #{rand_word} #{hash}"
|
||||
tweet = client.update(
|
||||
text,
|
||||
)
|
||||
article = nil
|
||||
5.times do
|
||||
Channel.fetch
|
||||
Scheduler.worker(true)
|
||||
article = Ticket::Article.find_by(message_id: tweet.id)
|
||||
break if article
|
||||
|
||||
ActiveRecord::Base.clear_all_connections!
|
||||
ActiveRecord::Base.connection.query_cache.clear
|
||||
sleep 10
|
||||
end
|
||||
assert(article, "article from customer with text '#{text}' message_id '#{tweet.id}' created")
|
||||
assert_equal(customer_login, article.from, 'ticket article from')
|
||||
assert_nil(article.to, 'ticket article to')
|
||||
|
||||
thread.exit
|
||||
thread.join
|
||||
|
||||
channel_thread = Channel.find(channel.id)
|
||||
channel_thread[:options]['sync']['search'].pop
|
||||
channel_thread.save!
|
||||
end
|
||||
|
||||
def hash_gen
|
||||
rand(999).to_s + (0...10).map { ('a'..'z').to_a[rand(26)] }.join
|
||||
end
|
||||
|
||||
def rand_word
|
||||
[
|
||||
'dog',
|
||||
'cat',
|
||||
'house',
|
||||
'home',
|
||||
'yesterday',
|
||||
'tomorrow',
|
||||
'new york',
|
||||
'berlin',
|
||||
'coffee script',
|
||||
'java script',
|
||||
'bob smith',
|
||||
'be open',
|
||||
'really nice',
|
||||
'stay tuned',
|
||||
'be a good boy',
|
||||
'invent new things',
|
||||
].sample
|
||||
end
|
||||
|
||||
end
|
|
@ -1,194 +0,0 @@
|
|||
require 'test_helper'
|
||||
|
||||
class TicketArticleTwitter < ActiveSupport::TestCase
|
||||
|
||||
test 'preferences cleanup' do
|
||||
|
||||
org_community = Organization.create_if_not_exists(
|
||||
name: 'Zammad Foundation',
|
||||
)
|
||||
user_community = User.create_or_update(
|
||||
login: 'article.twitter@example.org',
|
||||
firstname: 'Article',
|
||||
lastname: 'Twitter',
|
||||
email: 'article.twitter@example.org',
|
||||
password: '',
|
||||
active: true,
|
||||
roles: [ Role.find_by(name: 'Customer') ],
|
||||
organization_id: org_community.id,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
|
||||
ticket1 = Ticket.create!(
|
||||
group_id: Group.first.id,
|
||||
customer_id: user_community.id,
|
||||
title: 'Tweet 1!',
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
twitter_preferences = {
|
||||
mention_ids: [1_234_567_890],
|
||||
geo: Twitter::NullObject.new,
|
||||
retweeted: false,
|
||||
possibly_sensitive: false,
|
||||
in_reply_to_user_id: 1_234_567_890,
|
||||
place: Twitter::NullObject.new,
|
||||
retweet_count: 0,
|
||||
source: '<a href="http://example.com/software/tweetbot/mac" rel="nofollow">Tweetbot for Mac</a>',
|
||||
favorited: false,
|
||||
truncated: false
|
||||
}
|
||||
preferences = {
|
||||
twitter: TweetBase.preferences_cleanup(twitter_preferences),
|
||||
links: [
|
||||
{
|
||||
url: 'https://twitter.com/statuses/123',
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
}
|
||||
article1 = Ticket::Article.create!(
|
||||
ticket_id: ticket1.id,
|
||||
type_id: Ticket::Article::Type.find_by(name: 'twitter status').id,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
from: '@example',
|
||||
body: 'some tweet',
|
||||
internal: false,
|
||||
preferences: preferences,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
assert(article1.preferences[:twitter])
|
||||
assert_equal(1_234_567_890, article1.preferences[:twitter][:mention_ids][0])
|
||||
assert_equal(ActiveSupport::HashWithIndifferentAccess, article1.preferences[:twitter][:geo].class)
|
||||
assert(article1.preferences[:twitter][:geo].blank?)
|
||||
assert_equal(ActiveSupport::HashWithIndifferentAccess, article1.preferences[:twitter][:place].class)
|
||||
assert(article1.preferences[:twitter][:place].blank?)
|
||||
|
||||
twitter_preferences = {
|
||||
mention_ids: [1_234_567_890],
|
||||
geo: Twitter::NullObject.new,
|
||||
retweeted: false,
|
||||
possibly_sensitive: false,
|
||||
in_reply_to_user_id: 1_234_567_890,
|
||||
place: Twitter::NullObject.new,
|
||||
retweet_count: 0,
|
||||
source: '<a href="http://example.com/software/tweetbot/mac" rel="nofollow">Tweetbot for Mac</a>',
|
||||
favorited: false,
|
||||
truncated: false
|
||||
}
|
||||
preferences = TweetBase.preferences_cleanup(
|
||||
twitter: twitter_preferences,
|
||||
links: [
|
||||
{
|
||||
url: 'https://twitter.com/statuses/123',
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
)
|
||||
article2 = Ticket::Article.create!(
|
||||
ticket_id: ticket1.id,
|
||||
type_id: Ticket::Article::Type.find_by(name: 'twitter status').id,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
from: '@example',
|
||||
body: 'some tweet',
|
||||
internal: false,
|
||||
preferences: preferences,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
assert(article2.preferences[:twitter])
|
||||
assert_equal(1_234_567_890, article2.preferences[:twitter][:mention_ids][0])
|
||||
assert_equal(ActiveSupport::HashWithIndifferentAccess, article2.preferences[:twitter][:geo].class)
|
||||
assert(article2.preferences[:twitter][:geo].blank?)
|
||||
assert_equal(ActiveSupport::HashWithIndifferentAccess, article2.preferences[:twitter][:place].class)
|
||||
assert(article2.preferences[:twitter][:place].blank?)
|
||||
|
||||
twitter_preferences = {
|
||||
mention_ids: [1_234_567_890],
|
||||
geo: Twitter::Geo.new(coordinates: [1, 1]),
|
||||
retweeted: false,
|
||||
possibly_sensitive: false,
|
||||
in_reply_to_user_id: 1_234_567_890,
|
||||
place: Twitter::Place.new(country: 'da', name: 'do', woeid: 1, id: 1),
|
||||
retweet_count: 0,
|
||||
source: '<a href="http://example.com/software/tweetbot/mac" rel="nofollow">Tweetbot for Mac</a>',
|
||||
favorited: false,
|
||||
truncated: false
|
||||
}
|
||||
preferences = {
|
||||
twitter: TweetBase.preferences_cleanup(twitter_preferences),
|
||||
links: [
|
||||
{
|
||||
url: 'https://twitter.com/statuses/123',
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
article3 = Ticket::Article.create!(
|
||||
ticket_id: ticket1.id,
|
||||
type_id: Ticket::Article::Type.find_by(name: 'twitter status').id,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
from: '@example',
|
||||
body: 'some tweet',
|
||||
internal: false,
|
||||
preferences: preferences,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
assert(article3.preferences[:twitter])
|
||||
assert_equal(1_234_567_890, article3.preferences[:twitter][:mention_ids][0])
|
||||
assert_equal(ActiveSupport::HashWithIndifferentAccess, article3.preferences[:twitter][:geo].class)
|
||||
assert_equal({ 'coordinates' => [1, 1] }, article3.preferences[:twitter][:geo])
|
||||
assert_equal(ActiveSupport::HashWithIndifferentAccess, article3.preferences[:twitter][:place].class)
|
||||
assert_equal({ 'country' => 'da', 'name' => 'do', 'woeid' => 1, 'id' => 1 }, article3.preferences[:twitter][:place])
|
||||
|
||||
twitter_preferences = {
|
||||
mention_ids: [1_234_567_890],
|
||||
geo: Twitter::Geo.new(coordinates: [1, 1]),
|
||||
retweeted: false,
|
||||
possibly_sensitive: false,
|
||||
in_reply_to_user_id: 1_234_567_890,
|
||||
place: Twitter::Place.new(country: 'da', name: 'do', woeid: 1, id: 1),
|
||||
retweet_count: 0,
|
||||
source: '<a href="http://example.com/software/tweetbot/mac" rel="nofollow">Tweetbot for Mac</a>',
|
||||
favorited: false,
|
||||
truncated: false
|
||||
}
|
||||
preferences = TweetBase.preferences_cleanup(
|
||||
twitter: twitter_preferences,
|
||||
links: [
|
||||
{
|
||||
url: 'https://twitter.com/statuses/123',
|
||||
target: '_blank',
|
||||
name: 'on Twitter',
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
article4 = Ticket::Article.create!(
|
||||
ticket_id: ticket1.id,
|
||||
type_id: Ticket::Article::Type.find_by(name: 'twitter status').id,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
from: '@example',
|
||||
body: 'some tweet',
|
||||
internal: false,
|
||||
preferences: preferences,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
assert(article4.preferences[:twitter])
|
||||
assert_equal(1_234_567_890, article4.preferences[:twitter][:mention_ids][0])
|
||||
assert_equal(ActiveSupport::HashWithIndifferentAccess, article4.preferences[:twitter][:geo].class)
|
||||
assert_equal({ 'coordinates' => [1, 1] }, article4.preferences[:twitter][:geo])
|
||||
assert_equal(ActiveSupport::HashWithIndifferentAccess, article4.preferences[:twitter][:place].class)
|
||||
assert_equal({ 'country' => 'da', 'name' => 'do', 'woeid' => 1, 'id' => 1 }, article4.preferences[:twitter][:place])
|
||||
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in a new issue