net-irc 0.0.5 → 0.0.6

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