net-irc 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog CHANGED
@@ -1,7 +1,16 @@
1
+ 2009-10-11 SATOH Hiroh <cho45@lowreal.net>
2
+
3
+ * [new]
4
+ Implemented Server#sessions which returns all sessions connected to
5
+ the server.
6
+ * Released 0.0.9
7
+
1
8
  2009-08-08 SATOH Hiroh <cho45@lowreal.net>
2
9
 
3
10
  * [bug]:
4
11
  Fixed to work on ruby1.9.1 (now can send iso-2022-jp)
12
+ * [new]
13
+ Implemented Message#ctcps returns embedded all ctcp messages (drry).
5
14
  * Released 0.0.8
6
15
 
7
16
  2009-02-19 SATOH Hiroh <cho45@lowreal.net>
data/Rakefile CHANGED
@@ -5,7 +5,6 @@ require 'rake/clean'
5
5
  require 'rake/packagetask'
6
6
  require 'rake/gempackagetask'
7
7
  require 'rake/rdoctask'
8
- require 'rake/contrib/rubyforgepublisher'
9
8
  require 'rake/contrib/sshpublisher'
10
9
  require 'fileutils'
11
10
  require 'spec/rake/spectask'
@@ -19,8 +18,7 @@ NAME = "net-irc"
19
18
  AUTHOR = "cho45"
20
19
  EMAIL = "cho45@lowreal.net"
21
20
  DESCRIPTION = "library for implementing IRC server and client"
22
- RUBYFORGE_PROJECT = "lowreal"
23
- HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
21
+ HOMEPATH = "http://cho45.stfuawsc.com/net-irc/"
24
22
  BIN_FILES = %w( )
25
23
  VERS = Net::IRC::VERSION.dup
26
24
 
@@ -57,7 +55,6 @@ spec = Gem::Specification.new do |s|
57
55
  s.email = EMAIL
58
56
  s.homepage = HOMEPATH
59
57
  s.executables = BIN_FILES
60
- s.rubyforge_project = RUBYFORGE_PROJECT
61
58
  s.bindir = "bin"
62
59
  s.require_path = "lib"
63
60
  s.autorequire = ""
