From 7aff2a4b08adc63dc5c80a867ab0ff3198554b95 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Tue, 7 Jul 2015 13:57:45 +0200 Subject: [PATCH 1/8] Fixed syntax. --- app/controllers/channels_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/channels_controller.rb b/app/controllers/channels_controller.rb index 0876ba4f8..d3ac62ff0 100644 --- a/app/controllers/channels_controller.rb +++ b/app/controllers/channels_controller.rb @@ -33,7 +33,7 @@ Example: "adapter":"Twitter", "group_id:": 1, "options":{ - auth: { + "auth": { "consumer_key":"PJ4c3dYYRtSZZZdOKo8ow", "consumer_secret":"ggAdnJE2Al1Vv0cwwvX5bdvKOieFs0vjCIh5M8Dxk", "oauth_token":"293437546-xxRa9g74CercnU5AvY1uQwLLGIYrV1ezYtpX8oKW", From fa3dc11e98f8bc789723dd0ff252ba414262fa3e Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Thu, 9 Jul 2015 11:38:06 +0200 Subject: [PATCH 2/8] Fixed wrong attribute name. --- app/controllers/channels_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/channels_controller.rb b/app/controllers/channels_controller.rb index d3ac62ff0..bb0dc9fac 100644 --- a/app/controllers/channels_controller.rb +++ b/app/controllers/channels_controller.rb @@ -42,17 +42,17 @@ Example: "sync":{ "search":[ { - "item":"#otrs", + "term":"#otrs", "type": "mixed", # optional, possible 'mixed' (default), 'recent', 'popular' "group_id:": 1, "limit": 1, # optional }, { - "item":"#zombie23", + "term":"#zombie23", "group_id:": 2, }, { - "item":"#otterhub", + "term":"#otterhub", "group_id:": 3, } ], From f9a13f216063fe094eaa5e5e15b12f561dad6e89 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Thu, 9 Jul 2015 13:36:41 +0200 Subject: [PATCH 3/8] Fixed indentation. --- test/integration/twitter_test.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/integration/twitter_test.rb b/test/integration/twitter_test.rb index d60f70400..0a2f4518d 100644 --- a/test/integration/twitter_test.rb +++ b/test/integration/twitter_test.rb @@ -35,9 +35,9 @@ class TwitterTest < ActiveSupport::TestCase area: 'Twitter::Inbound', options: { auth: { - consumer_key: consumer_key, - consumer_secret: consumer_secret, - oauth_token: armin_theo_token, + consumer_key: consumer_key, + consumer_secret: consumer_secret, + oauth_token: armin_theo_token, oauth_token_secret: armin_theo_token_secret, }, sync: { @@ -189,9 +189,9 @@ class TwitterTest < ActiveSupport::TestCase client.destroy_direct_message(dm.id) } client = Twitter::REST::Client.new( - consumer_key: consumer_key, - consumer_secret: consumer_secret, - access_token: me_bauer_token, + consumer_key: consumer_key, + consumer_secret: consumer_secret, + access_token: me_bauer_token, access_token_secret: me_bauer_token_secret ) dms = client.direct_messages( count: 200 ) From 4df77067cc7d9004aea8f54ca005a39f7e5a06ff Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Thu, 9 Jul 2015 13:37:16 +0200 Subject: [PATCH 4/8] Fixed bug: Missing 'in_reply_to' attribute in case of status replies. --- lib/tweet.rb | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/tweet.rb b/lib/tweet.rb index ffead1269..8c75b3a6b 100644 --- a/lib/tweet.rb +++ b/lib/tweet.rb @@ -141,15 +141,21 @@ class Tweet article_type = 'twitter direct-message' end + in_reply_to = nil + if tweet.respond_to?('in_reply_to_status_id') && tweet.in_reply_to_status_id && tweet.in_reply_to_status_id.to_s != '' + in_reply_to = tweet.in_reply_to_status_id + end + Ticket::Article.create( - from: user.login, - to: to, - body: tweet.text, - message_id: tweet.id, - ticket_id: ticket.id, - type: Ticket::Article::Type.find_by( name: article_type ), - sender: Ticket::Article::Sender.find_by( name: 'Customer' ), - internal: false, + from: user.login, + to: to, + body: tweet.text, + message_id: tweet.id, + ticket_id: ticket.id, + in_reply_to: in_reply_to, + type: Ticket::Article::Type.find_by( name: article_type ), + sender: Ticket::Article::Sender.find_by( name: 'Customer' ), + internal: false, ) end From 01df55e6de24b674ccb6f2df52a834ce89423df9 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Thu, 9 Jul 2015 14:58:19 +0200 Subject: [PATCH 5/8] Fixed bug: Closed direct-message tickets get reopened instead of not-closed reused. --- lib/tweet.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tweet.rb b/lib/tweet.rb index 8c75b3a6b..9be9240be 100644 --- a/lib/tweet.rb +++ b/lib/tweet.rb @@ -99,7 +99,7 @@ class Tweet if tweet.class.to_s == 'Twitter::DirectMessage' ticket = Ticket.find_by( customer_id: user.id, - state: Ticket::State.where( + state: Ticket::State.where.not( state_type_id: Ticket::StateType.where( name: 'closed', ) From a03627debf762f2d4bf1b23af57c1009dfb268c2 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Thu, 9 Jul 2015 16:00:22 +0200 Subject: [PATCH 6/8] Fixed bug: Article type is not interpolated. --- lib/tweet.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tweet.rb b/lib/tweet.rb index 9be9240be..185c06b99 100644 --- a/lib/tweet.rb +++ b/lib/tweet.rb @@ -221,7 +221,7 @@ class Tweet } ) else - fail "Can't handle unknown twitter article type 'article[:type]'." + fail "Can't handle unknown twitter article type '#{article[:type]}'." end Rails.logger.debug tweet.inspect From 51bf6eb0f13d5ae1c08d6aad27baad92d2e40398 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Thu, 9 Jul 2015 16:19:33 +0200 Subject: [PATCH 7/8] Removed unneeded line. --- app/models/channel/twitter.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/channel/twitter.rb b/app/models/channel/twitter.rb index e57f9453c..cf4d8fb10 100644 --- a/app/models/channel/twitter.rb +++ b/app/models/channel/twitter.rb @@ -23,7 +23,6 @@ class Channel::Twitter @channel = Channel.find_by( area: 'Twitter::Inbound', active: true ) @tweet = Tweet.new( @channel[:options][:auth] ) - @sync = @channel[:options][:sync] tweet = @tweet.from_article(article) disconnect From be8021388859c08cf4a7faf79770da9608cb049b Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Thu, 9 Jul 2015 16:56:01 +0200 Subject: [PATCH 8/8] Initial version of facebook channel connector. --- app/models/channel/facebook.rb | 67 +++-- .../ticket/article/communicate_facebook.rb | 23 +- lib/facebook.rb | 239 ++++++++++++++++++ 3 files changed, 296 insertions(+), 33 deletions(-) create mode 100644 lib/facebook.rb diff --git a/app/models/channel/facebook.rb b/app/models/channel/facebook.rb index 2c9b4e517..4cc289611 100644 --- a/app/models/channel/facebook.rb +++ b/app/models/channel/facebook.rb @@ -1,35 +1,56 @@ -# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ +# Copyright (C) 2012-2015 Zammad Foundation, http://zammad-foundation.org/ -#require 'rubygems' -#require 'twitter' +require 'facebook' class Channel::Facebook - # def fetch(:oauth_token, :oauth_token_secret) - def fetch + def fetch (channel) + + @channel = channel + @facebook = Facebook.new( @channel[:options][:auth] ) + @sync = @channel[:options][:sync] + + Rails.logger.debug 'facebook fetch started' + + fetch_feed + + disconnect + + Rails.logger.debug 'facebook fetch completed' + end + + def send(article, _notification = false) + + @channel = Channel.find_by( area: 'Facebook::Inbound', active: true ) + @facebook = Facebook.new( @channel[:options][:auth] ) + + tweet = @facebook.from_article(article) + disconnect + + tweet end def disconnect - + @facebook.disconnect end - def send + private - Rails.logger.debug('face!!!!!!!!!!!!!!') - graph_api = Koala::Facebook::API.new( - 'AAACqTciZAPsQBAHO9DbM333y2DcL5kccHyIObZB7WhaZBVUXUIeBNChkshvShCgiN6uwZC3r3l4cDvAZAPTArNIkemEraojzN1veNPZBADQAZDZD' - ) - graph_api.put_object( - 'id', - 'comments', - { - message: body - } - ) - # client.direct_message_create( - # 'medenhofer', - # self.body, - # options = {} - # ) + def fetch_feed + + return if !@sync[:group_id] + + counter = 0 + feed = @facebook.client.get_connections('me', 'feed') + feed.each { |f| + + break if @sync[:limit] && @sync[:limit] <= counter + + post = @facebook.client.get_object( f['id'] ) + + @facebook.to_group( post, @sync[:group_id] ) + + counter += 1 + } end end diff --git a/app/models/observer/ticket/article/communicate_facebook.rb b/app/models/observer/ticket/article/communicate_facebook.rb index ea6e4bf84..54b4578ee 100644 --- a/app/models/observer/ticket/article/communicate_facebook.rb +++ b/app/models/observer/ticket/article/communicate_facebook.rb @@ -1,5 +1,7 @@ # Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ +require 'channel/facebook' + class Observer::Ticket::Article::CommunicateFacebook < ActiveRecord::Observer observe 'ticket::_article' @@ -13,17 +15,18 @@ class Observer::Ticket::Article::CommunicateFacebook < ActiveRecord::Observer return 1 if sender.nil? return 1 if sender['name'] == 'Customer' - # only apply on emails + # only apply for facebook type = Ticket::Article::Type.lookup( id: record.type_id ) - return if type['name'] != 'facebook' + return if type['name'] !~ /\Afacebook/ - a = Channel::Facebook.new - a.send( - { - from: 'me@znuny.com', - to: 'medenhofer', - body: record.body - } - ) + facebook = Channel::Facebook.new + post = facebook.send({ + type: type['name'], + to: record.to, + body: record.body, + in_reply_to: record.in_reply_to + }) + record.message_id = post['id'] + record.save end end diff --git a/lib/facebook.rb b/lib/facebook.rb new file mode 100644 index 000000000..a2b8d880e --- /dev/null +++ b/lib/facebook.rb @@ -0,0 +1,239 @@ +# Copyright (C) 2012-2015 Zammad Foundation, http://zammad-foundation.org/ + +require 'koala' + +class Facebook + + attr_accessor :client, :account + + def initialize(options) + + connect( options[:auth][:access_token] ) + + page_access_token = access_token_for_page( options[:sync] ) + + if page_access_token + connect( page_access_token ) + end + + @account = client.get_object('me') + end + + def connect(access_token) + @client = Koala::Facebook::API.new( access_token ) + end + + def disconnect + + return if !@client + + @client = nil + end + + def pages + pages = [] + @client.get_connections('me', 'accounts').each { |page| + pages.push({ + id: page['id'], + name: page['name'], + access_token: page['access_token'], + }) + } + pages + end + + def user(post) + + return if !post['from'] + return if !post['from']['id'] + return if !post['from']['name'] + + return if !post['from']['id'] == @account['id'] + + @client.get_object( post['from']['id'] ) + end + + def to_user(post) + + Rails.logger.debug 'Create user from post...' + Rails.logger.debug post.inspect + + # do post_user lookup + post_user = user(post) + + return if !post_user + + auth = Authorization.find_by( uid: post_user['id'], provider: 'facebook' ) + + # create or update user + user_data = { + login: post_user['id'], # TODO + firstname: post_user['first_name'] || post_user['name'], + lastname: post_user['last_name'] || '', + email: '', + password: '', + # TODO: image_source: '', + # TODO: note: '', + active: true, + roles: Role.where( name: 'Customer' ), + } + if auth + user_data[:id] = auth.user_id + end + user = User.create_or_update( user_data ) + + # create or update authorization + auth_data = { + uid: post_user['id'], + username: post_user['id'], # TODO + user_id: user.id, + provider: 'facebook' + } + if auth + auth.update_attributes( auth_data ) + else + Authorization.new( auth_data ) + end + + UserInfo.current_user_id = user.id + + user + end + + def to_ticket(post, group_id) + + Rails.logger.debug 'Create ticket from post...' + Rails.logger.debug post.inspect + Rails.logger.debug group_id.inspect + + user = to_user(post) + return if !user + + Ticket.create( + customer_id: user.id, + title: "#{post['message'][0, 37]}...", + group_id: group_id, + state: Ticket::State.find_by( name: 'new' ), + priority: Ticket::Priority.find_by( name: '2 normal' ), + ) + end + + def to_article(post, ticket) + + Rails.logger.debug 'Create article from post...' + Rails.logger.debug post.inspect + Rails.logger.debug ticket.inspect + + # set ticket state to open if not new + if ticket.state.name != 'new' + ticket.state = Ticket::State.find_by( name: 'open' ) + ticket.save + end + + user = to_user(comment) + return if !user + + feed_post = { + from: user.name, + body: post['message'], + message_id: post['id'], + type: Ticket::Article::Type.find_by( name: 'facebook feed post' ), + } + articles = [] + articles.push( feed_post ) + + post['comments']['data'].each { |comment| + + user = to_user(comment) + + next if !user + + post_comment = { + from: user.name, + body: comment['message'], + message_id: comment['id'], + type: Ticket::Article::Type.find_by( name: 'facebook feed comment' ), + } + articles.push( post_comment ) + + # TODO: sub-comments + # comment_data = @client.get_object( comment['id'] ) + } + + articles.invert.each { |article| + + break if Ticket::Article.find_by( message_id: article[:message_id] ) + + article = { + to: @account['name'], + ticket_id: ticket.id, + internal: false, + }.merge( article ) + + Ticket::Article.create( article ) + } + end + + def to_group(post, group_id) + + Rails.logger.debug 'import post' + + ticket = nil + # use transaction + ActiveRecord::Base.transaction do + + UserInfo.current_user_id = 1 + + existing_article = Ticket::Article.find_by( message_id: post['id'] ) + if existing_article + ticket = existing_article.ticket + else + ticket = to_ticket(post, group_id) + return if !ticket + end + + to_article(post, ticket) + + # execute ticket events + Observer::Ticket::Notification.transaction + end + + ticket + end + + def from_article(article) + + post = nil + # TODO: article[:type] == 'facebook feed post' + if article[:type] == 'facebook feed comment' + + Rails.logger.debug 'Create feed comment from article...' + + post = @client.put_wall_post(article[:body], {}, article[:in_reply_to]) + else + fail "Can't handle unknown facebook article type '#{article[:type]}'." + end + + Rails.logger.debug post.inspect + @client.get_object( post['id'] ) + end + + private + + def access_token_for_page(lookup) + + access_token = nil + pages.each { |page| + + next if !lookup[:page_id] && !lookup[:page] + next if lookup[:page_id] && lookup[:page_id].to_s != page[:id] + next if lookup[:page] && lookup[:page] != page[:name] + + access_token = page[:access_token] + + break + } + + access_token + end +end