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.
Files changed (71) hide show
  1. data/.rvmrc +1 -1
  2. data/.travis.yml +6 -0
  3. data/Gemfile +1 -4
  4. data/Gemfile.lock +43 -23
  5. data/Gemfile.travis +12 -0
  6. data/Rakefile +9 -0
  7. data/TODO +4 -1
  8. data/lib/twat.rb +30 -26
  9. data/lib/twat/argparse.rb +36 -79
  10. data/lib/twat/config.rb +22 -34
  11. data/lib/twat/endpoint.rb +11 -5
  12. data/lib/twat/endpoints/base.rb +39 -0
  13. data/lib/twat/endpoints/identica.rb +5 -1
  14. data/lib/twat/endpoints/twitter.rb +5 -1
  15. data/lib/twat/exceptions.rb +93 -46
  16. data/lib/twat/follow_mixin.rb +134 -0
  17. data/lib/twat/options.rb +28 -12
  18. data/lib/twat/subcommand.rb +34 -0
  19. data/lib/twat/subcommands/add.rb +17 -0
  20. data/lib/twat/subcommands/base.rb +122 -0
  21. data/lib/twat/subcommands/config.rb +15 -0
  22. data/lib/twat/subcommands/delete.rb +24 -0
  23. data/lib/twat/subcommands/finger.rb +23 -0
  24. data/lib/twat/subcommands/follow.rb +18 -0
  25. data/lib/twat/subcommands/follow_tag.rb +19 -0
  26. data/lib/twat/subcommands/list.rb +17 -0
  27. data/lib/twat/subcommands/mentions.rb +20 -0
  28. data/lib/twat/subcommands/set.rb +19 -0
  29. data/lib/twat/subcommands/track.rb +33 -0
  30. data/lib/twat/subcommands/update.rb +21 -0
  31. data/lib/twat/subcommands/update_config.rb +16 -0
  32. data/lib/twat/subcommands/version.rb +14 -0
  33. data/lib/twat/twatopt.rb +0 -0
  34. data/lib/twat/tweetstack.rb +38 -0
  35. data/lib/twat/version.rb +7 -0
  36. data/man/twat.1 +8 -5
  37. data/spec/argparse_spec.rb +48 -0
  38. data/spec/helpers/environment.rb +47 -0
  39. data/spec/helpers/fileutils.rb +18 -0
  40. data/spec/helpers/fixtures/core.rb +30 -0
  41. data/spec/helpers/fixtures/migrations.rb +13 -0
  42. data/spec/helpers/oauth.rb +15 -0
  43. data/spec/spec_helper.rb +13 -0
  44. data/spec/subcommands/add_spec.rb +61 -0
  45. data/spec/subcommands/config_spec.rb +13 -0
  46. data/spec/subcommands/delete_spec.rb +12 -0
  47. data/spec/subcommands/finger_spec.rb +41 -0
  48. data/spec/subcommands/follow_spec.rb +12 -0
  49. data/spec/subcommands/follow_tag_spec.rb +52 -0
  50. data/spec/subcommands/list_spec.rb +8 -0
  51. data/spec/subcommands/mentions_spec.rb +24 -0
  52. data/spec/subcommands/set_spec.rb +126 -0
  53. data/spec/subcommands/track_spec.rb +23 -0
  54. data/spec/subcommands/update_config_spec.rb +23 -0
  55. data/spec/subcommands/update_spec.rb +12 -0
  56. data/spec/subcommands/version_spec.rb +12 -0
  57. data/spec/twat_cli_spec.rb +102 -0
  58. data/spec/twat_spec.rb +12 -0
  59. data/twat.gemspec +6 -2
  60. metadata +135 -22
  61. data/lib/twat/actions.rb +0 -85
  62. data/lib/twat/actions/add.rb +0 -36
  63. data/lib/twat/actions/delete.rb +0 -14
  64. data/lib/twat/actions/follow.rb +0 -148
  65. data/lib/twat/actions/follow_user.rb +0 -11
  66. data/lib/twat/actions/setoption.rb +0 -14
  67. data/lib/twat/actions/show.rb +0 -12
  68. data/lib/twat/actions/tweet.rb +0 -14
  69. data/lib/twat/actions/updateconfig.rb +0 -9
  70. data/lib/twat/actions/user_feed.rb +0 -17
  71. 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
+ {"&lt;" => "<", "&gt;" => ">", "&amp;" => "&", "&quot;" => '"' }.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,15 @@
1
+ module Twat::Subcommands
2
+ class Config < Base
3
+
4
+ def run
5
+ raise AlreadyConfigured if config.exists?
6
+ config.create!
7
+ end
8
+
9
+ def self.usage
10
+ "Usage: twat config"
11
+ end
12
+
13
+ end
14
+ COMMANDS['config'] = Config
15
+ 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,18 @@
1
+ module Twat::Subcommands
2
+ class Follow < Base
3
+
4
+ def run
5
+ needs_arguments(1)
6
+ auth!
7
+
8
+ Twitter.follow(@argv[0])
9
+ end
10
+
11
+ def self.usage
12
+ "Usage: twat follow USERNAME"
13
+ end
14
+
15
+ end
16
+ COMMANDS['follow'] = Follow
17
+ end
18
+
@@ -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
@@ -0,0 +1,16 @@
1
+ module Twat::Subcommands
2
+ class UpdateConfig < Base
3
+
4
+ def run
5
+
6
+ config.update_config
7
+
8
+ end
9
+
10
+ def self.usage
11
+ "Usage: twat update_config"
12
+ end
13
+
14
+ end
15
+ COMMANDS['update_config'] = UpdateConfig
16
+ end
@@ -0,0 +1,14 @@
1
+ module Twat::Subcommands
2
+ class Version < Base
3
+
4
+ def run
5
+ puts "twat: #{Twat::VERSION}"
6
+ end
7
+
8
+ def self.usage
9
+ "Usage: twat version"
10
+ end
11
+
12
+ end
13
+ COMMANDS['version'] = Version
14
+ end
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