atig 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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