atig 0.0.1

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 (171) hide show
  1. data/.gitignore +24 -0
  2. data/Gemfile +3 -0
  3. data/README.mkdn +52 -0
  4. data/Rakefile +15 -0
  5. data/atig.gemspec +25 -0
  6. data/bin/atig +74 -0
  7. data/docs/OMakefile +32 -0
  8. data/docs/OMakeroot +45 -0
  9. data/docs/_static/allow.png +0 -0
  10. data/docs/_static/emacs.png +0 -0
  11. data/docs/_static/irc_setting.png +0 -0
  12. data/docs/_static/irssi.png +0 -0
  13. data/docs/_static/limechat.png +0 -0
  14. data/docs/_static/limechat_s.png +0 -0
  15. data/docs/_static/oauth_channel.png +0 -0
  16. data/docs/_static/screenshot.png +0 -0
  17. data/docs/_static/structure.png +0 -0
  18. data/docs/_static/verify.png +0 -0
  19. data/docs/changelog.rst +96 -0
  20. data/docs/commandline_options.rst +21 -0
  21. data/docs/commands.rst +84 -0
  22. data/docs/conf.py +194 -0
  23. data/docs/config.rst +159 -0
  24. data/docs/feature.rst +41 -0
  25. data/docs/graphics.graffle +1995 -0
  26. data/docs/hacking_guide.rst +43 -0
  27. data/docs/index.rst +109 -0
  28. data/docs/irc.rst +31 -0
  29. data/docs/options.rst +75 -0
  30. data/docs/quickstart.rst +89 -0
  31. data/docs/resize.sh +7 -0
  32. data/docs/tiarra.rst +2 -0
  33. data/docs/tig.rst +21 -0
  34. data/lib/atig.rb +19 -0
  35. data/lib/atig/agent.rb +8 -0
  36. data/lib/atig/agent/agent.rb +38 -0
  37. data/lib/atig/agent/clenup.rb +23 -0
  38. data/lib/atig/agent/dm.rb +35 -0
  39. data/lib/atig/agent/following.rb +45 -0
  40. data/lib/atig/agent/full_list.rb +20 -0
  41. data/lib/atig/agent/list.rb +55 -0
  42. data/lib/atig/agent/list_status.rb +46 -0
  43. data/lib/atig/agent/mention.rb +13 -0
  44. data/lib/atig/agent/other_list.rb +18 -0
  45. data/lib/atig/agent/own_list.rb +18 -0
  46. data/lib/atig/agent/stream_follow.rb +38 -0
  47. data/lib/atig/agent/timeline.rb +13 -0
  48. data/lib/atig/agent/user_stream.rb +31 -0
  49. data/lib/atig/basic_twitter.rb +116 -0
  50. data/lib/atig/bitly.rb +52 -0
  51. data/lib/atig/channel.rb +5 -0
  52. data/lib/atig/channel/channel.rb +17 -0
  53. data/lib/atig/channel/dm.rb +14 -0
  54. data/lib/atig/channel/list.rb +76 -0
  55. data/lib/atig/channel/mention.rb +20 -0
  56. data/lib/atig/channel/retweet.rb +28 -0
  57. data/lib/atig/channel/timeline.rb +74 -0
  58. data/lib/atig/command.rb +21 -0
  59. data/lib/atig/command/autofix.rb +58 -0
  60. data/lib/atig/command/command.rb +24 -0
  61. data/lib/atig/command/command_helper.rb +95 -0
  62. data/lib/atig/command/destroy.rb +44 -0
  63. data/lib/atig/command/dm.rb +31 -0
  64. data/lib/atig/command/favorite.rb +27 -0
  65. data/lib/atig/command/info.rb +50 -0
  66. data/lib/atig/command/limit.rb +15 -0
  67. data/lib/atig/command/location.rb +23 -0
  68. data/lib/atig/command/name.rb +18 -0
  69. data/lib/atig/command/option.rb +37 -0
  70. data/lib/atig/command/refresh.rb +18 -0
  71. data/lib/atig/command/reply.rb +37 -0
  72. data/lib/atig/command/retweet.rb +63 -0
  73. data/lib/atig/command/search.rb +51 -0
  74. data/lib/atig/command/spam.rb +26 -0
  75. data/lib/atig/command/status.rb +41 -0
  76. data/lib/atig/command/thread.rb +44 -0
  77. data/lib/atig/command/time.rb +32 -0
  78. data/lib/atig/command/uptime.rb +32 -0
  79. data/lib/atig/command/user.rb +42 -0
  80. data/lib/atig/command/user_info.rb +27 -0
  81. data/lib/atig/command/version.rb +49 -0
  82. data/lib/atig/command/whois.rb +39 -0
  83. data/lib/atig/db/db.rb +60 -0
  84. data/lib/atig/db/followings.rb +131 -0
  85. data/lib/atig/db/listenable.rb +22 -0
  86. data/lib/atig/db/lists.rb +76 -0
  87. data/lib/atig/db/roman.rb +30 -0
  88. data/lib/atig/db/sized_uniq_array.rb +62 -0
  89. data/lib/atig/db/sql.rb +35 -0
  90. data/lib/atig/db/statuses.rb +147 -0
  91. data/lib/atig/db/transaction.rb +47 -0
  92. data/lib/atig/exception_util.rb +26 -0
  93. data/lib/atig/gateway.rb +62 -0
  94. data/lib/atig/gateway/channel.rb +99 -0
  95. data/lib/atig/gateway/session.rb +326 -0
  96. data/lib/atig/http.rb +95 -0
  97. data/lib/atig/ifilter.rb +7 -0
  98. data/lib/atig/ifilter/expand_url.rb +74 -0
  99. data/lib/atig/ifilter/retweet.rb +14 -0
  100. data/lib/atig/ifilter/retweet_time.rb +16 -0
  101. data/lib/atig/ifilter/sanitize.rb +18 -0
  102. data/lib/atig/ifilter/strip.rb +15 -0
  103. data/lib/atig/ifilter/utf7.rb +26 -0
  104. data/lib/atig/ifilter/xid.rb +36 -0
  105. data/lib/atig/levenshtein.rb +49 -0
  106. data/lib/atig/monkey.rb +4 -0
  107. data/lib/atig/oauth-patch.rb +40 -0
  108. data/lib/atig/oauth.rb +55 -0
  109. data/lib/atig/ofilter.rb +4 -0
  110. data/lib/atig/ofilter/escape_url.rb +102 -0
  111. data/lib/atig/ofilter/footer.rb +20 -0
  112. data/lib/atig/ofilter/geo.rb +17 -0
  113. data/lib/atig/ofilter/short_url.rb +47 -0
  114. data/lib/atig/option.rb +90 -0
  115. data/lib/atig/scheduler.rb +79 -0
  116. data/lib/atig/search.rb +22 -0
  117. data/lib/atig/search_twitter.rb +21 -0
  118. data/lib/atig/sized_hash.rb +33 -0
  119. data/lib/atig/stream.rb +66 -0
  120. data/lib/atig/twitter.rb +79 -0
  121. data/lib/atig/twitter_struct.rb +63 -0
  122. data/lib/atig/unu.rb +27 -0
  123. data/lib/atig/update_checker.rb +53 -0
  124. data/lib/atig/url_escape.rb +62 -0
  125. data/lib/atig/util.rb +16 -0
  126. data/lib/atig/version.rb +3 -0
  127. data/lib/memory_profiler.rb +77 -0
  128. data/spec/command/autofix_spec.rb +35 -0
  129. data/spec/command/destroy_spec.rb +98 -0
  130. data/spec/command/dm_spec.rb +28 -0
  131. data/spec/command/favorite_spec.rb +55 -0
  132. data/spec/command/limit_spec.rb +27 -0
  133. data/spec/command/location_spec.rb +25 -0
  134. data/spec/command/name_spec.rb +19 -0
  135. data/spec/command/option_spec.rb +133 -0
  136. data/spec/command/refresh_spec.rb +22 -0
  137. data/spec/command/reply_spec.rb +79 -0
  138. data/spec/command/retweet_spec.rb +66 -0
  139. data/spec/command/spam_spec.rb +27 -0
  140. data/spec/command/status_spec.rb +44 -0
  141. data/spec/command/thread_spec.rb +91 -0
  142. data/spec/command/time_spec.rb +52 -0
  143. data/spec/command/uptime_spec.rb +55 -0
  144. data/spec/command/user_info_spec.rb +42 -0
  145. data/spec/command/user_spec.rb +50 -0
  146. data/spec/command/version_spec.rb +67 -0
  147. data/spec/command/whois_spec.rb +78 -0
  148. data/spec/db/followings_spec.rb +100 -0
  149. data/spec/db/listenable_spec.rb +32 -0
  150. data/spec/db/lists_spec.rb +104 -0
  151. data/spec/db/roman_spec.rb +17 -0
  152. data/spec/db/sized_uniq_array_spec.rb +63 -0
  153. data/spec/db/statuses_spec.rb +180 -0
  154. data/spec/ifilter/expand_url_spec.rb +44 -0
  155. data/spec/ifilter/retweet_spec.rb +28 -0
  156. data/spec/ifilter/retweet_time_spec.rb +25 -0
  157. data/spec/ifilter/sanitize_spec.rb +25 -0
  158. data/spec/ifilter/sid_spec.rb +29 -0
  159. data/spec/ifilter/strip_spec.rb +23 -0
  160. data/spec/ifilter/tid_spec.rb +29 -0
  161. data/spec/ifilter/utf7_spec.rb +30 -0
  162. data/spec/levenshtein_spec.rb +24 -0
  163. data/spec/ofilter/escape_url_spec.rb +50 -0
  164. data/spec/ofilter/footer_spec.rb +32 -0
  165. data/spec/ofilter/geo_spec.rb +33 -0
  166. data/spec/ofilter/short_url_spec.rb +127 -0
  167. data/spec/option_spec.rb +91 -0
  168. data/spec/sized_hash_spec.rb +45 -0
  169. data/spec/spec_helper.rb +35 -0
  170. data/spec/update_checker_spec.rb +55 -0
  171. metadata +326 -0
