net-irc 0.0.5 → 0.0.6

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.
data/ChangeLog CHANGED
@@ -1,3 +1,10 @@
1
+ 2009-02-01 SATOH Hiroh <cho45@lowreal.net>
2
+
3
+ * [bug]:
4
+ Fixed to work on ruby1.9.1
5
+ * [release]:
6
+ Released 0.0.6
7
+
1
8
  2008-07-06 SATOH Hiroh <cho45@lowreal.net>
2
9
 
3
10
  * [interface]:
data/Rakefile CHANGED
@@ -41,7 +41,7 @@ task :package => [:clean]
41
41
  Spec::Rake::SpecTask.new do |t|
42
42
  t.spec_opts = ['--options', "spec/spec.opts"]
43
43
  t.spec_files = FileList['spec/*_spec.rb']
44
- t.rcov = true
44
+ #t.rcov = true
45
45
  end
46
46
 
47
47
  spec = Gem::Specification.new do |s|
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+
3
+
4
+ $LOAD_PATH << "lib"
5
+ $LOAD_PATH << "../lib"
6
+
7
+ require "rubygems"
8
+ require "net/irc"
9
+
10
+ require "pp"
11
+
12
+ class EchoBot < Net::IRC::Client
13
+ def initialize(*args)
14
+ super
15
+ end
16
+
17
+ def on_rpl_welcome(m)
18
+ post JOIN, "#bot_test"
19
+ end
20
+
21
+ def on_privmsg(m)
22
+ post NOTICE, m[0], m[1]
23
+ end
24
+ end
25
+
26
+ EchoBot.new("foobar", "6667", {
27
+ :nick => "foobartest",
28
+ :user => "foobartest",
29
+ :real => "foobartest",
30
+ }).start
31
+
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << "lib"
4
+ $LOAD_PATH << "../lib"
5
+
6
+ $KCODE = "u" # json use this
7
+
8
+ require "rubygems"
9
+ require "net/irc"
10
+ require "sdbm"
11
+ require "tmpdir"
12
+ require "uri"
13
+ require "mechanize"
14
+ require "rexml/document"
15
+
16
+ class GmailNotifier < Net::IRC::Server::Session
17
+ def server_name
18
+ "gmail"
19
+ end
20
+
21
+ def server_version
22
+ "0.0.0"
23
+ end
24
+
25
+ def main_channel
26
+ "#gmail"
27
+ end
28
+
29
+ def initialize(*args)
30
+ super
31
+ @agent = WWW::Mechanize.new
32
+ end
33
+
34
+ def on_user(m)
35
+ super
36
+ post @prefix, JOIN, main_channel
37
+ post server_name, MODE, main_channel, "+o", @prefix.nick
38
+
39
+ @real, *@opts = @opts.name || @real.split(/\s+/)
40
+ @opts ||= []
41
+
42
+ start_observer
43
+ end
44
+
45
+ def on_disconnected
46
+ @observer.kill rescue nil
47
+ end
48
+
49
+ def on_privmsg(m)
50
+ super
51
+ case m[1]
52
+ when 'list'
53
+ check_mail
54
+ end
55
+ end
56
+
57
+ def on_ctcp(target, message)
58
+ end
59
+
60
+ def on_whois(m)
61
+ end
62
+
63
+ def on_who(m)
64
+ end
65
+
66
+ def on_join(m)
67
+ end
68
+
69
+ def on_part(m)
70
+ end
71
+
72
+ private
73
+ def start_observer
74
+ @observer = Thread.start do
75
+ Thread.abort_on_exception = true
76
+ loop do
77
+ begin
78
+ @agent.auth(@real, @pass)
79
+ page = @agent.get(URI.parse("https://gmail.google.com/gmail/feed/atom"))
80
+ feed = REXML::Document.new page.body
81
+ db = SDBM.open("#{Dir.tmpdir}/#{@real}.db", 0666)
82
+ feed.get_elements('/feed/entry').reverse.each do |item|
83
+ id = item.text('id')
84
+ if db.include?(id)
85
+ next
86
+ else
87
+ db[id] = "1"
88
+ end
89
+ post server_name, PRIVMSG, main_channel, "Subject: #{item.text('title')} From: #{item.text('author/name')}"
90
+ post server_name, PRIVMSG, main_channel, "#{item.text('summary')}"
91
+ end
92
+ rescue Exception => e
93
+ @log.error e.inspect
94
+ ensure
95
+ db.close rescue nil
96
+ end
97
+ sleep 60 * 5
98
+ end
99
+ end
100
+ end
101
+
102
+ def check_mail
103
+ begin
104
+ @agent.auth(@real, @pass)
105
+ page = @agent.get(URI.parse("https://gmail.google.com/gmail/feed/atom"))
106
+ feed = REXML::Document.new page.body
107
+ db = SDBM.open("#{Dir.tmpdir}/#{@real}.db", 0666)
108
+ feed.get_elements('/feed/entry').reverse.each do |item|
109
+ id = item.text('id')
110
+ if db.include?(id)
111
+ #next
112
+ else
113
+ db[id] = "1"
114
+ end
115
+ post server_name, PRIVMSG, main_channel, "Subject: #{item.text('title')} From: #{item.text('author/name')}"
116
+ post server_name, PRIVMSG, main_channel, "#{item.text('summary')}"
117
+ end
118
+ rescue Exception => e
119
+ @log.error e.inspect
120
+ ensure
121
+ db.close rescue nil
122
+ end
123
+ end
124
+ end
125
+
126
+ if __FILE__ == $0
127
+ require "optparse"
128
+
129
+ opts = {
130
+ :port => 16800,
131
+ :host => "localhost",
132
+ :log => nil,
133
+ :debug => false,
134
+ :foreground => false,
135
+ }
136
+
137
+ OptionParser.new do |parser|
138
+ parser.instance_eval do
139
+ self.banner = <<-EOB.gsub(/^\t+/, "")
140
+ Usage: #{$0} [opts]
141
+
142
+ EOB
143
+
144
+ separator ""
145
+
146
+ separator "Options:"
147
+ on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
148
+ opts[:port] = port
149
+ end
150
+
151
+ on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
152
+ opts[:host] = host
153
+ end
154
+
155
+ on("-l", "--log LOG", "log file") do |log|
156
+ opts[:log] = log
157
+ end
158
+
159
+ on("--debug", "Enable debug mode") do |debug|
160
+ opts[:log] = $stdout
161
+ opts[:debug] = true
162
+ end
163
+
164
+ on("-f", "--foreground", "run foreground") do |foreground|
165
+ opts[:log] = $stdout
166
+ opts[:foreground] = true
167
+ end
168
+
169
+ parse!(ARGV)
170
+ end
171
+ end
172
+
173
+ opts[:logger] = Logger.new(opts[:log], "daily")
174
+ opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
175
+
176
+ def daemonize(foreground=false)
177
+ trap("SIGINT") { exit! 0 }
178
+ trap("SIGTERM") { exit! 0 }
179
+ trap("SIGHUP") { exit! 0 }
180
+ return yield if $DEBUG || foreground
181
+ Process.fork do
182
+ Process.setsid
183
+ Dir.chdir "/"
184
+ File.open("/dev/null") {|f|
185
+ STDIN.reopen f
186
+ STDOUT.reopen f
187
+ STDERR.reopen f
188
+ }
189
+ yield
190
+ end
191
+ exit! 0
192
+ end
193
+
194
+ daemonize(opts[:debug] || opts[:foreground]) do
195
+ Net::IRC::Server.new(opts[:host], opts[:port], GmailNotifier, opts).start
196
+ end
197
+ end
198
+
199
+ # Local Variables:
200
+ # coding: utf-8
201
+ # End:
@@ -191,7 +191,7 @@ if __FILE__ == $0
191
191
  require "optparse"