@@ -89,6 +86,10 @@ task :uninstall => [:clean] do
89
86
  sh %{sudo gem uninstall #{NAME}}
90
87
  end
91
88
 
89
+ task :upload_doc => [:rdoc] do
90
+ sh %{rsync --update -avptr html/ lowreal@cho45.stfuawsc.com:/virtual/lowreal/public_html/cho45.stfuawsc.com/net-irc}
91
+ end
92
+
92
93
 
93
94
  Rake::RDocTask.new do |rdoc|
94
95
  rdoc.rdoc_dir = 'html'
@@ -104,24 +105,14 @@ Rake::RDocTask.new do |rdoc|
104
105
  end
105
106
  end
106
107
 
107
- desc "Publish to RubyForge"
108
- task :rubyforge => [:rdoc, :package] do
109
- require 'rubyforge'
110
- @local_dir = "html"
111
- @host = "cho45@rubyforge.org"
112
- @remote_dir = "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}/#{NAME}"
113
- sh %{rsync -r --delete --verbose #{@local_dir}/ #{@host}:#{@remote_dir}}
114
- end
115
-
116
108
  Rake::ShipitTask.new do |s|
117
- s.Step.new {
118
- system("svn", "up")
119
- }.and {}
120
109
  s.ChangeVersion "lib/net/irc.rb", "VERSION"
121
110
  s.Commit
122
- s.Task :clean, :package
123
- s.RubyForge
111
+ s.Task :clean, :package, :upload_doc
112
+ s.Step.new {
113
+ }.and {
114
+ system("gem", "push", "pkg/net-irc-#{VERS}.gem")
115
+ }
124
116
  s.Tag
125
117
  s.Twitter
126
- s.Task :rubyforge
127
118
  end
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env ruby
2
+ # vim:encoding=UTF-8:
3
+ $KCODE = "u" if RUBY_VERSION < "1.9" # json use this
4
+
5
+ require 'uri'
6
+ require 'net/http'
7
+ require 'stringio'
8
+ require 'zlib'
9
+ require 'nkf'
10
+
11
+ class ThreadData
12
+ class UnknownThread < StandardError; end
13
+
14
+ attr_accessor :uri
15
+ attr_accessor :last_modified, :size
16
+
17
+ Line = Struct.new(:n, :name, :mail, :misc, :body, :opts, :id) do
18
+ def aa?
19
+ body = self.body
20
+ return false if body.count("\n") < 3
21
+
22
+ significants = body.scan(/[>\n0-9a-z0-9A-Za-zA-Zぁ-んァ-ン一-龠]/u).size.to_f
23
+ body_length = body.scan(/./u).size
24
+ is_aa = 1 - significants / body_length
25
+
26
+ is_aa > 0.6
27
+ end
28
+ end
29
+
30
+ def initialize(thread_uri)
31
+ @uri = URI(thread_uri)
32
+ _, _, _, @board, @num, = *@uri.path.split('/')
33
+ @dat = []
34
+ end
35
+
36
+ def length
37
+ @dat.length
38
+ end
39
+
40
+ def subject
41
+ retrieve(true) if @dat.size.zero?
42
+ self[1].opts || ""
43
+ end
44
+
45
+ def [](n)
46
+ l = @dat[n - 1]
47
+ return nil unless l
48
+ name, mail, misc, body, opts = * l.split(/<>/)
49
+ id = misc[/ID:([^\s]+)/, 1]
50
+
51
+ body.gsub!(/<br>/, "\n")
52
+ body.gsub!(/<[^>]+>/, "")
53
+ body.gsub!(/^\s+|\s+$/, "")
54
+ body.gsub!(/&(gt|lt|amp|nbsp);/) {|s|
55
+ { 'gt' => ">", 'lt' => "<", 'amp' => "&", 'nbsp' => " " }[$1]
56
+ }
57
+
58
+ Line.new(n, name, mail, misc, body, opts, id)
59
+ end
60
+
61
+ def dat
62
+ @num
63
+ end
64
+
65
+ def retrieve(force=false)
66
+ @dat = [] if @force
67
+
68
+ res = Net::HTTP.start(@uri.host, @uri.port) do |http|
69
+ req = Net::HTTP::Get.new('/%s/dat/%d.dat' % [@board, @num])
70
+ req['User-Agent'] = 'Monazilla/1.00 (2ig.rb/0.0e)'
71
+ req['Accept-Encoding'] = 'gzip' unless @size
72
+ unless force
73
+ req['If-Modified-Since'] = @last_modified if @last_modified
74
+ req['Range'] = "bytes=%d-" % @size if @size
75
+ end
76
+
77
+ http.request(req)
78
+ end
79
+
80
+ ret = nil
81
+ case res.code.to_i
82
+ when 200, 206
83
+ body = res.body
84
+ if res['Content-Encoding'] == 'gzip'
85
+ body = StringIO.open(body, 'rb') {|io| Zlib::GzipReader.new(io).read }
86
+ end
87
+
88
+ @last_modified = res['Last-Modified']
89
+ if res.code == '206'
90
+ @size += body.size
91
+ else
92
+ @size = body.size
93
+ end
94
+
95
+ body = NKF.nkf('-w', body)
96
+
97
+ curr = @dat.size + 1
98
+ @dat.concat(body.split(/\n/))
99
+ last = @dat.size
100
+
101
+ (curr..last).map {|n|
102
+ self[n]
103
+ }
104
+ when 416 # たぶん削除が発生
105
+ p ['416']
106
+ retrieve(true)
107
+ []
108
+ when 304 # Not modified
109
+ []
110
+ when 302 # dat 落ち
111
+ p ['302', res['Location']]
112
+ raise UnknownThread
113
+ else
114
+ p ['Unknown Status:', res.code]
115
+ []
116
+ end
117
+ end
118
+
119
+ def canonicalize_subject(subject)
120
+ subject.gsub(/[A-Za-z0-9]/u) {|c|
121
+ c.unpack("U*").map {|i| i - 65248 }.pack("U*")
122
+ }
123
+ end
124
+
125
+ def guess_next_thread
126
+ res = Net::HTTP.start(@uri.host, @uri.port) do |http|
127
+ req = Net::HTTP::Get.new('/%s/subject.txt' % @board)
128
+ req['User-Agent'] = 'Monazilla/1.00 (2ig.rb/0.0e)'
129
+ http.request(req)
130
+ end
131
+
132
+ recent_posted_threads = (900..999).inject({}) {|r,i|
133
+ line = self[i]
134
+ line.body.scan(%r|ttp://#{@uri.host}/test/read.cgi/[^/]+/\d+/|).each do |uri|
135
+ r["h#{uri}"] = i
136
+ end if line
137
+ r
138
+ }
139
+
140
+ current_subject = canonicalize_subject(self.subject)
141
+ current_thread_rev = current_subject.scan(/\d+/).map {|d| d.to_i }
142
+ current = current_subject.scan(/./u)
143
+
144
+ body = NKF.nkf('-w', res.body)
145
+ threads = body.split(/\n/).map {|l|
146
+ dat, rest = *l.split(/<>/)
147
+ dat.sub!(/\.dat$/, "")
148
+
149
+ uri = "http://#{@uri.host}/test/read.cgi/#{@board}/#{dat}/"
150
+
151
+ subject, n = */(.+?) \((\d+)\)/.match(rest).captures
152
+ canonical_subject = canonicalize_subject(subject)
153
+ thread_rev = canonical_subject[/\d+/].to_i
154
+
155
+ distance = (dat == self.dat) ? Float::MAX :
156
+ (subject == self.subject) ? 0 :
157
+ levenshtein(canonical_subject.scan(/./u), current)
158
+ continuous_num = current_thread_rev.find {|rev| rev == thread_rev - 1 }
159
+ appear_recent = recent_posted_threads[uri]
160
+
161
+ score = distance
162
+ score -= 10 if continuous_num
163
+ score -= 10 if appear_recent
164
+ score += 10 if dat.to_i < self.dat.to_i
165
+ {
166
+ :uri => uri,
167
+ :dat => dat,
168
+ :subject => subject,
169
+ :distance => distance,
170
+ :continuous_num => continuous_num,
171
+ :appear_recent => appear_recent,
172
+ :score => score.to_f
173
+ }
174
+ }.sort_by {|o|
175
+ o[:score]
176
+ }
177
+
178
+ threads
179
+ end
180
+
181
+ def levenshtein(a, b)
182
+ case
183
+ when a.empty?
184
+ b.length
185
+ when b.empty?
186
+ a.length
187
+ when a == b
188
+ 0
189
+ else
190
+ d = Array.new(a.length + 1) { |s|
191
+ Array.new(b.length + 1, 0)
192
+ }
193
+
194
+ (0..a.length).each do |i|
195
+ d[i][0] = i
196
+ end
197
+
198
+ (0..b.length).each do |j|
199
+ d[0][j] = j
200
+ end
201
+
202
+ (1..a.length).each do |i|
203
+ (1..b.length).each do |j|
204
+ cost = (a[i - 1] == b[j - 1]) ? 0 : 1
205
+ d[i][j] = [
206
+ d[i-1][j ] + 1,
207
+ d[i ][j-1] + 1,
208
+ d[i-1][j-1] + cost
209
+ ].min
210
+ end
211
+ end
212
+
213
+ d[a.length][b.length]
214
+ end
215
+ end
216
+ end
217
+
218
+ if __FILE__ == $0
219
+ require 'pp'
220
+ thread = ThreadData.new(ARGV[0])
221
+ pp thread.guess_next_thread.reverse
222
+
223
+ p thread.subject
224
+ end
225
+
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env ruby
2
+ # vim:encoding=UTF-8:
3
+
4
+ $LOAD_PATH << "lib"
5
+ $LOAD_PATH << "../lib"
6
+
7
+ $KCODE = "u" if RUBY_VERSION < "1.9" # json use this
8
+
9
+ require "rubygems"
10
+ require "net/irc"
11
+ require "logger"
12
+ require "pathname"
13
+ require "yaml"
14
+ require 'uri'
15
+ require 'net/http'
16
+ require 'nkf'
17
+ require 'stringio'
18
+ require 'zlib'
19
+
20
+ require "#{Pathname.new(__FILE__).parent.expand_path}/2ch.rb"
21
+ Net::HTTP.version_1_2
22
+
23
+ class NiChannelIrcGateway < Net::IRC::Server::Session
24
+ def server_name
25
+ "2ch"
26
+ end
27
+
28
+ def server_version
29
+ "0.0.0"
30
+ end
31
+
32
+
33
+ def initialize(*args)
34
+ super
35
+ @channels = {}
36
+ end
37
+
38
+ def on_disconnected
39
+ @channels.each do |chan, info|
40
+ begin
41
+ info[:observer].kill if info[:observer]
42
+ rescue
43
+ end
44
+ end
45
+ end
46
+
47
+ def on_user(m)
48
+ super
49
+ @real, *@opts = @real.split(/\s+/)
50
+ @opts ||= []
51
+ end
52
+
53
+ def on_join(m)
54
+ channels = m.params.first.split(/,/)
55
+ channels.each do |channel|
56
+ @channels[channel] = {
57
+ :topic => "",
58
+ :dat => nil,
59
+ :interval => nil,
60
+ :observer => nil,
61
+ } unless @channels.key?(channel)
62
+ post @prefix, JOIN, channel
63
+ post nil, RPL_NAMREPLY, @prefix.nick, "=", channel, "@#{@prefix.nick}"
64
+ post nil, RPL_ENDOFNAMES, @prefix.nick, channel, "End of NAMES list"
65
+ end
66
+ end
67
+
68
+ def on_part(m)
69
+ channel = m.params[0]
70
+ if @channels.key?(channel)
71
+ info = @channels.delete(channel)
72
+ info[:observer].kill if info[:observer]
73
+ post @prefix, PART, channel
74
+ end
75
+ end
76
+
77
+ def on_privmsg(m)
78
+ target, mesg = *m.params
79
+ m.ctcps.each {|ctcp| on_ctcp(target, ctcp) } if m.ctcp?
80
+ end
81
+
82
+ def on_ctcp(target, mesg)
83
+ type, mesg = mesg.split(" ", 2)
84
+ method = "on_ctcp_#{type.downcase}".to_sym
85
+ send(method, target, mesg) if respond_to? method, true
86
+ end
87
+
88
+ def on_ctcp_action(target, mesg)
89
+ command, *args = mesg.split(" ")
90
+ command.downcase!
91
+
92
+ case command
93
+ when 'next'
94
+ if @channels.key?(target)
95
+ guess_next_thread(target)
96
+ end
97
+ end
98
+ rescue Exception => e
99
+ @log.error e.inspect
100
+ e.backtrace.each do |l|
101
+ @log.error "\t#{l}"
102
+ end
103
+ end
104
+
105
+ def on_topic(m)
106
+ channel, topic, = m.params
107
+ p m.params
108
+ if @channels.key?(channel)
109
+ info = @channels[channel]
110
+
111
+ unless topic
112
+ post nil, '332', channel, info[:topic]
113
+ return
114
+ end
115
+
116
+ uri, interval = *topic.split(/\s/)
117
+ interval = interval.to_i
118
+
119
+ post @prefix, TOPIC, channel, topic
120
+
121
+ case
122
+ when !info[:dat], uri != info[:dat].uri
123
+ post @prefix, NOTICE, channel, "Thread URL has been changed."
124
+ info[:dat] = ThreadData.new(uri)
125
+ create_observer(channel)
126
+ when info[:interval] != interval
127
+ post @prefix, NOTICE, channel, "Interval has been changed."
128
+ create_observer(channel)
129
+ end
130
+ info[:topic] = topic
131
+ info[:interval] = interval > 0 ? interval : 90
132
+ end
133
+ end
134
+
135
+ def guess_next_thread(channel)
136
+ info = @channels[channel]
137
+ post server_name, NOTICE, channel, "Current Thread: #{info[:dat].subject}"
138
+ threads = info[:dat].guess_next_thread
139
+ threads.first(3).each do |t|
140
+ if t[:continuous_num] && t[:appear_recent]
141
+ post server_name, NOTICE, channel, "#{t[:uri]} \003%d#{t[:subject]}\017" % 10
142
+ else
143
+ post server_name, NOTICE, channel, "#{t[:uri]} #{t[:subject]}"
144
+ end
145
+ end
146
+ threads
147
+ end
148
+
149
+ def create_observer(channel)
150
+ info = @channels[channel]
151
+ info[:observer].kill if info[:observer]
152
+
153
+ @log.debug "create_observer %s, interval %d" % [channel, info[:interval]]
154
+ info[:observer] = Thread.start(info, channel) do |info, channel|
155
+ Thread.pass
156
+
157
+ loop do
158
+ begin
159
+ sleep info[:interval]
160
+ @log.debug "retrieving (interval %d) %s..." % [info[:interval], info[:dat].uri]
161
+ info[:dat].retrieve.last(100).each do |line|
162
+ priv_line channel, line
163
+ end
164
+
165
+ if info[:dat].length >= 1000
166
+ post server_name, NOTICE, channel, "Thread is over 1000. Guessing next thread..."
167
+ guess_next_thread(channel)
168
+ break
169
+ end
170
+ rescue UnknownThread
171
+ # pass
172
+ rescue Exception => e
173
+ @log.error "Error: #{e.inspect}"
174
+ e.backtrace.each do |l|
175
+ @log.error "\t#{l}"
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ def priv_line(channel, line)
183
+ post "%d{%s}" % [line.n, line.id], PRIVMSG, channel, line.aa?? encode_aa(line.body) : line.body
184
+ end
185
+
186
+ def encode_aa(aa)
187
+ uri = URI('http://tinyurl.com/api-create.php')
188
+ uri.query = 'url=' + URI.escape(<<-EOS.gsub(/[\n\t]/, ''))
189
+ data:text/html,<pre style='font-family:"IPA モナー Pゴシック"'>#{aa.gsub(/\n/, '<br>')}</pre>
190
+ EOS
191
+ Net::HTTP.get(uri.host, uri.request_uri, uri.port)
192
+ end
193
+ end
194
+
195
+ if __FILE__ == $0
196
+ require "optparse"
197
+
198
+ opts = {
199
+ :port => 16701,
200
+ :host => "localhost",
201
+ :log => nil,
202
+ :debug => false,
203
+ :foreground => false,
204
+ }
205
+
206
+ OptionParser.new do |parser|
207
+ parser.instance_eval do
208
+ self.banner = <<-EOB.gsub(/^\t+/, "")
209
+ Usage: #{$0} [opts]
210
+
211
+ EOB
212
+
213
+ separator ""
214
+
215
+ separator "Options:"
216
+ on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
217
+ opts[:port] = port
218
+ end
219
+
220
+ on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
221
+ opts[:host] = host
222
+ end
223
+
224
+ on("-l", "--log LOG", "log file") do |log|
225
+ opts[:log] = log
226
+ end
227
+
228
+ on("--debug", "Enable debug mode") do |debug|
229
+ opts[:log] = $stdout
230
+ opts[:debug] = true
231
+ end
232
+
233
+ on("-f", "--foreground", "run foreground") do |foreground|
234
+ opts[:log] = $stdout
235
+ opts[:foreground] = true
236
+ end
237
+
238
+ parse!(ARGV)
239
+ end
240
+ end
241
+
242
+ opts[:logger] = Logger.new(opts[:log], "daily")
243
+ opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
244
+
245
+ def daemonize(foreground=false)
246
+ trap("SIGINT") { exit! 0 }
247
+ trap("SIGTERM") { exit! 0 }
248
+ trap("SIGHUP") { exit! 0 }
249
+ return yield if $DEBUG || foreground
250
+ Process.fork do
251
+ Process.setsid
252
+ Dir.chdir "/"
253
+ File.open("/dev/null") {|f|
254
+ STDIN.reopen f
255
+ STDOUT.reopen f
256
+ STDERR.reopen f
257
+ }
258
+ yield
259
+ end
260
+ exit! 0
261
+ end
262
+
263
+ daemonize(opts[:debug] || opts[:foreground]) do
264
+ Net::IRC::Server.new(opts[:host], opts[:port], NiChannelIrcGateway, opts).start
265
+ end
266
+ end
267
+