pry-send_tweet.rb 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ class Pry::SendTweet::ReadTweets < Pry::SendTweet::BaseCommand
2
+ match 'read-tweets'
3
+ description 'A command for reading tweets.'
4
+ banner <<-BANNER
5
+ read-tweets [OPTIONS]
6
+
7
+ #{description}
8
+ BANNER
9
+
10
+ def options(o)
11
+ o.on 't=', 'tweeter=',
12
+ 'A username whose timeline you want to read.'
13
+ o.on 'c=', 'count=',
14
+ "The number of tweets to read. The maximum is 200, and the default is " \
15
+ "#{Pry::SendTweet::DEFAULT_READ_SIZE}."
16
+ o.on 'l=', 'likes=',
17
+ 'Read tweets you or another user have liked.',
18
+ argument: :optional
19
+ o.on 'r=', 'replies=', 'A username whose replies you want to read'
20
+ o.on 'm', 'mentions', 'Read tweets that @mention you.'
21
+ o.on 'w', 'with-retweets', 'Include retweets.'
22
+ end
23
+
24
+ def process
25
+ super
26
+ case
27
+ when opts.present?('replies') then show_replies(user: opts['replies'])
28
+ when opts.present?('likes') then show_likes(user: opts['likes'])
29
+ when opts.present?('mentions') then show_mentions
30
+ else show_tweets_from_timeline(user: opts['tweeter'])
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def show_replies(user:)
37
+ render_tweets lambda {
38
+ username = find_usernames_in(user).first
39
+ twitter.user_timeline(username, timeline_options).select {|tweet|
40
+ tweet.reply? &&
41
+ tweet.in_reply_to_screen_name? &&
42
+ tweet.in_reply_to_screen_name.downcase != username.downcase
43
+ }
44
+ },
45
+ title: format("Replies sent by %{user}", user: bold("@#{user}"))
46
+ end
47
+
48
+ def show_likes(user:)
49
+ if user
50
+ user = find_usernames_in(user).first
51
+ render_tweets lambda { twitter.favorites(user, count: opts['count'] || Pry::SendTweet::DEFAULT_READ_SIZE)},
52
+ title: "Tweets liked by #{bright_white('@'+user)}"
53
+ else
54
+ render_tweets lambda { twitter.favorites(count: opts['count'] || Pry::SendTweet::DEFAULT_READ_SIZE) },
55
+ title: "Liked tweets"
56
+ end
57
+ end
58
+
59
+ def show_tweets_from_timeline(user:)
60
+ if user
61
+ render_tweets lambda { twitter.user_timeline(user, timeline_options) },
62
+ title: '@'+user
63
+ else
64
+ render_tweets lambda { twitter.home_timeline(timeline_options) },
65
+ title: "Twitter.com"
66
+ end
67
+ end
68
+
69
+ def show_mentions
70
+ render_tweets lambda { twitter.mentions(timeline_options) },
71
+ title: "Tweets that #{bright_white('@mention')} #{bright_blue('you')}"
72
+ end
73
+
74
+ def timeline_options
75
+ {
76
+ tweet_mode: 'extended',
77
+ include_rts: opts.present?('with-retweets'),
78
+ count: opts.present?('count') ? opts['count'] : Pry::SendTweet::DEFAULT_READ_SIZE
79
+ }
80
+ end
81
+
82
+ Pry.commands.add_command self
83
+ end
@@ -0,0 +1,141 @@
1
+ class Pry::SendTweet::SendTweet < Pry::SendTweet::BaseCommand
2
+ MAX_TWEET_SIZE = 280
3
+
4
+ match 'send-tweet'
5
+ description 'Send a tweet'
6
+ command_options argument_required: false
7
+ banner <<-BANNER
8
+ send-tweet [options]
9
+
10
+ Send a tweet.
11
+ BANNER
12
+
13
+ def options(o)
14
+ o.on 'f=', 'file=',
15
+ 'One or more paths to image(s) to attach to a tweet',
16
+ as: Array
17
+ o.on 'r=', 'reply-to=',
18
+ 'An absolute url to a tweet you want to reply to.'
19
+ o.on 's=', 'self-destruct=',
20
+ 'The number of seconds (represented as a number or a timestamp in the ' \
21
+ 'format of HH:MM:SS) to wait before automatically deleting the tweet ' \
22
+ 'asynchronously'
23
+ o.on 'd=', 'delay=',
24
+ 'The number of seconds (represented as a number or a timestamp in the ' \
25
+ 'format of HH:MM:SS) to wait before creating the tweet asynchronously'
26
+ o.on 'n', 'no-newline',
27
+ "Remove newlines (\\n) from a tweet before sending it"
28
+ end
29
+
30
+ def process(args)
31
+ super
32
+ tweet_contents = compose_tweet_with_editor
33
+ create_tweet = opts.present?(:file) ? lambda { send_tweet_with_media(tweet_contents) } :
34
+ lambda { send_textual_tweet(tweet_contents) }
35
+ if opts.present?('delay')
36
+ time_obj, sleep_seconds = parse_duration_str(opts['delay'])
37
+ Thread.new {
38
+ sleep sleep_seconds
39
+ create_tweet.call
40
+ }
41
+ publish_time = (time_obj ? time_obj : Time.now + sleep_seconds)
42
+ .getlocal
43
+ .strftime(time_format)
44
+ page bold("Tweet will be published at approximately #{publish_time}.")
45
+ else
46
+ tweet = create_tweet.call
47
+ page tweet.url
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def no_newline?
54
+ opts.present?('no-newline')
55
+ end
56
+
57
+ def send_tweet_with_media(tweet_contents)
58
+ medias = opts['file'].map{|p| File.new p, 'r'}
59
+ tweet = twitter.update_with_media(tweet_contents, medias, tweet_options)
60
+ tweet.tap {|t| self_destruct! t.id, opts['self-destruct'] }
61
+ ensure
62
+ medias.each(&:close)
63
+ end
64
+
65
+ def send_textual_tweet(tweet_contents)
66
+ tweet = twitter.update(tweet_contents, tweet_options)
67
+ tweet.tap {|t| self_destruct! t.id, opts['self-destruct'] }
68
+ end
69
+
70
+ def prepend_username_to_reply!(tweet_url, file)
71
+ username = tweet_username_from(tweet_url)
72
+ file.write "@#{username} "
73
+ end
74
+
75
+ def tweet_username_from(tweet_url)
76
+ File.basename File.dirname(File.dirname(tweet_url))
77
+ end
78
+
79
+ def replying_to_other_tweet?
80
+ opts.present?('reply-to')
81
+ end
82
+
83
+ def tweet_options
84
+ options = {}
85
+ options.merge!({
86
+ in_reply_to_status_id: Integer(File.basename(opts['reply-to']))
87
+ }) if replying_to_other_tweet?
88
+ options
89
+ end
90
+
91
+ def compose_tweet_with_editor
92
+ tweet = Tempfile.open('pry-send_tweet--compose-tweet') do |file|
93
+ if replying_to_other_tweet?
94
+ # During experimentation I noticed that without prefixing who we are
95
+ # replying to, the tweet we send will not be considered a reply even
96
+ # with reply_to_status_id being set.
97
+ prepend_username_to_reply!(opts['reply-to'], file)
98
+ end
99
+ file.rewind
100
+ Pry::Editor.new(_pry_).invoke_editor(file.path, 0)
101
+ lines = file.read.each_line
102
+ no_newline? ? lines.map(&:chomp).join(' ') : lines.to_a.join
103
+ end
104
+ validate_tweet!(tweet)
105
+ tweet
106
+ end
107
+
108
+ def validate_tweet!(tweet)
109
+ if tweet.strip.empty?
110
+ raise Pry::CommandError, "Can't post an empty tweet."
111
+ elsif tweet.size > MAX_TWEET_SIZE
112
+ raise Pry::CommandError, "The tweet: \n" +
113
+ word_wrap(tweet) +
114
+ "\nis too big to publish, try to use less characters."
115
+ end
116
+ end
117
+
118
+ def self_destruct!(tweet_id, duration)
119
+ return if !duration
120
+ _, sleep_seconds = parse_duration_str(duration)
121
+ page bold("Tweet due to self destruct in #{sleep_seconds} seconds")
122
+ Thread.new do
123
+ sleep sleep_seconds
124
+ twitter.destroy_status(tweet_id)
125
+ end
126
+ end
127
+
128
+ def parse_duration_str(str)
129
+ if str =~ /\A\d+\z/
130
+ sleep_seconds = Integer(str)
131
+ elsif str =~ /\A\d{2}:\d{2}\z/ || str =~ /\A\d{2}:\d{2}:\d{2}\z/
132
+ time_obj = Time.parse(str)
133
+ sleep_seconds = Integer(time_obj - Time.now)
134
+ else
135
+ raise Pry::CommandError, "The argument --delay='#{str}' is not " \
136
+ "something I understand."
137
+ end
138
+ [time_obj, sleep_seconds]
139
+ end
140
+ Pry.commands.add_command self
141
+ end
@@ -0,0 +1,109 @@
1
+
2
+ class Pry::SendTweet::TwitterAction < Pry::SendTweet::BaseCommand
3
+ require_relative 'twitter_action/profile_actions'
4
+ require_relative 'twitter_action/suggested_actions'
5
+ require_relative 'twitter_action/like_actions'
6
+ require_relative 'twitter_action/follow_actions'
7
+ require_relative 'twitter_action/mute_actions'
8
+ require_relative 'twitter_action/delete_tweet_actions'
9
+ include ProfileActions
10
+ include SuggestedActions
11
+ include LikeActions
12
+ include FollowActions
13
+ include MuteActions
14
+ include DeleteTweetActions
15
+
16
+ match 'twitter-action'
17
+ description 'Like, unlike, follow, unfollow and more'
18
+ banner <<-B
19
+ twitter-action OPTIONS
20
+
21
+ Like, unlike, follow, unfollow and more.
22
+ B
23
+
24
+ def options(o)
25
+ #
26
+ # Like / unlike tweets
27
+ #
28
+ o.on 'like-tweet=', 'Like one or more tweets', as: Array
29
+ o.on 'unlike-tweet=', 'Unlike one or more tweets', as: Array
30
+ #
31
+ # Follow / unfollow a user
32
+ #
33
+ o.on 'follow=', 'Follow one or more users', as: Array
34
+ o.on 'unfollow=', 'Unfollow one or more users', as: Array
35
+ #
36
+ # Delete tweets and reweets
37
+ #
38
+ o.on 'delete-retweet=', 'Delete one or more retweets', as: Array
39
+ o.on 'delete-tweet=', 'Delete one or more tweets', as: Array
40
+ #
41
+ # Retweet a tweet
42
+ #
43
+ o.on 'retweet=', 'Retweet one or more tweets', as: Array
44
+ #
45
+ # Profile management
46
+ #
47
+ o.on 'set-profile-bio', 'Set your profiles bio'
48
+ o.on 'set-profile-url=', 'Set the URL associated with your profile'
49
+ o.on 'set-profile-banner-image=', 'Set your profiles banner image'
50
+ o.on 'set-profile-name=', 'Set the name associated with your profile'
51
+ o.on 'set-profile-image=', 'Set your profile image'
52
+ #
53
+ # Suggestions
54
+ #
55
+ o.on 'suggested-topics', 'View topics suggested by Twitter'
56
+ o.on 'suggested-users=', 'A topic from which to view users Twitter ' \
57
+ 'suggests following'
58
+ o.on 'suggested-lang=', 'Restrict suggestion results to a specific language, ' \
59
+ 'given in ISO 639-1 format. Default is "en"'
60
+ #
61
+ # Muting
62
+ #
63
+ o.on 'mute-user=', 'One or more usersname to mute', as: Array
64
+ o.on 'unmute-user=', 'One or more usernames to unmute', as: Array
65
+ end
66
+
67
+ def process
68
+ super
69
+ case
70
+ when opts.present?('like-tweet') then like_tweet(opts['like-tweet'])
71
+ when opts.present?('unlike-tweet') then unlike_tweet(opts['unlike-tweet'])
72
+ when opts.present?('follow') then follow_user(opts['follow'])
73
+ when opts.present?('unfollow') then unfollow_user(opts['unfollow'])
74
+ when opts.present?('delete-tweet') then delete_tweet(opts['delete-tweet'])
75
+ when opts.present?('delete-retweet') then delete_retweet(opts['delete-retweet'])
76
+ when opts.present?('retweet') then retweet_tweet(opts['retweet'])
77
+ when opts.present?('set-profile-banner-image') then set_profile_banner_image(opts['set-profile-banner-image'])
78
+ when opts.present?('set-profile-name') then set_profile_name(opts['set-profile-name'])
79
+ when opts.present?('set-profile-image') then set_profile_image(opts['set-profile-image'])
80
+ when opts.present?('set-profile-url') then set_profile_url(opts['set-profile-url'])
81
+ when opts.present?('set-profile-bio') then set_profile_bio
82
+ when opts.present?('suggested-topics') then suggested_topics
83
+ when opts.present?('suggested-users') then suggested_users(opts['suggested-users'])
84
+ when opts.present?('mute-user') then mute_user(opts['mute-user'])
85
+ when opts.present?('unmute-user') then unmute_user(opts['unmute-user'])
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def retweet_tweet(tweets)
92
+ retweets = twitter.retweet(tweets)
93
+ error_msg = word_wrap("No retweets created. Are you sure you gave a valid reference to " \
94
+ "one or more tweets, and that you haven't already retweeted the supplied " \
95
+ "tweet(s) ..?")
96
+ retweets.size > 0 ? render_tweets(retweets, title: bright_green("Retweeted tweets"), timeout: false) :
97
+ page(twitter_error(error_msg))
98
+ rescue Twitter::Error => e
99
+ page twitter_error(e)
100
+ end
101
+
102
+ def display_followed_or_unfollowed(user, index)
103
+ sn = "@#{user.screen_name}"
104
+ "#{sn} #{bold(bright_blue('|'))} #{user.url}"
105
+ end
106
+
107
+ Pry.commands.add_command(self)
108
+ Pry.commands.alias_command "on-twitter", "twitter-action"
109
+ end
@@ -0,0 +1,16 @@
1
+ module Pry::SendTweet::TwitterAction::DeleteTweetActions
2
+ def delete_retweet(retweets)
3
+ tweets = twitter.unretweet(retweets)
4
+ tweets.size > 0 ? render_tweets(tweets, title: bold("Deleted retweets"), timeout: false) :
5
+ page(twitter_error("No retweets deleted. Are you sure you gave a valid reference to the retweet?"))
6
+ rescue Twitter::Error => e
7
+ page twitter_error(e)
8
+ end
9
+
10
+ def delete_tweet(tweets)
11
+ tweets = twitter.destroy_status(tweets)
12
+ render_tweets tweets, title: bold("Deleted tweets"), timeout: false
13
+ rescue Twitter::Error => e
14
+ page twitter_error(e)
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ module Pry::SendTweet::TwitterAction::FollowActions
2
+ def follow_user(users)
3
+ users = find_usernames_in(*users)
4
+ follows = twitter.follow(users)
5
+ follows.empty? ? page("Are you sure you are not already following #{join_usernames(users)} ?") :
6
+ page(numbered_list(bold("Followed:"), follows) {|user, index|
7
+ display_followed_or_unfollowed(user, index)
8
+ })
9
+ rescue Twitter::Error => e
10
+ page twitter_error(e)
11
+ end
12
+
13
+ def unfollow_user(users)
14
+ users = find_usernames_in(*users)
15
+ unfollows = twitter.unfollow(users)
16
+ unfollows.empty? ? page("Are you sure you are following #{join_usernames(users)} ?") :
17
+ page(numbered_list(bold("Unfollowed:"), unfollows) {|user, index|
18
+ display_followed_or_unfollowed(user, index)
19
+ })
20
+ rescue Twitter::Error => e
21
+ page twitter_error(e)
22
+ end
23
+
24
+ private
25
+ def join_usernames(users)
26
+ users.map do |username|
27
+ bold("@#{username}")
28
+ end.join(', ')
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ module Pry::SendTweet::TwitterAction::LikeActions
2
+ def like_tweet(tweets)
3
+ liked = twitter.favorite(tweets)
4
+ liked.size > 0 ? render_tweets(liked, title: bold("Liked tweets"), timeout: false) :
5
+ page(twitter_error("Tweet(s) not liked. Maybe you liked those tweet(s) already?"))
6
+ rescue Timeout::Error => e
7
+ page twitter_error(e)
8
+ end
9
+
10
+ def unlike_tweet(tweets)
11
+ unliked = twitter.unfavorite(tweets)
12
+ unliked.size > 0 ? render_tweets(unliked, title: bold("Unliked tweets"), timeout: false) :
13
+ page(twitter_error("Tweet(s) not unliked. Maybe you haven't liked those tweet(s)?"))
14
+ rescue Twitter::Error => e
15
+ page twitter_error(e)
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module Pry::SendTweet::TwitterAction::MuteActions
2
+ def mute_user(strings)
3
+ muted = twitter.mute(*find_usernames_in(*strings))
4
+ muted.empty? ? page("No users muted. Maybe you muted them already?") :
5
+ page(numbered_list("Muted users", muted) {|user, index|
6
+ display_followed_or_unfollowed(user, index)
7
+ })
8
+ rescue Twitter::Error => e
9
+ page twitter_error(e)
10
+ end
11
+
12
+ def unmute_user(strings)
13
+ unmuted = twitter.unmute(*find_usernames_in(*strings))
14
+ page numbered_list("Unmuted users", unmuted) {|user, index|
15
+ display_followed_or_unfollowed(user, index)
16
+ }
17
+ rescue Twitter::Error => e
18
+ page twitter_error(e)
19
+ end
20
+ end
@@ -0,0 +1,42 @@
1
+ module Pry::SendTweet::TwitterAction::ProfileActions
2
+ def set_profile_url(url)
3
+ if twitter.update_profile url: url
4
+ page "Profile URL updated"
5
+ else
6
+ raise Pry::CommandError, "Something went wrong"
7
+ end
8
+ end
9
+
10
+ def set_profile_image(path)
11
+ twitter.update_profile_image(path)
12
+ page "Profile image updated"
13
+ rescue Twitter::Error => e
14
+ page twitter_error(e)
15
+ end
16
+
17
+ def set_profile_banner_image(path)
18
+ base64 = Base64.strict_encode64 File.binread(path)
19
+ twitter.update_profile_banner(base64)
20
+ page "Profile banner updated"
21
+ rescue Twitter::Error => e
22
+ page twitter_error(e)
23
+ end
24
+
25
+ def set_profile_name(name)
26
+ twitter.update_profile name: name
27
+ page "Profile name updated"
28
+ rescue Twitter::Error => e
29
+ page twitter_error(e)
30
+ end
31
+
32
+ def set_profile_bio
33
+ Tempfile.open('pry-send_tweet-set-bio') do |file|
34
+ Pry::Editor.new(_pry_).invoke_editor(file.path, 0)
35
+ file.rewind
36
+ twitter.update_profile description: file.read
37
+ page "Profile bio updated"
38
+ end
39
+ rescue Twitter::Error => e
40
+ page twitter_error(e)
41
+ end
42
+ end