twat 0.6.3 → 0.9.2
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.
- data/.rvmrc +1 -1
- data/.travis.yml +6 -0
- data/Gemfile +1 -4
- data/Gemfile.lock +43 -23
- data/Gemfile.travis +12 -0
- data/Rakefile +9 -0
- data/TODO +4 -1
- data/lib/twat.rb +30 -26
- data/lib/twat/argparse.rb +36 -79
- data/lib/twat/config.rb +22 -34
- data/lib/twat/endpoint.rb +11 -5
- data/lib/twat/endpoints/base.rb +39 -0
- data/lib/twat/endpoints/identica.rb +5 -1
- data/lib/twat/endpoints/twitter.rb +5 -1
- data/lib/twat/exceptions.rb +93 -46
- data/lib/twat/follow_mixin.rb +134 -0
- data/lib/twat/options.rb +28 -12
- data/lib/twat/subcommand.rb +34 -0
- data/lib/twat/subcommands/add.rb +17 -0
- data/lib/twat/subcommands/base.rb +122 -0
- data/lib/twat/subcommands/config.rb +15 -0
- data/lib/twat/subcommands/delete.rb +24 -0
- data/lib/twat/subcommands/finger.rb +23 -0
- data/lib/twat/subcommands/follow.rb +18 -0
- data/lib/twat/subcommands/follow_tag.rb +19 -0
- data/lib/twat/subcommands/list.rb +17 -0
- data/lib/twat/subcommands/mentions.rb +20 -0
- data/lib/twat/subcommands/set.rb +19 -0
- data/lib/twat/subcommands/track.rb +33 -0
- data/lib/twat/subcommands/update.rb +21 -0
- data/lib/twat/subcommands/update_config.rb +16 -0
- data/lib/twat/subcommands/version.rb +14 -0
- data/lib/twat/twatopt.rb +0 -0
- data/lib/twat/tweetstack.rb +38 -0
- data/lib/twat/version.rb +7 -0
- data/man/twat.1 +8 -5
- data/spec/argparse_spec.rb +48 -0
- data/spec/helpers/environment.rb +47 -0
- data/spec/helpers/fileutils.rb +18 -0
- data/spec/helpers/fixtures/core.rb +30 -0
- data/spec/helpers/fixtures/migrations.rb +13 -0
- data/spec/helpers/oauth.rb +15 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/subcommands/add_spec.rb +61 -0
- data/spec/subcommands/config_spec.rb +13 -0
- data/spec/subcommands/delete_spec.rb +12 -0
- data/spec/subcommands/finger_spec.rb +41 -0
- data/spec/subcommands/follow_spec.rb +12 -0
- data/spec/subcommands/follow_tag_spec.rb +52 -0
- data/spec/subcommands/list_spec.rb +8 -0
- data/spec/subcommands/mentions_spec.rb +24 -0
- data/spec/subcommands/set_spec.rb +126 -0
- data/spec/subcommands/track_spec.rb +23 -0
- data/spec/subcommands/update_config_spec.rb +23 -0
- data/spec/subcommands/update_spec.rb +12 -0
- data/spec/subcommands/version_spec.rb +12 -0
- data/spec/twat_cli_spec.rb +102 -0
- data/spec/twat_spec.rb +12 -0
- data/twat.gemspec +6 -2
- metadata +135 -22
- data/lib/twat/actions.rb +0 -85
- data/lib/twat/actions/add.rb +0 -36
- data/lib/twat/actions/delete.rb +0 -14
- data/lib/twat/actions/follow.rb +0 -148
- data/lib/twat/actions/follow_user.rb +0 -11
- data/lib/twat/actions/setoption.rb +0 -14
- data/lib/twat/actions/show.rb +0 -12
- data/lib/twat/actions/tweet.rb +0 -14
- data/lib/twat/actions/updateconfig.rb +0 -9
- data/lib/twat/actions/user_feed.rb +0 -17
- data/lib/twat/actions/version.rb +0 -9
data/lib/twat/endpoint.rb
CHANGED
@@ -1,16 +1,22 @@
|
|
1
1
|
module Twat
|
2
2
|
|
3
|
-
|
3
|
+
ENDPOINTS = ["identi.ca", "twitter"]
|
4
|
+
|
5
|
+
%w[base twitter identica].each do |filename|
|
4
6
|
require File.join(File.expand_path("../endpoints", __FILE__), filename)
|
5
7
|
end
|
6
8
|
|
7
9
|
# Proxies to the actual endpoint classes
|
8
10
|
class Endpoint
|
9
11
|
def self.new(endpoint)
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
begin
|
13
|
+
@implementation = {
|
14
|
+
:twitter => Endpoints::Twitter,
|
15
|
+
:"identi.ca" => Endpoints::Identica
|
16
|
+
}[endpoint].new
|
17
|
+
rescue NoMethodError
|
18
|
+
raise Exceptions::NoSuchEndpoint
|
19
|
+
end
|
14
20
|
end
|
15
21
|
end
|
16
22
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Twat::Endpoints
|
2
|
+
class Base
|
3
|
+
|
4
|
+
@@endpoint_set=nil
|
5
|
+
|
6
|
+
def authorize_account(name)
|
7
|
+
oauth_conf = oauth_options.merge ({ :site => url })
|
8
|
+
|
9
|
+
oauth = OAuth::Consumer.new(consumer_info[:consumer_key], consumer_info[:consumer_secret], oauth_conf)
|
10
|
+
token_request = oauth.get_request_token()
|
11
|
+
puts "Please authenticate the application at #{token_request.authorize_url}, then enter pin"
|
12
|
+
pin = STDIN.gets.chomp
|
13
|
+
begin
|
14
|
+
access_token = token_request.get_access_token(oauth_verifier: pin)
|
15
|
+
account_settings = {
|
16
|
+
oauth_token: access_token.token,
|
17
|
+
oauth_token_secret: access_token.secret,
|
18
|
+
endpoint: endpoint_name
|
19
|
+
}
|
20
|
+
config.accounts[name.to_sym] = account_settings
|
21
|
+
config.save!
|
22
|
+
rescue OAuth::Unauthorized
|
23
|
+
puts "Couldn't authenticate you, did you enter the pin correctly?"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def config
|
28
|
+
return @config if @config
|
29
|
+
@config = ::Twat::Config.new
|
30
|
+
@config.create! unless @config.exists?
|
31
|
+
return @config
|
32
|
+
end
|
33
|
+
|
34
|
+
def endpoint_name
|
35
|
+
raise "Endpoint not set! Naughty programmer"
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
data/lib/twat/exceptions.rb
CHANGED
@@ -1,53 +1,100 @@
|
|
1
1
|
module Twat
|
2
|
-
|
3
|
-
class NoDefaultAccount < Exception; end
|
4
|
-
class NoSuchCommand < Exception; end
|
5
|
-
class NoConfigFile < Exception; end
|
6
|
-
class RequiresOptVal < Exception; end
|
7
|
-
class Usage < Exception; end
|
8
|
-
class InvalidCredentials < Exception; end
|
9
|
-
class ConfigVersionIncorrect < Exception; end
|
10
|
-
class InvalidSetOpt < Exception; end
|
11
|
-
class InvalidBool < Exception; end
|
12
|
-
class InvalidInt < Exception; end
|
13
|
-
class TweetTooLong < Exception; end
|
2
|
+
module Exceptions
|
14
3
|
|
15
|
-
|
16
|
-
|
4
|
+
class TwatException < Exception; end
|
5
|
+
|
6
|
+
class Usage < TwatException; end
|
7
|
+
|
8
|
+
class AlreadyConfigured < TwatException
|
9
|
+
def msg
|
10
|
+
"Already configured, use add to add more accounts"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class ArgumentRequired < TwatException
|
15
|
+
def msg
|
16
|
+
"This command requires an argument"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
class NoSuchAccount < TwatException
|
20
|
+
def msg
|
21
|
+
"No such account"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
class NoDefaultAccount < TwatException
|
25
|
+
def msg
|
26
|
+
"No default account configured (try twat set default [account])."
|
27
|
+
end
|
28
|
+
end
|
29
|
+
class NoSuchCommand < TwatException
|
30
|
+
def msg
|
31
|
+
"No such command"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
class NoSuchEndpoint < TwatException
|
35
|
+
def msg
|
36
|
+
"Available endpoints are #{::Twat::ENDPOINTS.join(", ")}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
class NoConfigFile < TwatException
|
40
|
+
def msg
|
41
|
+
"No config file, create one with twat -a [user|nick]"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
class RequiresOptVal < TwatException
|
45
|
+
def msg
|
46
|
+
"--set must take an option=value pair as arguments"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
class InvalidCredentials < TwatException
|
50
|
+
def msg
|
51
|
+
["Invalid credentials, try reauthenticating with",
|
52
|
+
"twat -a ACCOUNT"]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
class ConfigVersionIncorrect < TwatException
|
56
|
+
def msg
|
57
|
+
"Your config file is out of date. Run with --update-config to rememdy"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
class InvalidSetOpt < TwatException
|
61
|
+
def msg
|
62
|
+
"There is no such configurable option"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
class InvalidBool < TwatException
|
66
|
+
def msg
|
67
|
+
"Invalid value, valid values are #{Options::BOOL_VALS.join("|")}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
class InvalidInt < TwatException
|
71
|
+
def msg
|
72
|
+
"Invalid value, must be an integer"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
class TweetTooLong < TwatException
|
76
|
+
def msg
|
77
|
+
"Twitter enforces a maximum status length of 140 characters"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
class NoSuchTweet < TwatException
|
81
|
+
def msg
|
82
|
+
"Specified tweet was not found"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
class NoInternetConnection < TwatException
|
86
|
+
def msg
|
87
|
+
"Couldn't connect to the endpoint"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def with_handled_exceptions(opts=nil)
|
17
92
|
begin
|
18
|
-
|
19
|
-
yield opts
|
93
|
+
yield
|
20
94
|
rescue Usage
|
21
|
-
opts.usage
|
22
|
-
rescue
|
23
|
-
puts
|
24
|
-
opts.usage
|
25
|
-
rescue NoDefaultAccount
|
26
|
-
puts "No default account configured."
|
27
|
-
rescue NoSuchCommand
|
28
|
-
puts "No such command"
|
29
|
-
opts.usage
|
30
|
-
rescue NoConfigFile
|
31
|
-
puts "No config file, create one with twat -a [user|nick]"
|
32
|
-
opts.usage
|
33
|
-
rescue InvalidSetOpt
|
34
|
-
puts "There is no such configurable option"
|
35
|
-
opts.usage
|
36
|
-
rescue RequiresOptVal
|
37
|
-
puts "--set must take an option=value pair as arguments"
|
38
|
-
rescue InvalidCredentials
|
39
|
-
puts "Invalid credentials, try reauthenticating with"
|
40
|
-
puts "twat -a #{opts[:account]}"
|
41
|
-
rescue ConfigVersionIncorrect
|
42
|
-
puts "Your config file is out of date. Run with --update-config to rememdy"
|
43
|
-
rescue InvalidBool
|
44
|
-
puts "Invalid value, valid values are #{Options::BOOL_VALID.join("|")}"
|
45
|
-
rescue InvalidInt
|
46
|
-
puts "Invalid value, must be an integer"
|
47
|
-
rescue Errno::ECONNRESET
|
48
|
-
puts "Connection was reset by third party."
|
49
|
-
rescue TweetTooLong
|
50
|
-
puts "Twitter enforces a maximum status length of 140 characters"
|
95
|
+
opts.usage if opts
|
96
|
+
rescue TwatException => e
|
97
|
+
puts e.msg
|
51
98
|
end
|
52
99
|
end
|
53
100
|
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module FollowMixin
|
2
|
+
POLLING_RESOLUTION = 20 # Readline scan time in hz
|
3
|
+
# Shim to bail out if we're in test
|
4
|
+
def untested
|
5
|
+
true
|
6
|
+
end
|
7
|
+
|
8
|
+
class PrintNothing < Exception; end
|
9
|
+
|
10
|
+
def run
|
11
|
+
auth!
|
12
|
+
enable_readline! if untested
|
13
|
+
@tweetstack = ::Twat::TweetStack.new
|
14
|
+
|
15
|
+
@failcount = 0
|
16
|
+
|
17
|
+
# Get 5 tweets
|
18
|
+
tweets = retrieve_tweets(:count => 5)
|
19
|
+
while untested do
|
20
|
+
begin
|
21
|
+
last_id = process_followed(tweets) if tweets.any?
|
22
|
+
(config.polling_interval * POLLING_RESOLUTION).times do
|
23
|
+
reader.tick
|
24
|
+
lines = 0
|
25
|
+
reader.each_line do |i|
|
26
|
+
lines += 1
|
27
|
+
handle_input(i)
|
28
|
+
end
|
29
|
+
sleep 1.0/POLLING_RESOLUTION if lines == 0
|
30
|
+
end
|
31
|
+
tweets = retrieve_tweets(:since_id => last_id)
|
32
|
+
@failcount = 0
|
33
|
+
rescue Interrupt
|
34
|
+
break
|
35
|
+
rescue Twitter::Error::ServiceUnavailable
|
36
|
+
break unless fail_or_bail
|
37
|
+
sleeptime = 60 * (@failcount + 1)
|
38
|
+
reader.puts_above "#{"(__-){".red}: the fail whale has been rolled out, sleeping for #{sleeptime} seconds"
|
39
|
+
reader.wait sleeptime
|
40
|
+
rescue Errno::ECONNRESET, Errno::ETIMEDOUT, SocketError
|
41
|
+
break unless fail_or_bail
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def retrieve_tweets(opts)
|
49
|
+
new_tweets(opts)
|
50
|
+
rescue SocketError
|
51
|
+
raise ::Twat::Exceptions::NoInternetConnection
|
52
|
+
end
|
53
|
+
|
54
|
+
def handle_input(inp)
|
55
|
+
ret = process_input(inp)
|
56
|
+
return if ret.nil?
|
57
|
+
|
58
|
+
if ret === true
|
59
|
+
reader.puts_above(inp.green)
|
60
|
+
elsif ret === false
|
61
|
+
reader.puts_above(inp.red)
|
62
|
+
else
|
63
|
+
reader.puts_above(ret)
|
64
|
+
end
|
65
|
+
rescue PrintNothing
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def process_input(inp)
|
70
|
+
case inp
|
71
|
+
when /^[rR][tT] ([0-9]{1,2})/
|
72
|
+
begin
|
73
|
+
retweet($1.to_i)
|
74
|
+
return true
|
75
|
+
rescue ::Twat::Exceptions::NoSuchTweet
|
76
|
+
return "#{inp.red} #{":: No such tweet".bold.red}"
|
77
|
+
end
|
78
|
+
when /^follow (.*)/
|
79
|
+
Twitter.follow($1)
|
80
|
+
return true
|
81
|
+
when /^reply ([0-9]{1,2}) (.*)$/
|
82
|
+
# TODO This should be a method of the tweetstack
|
83
|
+
in_reply_to = get_tweet($1.to_i)
|
84
|
+
msg = "@#{in_reply_to.as_user} #{$2}"
|
85
|
+
|
86
|
+
if msg.length > 140
|
87
|
+
return "#{msg} #{":: Too long".bold.red}"
|
88
|
+
else
|
89
|
+
Twitter.update(msg, in_reply_to_status_id: in_reply_to.id)
|
90
|
+
return msg.green
|
91
|
+
end
|
92
|
+
else
|
93
|
+
# Assume they want to tweet something
|
94
|
+
if inp.length > 140
|
95
|
+
return "#{inp.red} #{":: Too long".bold.red}"
|
96
|
+
else
|
97
|
+
Twitter.update(inp)
|
98
|
+
return true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def get_tweet(idx)
|
104
|
+
raise ::Twat::Exceptions::NoSuchTweet unless @tweetstack.include?(idx)
|
105
|
+
@tweetstack[idx]
|
106
|
+
end
|
107
|
+
|
108
|
+
# Wrapper methods from the tweetstack implementation
|
109
|
+
def retweet(idx)
|
110
|
+
Twitter.retweet(get_tweet(idx).id)
|
111
|
+
end
|
112
|
+
|
113
|
+
def fail_or_bail
|
114
|
+
if @failcount > 2
|
115
|
+
puts "3 consecutive failures, giving up"
|
116
|
+
else
|
117
|
+
@failcount += 1
|
118
|
+
return true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def process_followed(tweets)
|
123
|
+
last_id = nil
|
124
|
+
tweets.reverse.each do |tweet|
|
125
|
+
id = @tweetstack << tweet
|
126
|
+
beep if config.beep? && tweet.text.mentions?(config.account_name)
|
127
|
+
reader.puts_above format(tweet, @tweetstack.last)
|
128
|
+
last_id = tweet.id
|
129
|
+
end
|
130
|
+
|
131
|
+
return last_id
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
data/lib/twat/options.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
module Twat
|
2
2
|
class Options
|
3
|
+
|
4
|
+
include ::Twat::Exceptions
|
5
|
+
|
3
6
|
BOOL_TRUE=["yes", "true", "1", "on"]
|
4
7
|
BOOL_FALSE=["no", "false", "0", "off"]
|
5
8
|
BOOL_VALS = BOOL_TRUE + BOOL_FALSE
|
@@ -23,41 +26,54 @@ module Twat
|
|
23
26
|
!!/^[0-9]+$/.match(val)
|
24
27
|
end
|
25
28
|
|
29
|
+
def self.to_bool(value)
|
30
|
+
case
|
31
|
+
when bool_true?(value)
|
32
|
+
return true
|
33
|
+
when bool_false?(value)
|
34
|
+
return false
|
35
|
+
else
|
36
|
+
raise InvalidBool
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
26
40
|
# A set of wrappers around the global config object to set given attributes
|
27
41
|
# Catching failures is convenient because of the method_missing? hook
|
28
42
|
def method_missing(sym, *args, &block)
|
29
|
-
|
30
|
-
raise InvalidSetOpt
|
43
|
+
raise InvalidSetOpt, "Options doesn't know about #{sym}"
|
31
44
|
end
|
32
45
|
|
33
46
|
def config
|
34
|
-
@config ||= Config.new
|
47
|
+
@config ||= Config.new.tap do |n|
|
48
|
+
raise NoConfigFile unless n.exists?
|
49
|
+
end
|
35
50
|
end
|
36
51
|
|
37
52
|
# This is deliberately not abstracted (it could be easily accessed from
|
38
53
|
# withing the method_missing method, but that will just lead to nastiness
|
39
54
|
# later when I implement colors, for example.
|
40
55
|
def default=(value)
|
41
|
-
|
42
|
-
unless config.accounts.include?(
|
56
|
+
value = value.to_sym
|
57
|
+
unless config.accounts.include?(value)
|
43
58
|
raise NoSuchAccount
|
44
59
|
end
|
45
60
|
|
46
|
-
config[:default] =
|
61
|
+
config[:default] = value
|
47
62
|
config.save!
|
48
63
|
end
|
49
64
|
|
50
65
|
def colors=(value)
|
51
|
-
|
52
|
-
raise InvalidBool unless Options.bool_valid?(val)
|
53
|
-
config[:colors] = val
|
66
|
+
config[:colors] = Options.to_bool(value)
|
54
67
|
config.save!
|
55
68
|
end
|
56
69
|
|
57
70
|
def beep=(value)
|
58
|
-
|
59
|
-
|
60
|
-
|
71
|
+
config[:beep] = Options.to_bool(value)
|
72
|
+
config.save!
|
73
|
+
end
|
74
|
+
|
75
|
+
def show_mentions=(value)
|
76
|
+
config[:show_mentions] = Options.to_bool(value)
|
61
77
|
config.save!
|
62
78
|
end
|
63
79
|
|