twat 0.6.3 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,34 @@
|
|
1
|
+
%w(base add config delete follow_tag follow list mentions set update update_config finger version track).each do |filename|
|
2
|
+
require File.join(File.expand_path("../subcommands/#{filename}", __FILE__))
|
3
|
+
end
|
4
|
+
|
5
|
+
module Twat
|
6
|
+
class Subcommand
|
7
|
+
|
8
|
+
def self.run
|
9
|
+
# This is evilbadscary, but seems like the best approach
|
10
|
+
$args = ::Twat::Args.new
|
11
|
+
# First, without commands dump user into follow mode
|
12
|
+
if ARGV.empty?
|
13
|
+
ARGV.insert(0, "follow_tag")
|
14
|
+
|
15
|
+
# Failing that, in the case of invalid commands, assume they want to
|
16
|
+
# tweet something.
|
17
|
+
# TODO, fuzzy match against the contents of COMMANDS and have a sook if
|
18
|
+
# they're close to an actual command
|
19
|
+
# FIXME Potentially the absense of a space would suggest that it's just a
|
20
|
+
# fucked effort at typing
|
21
|
+
elsif !Subcommands::COMMANDS.include?(ARGV[0])
|
22
|
+
ARGV.insert(0, "update")
|
23
|
+
end
|
24
|
+
|
25
|
+
# There is really no reason why this needs to be explicitly mention
|
26
|
+
# in this layout, we could just as easily look for a class in
|
27
|
+
# Subcommands:: that matches by name, however this avoids some ugly
|
28
|
+
# metaprogramming with minimal overhead, and also leaves the door
|
29
|
+
# open for aliasing etc
|
30
|
+
Subcommands::COMMANDS[ARGV[0]].new(ARGV[1..-1]).run!
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Twat::Subcommands
|
2
|
+
class Add < Base
|
3
|
+
|
4
|
+
def run
|
5
|
+
needs_arguments(1)
|
6
|
+
|
7
|
+
endpoint = ::Twat::Endpoint.new(args.endpoint)
|
8
|
+
endpoint.authorize_account(@argv[0])
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.usage
|
12
|
+
"Usage: twat add accountname"
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
COMMANDS['add'] = Add
|
17
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module ::Twitter
|
2
|
+
class Status
|
3
|
+
def as_user
|
4
|
+
begin
|
5
|
+
user.screen_name
|
6
|
+
rescue NoMethodError
|
7
|
+
from_user
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Twat::Subcommands
|
14
|
+
COMMANDS = {}
|
15
|
+
class Base
|
16
|
+
|
17
|
+
include ::Twat::Exceptions
|
18
|
+
|
19
|
+
attr_reader :opts
|
20
|
+
|
21
|
+
def initialize(argv)
|
22
|
+
@argv=argv
|
23
|
+
end
|
24
|
+
|
25
|
+
def run!
|
26
|
+
with_handled_exceptions(args) do
|
27
|
+
run
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def needs_arguments(n)
|
32
|
+
unless @argv.length == n
|
33
|
+
usage_and_exit!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def needs_at_least(n)
|
38
|
+
unless @argv.length >= n
|
39
|
+
usage_and_exit!
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def usage_and_exit!
|
44
|
+
usage
|
45
|
+
exit
|
46
|
+
end
|
47
|
+
|
48
|
+
def auth!
|
49
|
+
Twitter.configure do |twit|
|
50
|
+
config.account.each do |key, value|
|
51
|
+
twit.send("#{key}=", value)
|
52
|
+
end
|
53
|
+
config.endpoint.consumer_info.each do |key, value|
|
54
|
+
twit.send("#{key}=", value)
|
55
|
+
end
|
56
|
+
twit.endpoint = config.endpoint.url
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def beep
|
61
|
+
print "\a"
|
62
|
+
end
|
63
|
+
|
64
|
+
def pad(n)
|
65
|
+
"%02d" % n
|
66
|
+
end
|
67
|
+
|
68
|
+
# Format a tweet all pretty like
|
69
|
+
def format(twt, idx = nil)
|
70
|
+
idx = pad(idx) if idx
|
71
|
+
text = deentitize(twt.text)
|
72
|
+
if config.colors?
|
73
|
+
buf = idx ? "#{idx.cyan}:" : ""
|
74
|
+
if twt.as_user == config.account_name.to_s
|
75
|
+
buf += "#{twt.as_user.bold.blue}: #{text}"
|
76
|
+
elsif text.mentions?(config.account_name)
|
77
|
+
buf += "#{twt.as_user.bold.red}: #{text}"
|
78
|
+
else
|
79
|
+
buf += "#{twt.as_user.bold.cyan}: #{text}"
|
80
|
+
end
|
81
|
+
buf.colorise!
|
82
|
+
else
|
83
|
+
buf = idx ? "#{idx}: " : ""
|
84
|
+
buf += "#{twt.as_user}: #{text}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def deentitize(text)
|
89
|
+
{"<" => "<", ">" => ">", "&" => "&", """ => '"' }.each do |k,v|
|
90
|
+
text.gsub!(k, v)
|
91
|
+
end
|
92
|
+
text
|
93
|
+
end
|
94
|
+
|
95
|
+
def config
|
96
|
+
@config ||= ::Twat::Config.new
|
97
|
+
end
|
98
|
+
|
99
|
+
def opts
|
100
|
+
@opts ||= ::Twat::Options.new
|
101
|
+
end
|
102
|
+
|
103
|
+
def args
|
104
|
+
# This is dangerous, but realistically I don't see how else to parse the
|
105
|
+
# args before creating an instance of a subclass
|
106
|
+
@args ||= $args
|
107
|
+
end
|
108
|
+
|
109
|
+
def enable_readline!
|
110
|
+
@reader = ReadlineNG::Reader.new
|
111
|
+
end
|
112
|
+
|
113
|
+
def reader
|
114
|
+
@reader
|
115
|
+
end
|
116
|
+
|
117
|
+
def usage
|
118
|
+
puts self.class.usage
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Twat::Subcommands
|
2
|
+
class Delete < Base
|
3
|
+
|
4
|
+
def run
|
5
|
+
needs_arguments(1)
|
6
|
+
if config.accounts.delete(@argv[0])
|
7
|
+
config.save!
|
8
|
+
puts "Successfully deleted"
|
9
|
+
else
|
10
|
+
puts "No such account"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.usage
|
15
|
+
"Usage: twat delete ACCOUNTNAME"
|
16
|
+
end
|
17
|
+
|
18
|
+
def updateconfig
|
19
|
+
config.update!
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
COMMANDS['delete'] = Delete
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Twat::Subcommands
|
2
|
+
class Finger < Base
|
3
|
+
|
4
|
+
def run
|
5
|
+
needs_at_least(1)
|
6
|
+
auth!
|
7
|
+
|
8
|
+
begin
|
9
|
+
Twitter.user_timeline(@argv[0], :count => (@argv[1] || 1).to_i).each do |tweet|
|
10
|
+
puts format(tweet)
|
11
|
+
end
|
12
|
+
# rescue Twitter::NotFound
|
13
|
+
# puts "#{@argv[0].bold.red} doesn't appear to be a valid user"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.usage
|
18
|
+
"Usage: twat finger USERNAME [count]"
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
COMMANDS['finger'] = Finger
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Twat::Subcommands
|
2
|
+
class FollowTag < Base
|
3
|
+
include FollowMixin
|
4
|
+
|
5
|
+
def self.usage
|
6
|
+
["Usage: twat",
|
7
|
+
"Usage: twat follow_stream #hashtag"]
|
8
|
+
end
|
9
|
+
|
10
|
+
def new_tweets(opts)
|
11
|
+
unless @argv.empty?
|
12
|
+
ret = Twitter.search(@argv.join(" "), opts)
|
13
|
+
else
|
14
|
+
ret = Twitter.home_timeline(opts)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
COMMANDS['follow_tag'] = FollowTag
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Twat::Subcommands
|
2
|
+
class List < Base
|
3
|
+
|
4
|
+
def run
|
5
|
+
auth!
|
6
|
+
Twitter.home_timeline(:count => @argv[0] || 5).reverse.each do |tweet|
|
7
|
+
format(tweet)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.usage
|
12
|
+
"Usage: twat list [count]"
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
COMMANDS['list'] = List
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Twat::Subcommands
|
2
|
+
class Mentions < Base
|
3
|
+
include FollowMixin
|
4
|
+
|
5
|
+
def run
|
6
|
+
needs_arguments(0)
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.usage
|
11
|
+
"Usage: twat mentions"
|
12
|
+
end
|
13
|
+
|
14
|
+
def new_tweets(opts)
|
15
|
+
ret = Twitter.mentions(opts)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
COMMANDS['mentions'] = Mentions
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Twat::Subcommands
|
2
|
+
class Set < Base
|
3
|
+
|
4
|
+
def run
|
5
|
+
needs_arguments(2)
|
6
|
+
k, v = @argv[0..1]
|
7
|
+
opts.send(:"#{k}=", v)
|
8
|
+
|
9
|
+
puts "Successfully set #{k} as #{v}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.usage
|
13
|
+
"Usage: twat set option value"
|
14
|
+
# TODO Dump available options
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
COMMANDS['set'] = Set
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
module Twat::Subcommands
|
3
|
+
class Track < Base
|
4
|
+
include FollowMixin
|
5
|
+
|
6
|
+
# TODO configless mixin
|
7
|
+
def auth! # Disable authentication
|
8
|
+
end
|
9
|
+
|
10
|
+
def config # Stub out a config object
|
11
|
+
@_config ||= if (c = super).exists?
|
12
|
+
c
|
13
|
+
else
|
14
|
+
OpenStruct.new({
|
15
|
+
:polling_interval => 60,
|
16
|
+
:colors? => true,
|
17
|
+
:beep? => false,
|
18
|
+
:account_name => "IMPOSSIBLEUSERNAME!"
|
19
|
+
})
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.usage
|
24
|
+
["Usage: twat track username"]
|
25
|
+
end
|
26
|
+
|
27
|
+
def new_tweets(opts)
|
28
|
+
Twitter.user_timeline(@argv[0], opts)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
COMMANDS['track'] = Track
|
33
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Twat::Subcommands
|
2
|
+
class Update < Base
|
3
|
+
|
4
|
+
def run
|
5
|
+
msg = @argv.join(" ")
|
6
|
+
raise TweetTooLong if msg.length > 140
|
7
|
+
usage_and_exit! if msg == ""
|
8
|
+
|
9
|
+
auth!
|
10
|
+
|
11
|
+
Twitter.update(msg)
|
12
|
+
#puts msg
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.usage
|
16
|
+
"Usage: twat update tweet goes here"
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
COMMANDS['update'] = Update
|
21
|
+
end
|
data/lib/twat/twatopt.rb
ADDED
File without changes
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Twat
|
2
|
+
class TweetStack
|
3
|
+
# A circularly linked list representing all of the tweets printed thus far,
|
4
|
+
# for the purposes of retrieving them after being printed
|
5
|
+
def initialize
|
6
|
+
@stack = {}
|
7
|
+
@_next = 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def [] k
|
11
|
+
@stack[k]
|
12
|
+
end
|
13
|
+
|
14
|
+
def << v
|
15
|
+
@stack[nxt] = v
|
16
|
+
end
|
17
|
+
|
18
|
+
def include? k
|
19
|
+
@stack.keys.include?(k)
|
20
|
+
end
|
21
|
+
|
22
|
+
def last
|
23
|
+
# I see the irony
|
24
|
+
@_next
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def nxt
|
30
|
+
if @_next == 99
|
31
|
+
@_next = 1
|
32
|
+
else
|
33
|
+
@_next += 1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|