data/lib/atig/bitly.rb ADDED
@@ -0,0 +1,52 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+ require 'rubygems'
3
+ require 'json'
4
+ require 'atig/http'
5
+
6
+ module Atig
7
+ class Bitly
8
+ class << self
9
+ def no_login(logger)
10
+ self.new logger, nil, nil
11
+ end
12
+
13
+ def login(logger, login, key)
14
+ self.new logger, login, key
15
+ end
16
+ end
17
+
18
+ def initialize(logger, login, key)
19
+ @log = logger
20
+ @login = login
21
+ @key = key
22
+ @http = Http.new logger
23
+ end
24
+
25
+ def shorten(url)
26
+ return url if url =~ /bit\.ly/
27
+ bitly = URI("http://api.bit.ly/v3/shorten")
28
+ if @login and @key
29
+ bitly.path = "/shorten"
30
+ bitly.query = {
31
+ :format => "json", :longUrl => url, :login => @login, :apiKey => @key,
32
+ }.to_query_str(";")
33
+ req = @http.req(:get, bitly, {})
34
+ res = @http.http(bitly, 5, 10).request(req)
35
+
36
+ res = JSON.parse(res.body)
37
+
38
+ if res['statusCode'] == "ERROR" then
39
+ @log.error res['errorMessage']
40
+ url
41
+ else
42
+ res["results"][url]['shortUrl']
43
+ end
44
+ else
45
+ url
46
+ end
47
+ rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e
48
+ @log.error e
49
+ url
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,5 @@
1
+ require 'atig/channel/timeline'
2
+ require 'atig/channel/mention'
3
+ require 'atig/channel/dm'
4
+ require 'atig/channel/list'
5
+ require 'atig/channel/retweet'
@@ -0,0 +1,17 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ module Atig
4
+ module Channel
5
+ class Channel
6
+ def initialize(context, gateway, db)
7
+ @db = db
8
+ @channel = gateway.channel channel_name, :handler=>self
9
+ @channel.join_me
10
+
11
+ db.statuses.listen do|entry|
12
+ @channel.topic entry if entry.user.id == db.me.id
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ module Atig
4
+ module Channel
5
+ class Dm
6
+ def initialize(context, gateway, db)
7
+ channel = gateway.channel db.me.screen_name
8
+ db.dms.listen do|dm|
9
+ channel.message(dm)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,76 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ module Atig
4
+ module Channel
5
+ class List
6
+ class Handler
7
+ def initialize(db, name)
8
+ @db = db
9
+ @name = name
10
+ end
11
+
12
+ def on_invite(api, nick)
13
+ return if @name.include? '^'
14
+
15
+ api.post("#{@db.me.screen_name}/#{@name}/members", :id => nick )
16
+ @db.lists.invalidate @name
17
+ end
18
+
19
+ def on_kick(api, nick)
20
+ return if @name.include? '^'
21
+
22
+ api.delete("#{@db.me.screen_name}/#{@name}/members", :id => nick )
23
+ @db.lists.invalidate @name
24
+ end
25
+
26
+ def on_who(&f)
27
+ return unless f
28
+ @db.lists[@name].users.each(&f)
29
+ end
30
+ end
31
+
32
+ def initialize(context, gateway, db)
33
+ @channels = Hash.new do|hash,name|
34
+ channel = gateway.channel "##{name}", :handler => Handler.new(db, name)
35
+ channel.join_me
36
+ hash[name] = channel
37
+ end
38
+
39
+ db.statuses.listen do|entry|
40
+ if entry.source == :list then
41
+ @channels[entry.list].message entry
42
+ else
43
+ lists = db.lists.find_by_screen_name(entry.user.screen_name)
44
+ lists.each{|name|
45
+ @channels[name].message entry
46
+ }
47
+ end
48
+ end
49
+
50
+ db.statuses.listen do|entry|
51
+ if entry.user.id == db.me.id
52
+ @channels.each{|_,channel|
53
+ channel.topic entry
54
+ }
55
+ end
56
+ end
57
+
58
+ db.lists.listen do|kind, name, users|
59
+ case kind
60
+ when :new
61
+ @channels[name].join_me
62
+ @channels[name].join db.lists[name].users
63
+ when :del
64
+ @channels[name].part_me "No longer follow the list #{name}"
65
+ when :join
66
+ @channels[name].join users
67
+ when :part
68
+ @channels[name].part users
69
+ when :mode
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
@@ -0,0 +1,20 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ require 'atig/channel/channel'
4
+ module Atig
5
+ module Channel
6
+ class Mention < Atig::Channel::Channel
7
+ def initialize(context, gateway, db)
8
+ super
9
+
10
+ db.statuses.listen do|entry|
11
+ if entry.status.text.include?("@#{db.me.screen_name}")
12
+ @channel.message(entry)
13
+ end
14
+ end
15
+ end
16
+
17
+ def channel_name; "#mention" end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+ require 'atig/channel/channel'
3
+
4
+ module Atig
5
+ module Channel
6
+ class Retweet < Atig::Channel::Channel
7
+ def initialize(context, gateway, db)
8
+ super
9
+
10
+ db.statuses.find_all(:limit=>50).reverse_each {|entry|
11
+ message entry
12
+ }
13
+
14
+ db.statuses.listen {|entry|
15
+ message entry
16
+ }
17
+ end
18
+
19
+ def channel_name; "#retweet" end
20
+
21
+ def message(entry)
22
+ if entry.status.retweeted_status then
23
+ @channel.message entry
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,74 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ require 'atig/channel/channel'
4
+ require 'atig/util'
5
+ require 'atig/update_checker'
6
+
7
+ module Atig
8
+ module Channel
9
+ class Timeline < Atig::Channel::Channel
10
+ include Util
11
+
12
+ def initialize(context, gateway, db)
13
+ super
14
+ @log = context.log
15
+
16
+ @channel.notify "Client options: #{context.opts.marshal_dump.inspect}"
17
+
18
+ # つないだときに発言がないとさみしいので
19
+ db.statuses.find_all(:limit=>50).reverse_each do|entry|
20
+ case entry.source
21
+ when :timeline, :me
22
+ @channel.message entry
23
+ end
24
+ end
25
+
26
+ # 最新版のチェック
27
+ daemon do
28
+ log :info,"check update"
29
+ messages = UpdateChecker.latest
30
+ unless messages.empty?
31
+ @channel.notify "\002New version is available.\017 run 'git pull'."
32
+ messages[0, 3].each do |m|
33
+ @channel.notify " \002#{m[/.+/]}\017"
34
+ end
35
+ @channel.notify(" ... and more. check it: http://mzp.github.com/atig/") if messages.size > 3
36
+ end
37
+ sleep (3*60*60)
38
+ end
39
+
40
+ db.statuses.listen do|entry|
41
+ if db.followings.include?(entry.user) or
42
+ entry.source == :timeline or
43
+ entry.source == :user_stream or
44
+ entry.source == :me then
45
+ @channel.message entry
46
+ end
47
+ end
48
+
49
+ @channel.send :join, db.followings.users
50
+
51
+ db.followings.listen do|kind, users|
52
+ @channel.send(kind, users) if @channel.respond_to?(kind)
53
+ end
54
+ end
55
+
56
+ def on_invite(api, nick)
57
+ api.post("friendships/create/#{nick}")
58
+ @db.followings.invalidate
59
+ end
60
+
61
+ def on_kick(api, nick)
62
+ api.post("friendships/destroy/#{nick}")
63
+ @db.followings.invalidate
64
+ end
65
+
66
+ def on_who(&f)
67
+ return unless f
68
+ @db.followings.users.each(&f)
69
+ end
70
+
71
+ def channel_name; "#twitter" end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,21 @@
1
+ require 'atig/command/retweet'
2
+ require 'atig/command/reply'
3
+ require 'atig/command/user'
4
+ require 'atig/command/favorite'
5
+ require 'atig/command/uptime'
6
+ require 'atig/command/destroy'
7
+ require 'atig/command/status'
8
+ require 'atig/command/thread'
9
+ require 'atig/command/time'
10
+ require 'atig/command/version'
11
+ require 'atig/command/user_info'
12
+ require 'atig/command/whois'
13
+ require 'atig/command/option'
14
+ require 'atig/command/location'
15
+ require 'atig/command/name'
16
+ require 'atig/command/autofix'
17
+ require 'atig/command/limit'
18
+ require 'atig/command/search'
19
+ require 'atig/command/refresh'
20
+ require 'atig/command/spam'
21
+ require 'atig/command/dm'
@@ -0,0 +1,58 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+ require 'atig/command/command'
3
+ require 'atig/levenshtein'
4
+
5
+ begin
6
+ require 'jcode'
7
+ rescue LoadError
8
+ end
9
+
10
+ module Atig
11
+ module Command
12
+ class Autofix < Atig::Command::Command
13
+ def initialize(*args); super end
14
+ def command_name; /(?:autofix|topic|overwwrite)!?/ end
15
+
16
+ def distance(s1, s2)
17
+ c1 = s1.split(//)
18
+ c2 = s2.split(//)
19
+ distance = Atig::Levenshtein.levenshtein c1, c2
20
+ distance.to_f / [ c1.size, c2.size ].max
21
+ end
22
+
23
+ def fix?(command, text, prev)
24
+ command[-1,1] == '!' or distance(text, prev.status.text) < 0.5
25
+ end
26
+
27
+ def action(target, mesg, command, args)
28
+ if args.empty?
29
+ yield "/me #{command} blah blah"
30
+ return
31
+ end
32
+ text = mesg.split(" ", 2)[1]
33
+ q = gateway.output_message(:status => text)
34
+
35
+ prev,*_ = db.statuses.find_by_user( db.me, :limit => 1)
36
+
37
+ unless fix?(command, q[:status], prev) then
38
+ api.delay(0, :retry=>3) do|t|
39
+ ret = t.post("statuses/update", q)
40
+ gateway.update_status ret, target
41
+ end
42
+ else
43
+ api.delay(0, :retry=>3) do|t|
44
+ yield "Similar update in previous. Conclude that it has error."
45
+ yield "And overwrite previous as new status: #{q[:status]}"
46
+
47
+ ret = t.post("statuses/update", q)
48
+ gateway.update_status ret, target
49
+ t.post("statuses/destroy/#{prev.status.id}")
50
+ db.statuses.transaction{|d|
51
+ d.remove_by_id prev.id
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,24 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+ module Atig
3
+ module Command
4
+ class Command
5
+ attr_reader :gateway, :api, :db, :opts
6
+ def initialize(context, gateway, api, db)
7
+ @log = context.log
8
+ @opts = context.opts
9
+ @gateway = gateway
10
+ @api = api
11
+ @db = db
12
+ @gateway.ctcp_action(*command_name) do |target, mesg, command, args|
13
+ action(target, mesg, command, args){|m|
14
+ gateway[target].notify m
15
+ }
16
+ end
17
+ end
18
+
19
+ def find_by_tid(tid)
20
+ @db.statuses.find_by_tid tid
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,95 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ class FakeGateway
4
+ attr_reader :names,:action,:filtered,:updated, :notified
5
+
6
+ def initialize(channel)
7
+ @channel = channel
8
+ end
9
+
10
+ def ctcp_action(*names, &action)
11
+ @names = names
12
+ @action = action
13
+ end
14
+
15
+ def output_message(m); @filtered = m end
16
+
17
+ def update_status(*args); @updated = args end
18
+
19
+ def [](name)
20
+ @notified = name
21
+ @channel
22
+ end
23
+
24
+ def server_name; "server-name" end
25
+ end
26
+
27
+ class FakeScheduler
28
+ def initialize(api)
29
+ @api = api
30
+ end
31
+
32
+ def delay(interval,opt={},&f)
33
+ f.call @api
34
+ end
35
+
36
+ def limit; @api.limit end
37
+ def remain; @api.remain end
38
+ def reset; @api.reset end
39
+ end
40
+
41
+ class FakeDb
42
+ attr_reader :statuses, :followings,:lists, :me
43
+ def initialize(statuses, followings,lists, me)
44
+ @statuses = statuses
45
+ @followings = followings
46
+ @lists = lists
47
+ @me = me
48
+ end
49
+
50
+ def transaction(&f)
51
+ f.call self
52
+ end
53
+ end
54
+
55
+ class FakeDbEntry
56
+ def initialize(name)
57
+ @name = name
58
+ end
59
+
60
+ def transaction(&f)
61
+ f.call self
62
+ end
63
+ end
64
+
65
+ module CommandHelper
66
+ def init(klass)
67
+ @log = mock 'log'
68
+ @opts = Atig::Option.new({})
69
+ context = OpenStruct.new :log=>@log, :opts=>@opts
70
+
71
+ @channel = mock 'channel'
72
+ @gateway = FakeGateway.new @channel
73
+ @api = mock 'api'
74
+ @statuses = FakeDbEntry.new 'status DB'
75
+ @followings = FakeDbEntry.new 'followings DB'
76
+ @lists = {
77
+ "A" => FakeDbEntry.new('list A'),
78
+ "B" => FakeDbEntry.new('list B')
79
+ }
80
+
81
+ @me = user 1,'me'
82
+ @db = FakeDb.new @statuses, @followings, @lists, @me
83
+ @command = klass.new context, @gateway, FakeScheduler.new(@api), @db
84
+ end
85
+
86
+ def call(channel, command, args)
87
+ @gateway.action.call channel, "#{command} #{args.join(' ')}", command, args
88
+ end
89
+
90
+ def stub_status(key, hash)
91
+ @statuses.stub!(key).and_return{|arg,*_|
92
+ hash.fetch(arg, hash[:default])
93
+ }
94
+ end
95
+ end