require 'logger'
module Salesforce
# ==This is the abstract to the 'XXXFeed' table in Salesforce Chatter
#
# Note: FeedPost cannot be directly queried. It must go through its parent. For example:
# SELECT Id, Type, FeedPost.Body FROM AccountFeed WHERE ParentId = AccountId ORDER BY CreatedDate DESC
#
# SalesObject's 3 digits code for the KeyPrefix:
# ID Prefix Entity Type
# 001 Account
# 006 Opportunity
# 003 Contact
# 00Q Lead
# 701 Campaign
# 00V Campaign Member
# 00D Organization
# 005 User
# 00E Profile
# 00N Custom Field Definition
#
# 0D5 FeedItem (post by everyone)
# 0F7 FeedPost (post made by this user)
# See: http://eng.genius.com/blog/2009/05/17/salesforcecom-api-gotchas-2/
class ChatterFeed
def initialize
#### define @logger
# http://ruby-doc.org/core/classes/@logger.html
# TODO use a log file, rather than Standout
@logger = Logger.new(STDOUT)
#@logger.level = @logger::WARN
end
#fields do
# timestamps
#end
BASE_FRAG = "Id, CreatedDate, Type, CreatedBy.Name, CreatedBy.Id"
FEEDPOST_WITH_CONTENT_DATA_FRAG = <<-HERE
FeedPost.Id, FeedPost.FeedItemId, FeedPost.ParentId, FeedPost.Type,
FeedPost.CreatedById, FeedPost.CreatedDate, FeedPost.SystemModStamp,
FeedPost.Title, FeedPost.Body, FeedPost.LinkUrl,
FeedPost.ContentData, FeedPost.ContentFileName, FeedPost.ContentDescription,
FeedPost.ContentType, FeedPost.ContentSize
HERE
FEEDPOST_WITHOUT_CONTENT_DATA_FRAG = <<-HERE
FeedPost.Id, FeedPost.FeedItemId, FeedPost.ParentId, FeedPost.Type,
FeedPost.CreatedById, FeedPost.CreatedDate, FeedPost.SystemModStamp,
FeedPost.Title, FeedPost.Body, FeedPost.LinkUrl, FeedPost.ContentSize
HERE
CREATOR_FRAG = "CreatedBy.FirstName, CreateBy.LastName, CreatedBy.Id"
FEED_TRACKED_CHANGE_FRAG = <<-HERE
Select Id, FieldName, OldValue, NewValue From FeedTrackedChanges
ORDER BY Id Desc
HERE
FEED_COMMENT_FRAG = <<-HERE
Select Id, CommentBody, CreatedDate, CreatedBy.Id, CreatedBy.FirstName,
CreatedBy.LastName FROM FeedComments ORDER BY CreatedDate
HERE
# find all chatter feeds based on object_type, query_string, and given binding
def search_chatter_feeds(object_type, query_string, binding, limit=100)
return get_all_chatter_feeds_with_attachments(nil, object_type, binding, 'no-attachment-for-search', limit, false, query_string)
end
# 1. It does not return the attached file.
# 2. See get_all_feed_posts_with_attachments(object_id, object_type, binding, directory_name, limit=100, get_attachment=true, query_string=nil)
# 7. returns Ruby Array of matching records
def get_all_chatter_feeds_without_attachments(object_id, object_type, binding, directory_name, limit=100)
return get_all_chatter_feeds_with_attachments(object_id, object_type, binding, directory_name, limit, false, nil)
end
# 1. It returns the attached file.
# 2. 'object_id' corresponds to the 'parent_id' column in the 'XXXFeed' table
# 3. 'object_type' can be one of the followings
# Account, Asset, Campaign, Case, Lead, Contact, Contract, Opportunity,
# Product2, Solution, User
# **Note**: UserProfileFeed and NewsFeed are special cases.
# UserProfileFeed -> select ... from UserProfileFeed with Userid='userid'
# NewsFeed -> select ... from NewsFeed where parentid = 'userid'
# 4. 'binding' can be passed in directly or using the inherited binding from the
# 'Salesforce::SfBase' base class.
# 5. 'directory_name is the directory in 'public/tmp' for saving the attachment
# 6. 'limit is the number of records to return
# 7. 'get_attachment' is a boolean flag, true gets the attachment,
# false does not get the attachment
# 8. query_string is used to search against the Salesforce. Note, if "object_id' is specified, query_string is ignored.
# 8. returns Ruby Array of matching records
# 9. TODO add conditions, e.g.
# search -> where FeedPost.CreatedDate > 2007-08-06T03:23:00.000Z and FeedPost.Body like '%post%'
# pagination ->
def get_all_chatter_feeds_with_attachments(object_id, object_type, binding, directory_name, limit=100, get_attachment=true, query_string=nil)
begin
#e.g. select Id, type, FeedPost.body from UserProfileFeed WITH UserId = '005A0000000S8aIIAS'
if !object_id.nil?
qstring = <<-HERE
SELECT #{BASE_FRAG}, #{FEEDPOST_WITHOUT_CONTENT_DATA_FRAG},
(#{FEED_TRACKED_CHANGE_FRAG}), (#{FEED_COMMENT_FRAG} LIMIT #{limit})
FROM #{object_type}Feed where parentid = \'#{object_id}\'
ORDER BY CreatedDate DESC limit #{limit}
HERE
elsif !query_string.nil?
# this is more like a search....
qstring = <<-HERE
SELECT #{BASE_FRAG}, #{FEEDPOST_WITHOUT_CONTENT_DATA_FRAG},
(#{FEED_TRACKED_CHANGE_FRAG}), (#{FEED_COMMENT_FRAG} LIMIT #{limit})
FROM #{object_type}Feed where FeedPost.Body like \'%#{query_string}%\'
ORDER BY CreatedDate DESC limit #{limit}
HERE
else
qstring = <<-HERE
SELECT #{BASE_FRAG}, #{FEEDPOST_WITHOUT_CONTENT_DATA_FRAG},
(#{FEED_TRACKED_CHANGE_FRAG}), (#{FEED_COMMENT_FRAG} LIMIT #{limit})
FROM #{object_type}Feed ORDER BY CreatedDate DESC limit #{limit}
HERE
end
@logger.info('qstring: ' + qstring)
# Note, if query FeedPost.ContentData, Salesforce only returns 1 entry at
# a time, you will get a 'queryLocator', which can be used by calling
# 'queryMore(queryLocator)'
feed_results = Array.new
results = binding.query(:queryString => qstring)
if results.queryResponse.result.records.is_a?(Hash)
# User has made only one feedpost
feed = Hash.new
feed = results.queryResponse.result.records
if (!feed.FeedPost.nil?) && (feed.FeedPost.ContentSize.to_i > 0)
feed = get_single_chatter_feed_with_attachment(feed.FeedPost.Id, feed[:type], binding, directory_name)
end
feed_results << feed
elsif results.queryResponse.result.records.is_a?(Array)
results.queryResponse.result.records.each do |an_feed_entry|
if (an_feed_entry.is_a?(Hash))
if (an_feed_entry[:Type] == "TrackedChange")
# This is a TrackedChange Feed, the FeedPost Body is nil
feed = Hash.new
feed = an_feed_entry
feed_results << feed
elsif (!an_feed_entry.FeedPost.nil?) && (an_feed_entry.FeedPost.ContentSize.to_i > 0) && (get_attachment)
# This signifies that:
# 1. results.queryResponse.result.records.FeedPost.Type is of "ContentPost"
# 2. results.queryResponse.result.records.FeedPost.type should be of "FeedPost"
an_entry_with_attachment = get_single_chatter_feed_with_attachment(an_feed_entry.FeedPost.Id, an_feed_entry[:type], binding, directory_name)
feed_results << an_entry_with_attachment
else
feed = Hash.new
feed = an_feed_entry
feed_results << feed
end
else
@logger.info "Result #{object_id} has a results.queryResponse that is either not a valid Hash, or with nil FeedPost."
end
end
end
#if there are more entries, query it again and show it all
while results.queryResponse.result.done.to_s.upcase != 'TRUE'
results = binding.queryMore(:queryString => results.queryResponse.result.queryLocator)
if results.queryResponse.result.records.is_a?(Hash)
# User has made only one feedpost
feed = Hash.new
feed = results.queryResponse.result.records
if (!feed.FeedPost.nil?) && (feed.FeedPost.ContentSize.to_i > 0)
feed = get_single_chatter_feed_with_attachment(feed.FeedPost.Id, feed[:type], binding, directory_name)
end
feed_results << feed
elsif results.queryResponse.result.records.is_a?(Array)
results.queryResponse.result.records.each do |an_feed_entry|
if (an_feed_entry.is_a?(Hash))
if (an_feed_entry[:Type] == "TrackedChange")
# This is a TrackedChange Feed, the FeedPost Body is nil
feed = Hash.new
feed = an_feed_entry
feed_results << feed
# check to see if it has attachments
elsif (!an_feed_entry.FeedPost.nil?) && (an_feed_entry.FeedPost.ContentSize.to_i > 0) && (get_attachment)
# This signifies that:
# 1. results.queryResponse.result.records.FeedPost.Type is of "ContentPost"
# 2. results.queryResponse.result.records.FeedPost.type should be of "FeedPost"
an_entry_with_attachment = get_single_chatter_feed_with_attachment(an_feed_entry.FeedPost.Id, an_feed_entry[:type], binding, directory_name)
feed_results << an_entry_with_attachment
else
feed = Hash.new
feed = an_feed_entry
feed_results << feed
end
else
@logger.info "Result #{object_id} has a results.queryResponse that is either an invalid Hash or a nil FeedPost."
end
end
end
end
return feed_results
rescue Exception => exception
@logger.info 'get_all_feed_posts_with_attachments: ' + exception.message
return feed_results
end
end
# 1. Returns a single feed with attachment.
# 2. 'feedpost_id is the id corresponding to this feedpost
# 3. 'feed_type' can be one of the followings
# Account, Asset, Campaign, Case, Lead, Contact, Contract, Opportunity,
# Product2, Solution, User
# **Note**: UserProfileFeed and NewsFeed are not included here, as they
# are special cases.
# 4. 'binding' can be passed in directly or using the inherited binding from the
# 'Salesforce::SfBase' base class.
# 5. 'directory_name is the directory in 'public/tmp' for saving the attachment
# 6. 'limit' is the number of feed comments to retrieve at a time
def get_single_chatter_feed_with_attachment(feedpost_id, feed_type, binding, directory_name, limit=100)
begin
expanded_qstring = <<-HERE
SELECT #{BASE_FRAG}, #{FEEDPOST_WITH_CONTENT_DATA_FRAG},
(#{FEED_TRACKED_CHANGE_FRAG}), (#{FEED_COMMENT_FRAG} LIMIT #{limit})
FROM #{feed_type} where FeedPost.Id= \'#{feedpost_id}\' ORDER BY CreatedDate DESC Limit 1
HERE
@logger.info('expanded_string: ' + expanded_qstring)
deep_result = binding.query(:queryString => expanded_qstring)
######### queryMore does not work on FeedPost with Content.##########
# locator = deep_result.queryResponse.result.queryLocator
# while !locator.nil?
# query_again = binding.queryMore(:queryLocator => locator)
# locator = query_again.queryMoreResponse.result.queryLocator
# end
#####################################################################
if !deep_result.Fault.nil?
raise ChatterFeedError.new(@logger, deep_result.Fault.faultstring.to_s)
end
if !deep_result.queryResponse.result.records.FeedComments.nil? && deep_result.queryResponse.result.records.FeedComments[:size].to_i > 1
# There are more than one feed comments. Therefore, search again without content-data.
# This is done, because having ContentData in the query string, it will only return only the 1st record of the feed comments.
qstring_without_content_data = <<-HERE
SELECT #{BASE_FRAG},
(#{FEED_TRACKED_CHANGE_FRAG}), (#{FEED_COMMENT_FRAG} LIMIT 20)
FROM #{feed_type} where FeedPost.Id= \'#{feedpost_id}\' ORDER BY CreatedDate DESC Limit 1
HERE
result_without_content_data = binding.query(:queryString => qstring_without_content_data)
list_of_feed_comments = result_without_content_data.queryResponse.result.records.FeedComments
deep_result.queryResponse.result.records.FeedComments[:records] = list_of_feed_comments[:records]
end
filename = deep_result.queryResponse.result.records.FeedPost.ContentFileName
filesize = deep_result.queryResponse.result.records.FeedPost.ContentSize
filedata = deep_result.queryResponse.result.records.FeedPost.ContentData
result_with_attachment = deep_result.queryResponse.result.records
local_file_name = nil
if filename.nil?
# remote filename is nil, do not write the local file
@logger.info("remote filename is nil, do not write the local file.")
else
# Downloaded files are put in the subdirectory matching each session-id in the /public/tmp directory.
# creates dir, if it does not exists
file_writer = Salesforce::FileWriter.new
local_file_name = file_writer.do_write_file(filename, filesize, filedata, directory_name)
result_with_attachment.FeedPost.store(:Attachment, local_file_name)
end
return result_with_attachment
end
rescue Exception => exception
@logger.info "Cannot create the file: " + exception.message
result_with_attachment.FeedPost[:attachment] = nil
return result_with_attachment
end
end
class ChatterFeedError < RuntimeError
def initialize(logger, message)
super(logger, message)
end
end
end