net-irc 0.0.8 → 0.0.9

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,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
+