192
192
 
193
193
  opts = {
194
- :port => 16700,
194
+ :port => 16702,
195
195
  :host => "localhost",
196
196
  :log => nil,
197
197
  :debug => false,
@@ -0,0 +1,733 @@
1
+ #!/usr/bin/env ruby
2
+ =begin
3
+ # hig.rb
4
+
5
+ ## Launch
6
+
7
+ $ ruby hig.rb
8
+
9
+ If you want to help:
10
+
11
+ $ ruby hig.rb --help
12
+
13
+ ## Configuration
14
+
15
+ Options specified by after irc realname.
16
+
17
+ Configuration example for Tiarra ( http://coderepos.org/share/wiki/Tiarra ).
18
+
19
+ haiku {
20
+ host: localhost
21
+ port: 16679
22
+ name: username@example.com athack jabber=username@example.com:jabberpasswd tid=10 ratio=10:3:5
23
+ password: password on Haiku
24
+ in-encoding: utf8
25
+ out-encoding: utf8
26
+ }
27
+
28
+ ### athack
29
+
30
+ If `athack` client option specified,
31
+ all nick in join message is leading with @.
32
+
33
+ So if you complemente nicks (e.g. Irssi),
34
+ it's good for Twitter like reply command (@nick).
35
+
36
+ In this case, you will see torrent of join messages after connected,
37
+ because NAMES list can't send @ leading nick (it interpreted op.)
38
+
39
+ ### tid=<color>
40
+
41
+ Apply id to each message for make favorites by CTCP ACTION.
42
+
43
+ /me fav id
44
+
45
+ <color> can be
46
+
47
+ 0 => white
48
+ 1 => black
49
+ 2 => blue navy
50
+ 3 => green
51
+ 4 => red
52
+ 5 => brown maroon
53
+ 6 => purple
54
+ 7 => orange olive
55
+ 8 => yellow
56
+ 9 => lightgreen lime
57
+ 10 => teal
58
+ 11 => lightcyan cyan aqua
59
+ 12 => lightblue royal
60
+ 13 => pink lightpurple fuchsia
61
+ 14 => grey
62
+ 15 => lightgrey silver
63
+
64
+
65
+ ### jabber=<jid>:<pass>
66
+
67
+ If `jabber=<jid>:<pass>` option specified,
68
+ use jabber to get friends timeline.
69
+
70
+ You must setup im notifing settings in the site and
71
+ install "xmpp4r-simple" gem.
72
+
73
+ $ sudo gem install xmpp4r-simple
74
+
75
+ Be careful for managing password.
76
+
77
+ ### alwaysim
78
+
79
+ Use IM instead of any APIs (e.g. post)
80
+
81
+ ### ratio=<timeline>:<friends>:<channel>
82
+
83
+ ## License
84
+
85
+ Ruby's by cho45
86
+
87
+ =end
88
+
89
+ $LOAD_PATH << "lib"
90
+ $LOAD_PATH << "../lib"
91
+
92
+ $KCODE = "u" # json use this
93
+
94
+ require "rubygems"
95
+ require "net/irc"
96
+ require "net/http"
97
+ require "uri"
98
+ require "json"
99
+ require "socket"
100
+ require "time"
101
+ require "logger"
102
+ require "yaml"
103
+ require "pathname"
104
+ require "cgi"
105
+ require "digest/md5"
106
+
107
+ Net::HTTP.version_1_2
108
+
109
+ class HaikuIrcGateway < Net::IRC::Server::Session
110
+ def server_name
111
+ "haikugw"
112
+ end
113
+
114
+ def server_version
115
+ "0.0.0"
116
+ end
117
+
118
+ def main_channel
119
+ "#haiku"
120
+ end
121
+
122
+ def api_base
123
+ URI(ENV["HAIKU_BASE"] || "http://h.hatena.ne.jp/api/")
124
+ end
125
+
126
+ def api_source
127
+ "hig.rb"
128
+ end
129
+
130
+ def jabber_bot_id
131
+ nil
132
+ end
133
+
134
+ def hourly_limit
135
+ 60
136
+ end
137
+
138
+ class ApiFailed < StandardError; end
139
+
140
+ def initialize(*args)
141
+ super
142
+ @channels = {}
143
+ @user_agent = "#{self.class}/#{server_version} (hig.rb)"
144
+ @map = nil
145
+ @counters = {} # for jabber fav
146
+ end
147
+
148
+ def on_user(m)
149
+ super
150
+ post @prefix, JOIN, main_channel
151
+ post server_name, MODE, main_channel, "+o", @prefix.nick
152
+
153
+ @real, *@opts = @opts.name || @real.split(/\s+/)
154
+ @opts = @opts.inject({}) {|r,i|
155
+ key, value = i.split("=")
156
+ r.update(key => value)
157
+ }
158
+ @tmap = TypableMap.new
159
+
160
+ if @opts["jabber"]
161
+ jid, pass = @opts["jabber"].split(":", 2)
162
+ @opts["jabber"].replace("jabber=#{jid}:********")
163
+ if jabber_bot_id
164
+ begin
165
+ require "xmpp4r-simple"
166
+ start_jabber(jid, pass)
167
+ rescue LoadError
168
+ log "Failed to start Jabber."
169
+ log 'Installl "xmpp4r-simple" gem or check your id/pass.'
170
+ finish
171
+ end
172
+ else
173
+ @opts.delete("jabber")
174
+ log "This gateway does not support Jabber bot."
175
+ end
176
+ end
177
+
178
+ log "Client Options: #{@opts.inspect}"
179
+ @log.info "Client Options: #{@opts.inspect}"
180
+
181
+ timeline_ratio, friends_ratio, channel_ratio = (@opts["ratio"] || "10:3:5").split(":").map {|ratio| ratio.to_i }
182
+ footing = (timeline_ratio + friends_ratio + channel_ratio).to_f
183
+
184
+ @timeline = []
185
+ @check_follows_thread = Thread.start do
186
+ loop do
187
+ begin
188
+ check_friends
189
+ check_keywords
190
+ rescue ApiFailed => e
191
+ @log.error e.inspect
192
+ rescue Exception => e
193
+ @log.error e.inspect
194
+ e.backtrace.each do |l|
195
+ @log.error "\t#{l}"
196
+ end
197
+ end
198
+ sleep freq(friends_ratio / footing)
199
+ end
200
+ end
201
+
202
+ return if @opts["jabber"]
203
+
204
+ @check_timeline_thread = Thread.start do
205
+ sleep 10
206
+ loop do
207
+ begin
208
+ check_timeline
209
+ rescue ApiFailed => e
210
+ @log.error e.inspect
211
+ rescue Exception => e
212
+ @log.error e.inspect
213
+ e.backtrace.each do |l|
214
+ @log.error "\t#{l}"
215
+ end
216
+ end
217
+ sleep freq(timeline_ratio / footing)
218
+ end
219
+ end
220
+ end
221
+
222
+ def on_disconnected
223
+ @check_follows_thread.kill rescue nil
224
+ @check_timeline_thread.kill rescue nil
225
+ @im_thread.kill rescue nil
226
+ @im.disconnect rescue nil
227
+ end
228
+
229
+ def on_privmsg(m)
230
+ return on_ctcp(m[0], ctcp_decoding(m[1])) if m.ctcp?
231
+ retry_count = 3
232
+ ret = nil
233
+ target, message = *m.params
234
+ begin
235
+ channel = target.sub(/^#/, "")
236
+ reply = message[/\s+>(.+)$/, 1]
237
+ if !reply && @opts.key?("alwaysim") && @im && @im.connected? # in jabber mode, using jabber post
238
+ message = "##{channel} #{message}" unless "##{channel}" == main_channel
239
+ ret = @im.deliver(jabber_bot_id, message)
240
+ post "#{nick}!#{nick}@#{api_base.host}", TOPIC, channel, message
241
+ else
242
+ channel = "" if "##{channel}" == main_channel
243
+ rid = rid_for(reply) if reply
244
+ ret = api("statuses/update", {"status" => message, "in_reply_to_status_id" => rid, "keyword" => channel})
245
+ log "Status Updated via API"
246
+ end
247
+ raise ApiFailed, "API failed" unless ret
248
+ check_timeline
249
+ rescue => e
250
+ @log.error [retry_count, e.inspect].inspect
251
+ if retry_count > 0
252
+ retry_count -= 1
253
+ @log.debug "Retry to setting status..."
254
+ retry
255
+ else
256
+ log "Some Error Happened on Sending #{message}. #{e}"
257
+ end
258
+ end
259
+ end
260
+
261
+ def on_ctcp(target, message)
262
+ _, command, *args = message.split(/\s+/)
263
+ case command
264
+ when "list"
265
+ nick = args[0]
266
+ @log.debug([ nick, message ])
267
+ res = api("statuses/user_timeline", { "id" => nick }).reverse_each do |s|
268
+ @log.debug(s)
269
+ post nick, NOTICE, main_channel, s
270
+ end
271
+
272
+ unless res
273
+ post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
274
+ end
275
+ when "fav"
276
+ target = args[0]
277
+ st = @tmap[target]
278
+ id = rid_for(target)
279
+ if st || id
280
+ unless id
281
+ if @im && @im.connected?
282
+ # IM のときはいろいろめんどうなことする
283
+ nick, count = *st
284
+ pos = @counters[nick] - count
285
+ @log.debug "%p %s %d/%d => %d" % [
286
+ st,
287
+ nick,
288
+ count,
289
+ @counters[nick],
290
+ pos
291
+ ]
292
+ res = api("statuses/user_timeline", { "id" => nick })
293
+ raise ApiFailed, "#{nick} may be private mode" if res.empty?
294
+ if res[pos]
295
+ id = res[pos]["id"]
296
+ else
297
+ raise ApiFailed, "#{pos} of #{nick} is not found."
298
+ end
299
+ else
300
+ id = st["id"]
301
+ end
302
+ end
303
+ res = api("favorites/create/#{id}", {})
304
+ post nil, NOTICE, main_channel, "Fav: #{res["screen_name"]}: #{res["text"].gsub(URI.regexp(%w|http https|), "http...")}"
305
+ else
306
+ post nil, NOTICE, main_channel, "No such id or status #{target}"
307
+ end
308
+ when "link"
309
+ tid = args[0]
310
+ st = @tmap[tid]
311
+ if st
312
+ st["link"] = (api_base + "/#{st["user"]["screen_name"]}/statuses/#{st["id"]}").to_s unless st["link"]
313
+ post nil, NOTICE, main_channel, st["link"]
314
+ else
315
+ post nil, NOTICE, main_channel, "No such id #{tid}"
316
+ end
317
+ end
318
+ rescue ApiFailed => e
319
+ log e.inspect
320
+ end
321
+
322
+ def on_whois(m)
323
+ nick = m.params[0]
324
+ f = (@friends || []).find {|i| i["screen_name"] == nick }
325
+ if f
326
+ post nil, RPL_WHOISUSER, @nick, nick, nick, api_base.host, "*", "#{f["name"]} / #{f["description"]}"
327
+ post nil, RPL_WHOISSERVER, @nick, nick, api_base.host, api_base.to_s
328
+ post nil, RPL_WHOISIDLE, @nick, nick, "0", "seconds idle"
329
+ post nil, RPL_ENDOFWHOIS, @nick, nick, "End of WHOIS list"
330
+ else
331
+ post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
332
+ end
333
+ end
334
+
335
+ def on_join(m)
336
+ channels = m.params[0].split(/\s*,\s*/)
337
+ channels.each do |channel|
338
+ next if channel == main_channel
339
+ begin
340
+ api("keywords/create/#{URI.escape(channel.sub(/^#/, ""))}")
341
+ @channels[channel] = {
342
+ :read => []
343
+ }
344
+ post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, channel
345
+ rescue => e
346
+ @log.debug e.inspect
347
+ post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
348
+ end
349
+ end
350
+ end
351
+
352
+ def on_part(m)
353
+ channel = m.params[0]
354
+ return if channel == main_channel
355
+ @channels.delete(channel)
356
+ api("keywords/destroy/#{URI.escape(channel.sub(/^#/, ""))}")
357
+ post "#{@nick}!#{@nick}@#{api_base.host}", PART, channel
358
+ end
359
+
360
+ def on_who(m)
361
+ channel = m.params[0]
362
+ case
363
+ when channel == main_channel
364
+ # "<channel> <user> <host> <server> <nick>
365
+ # ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
366
+ # :<hopcount> <real name>"
367
+ @friends.each do |f|
368
+ user = nick = f["screen_name"]
369
+ host = serv = api_base.host
370
+ real = f["name"]
371
+ post nil, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}"
372
+ end
373
+ post nil, RPL_ENDOFWHO, @nick, channel
374
+ when @groups.key?(channel)
375
+ @groups[channel].each do |name|
376
+ f = @friends.find {|i| i["screen_name"] == name }
377
+ user = nick = f["screen_name"]
378
+ host = serv = api_base.host
379
+ real = f["name"]
380
+ post nil, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}"
381
+ end
382
+ post nil, RPL_ENDOFWHO, @nick, channel
383
+ else
384
+ post nil, ERR_NOSUCHNICK, @nick, nick, "No such nick/channel"
385
+ end
386
+ end
387
+
388
+ private
389
+ def check_timeline
390
+ api("statuses/friends_timeline").reverse_each do |s|
391
+ begin
392
+ id = s["id"]
393
+ next if id.nil? || @timeline.include?(id)
394
+ @timeline << id
395
+ nick = s["user"]["id"]
396
+ mesg = generate_status_message(s)
397
+
398
+ tid = @tmap.push(s)
399
+
400
+ @log.debug [id, nick, mesg]
401
+
402
+ channel = "##{s["keyword"]}"
403
+ case
404
+ when s["keyword"].match(/^id:/)
405
+ channel = main_channel
406
+ when !@channels.keys.include?(channel)
407
+ channel = main_channel
408
+ mesg = "%s = %s" % [s["keyword"], mesg]
409
+ end
410
+
411
+ if nick == @nick # 自分のときは topic に
412
+ post "#{nick}!#{nick}@#{api_base.host}", TOPIC, channel, mesg
413
+ else
414
+ if @opts["tid"]
415
+ message(nick, channel, "%s \x03%s [%s]" % [mesg, @opts["tid"], tid])
416
+ else
417
+ message(nick, channel, "%s" % [mesg, tid])
418
+ end
419
+ end
420
+ rescue => e
421
+ @log.debug "Error: %p" % e
422
+ end
423
+ end
424
+ @log.debug "@timeline.size = #{@timeline.size}"
425
+ @timeline = @timeline.last(100)
426
+ end
427
+
428
+ def generate_status_message(s)
429
+ mesg = s["text"]
430
+ mesg.sub!("#{s["keyword"]}=", "") unless s["keyword"] =~ /^id:/
431
+ mesg << " > #{s["in_reply_to_user_id"]}" unless s["in_reply_to_user_id"].empty?
432
+
433
+ @log.debug(mesg)
434
+ mesg
435
+ end
436
+
437
+ def check_friends
438
+ first = true unless @friends
439
+ @friends ||= []
440
+ friends = api("statuses/friends")
441
+ if first && !@opts.key?("athack")
442
+ @friends = friends
443
+ post nil, RPL_NAMREPLY, @nick, "=", main_channel, @friends.map{|i| "@#{i["screen_name"]}" }.join(" ")
444
+ post nil, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list"
445
+ else
446
+ prv_friends = @friends.map {|i| i["screen_name"] }
447
+ now_friends = friends.map {|i| i["screen_name"] }
448
+
449
+ # Twitter API bug?
450
+ return if !first && (now_friends.length - prv_friends.length).abs > 10
451
+
452
+ (now_friends - prv_friends).each do |join|
453
+ join = "@#{join}" if @opts.key?("athack")
454
+ post "#{join}!#{join}@#{api_base.host}", JOIN, main_channel
455
+ end
456
+ (prv_friends - now_friends).each do |part|
457
+ part = "@#{part}" if @opts.key?("athack")
458
+ post "#{part}!#{part}@#{api_base.host}", PART, main_channel, ""
459
+ end
460
+ @friends = friends
461
+ end
462
+ end
463
+
464
+ def check_keywords
465
+ keywords = api("statuses/keywords").map {|i| "##{i["title"]}" }
466
+ current = @channels.keys
467
+ current.delete main_channel
468
+
469
+ (current - keywords).each do |part|
470
+ @channels.delete(part)
471
+ post "#{@nick}!#{@nick}@#{api_base.host}", PART, part
472
+ end
473
+
474
+ (keywords - current).each do |join|
475
+ @channels[join] = {
476
+ :read => []
477
+ }
478
+ post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, join
479
+ end
480
+ end
481
+
482
+ def freq(ratio)
483
+ ret = 3600 / (hourly_limit * ratio).round
484
+ @log.debug "Frequency: #{ret}"
485
+ ret
486
+ end
487
+
488
+ def start_jabber(jid, pass)
489
+ @log.info "Logging-in with #{jid} -> jabber_bot_id: #{jabber_bot_id}"
490
+ @im = Jabber::Simple.new(jid, pass)
491
+ @im.add(jabber_bot_id)
492
+ @im_thread = Thread.start do
493
+ loop do
494
+ begin
495
+ @im.received_messages.each do |msg|
496
+ @log.debug [msg.from, msg.body]
497
+ if msg.from.strip == jabber_bot_id
498
+ # Haiku -> 'nick(id): msg'
499
+ body = msg.body.sub(/^(.+?)(?:\((.+?)\))?: /, "")
500
+ if Regexp.last_match
501
+ nick, id = Regexp.last_match.captures
502
+ body = CGI.unescapeHTML(body)
503
+
504
+ case
505
+ when nick == "投稿完了"
506
+ log "#{nick}: #{body}"
507
+ when nick == "チャンネル投稿完了"
508
+ log "#{nick}: #{body}"
509
+ when body =~ /^#([a-z_]+)\s+(.+)$/i
510
+ # channel message or not
511
+ message(id || nick, "##{Regexp.last_match[1]}", Regexp.last_match[2])
512
+ when nick == "photo" && body =~ %r|^http://haiku\.jp/user/([^/]+)/|
513
+ nick = Regexp.last_match[1]
514
+ message(nick, main_channel, body)
515
+ else
516
+ @counters[nick] ||= 0
517
+ @counters[nick] += 1
518
+ tid = @tmap.push([nick, @counters[nick]])
519
+ message(nick, main_channel, "%s \x03%s [%s]" % [body, @opts["tid"], tid])
520
+ end
521
+ end
522
+ end
523
+ end
524
+ rescue Exception => e
525
+ @log.error "Error on Jabber loop: #{e.inspect}"
526
+ e.backtrace.each do |l|
527
+ @log.error "\t#{l}"
528
+ end
529
+ end
530
+ sleep 1
531
+ end
532
+ end
533
+ end
534
+
535
+ def require_post?(path)
536
+ [
537
+ %r|/update|,
538
+ %r|/create|,
539
+ %r|/destroy|,
540
+ ].any? {|i| i === path }
541
+ end
542
+
543
+ def api(path, q={})
544
+ ret = {}
545
+ q["source"] ||= api_source
546
+
547
+ uri = api_base.dup
548
+ uri.path = "/api/#{path}.json"
549
+ uri.query = q.inject([]) {|r,(k,v)| v ? r << "#{k}=#{URI.escape(v, /[^:,-.!~*'()\w]/n)}" : r }.join("&")
550
+
551
+
552
+ req = nil
553
+ if require_post?(path)
554
+ req = Net::HTTP::Post.new(uri.path)
555
+ req.body = uri.query
556
+ else
557
+ req = Net::HTTP::Get.new(uri.request_uri)
558
+ end
559
+ req.basic_auth(@real, @pass)
560
+ req["User-Agent"] = @user_agent
561
+ req["If-Modified-Since"] = q["since"] if q.key?("since")
562
+
563
+ @log.debug uri.inspect
564
+ ret = Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
565
+
566
+ case ret
567
+ when Net::HTTPOK # 200
568
+ ret = JSON.parse(ret.body.gsub(/:'/, ':"').gsub(/',/, '",').gsub(/'(y(?:es)?|no?|true|false|null)'/, '"\1"'))
569
+ raise ApiFailed, "Server Returned Error: #{ret["error"]}" if ret.kind_of?(Hash) && ret["error"]
570
+ ret
571
+ when Net::HTTPNotModified # 304
572
+ []
573
+ when Net::HTTPBadRequest # 400
574
+ # exceeded the rate limitation
575
+ raise ApiFailed, "#{ret.code}: #{ret.message}"
576
+ else
577
+ raise ApiFailed, "Server Returned #{ret.code} #{ret.message}"
578
+ end
579
+ rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e
580
+ raise ApiFailed, e.inspect
581
+ end
582
+
583
+ def message(sender, target, str)
584
+ sender = "#{sender}!#{sender}@#{api_base.host}"
585
+ post sender, PRIVMSG, target, str.gsub(/\s+/, " ")
586
+ end
587
+
588
+ def log(str)
589
+ str.gsub!(/\n/, " ")
590
+ post server_name, NOTICE, main_channel, str
591
+ end
592
+
593
+ # return rid of most recent matched status with text
594
+ def rid_for(text)
595
+ target = Regexp.new(Regexp.quote(text.strip), "i")
596
+ status = api("statuses/friends_timeline").find {|i|
597
+ next false if i["user"]["name"] == @nick # 自分は除外
598
+ i["text"] =~ target
599
+ }
600
+
601
+ @log.debug "Looking up status contains #{text.inspect} -> #{status.inspect}"
602
+ status ? status["id"] : nil
603
+ end
604
+
605
+ class TypableMap < Hash
606
+ Roma = %w|k g ky gy s z sh j t d ch n ny h b p hy by py m my y r ry w v q|.unshift("").map {|consonant|
607
+ case
608
+ when consonant.size > 1, consonant == "y"
609
+ %w|a u o|
610
+ when consonant == "q"
611
+ %w|a i e o|
612
+ else
613
+ %w|a i u e o|
614
+ end.map {|vowel| "#{consonant}#{vowel}" }
615
+ }.flatten
616
+
617
+ def initialize(size=1)
618
+ @seq = Roma
619
+ @map = {}
620
+ @n = 0
621
+ @size = size
622
+ end
623
+
624
+ def generate(n)
625
+ ret = []
626
+ begin
627
+ n, r = n.divmod(@seq.size)
628
+ ret << @seq[r]
629
+ end while n > 0
630
+ ret.reverse.join
631
+ end
632
+
633
+ def push(obj)
634
+ id = generate(@n)
635
+ self[id] = obj
636
+ @n += 1
637
+ @n = @n % (@seq.size ** @size)
638
+ id
639
+ end
640
+ alias << push
641
+
642
+ def clear
643
+ @n = 0
644
+ super
645
+ end
646
+
647
+ private :[]=
648
+ undef update, merge, merge!, replace
649
+ end
650
+
651
+
652
+ end
653
+
654
+ if __FILE__ == $0
655
+ require "optparse"
656
+
657
+ opts = {
658
+ :port => 16679,
659
+ :host => "localhost",
660
+ :log => nil,
661
+ :debug => false,
662
+ :foreground => false,
663
+ }
664
+
665
+ OptionParser.new do |parser|
666
+ parser.instance_eval do
667
+ self.banner = <<-EOB.gsub(/^\t+/, "")
668
+ Usage: #{$0} [opts]
669
+
670
+ EOB
671
+
672
+ separator ""
673
+
674
+ separator "Options:"
675
+ on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
676
+ opts[:port] = port
677
+ end
678
+
679
+ on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
680
+ opts[:host] = host
681
+ end
682
+
683
+ on("-l", "--log LOG", "log file") do |log|
684
+ opts[:log] = log
685
+ end
686
+
687
+ on("--debug", "Enable debug mode") do |debug|
688
+ opts[:log] = $stdout
689
+ opts[:debug] = true
690
+ end
691
+
692
+ on("-f", "--foreground", "run foreground") do |foreground|
693
+ opts[:log] = $stdout
694
+ opts[:foreground] = true
695
+ end
696
+
697
+ on("-n", "--name [user name or email address]") do |name|
698
+ opts[:name] = name
699
+ end
700
+
701
+ parse!(ARGV)
702
+ end
703
+ end
704
+
705
+ opts[:logger] = Logger.new(opts[:log], "daily")
706
+ opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
707
+
708
+ # def daemonize(foreground=false)
709
+ # trap("SIGINT") { exit! 0 }
710
+ # trap("SIGTERM") { exit! 0 }
711
+ # trap("SIGHUP") { exit! 0 }
712
+ # return yield if $DEBUG || foreground
713
+ # Process.fork do
714
+ # Process.setsid
715
+ # Dir.chdir "/"
716
+ # File.open("/dev/null") {|f|
717
+ # STDIN.reopen f
718
+ # STDOUT.reopen f
719
+ # STDERR.reopen f
720
+ # }
721
+ # yield
722
+ # end
723
+ # exit! 0
724
+ # end
725
+
726
+ # daemonize(opts[:debug] || opts[:foreground]) do
727
+ Net::IRC::Server.new(opts[:host], opts[:port], HaikuIrcGateway, opts).start
728
+ # end
729
+ end
730
+
731
+ # Local Variables:
732
+ # coding: utf-8
733
+ # End: