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
@@ -0,0 +1,62 @@
1
+ require 'atig/gateway/session'
2
+
3
+ Atig::Gateway::Session.agents = [
4
+ Atig::Agent::FullList,
5
+ Atig::Agent::Following,
6
+ Atig::Agent::ListStatus,
7
+ Atig::Agent::Mention,
8
+ Atig::Agent::Dm,
9
+ Atig::Agent::Timeline,
10
+ Atig::Agent::Cleanup,
11
+ Atig::Agent::UserStream,
12
+ ]
13
+
14
+ Atig::Gateway::Session.ifilters = [
15
+ Atig::IFilter::Retweet,
16
+ Atig::IFilter::RetweetTime,
17
+ Atig::IFilter::Utf7,
18
+ Atig::IFilter::Sanitize,
19
+ Atig::IFilter::ExpandUrl,
20
+ Atig::IFilter::Strip.new(%w{ *tw* *Sh*}),
21
+ Atig::IFilter::Tid,
22
+ Atig::IFilter::Sid
23
+ ]
24
+
25
+ Atig::Gateway::Session.ofilters = [
26
+ Atig::OFilter::EscapeUrl,
27
+ Atig::OFilter::ShortUrl,
28
+ Atig::OFilter::Geo,
29
+ Atig::OFilter::Footer,
30
+ ]
31
+
32
+ Atig::Gateway::Session.commands = [
33
+ Atig::Command::Retweet,
34
+ Atig::Command::Reply,
35
+ Atig::Command::User,
36
+ Atig::Command::Favorite,
37
+ Atig::Command::Uptime,
38
+ Atig::Command::Destroy,
39
+ Atig::Command::Status,
40
+ Atig::Command::Thread,
41
+ Atig::Command::Time,
42
+ Atig::Command::Version,
43
+ Atig::Command::UserInfo,
44
+ Atig::Command::Whois,
45
+ Atig::Command::Option,
46
+ Atig::Command::Location,
47
+ Atig::Command::Name,
48
+ Atig::Command::Autofix,
49
+ Atig::Command::Limit,
50
+ Atig::Command::Search,
51
+ Atig::Command::Refresh,
52
+ Atig::Command::Spam,
53
+ Atig::Command::Dm,
54
+ ]
55
+
56
+ Atig::Gateway::Session.channels = [
57
+ Atig::Channel::Timeline,
58
+ Atig::Channel::Mention,
59
+ Atig::Channel::Dm,
60
+ Atig::Channel::List,
61
+ Atig::Channel::Retweet,
62
+ ]
@@ -0,0 +1,99 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ module Atig
4
+ module Gateway
5
+ class Channel
6
+ class << self
7
+ def delegate(*ids)
8
+ ids.each do|id|
9
+ module_eval <<END
10
+ def on_#{id}(*args)
11
+ @handler && @handler.respond_to?(:on_#{id}) && @handler.on_#{id}(*args)
12
+ end
13
+ END
14
+ end
15
+ end
16
+ end
17
+
18
+ include Net::IRC::Constants
19
+
20
+ MAX_MODE_PARAMS = 3
21
+
22
+ def initialize(opts={})
23
+ @session = opts[:session]
24
+ @name = opts[:name]
25
+ @filters = opts[:filters] || []
26
+ @opts = opts[:opts]
27
+ @prefix = opts[:prefix]
28
+ @nick = opts[:nick]
29
+ @handler = opts[:handler]
30
+ end
31
+
32
+ delegate :invite, :kick, :who
33
+
34
+ def join_me
35
+ @session.post @prefix, JOIN, @name
36
+ @session.post @session.server_name, MODE, @name, "+mto", @nick
37
+ @session.post @session.server_name, MODE, @name, "+q", @nick
38
+ end
39
+
40
+ def part_me(msg)
41
+ @session.post @prefix, PART, @name, msg
42
+ end
43
+
44
+ def message(entry, command = PRIVMSG)
45
+ GC.start
46
+ user = entry.user
47
+ screen_name = user.screen_name
48
+ prefix = prefix user
49
+ str = run_filters entry
50
+
51
+ @session.post prefix, command, @name, str
52
+ end
53
+
54
+ def notify(str)
55
+ @session.post @session.server_name, NOTICE, @name, str.gsub(/\r\n|[\r\n]/, " ")
56
+ end
57
+
58
+ def topic(entry)
59
+ str = run_filters entry
60
+ @session.post @prefix, TOPIC, @name, str
61
+ end
62
+
63
+ def join(users)
64
+ params = []
65
+ users.each do |user|
66
+ prefix = prefix(user)
67
+ @session.post prefix, JOIN, @name
68
+ case
69
+ when user.protected
70
+ params << ["v", prefix.nick]
71
+ when user.only
72
+ params << ["o", prefix.nick]
73
+ end
74
+ next if params.size < MAX_MODE_PARAMS
75
+
76
+ @session.post @session.server_name, MODE, @name, "+#{params.map {|m,_| m }.join}", *params.map {|_,n| n}
77
+ params = []
78
+ end
79
+ @session.post @session.server_name, MODE, @name, "+#{params.map {|m,_| m }.join}", *params.map {|_,n| n}
80
+ end
81
+
82
+ def part(users)
83
+ users.each do|u|
84
+ @session.post prefix(u), PART, @name, ""
85
+ end
86
+ end
87
+
88
+ private
89
+ def run_filters(entry)
90
+ status = entry.status.merge(:tid=>entry.tid, :sid=>entry.sid)
91
+ @filters.inject(status) {|x, f| f.call x }.text
92
+ end
93
+
94
+ def prefix(u)
95
+ @session.prefix u
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,326 @@
1
+ # -*- mode:ruby; coding:utf-8 -*-
2
+
3
+ require 'fileutils'
4
+ require 'atig/util'
5
+ require "ostruct"
6
+ require "time"
7
+ require 'yaml'
8
+ require 'atig/url_escape'
9
+ require 'atig/twitter'
10
+ require 'atig/search_twitter'
11
+ require 'atig/stream'
12
+ require 'atig/oauth'
13
+ require 'atig/db/db'
14
+ require 'atig/gateway/channel'
15
+ require 'atig/option'
16
+
17
+ begin
18
+ require 'continuation'
19
+ rescue LoadError => e
20
+ end
21
+
22
+ module Atig
23
+ module Gateway
24
+ class Session < Net::IRC::Server::Session
25
+ include Util
26
+
27
+ class << self
28
+ def self.class_writer(*ids)
29
+ ids.each do|id|
30
+ module_eval <<END
31
+ def #{id}=(arg)
32
+ @@#{id} = arg
33
+ end
34
+ END
35
+ end
36
+ end
37
+
38
+ class_writer :commands, :agents, :ifilters, :ofilters, :channels
39
+ end
40
+
41
+ def initialize(*args)
42
+ super
43
+ @on_message = nil
44
+ end
45
+
46
+ def post(*args)
47
+ super
48
+ end
49
+
50
+ def update_status(ret, target, msg='')
51
+ @db.transaction do|db|
52
+ db.statuses.add(:source => :me, :status => ret, :user => ret.user )
53
+ end
54
+
55
+ msg = "(#{msg})" unless msg.empty?
56
+ self[target].notify "Status updated #{msg}"
57
+ end
58
+
59
+ def channel(name,opts={})
60
+ opts.update(:session => self,
61
+ :name => name,
62
+ :filters => @ifilters,
63
+ :prefix => @prefix,
64
+ :nick => @nick,
65
+ :opts => @opts)
66
+ channel = Channel.new opts
67
+ @channels[name] = channel
68
+ channel
69
+ end
70
+
71
+ def [](name)
72
+ @channels[name]
73
+ end
74
+
75
+ def output_message(query)
76
+ @ofilters.inject(query) {|x, f| f.call x }
77
+ end
78
+
79
+ def ctcp_action(*commands, &block)
80
+ commands.each do |command|
81
+ @ctcp_actions[command] = block
82
+ end
83
+ end
84
+
85
+ def prefix(u)
86
+ nick = u.screen_name
87
+ nick = "@#{nick}" if @opts.athack
88
+ user = "id=%.9d" % u.id
89
+ host = "twitter"
90
+ host += "/protected" if u.protected
91
+
92
+ Net::IRC::Prefix.new("#{nick}!#{user}@#{host}")
93
+ end
94
+
95
+ def topic(entry)
96
+ @channels.each{|_, ch| ch.topic entry }
97
+ end
98
+
99
+ protected
100
+ def on_message(m)
101
+ GC.start
102
+ @on_message.call(m) if @on_message
103
+ end
104
+
105
+ def on_user(m)
106
+ super
107
+ @thread_group = ThreadGroup.new
108
+ @thread_group.add Thread.current
109
+ @ctcp_actions = {}
110
+ @channels = {}
111
+ load_config
112
+
113
+ @opts = Atig::Option.parse @real
114
+ context = OpenStruct.new(:log=>@log, :opts=>@opts)
115
+
116
+ oauth = OAuth.new(context, @nick)
117
+ unless oauth.verified? then
118
+ channel = channel '#oauth'
119
+ channel.join_me
120
+ channel.notify "Please approve me at #{oauth.url}"
121
+ callcc{|cc|
122
+ @on_message = lambda{|x|
123
+ if x.command.downcase == 'privmsg' then
124
+ _, mesg = *x.params
125
+ if oauth.verify(mesg.strip)
126
+ channel.part_me "Verified"
127
+ save_config
128
+ @on_message = nil
129
+ cc.call
130
+ end
131
+ end
132
+ return true
133
+ }
134
+ return
135
+ }
136
+ end
137
+
138
+ log :debug, "initialize Twitter"
139
+ twitter = Twitter.new context, oauth.access
140
+ search = SearchTwitter.new context
141
+ stream = Stream.new context, @nick,@pass if @opts.stream
142
+ @api = Scheduler.new context, twitter, search, stream
143
+
144
+ log :debug, "initialize filter"
145
+ @ifilters = run_new @@ifilters, context
146
+ @ofilters = run_new @@ofilters, context
147
+
148
+ @api.delay(0) do|t|
149
+ me = t.post "account/update_profile"
150
+ unless me then
151
+ log :info, <<END
152
+ Failed to access API.
153
+ Please check Twitter Status <http://status.twitter.com/> and try again later.
154
+ END
155
+ finish
156
+ end
157
+ @prefix = prefix me
158
+ @user = @prefix.user
159
+ @host = @prefix.host
160
+
161
+ post server_name, MODE, @nick, "+o"
162
+
163
+ @db = Atig::Db::Db.new context, :me=>me, :size=> 100
164
+ run_new @@commands, context, self, @api, @db
165
+ run_new @@agents , context, @api, @db
166
+ run_new @@channels, context, self, @db
167
+
168
+ @db.statuses.add :user => me, :source => :me, :status => me.status
169
+ end
170
+ end
171
+
172
+ def run_new(klasses,*args)
173
+ (klasses || []).map do|klass|
174
+ if klass.respond_to?(:new)
175
+ klass.new(*args)
176
+ else
177
+ klass
178
+ end
179
+ end
180
+ end
181
+
182
+ def on_disconnected
183
+ (@thread_group.list - [Thread.current]).each {|t| t.kill }
184
+ end
185
+
186
+ CONFIG_FILE = File.expand_path("~/.atig/oauth")
187
+ def save_config
188
+ FileUtils.mkdir_p File.dirname(CONFIG_FILE)
189
+ File.open(CONFIG_FILE, "w") {|io|
190
+ YAML.dump(OAuth.dump,io)
191
+ }
192
+ FileUtils.chmod 0600, CONFIG_FILE
193
+ end
194
+
195
+ def load_config
196
+ FileUtils.mkdir_p File.dirname(CONFIG_FILE)
197
+ OAuth.load(YAML.load_file(CONFIG_FILE)) rescue nil
198
+ end
199
+
200
+ def on_privmsg(m)
201
+ target, mesg = *m.params
202
+ m.ctcps.each {|ctcp| on_ctcp(target, ctcp) } if m.ctcp?
203
+
204
+ case
205
+ when mesg.empty?
206
+ return
207
+ when mesg.sub!(/\A +/, "")
208
+ on_ctcp_action(target, mesg)
209
+ when target[0] != ?#
210
+ channel target
211
+ on_ctcp_action(target, "dm #{target} #{mesg}")
212
+ when (@opts.old_style_reply and mesg =~ /\A@(?>([A-Za-z0-9_]{1,15}))[^A-Za-z0-9_]/)
213
+ on_ctcp_action(target, "reply #{$1} #{mesg}")
214
+ else
215
+ on_ctcp_action(target, "status #{mesg}")
216
+ end
217
+ end
218
+
219
+ def on_ctcp(target, mesg)
220
+ if mesg.respond_to? :encoding!
221
+ mesg.encoding! "UTF-8"
222
+ end
223
+ type, mesg = mesg.split(" ", 2)
224
+ method = "on_ctcp_#{type.downcase}".to_sym
225
+ send(method, target, mesg) if respond_to? method, true
226
+ end
227
+
228
+ def on_ctcp_action(target, mesg)
229
+ command, *args = mesg.split(" ")
230
+ last_match = nil
231
+ commond = command.to_s.downcase
232
+ _, action = @ctcp_actions.find{|define, f|
233
+ r = (define === command)
234
+ last_match = Regexp.last_match
235
+ r
236
+ }
237
+ if action then
238
+ safe {
239
+ action.call(target, mesg, last_match || command, args)
240
+ }
241
+ else
242
+ self[target].notify "[atig.rb] CTCP ACTION COMMANDS:"
243
+ @ctcp_actions.keys.each do |c|
244
+ self[target].notify c.to_s
245
+ end
246
+ end
247
+ end
248
+
249
+ def on_invite(m)
250
+ nick, channel = *m.params
251
+ if not nick.screen_name? or @db.me.screen_name.casecmp(nick).zero?
252
+ post server_name, ERR_NOSUCHNICK, nick, "No such nick: #{nick}" # or yourself
253
+ return
254
+ end
255
+
256
+ unless @channels.key? channel
257
+ post server_name, ERR_NOSUCHNICK, nick, "No such channel: #{channel}"
258
+ return
259
+ end
260
+
261
+ @api.delay(0){|api| @channels[channel].on_invite(api, nick) }
262
+ end
263
+
264
+ def on_kick(m)
265
+ channel, nick, msg = *m.params
266
+
267
+ if not nick.screen_name? or @db.me.screen_name.casecmp(nick).zero?
268
+ post server_name, ERR_NOSUCHNICK, nick, "No such nick: #{nick}" # or yourself
269
+ return
270
+ end
271
+
272
+ unless @channels.key? channel
273
+ post server_name, ERR_NOSUCHNICK, nick, "No such channel: #{channel}"
274
+ return
275
+ end
276
+
277
+ @api.delay(0){|api| @channels[channel].on_kick(api, nick) }
278
+ end
279
+
280
+ def on_whois(m)
281
+ nick = m.params[0]
282
+ unless nick.screen_name?
283
+ post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
284
+ return
285
+ end
286
+ on_ctcp_action(nil, "whois #{nick}")
287
+ end
288
+
289
+ def on_topic(m)
290
+ channel,topic = *m.params
291
+ on_ctcp_action(channel, "topic #{topic}")
292
+ end
293
+
294
+ def on_who(m)
295
+ channel = m.params[0]
296
+
297
+ unless @channels.key? channel
298
+ post server_name, ERR_NOSUCHNICK, nick, "No such channel: #{channel}"
299
+ return
300
+ end
301
+ @channels[channel].on_who do|user|
302
+ # "<channel> <user> <host> <server> <nick>
303
+ # ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
304
+ # :<hopcount> <real name>"
305
+ prefix = prefix(user)
306
+ server = 'twitter.com'
307
+ mode = case prefix.nick
308
+ when @nick then "~"
309
+ else "+"
310
+ end
311
+ real = user.name
312
+ post server_name, RPL_WHOREPLY, @nick, channel, prefix.user, prefix.host, server, prefix.nick, "H*#{mode}", "1 #{real}"
313
+ end
314
+ post server_name, RPL_ENDOFWHO, @nick, channel
315
+ end
316
+
317
+ def available_user_modes
318
+ "o"
319
+ end
320
+
321
+ def available_channel_modes
322
+ "mntiovah"
323
+ end
324
+ end
325
+ end
326
+ end