Instantly share code, notes, and snippets.

Embed
What would you like to do?
Twitter DM with Shared media
# This is from forked Twitter gem: https://github.com/sqlninja/twitter
# twitter/lib/rest/
require 'twitter/arguments'
require 'twitter/direct_message'
require 'twitter/direct_message_event'
require 'twitter/rest/upload_utils'
require 'twitter/rest/utils'
require 'twitter/user'
require 'twitter/utils'
module Twitter
module REST
module DirectMessages
include Twitter::REST::UploadUtils
include Twitter::REST::Utils
include Twitter::Utils
# Returns all Direct Message events for the authenticated user (both sent and received) within the last 30 days. Sorted in reverse-chronological order.
# @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/list-events
# @note This method requires an access token with RWD (read, write & direct message) permissions. Consult The Application Permission Model for more information.
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [Array<Twitter::DirectMessageEvent>] Direct message events sent by and received by the authenticating user.
# @param options [Hash] A customizable set of options.
# @option options [Integer] :count Specifies the number of records to retrieve. Must be less than or equal to 50. Default is 20
# @option options [String] :cursor Specifies the cursor position of results to retrieve.
def direct_messages_events(options = {})
limit = options.fetch(:count, 20)
perform_get_with_cursor('/1.1/direct_messages/events/list.json', options.merge!(no_default_cursor: true, count: 50, limit: limit), :events, Twitter::DirectMessageEvent)
end
# Returns all Direct Messages for the authenticated user (both sent and received) within the last 30 days. Sorted in reverse-chronological order.
# @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/list-events
# @note This method requires an access token with RWD (read, write & direct message) permissions. Consult The Application Permission Model for more information.
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [Array<Twitter::DirectMessage>] Direct messages sent by and received by the authenticating user.
# @param options [Hash] A customizable set of options.
# @option options [Integer] :count Specifies the number of records to retrieve. Must be less than or equal to 50. Default is 20
# @option options [String] :cursor Specifies the cursor position of results to retrieve.
def direct_messages_list(options = {})
direct_messages_events(options).collect(&:direct_message)
end
# Returns Direct Messages received by the authenticated user within the last 30 days. Sorted in reverse-chronological order.
# @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/list-events
# @note This method requires an access token with RWD (read, write & direct message) permissions. Consult The Application Permission Model for more information.
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [Array<Twitter::DirectMessage>] Direct messages received by the authenticating user.
# @param options [Hash] A customizable set of options.
# @option options [Integer] :count Specifies the number of records (sent and received dms) to retrieve. Must be less than or equal to 50. Default is 50
# this count does not directly correspond to the output, as we pull sent and received messages from twitter and only present received to the user
# @option options [String] :cursor Specifies the cursor position of results to retrieve.
def direct_messages_received(options = {})
limit = options.fetch(:count, 20)
direct_messages_list(options).select { |dm| dm.recipient_id == user_id }.first(limit)
end
# Returns Direct Messages sent by the authenticated user within the last 30 days. Sorted in reverse-chronological order.
# @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/list-events
# @note This method requires an access token with RWD (read, write & direct message) permissions. Consult The Application Permission Model for more information.
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [Array<Twitter::DirectMessage>] Direct messages sent by the authenticating user.
# @param options [Hash] A customizable set of options.
# @option options [Integer] :count Specifies the number of records (sent and received dms) to retrieve. Must be less than or equal to 50. Default is 50
# this count does not directly correspond to the output, as we pull sent and received messages from twitter and only present received to the user
# @option options [String] :cursor Specifies the cursor position of results to retrieve.
def direct_messages_sent(options = {})
limit = options.fetch(:count, 20)
direct_messages_list(options).select { |dm| dm.sender_id == user_id }.first(limit)
end
# Returns a direct message
#
# @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-event
# @note This method requires an access token with RWD (read, write & direct message) permissions. Consult The Application Permission Model for more information.
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [Twitter::DirectMessage] The requested message.
# @param id [Integer] A direct message ID.
# @param options [Hash] A customizable set of options.
def direct_message(id, options = {})
direct_message_event(id, options).direct_message
end
# Returns a direct message event
#
# @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-event
# @note This method requires an access token with RWD (read, write & direct message) permissions. Consult The Application Permission Model for more information.
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [Twitter::DirectMessageEvent] The requested message.
# @param id [Integer] A direct message ID.
# @param options [Hash] A customizable set of options.
def direct_message_event(id, options = {})
options = options.dup
options[:id] = id
perform_get_with_object('/1.1/direct_messages/events/show.json', options, Twitter::DirectMessageEvent)
end
# Returns direct messages specified in arguments, or, if no arguments are given, returns direct messages received by authenticating user
# @note This method requires an access token with RWD (read, write & direct message) permissions. Consult The Application Permission Model for more information.
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [Array<Twitter::DirectMessage>] The requested messages.
# @overload direct_messages(options = {})
# Returns the 20 most recent direct messages sent to the authenticating user
# @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/list-events
# @param options [Hash] A customizable set of options.
# @option options [Integer] :count Specifies the number of records (sent and received dms) to retrieve. Must be less than or equal to 50. Default is 50
# this count does not directly correspond to the output, as we pull sent and received messages from twitter and only present received to the user
# @option options [String] :cursor Specifies the cursor position of results to retrieve.
# @overload direct_messages(*ids)
# Returns direct messages
#
# @see https://dev.twitter.com/rest/reference/get/direct_messages/show
# @param ids [Enumerable<Integer>] A collection of direct message IDs.
# @overload direct_messages(*ids, options)
# Returns direct messages
#
# @see https://dev.twitter.com/rest/reference/get/direct_messages/show
# @param ids [Enumerable<Integer>] A collection of direct message IDs.
# @param options [Hash] A customizable set of options.
def direct_messages(*args)
arguments = Twitter::Arguments.new(args)
if arguments.empty?
direct_messages_received(arguments.options)
else
pmap(arguments) do |id|
direct_message(id, arguments.options)
end
end
end
# Destroys direct messages
#
# @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/delete-message-event
# @note This method requires an access token with RWD (read, write & direct message) permissions. Consult The Application Permission Model for more information.
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [nil] Response body from Twitter is nil if successful
# @overload destroy_direct_message(*ids)
# @param ids [Enumerable<Integer>] A collection of direct message IDs.
def destroy_direct_message(*ids)
perform_requests(:delete, '/1.1/direct_messages/events/destroy.json', ids)
end
# Sends a new direct message to the specified user from the authenticating user
#
# @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-event
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [Twitter::DirectMessage] The sent message.
# @param user [Integer, String, Twitter::User] A Twitter user ID
# @param text [String] The text of your direct message, up to 10,000 characters.
# @param options [Hash] A customizable set of options.
def create_direct_message(user_id, text, options = {})
event = perform_request_with_object(:json_post, '/1.1/direct_messages/events/new.json', format_json_options(user_id, text, options), Twitter::DirectMessageEvent)
event.direct_message
end
alias d create_direct_message
alias m create_direct_message
alias dm create_direct_message
# Create a new direct message event to the specified user from the authenticating user
#
# @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-event
# @note This method requires an access token with RWD (read, write & direct message) permissions. Consult The Application Permission Model for more information.
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [Twitter::DirectMessageEvent] The created direct message event.
# @param user [Integer, String, Twitter::User] A Twitter user ID, screen name, URI, or object.
# @param text [String] The text of your direct message, up to 10,000 characters.
# @param options [Hash] A customizable set of options.
def create_direct_message_event(user, text)
options = {}
options[:event] = {type: 'message_create', message_create: {target: {recipient_id: extract_id(user)}, message_data: {text: text}}}
response = Twitter::REST::Request.new(self, :json_post, '/1.1/direct_messages/events/new.json', options).perform
if response.present?
Twitter::DirectMessageEvent.new(response[:event])
else
raise('Unable to send message, Twitter response object was empty')
end
end
# Create a new direct message event to the specified user from the authenticating user with media
#
# @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-event
# @see https://developer.twitter.com/en/docs/direct-messages/message-attachments/guides/attaching-media
# @note This method requires an access token with RWD (read, write & direct message) permissions. Consult The Application Permission Model for more information.
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [Twitter::DirectMessageEvent] The created direct message event.
# @param user [Integer, String, Twitter::User] A Twitter user ID, screen name, URI, or object.
# @param text [String] The text of your direct message, up to 10,000 characters.
# @param media [File] A media file (PNG, JPEG, GIF or MP4).
# @param options [Hash] A customizable set of options.
def create_direct_message_event_with_media(user, text, media, options = {})
media_id = upload(media, media_category_prefix: 'dm')[:media_id]
options = options.dup
options[:event] = {type: 'message_create', message_create: {target: {recipient_id: extract_id(user)}, message_data: {text: text, attachment: {type: 'media', media: {id: media_id}}}}}
response = Twitter::REST::Request.new(self, :json_post, '/1.1/direct_messages/events/new.json', options).perform
if response.present?
Twitter::DirectMessageEvent.new(response[:event])
else
raise('Unable to send message, Twitter response object was empty')
end
end
# Create a new direct message event to the specified user from the authenticating user with media_id from a media object already uploaded
#
# @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-event
# @see https://developer.twitter.com/en/docs/direct-messages/message-attachments/guides/attaching-media
# @note This method requires an access token with RWD (read, write & direct message) permissions. Consult The Application Permission Model for more information.
# @rate_limited Yes
# @authentication Requires user context
# @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
# @return [Twitter::DirectMessageEvent] The created direct message event.
# @param user [Integer, String, Twitter::User] A Twitter user ID, screen name, URI, or object.
# @param text [String] The text of your direct message, up to 10,000 characters.
# @param media_id [String] A media id from the object upload to twitter.
# @param options [Hash] A customizable set of options.
def create_direct_message_event_with_media_id(user, text, media_id, options = {})
options = options.dup
options[:event] = {type: 'message_create', message_create: {target: {recipient_id: extract_id(user)}, message_data: {text: text, attachment: {type: 'media', media: {id: media_id}}}}}
response = Twitter::REST::Request.new(self, :json_post, '/1.1/direct_messages/events/new.json', options).perform
if response.present?
Twitter::DirectMessageEvent.new(response[:event])
else
raise('Unable to send message, Twitter response object was empty')
end
end
private
def format_json_options(user_id, text, options)
{'event': {'type': 'message_create', 'message_create': {'target': {'recipient_id': user_id}, 'message_data': {'text': text}.merge(options)}}}
end
end
end
end
class MessageQueueJob < ApplicationJob
def perform(scheduled_time)
process_twitter_queue(scheduled_time)
end
private
def process_twitter_queue(scheduled_time)
p "Starting twitter message queue at #{Time.at(scheduled_time)}"
# Get all messages ready to send based on send_at
messages = Message.where(status: 'Pending')
.or(Message.where(status: 'In Progress'))
.includes(:message_recipients)
.joins( :message_recipients )
.where( :message_recipients => { status: 'pending' } )
.where( "message_recipients.send_at <= ?", Time.at(scheduled_time) )
# p "Message SQL CMD: #{messages.to_sql}"
p "#{ActionController::Base.helpers.pluralize(messages.count, 'message')} to be sent"
messages.each do |message|
# Get all pending recipients for the message
recipients = message.message_recipients.where(platform: 'twitter', status: 'pending').where('send_at <= ?', Time.at(scheduled_time))
# recipients = MessageRecipient.where(platform: 'twitter', message_id: message.id, status: 'pending').where('send_at <= ?', Time.at(scheduled_time))
message.update(status: 'In Progress') if recipients.count > 0
p "Message id: #{message.id} to be sent to #{ActionController::Base.helpers.pluralize(recipients.count, 'recipient')}"
# Get the media object if present
media = Medium.find(message.media_id) if message.media_id.present?
# p "Media: #{media}"
# if the media search above returned an object then we upload it to twitter
twitter_media_id = nil
if media.present?
# file = Paperclip.io_adapters.for(media.object).read
temp_file = Tempfile.new(media.object_file_name)
temp_file.binmode
media.object.copy_to_local_file(:medium, temp_file.path)
# NOTE: Does not appear that marking a media object as shared is working so for now the solution is to upload media for each recipient otherwise this could be done once per message
twitter_media_id = MediaHelper.upload_media_to_twitter(message.oauth_token, message.oauth_secret, temp_file)
# p "Twitter media id: #{twitter_media_id}"
end
# Attempt to send message and record status on recipient
recipients.each do |rec|
# p '****** Debug ******'
# p "Message: #{message.inspect}"
# p "Recipient: #{rec.inspect}"
result = MessagesHelper.send_twitter_dm(message.oauth_token, message.oauth_secret, message.body, rec.recipient, rec.name, twitter_media_id)
if result.present?
rec.platform_id = result['id']
rec.status = result['status']
rec.response = result['response']
rec.sent_at = Time.now if result['status'] == 'sent'
rec.save
end
end
p "Message status: #{message.current_status}"
end
p "Ending twitter message queue at #{Time.at(scheduled_time)}"
end
end
###This is from forked Twitter gem: https://github.com/sqlninja/twitter
# twitter/lib/rest/utils.rb
require 'addressable/uri'
require 'twitter/arguments'
require 'twitter/cursor'
require 'twitter/rest/request'
require 'twitter/user'
require 'twitter/utils'
require 'uri'
module Twitter
module REST
module Utils
include Twitter::Utils
DEFAULT_CURSOR = -1
# Uploads images and videos. Videos require multiple requests and uploads in chunks of 5 Megabytes.
# The only supported video format is mp4.
#
# @see https://dev.twitter.com/rest/public/uploading-media
def upload(media, media_category_prefix: 'tweet', shared: false)
return chunk_upload(media, 'video/mp4', "#{media_category_prefix}_video", shared) if File.extname(media) == '.mp4'
return chunk_upload(media, 'image/gif', "#{media_category_prefix}_gif", shared) if File.extname(media) == '.gif' && File.size(media) > 5_000_000
Twitter::REST::Request.new(self, :multipart_post, 'https://upload.twitter.com/1.1/media/upload.json', key: :media, file: media).perform
end
private
# rubocop:disable MethodLength
def chunk_upload(media, media_type, media_category, shared)
init = Twitter::REST::Request.new(self, :post, 'https://upload.twitter.com/1.1/media/upload.json',
command: 'INIT',
media_type: media_type,
shared: shared,
media_category: media_category,
total_bytes: media.size).perform
until media.eof?
chunk = media.read(5_000_000)
seg ||= -1
Twitter::REST::Request.new(self, :multipart_post, 'https://upload.twitter.com/1.1/media/upload.json',
command: 'APPEND',
media_id: init[:media_id],
segment_index: seg += 1,
key: :media,
file: StringIO.new(chunk)).perform
end
media.close
Twitter::REST::Request.new(self, :post, 'https://upload.twitter.com/1.1/media/upload.json',
command: 'FINALIZE', media_id: init[:media_id]).perform
end
# rubocop:enable MethodLength
# Take a URI string or Twitter::Identity object and return its ID
#
# @param object [Integer, String, URI, Twitter::Identity] An ID, URI, or object.
# @return [Integer]
def extract_id(object)
case object
when ::Integer
object
when ::String
object.split('/').last.to_i
when URI, Addressable::URI
object.path.split('/').last.to_i
when Twitter::Identity
object.id
end
end
# @param path [String]
# @param options [Hash]
def perform_get(path, options = {})
perform_request(:get, path, options)
end
# @param path [String]
# @param options [Hash]
def perform_post(path, options = {})
perform_request(:post, path, options)
end
# @param request_method [Symbol]
# @param path [String]
# @param options [Hash]
def perform_request(request_method, path, options = {})
Twitter::REST::Request.new(self, request_method, path, options).perform
end
# @param path [String]
# @param options [Hash]
# @param klass [Class]
def perform_get_with_object(path, options, klass)
perform_request_with_object(:get, path, options, klass)
end
# @param path [String]
# @param options [Hash]
# @param klass [Class]
def perform_post_with_object(path, options, klass)
perform_request_with_object(:post, path, options, klass)
end
# @param request_method [Symbol]
# @param path [String]
# @param options [Hash]
# @param klass [Class]
def perform_request_with_object(request_method, path, options, klass)
response = perform_request(request_method, path, options)
klass.new(response)
end
# @param path [String]
# @param options [Hash]
# @param klass [Class]
def perform_get_with_objects(path, options, klass)
perform_request_with_objects(:get, path, options, klass)
end
# @param path [String]
# @param options [Hash]
# @param klass [Class]
def perform_post_with_objects(path, options, klass)
perform_request_with_objects(:post, path, options, klass)
end
# @param request_method [Symbol]
# @param path [String]
# @param options [Hash]
# @param klass [Class]
def perform_request_with_objects(request_method, path, options, klass)
perform_request(request_method, path, options).collect do |element|
klass.new(element)
end
end
# @param path [String]
# @param options [Hash]
# @param collection_name [Symbol]
# @param klass [Class]
def perform_get_with_cursor(path, options, collection_name, klass = nil)
limit = options.delete(:limit)
if options[:no_default_cursor]
options.delete(:no_default_cursor)
else
merge_default_cursor!(options)
end
request = Twitter::REST::Request.new(self, :get, path, options)
Twitter::Cursor.new(collection_name.to_sym, klass, request, limit)
end
# @param request_method [Symbol]
# @param path [String]
# @param args [Array]
# @return [Array<Twitter::User>]
def parallel_users_from_response(request_method, path, args)
arguments = Twitter::Arguments.new(args)
pmap(arguments) do |user|
perform_request_with_object(request_method, path, merge_user(arguments.options, user), Twitter::User)
end
end
# @param request_method [Symbol]
# @param path [String]
# @param args [Array]
# @return [Array<Twitter::User>]
def users_from_response(request_method, path, args)
arguments = Twitter::Arguments.new(args)
merge_user!(arguments.options, arguments.pop || user_id) unless arguments.options[:user_id] || arguments.options[:screen_name]
perform_request_with_objects(request_method, path, arguments.options, Twitter::User)
end
# @param klass [Class]
# @param request_method [Symbol]
# @param path [String]
# @param args [Array]
# @return [Array]
def objects_from_response_with_user(klass, request_method, path, args)
arguments = Twitter::Arguments.new(args)
merge_user!(arguments.options, arguments.pop)
perform_request_with_objects(request_method, path, arguments.options, klass)
end
# @param klass [Class]
# @param request_method [Symbol]
# @param path [String]
# @param args [Array]
# @return [Array]
def parallel_objects_from_response(klass, request_method, path, args)
arguments = Twitter::Arguments.new(args)
pmap(arguments) do |object|
perform_request_with_object(request_method, path, arguments.options.merge(id: extract_id(object)), klass)
end
end
# @param request_method [Symbol]
# @param path [String]
# @param ids [Array]
# @return nil
def perform_requests(request_method, path, ids)
ids.each do |id|
perform_request(request_method, path, id: id)
end
nil
end
# @param collection_name [Symbol]
# @param klass [Class]
# @param path [String]
# @param args [Array]
# @return [Twitter::Cursor]
def cursor_from_response_with_user(collection_name, klass, path, args)
arguments = Twitter::Arguments.new(args)
merge_user!(arguments.options, arguments.pop || user_id) unless arguments.options[:user_id] || arguments.options[:screen_name]
perform_get_with_cursor(path, arguments.options, collection_name, klass)
end
def user_id
@user_id ||= verify_credentials(skip_status: true).id
end
def user_id?
instance_variable_defined?(:@user_id)
end
def merge_default_cursor!(options)
options[:cursor] = DEFAULT_CURSOR unless options[:cursor]
end
# Take a user and merge it into the hash with the correct key
#
# @param hash [Hash]
# @param user [Integer, String, Twitter::User] A Twitter user ID, screen name, URI, or object.
# @return [Hash]
def merge_user(hash, user, prefix = nil)
merge_user!(hash.dup, user, prefix)
end
# Take a user and merge it into the hash with the correct key
#
# @param hash [Hash]
# @param user [Integer, String, URI, Twitter::User] A Twitter user ID, screen name, URI, or object.
# @return [Hash]
def merge_user!(hash, user, prefix = nil)
case user
when Integer
set_compound_key('user_id', user, hash, prefix)
when String
set_compound_key('screen_name', user, hash, prefix)
when URI, Addressable::URI
set_compound_key('screen_name', user.path.split('/').last, hash, prefix)
when Twitter::User
set_compound_key('user_id', user.id, hash, prefix)
end
end
def set_compound_key(key, value, hash, prefix = nil)
compound_key = [prefix, key].compact.join('_').to_sym
hash[compound_key] = value
hash
end
# Take a multiple users and merge them into the hash with the correct keys
#
# @param hash [Hash]
# @param users [Enumerable<Integer, String, Twitter::User>] A collection of Twitter user IDs, screen_names, or objects.
# @return [Hash]
def merge_users(hash, users)
copy = hash.dup
merge_users!(copy, users)
copy
end
# Take a multiple users and merge them into the hash with the correct keys
#
# @param hash [Hash]
# @param users [Enumerable<Integer, String, URI, Twitter::User>] A collection of Twitter user IDs, screen_names, URIs, or objects.
# @return [Hash]
def merge_users!(hash, users)
user_ids, screen_names = collect_users(users)
hash[:user_id] = user_ids.join(',') unless user_ids.empty?
hash[:screen_name] = screen_names.join(',') unless screen_names.empty?
end
def collect_users(users) # rubocop:disable MethodLength
user_ids = []
screen_names = []
users.each do |user|
case user
when Integer then user_ids << user
when Twitter::User then user_ids << user.id
when String then screen_names << user
when URI, Addressable::URI then screen_names << user.path.split('/').last
end
end
[user_ids, screen_names]
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment