Implemented issue #2023 - Twitter Account Activity API support.

This commit is contained in:
Martin Edenhofer 2018-12-03 15:10:36 +01:00
parent 6f4d584fae
commit 6e061ab894
47 changed files with 10025 additions and 2337 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +0,0 @@
# Monkey-patch HTTP::URI
class HTTP::URI
def port
443 if https?
end
end

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View 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

View file

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

View 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"
}
}
}

View 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"
}
]
}

View 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"
}
}
}

View 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"
}
]
}

View 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"
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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