Index: lang/ruby/citrus/trunk/plugins/nico_search.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/nico_search.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/nico_search.rb (revision 6624)
@@ -0,0 +1,63 @@
+
+require 'rubygems'
+require 'mechanize'
+require 'hpricot'
+
+class NicoSearch < Citrus::Plugin
+	def initialize(*args)
+		super
+		@prefix = @config['prefix'] || 'ns'
+		@number = @config['number'] || 3
+		@limit  = @config['limit']  || 10
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^#{@prefix}(?::(\d+))?\s+(.+)$/
+			result = search(Regexp.last_match[2], Regexp.last_match[1] && Regexp.last_match[1].to_i)
+			result.each { |line| notice(channel, line) }
+		end
+	end
+
+	private
+	def search (string, shu=nil)
+		number     = shu.nil? || shu.zero? ? @number : shu <= @limit ? shu : @limit
+		result     = Array.new
+		keywords   = string.split(/\s+/).collect{ |item| URI.escape(item, /[^-.!~*'()\w]/n) }.join('+')
+		login_uri  = "https://secure.nicovideo.jp/secure/login?mail=#{@config['user']}&password=#{@config['pass']}&site=niconico"
+		search_uri = "http://www.nicovideo.jp/search/#{keywords}?sort=v"
+
+		result << search_uri
+
+		agent = WWW::Mechanize.new
+		agent.get login_uri
+		page  = agent.get search_uri
+		(page.root/'a[@class=video]').each do |a|
+			break if result.size > number
+			result << "#{a.inner_text} #{a[:href]}"
+		end
+		result
+	end
+end
+
+
+tests do
+	describe NicoSearch do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = NicoSearch.new(@core, { "NicoSearch" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/http/default.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/http/default.rb (revision 6700)
+++ lang/ruby/citrus/trunk/plugins/http/default.rb (revision 6700)
@@ -0,0 +1,100 @@
+#!/usr/bin/env ruby
+
+require "net/http"
+require "net/https"
+require "net/http/paranoid"
+require "image_size" # gem install imagesize
+require "timeout"
+
+class Default < Handler
+	MAX_REDIRECT = 10
+	HEADERS      = {
+		"User-Agent" => "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12",
+		"Accept"     => "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
+	}
+
+	def process(uri)
+		timeout(5) do
+			http(uri)
+		end
+	end
+
+	def http(uri, headers=HEADERS, limit=MAX_REDIRECT)
+		return "Redirect loop?: last:#{uri}" if limit <= 0
+		paranoid = Net::HTTP::Paranoid.new
+		paranoid.whitelist = @parent.config["whitelist"]
+		paranoid.blacklist = @parent.config["blacklist"]
+
+		log uri
+		ret = ''
+		http = Net::HTTP.new(uri.host, uri.port)
+		http.use_ssl = (uri.scheme == "https")
+		paranoid.wrap(http).start do |http|
+			r = http.head(uri.request_uri, headers)
+			log r.code.inspect
+			case r.code.to_i
+			when 200
+				case r["Content-Type"]
+				when /html/
+					ret = html(http.get(uri.request_uri, headers.merge({
+						"Range" => "0-5000"
+					})))
+				when /image\//
+					ret = image(http.get(uri.request_uri, headers))
+				else
+					if r["Content-Length"]
+						size = r["Content-Length"].to_i / 1024
+						ret = "[#{r["Content-Type"]}] #{size}KB"
+					else
+						ret = "[#{r["Content-Type"]}]"
+					end
+				end
+			when 401
+				realm = r["WWW-Authenticate"][/Basic realm="([^"]+)"/, 1]
+				auth  =  @config["http_auth"].find {|e| e["host"] == uri.host and e["realm"] == realm }
+				if auth
+					auth = "Basic " + ["#{auth["user"]}:#{auth["pass"]}"].pack("m")
+					ret = http(uri, headers.update({'Authorization' => auth}), limit-1)
+				else
+					ret = realm
+				end
+			when 300 .. 399
+				loc = URI(r["Location"])
+				loc = uri + loc if loc.relative?
+				ret = http(loc, headers, limit-1)
+			else
+				ret = "[#{r.code} #{r.message}]"
+			end
+		end
+
+		ret
+	end
+
+	def image(res)
+		size = res.body.length / 1024
+		img = ImageSize.new(res.body)
+		ret =  "#{img.get_type} Image, "
+		ret << "#{img.get_width || "?"}x#{img.get_height || "?"} "
+		ret << "#{size.to_i}KB"
+	end
+
+	def html(res)
+		title = res.body[/<title.*?>(.*?)<\/title\s*>/imn, 1]
+		title = "タイトル無し " if !title || title.empty?
+		title = title.gsub(/\s+/, " ").gsub(/<.*?>/, "").to_u8
+		title.gsub!(/&#(x)?([0-9a-f]+);/i) do |m|
+			[$1 ? $2.hex : $2.to_i].pack("U")
+		end
+		if title.size > 70
+			title = title[/.{0,60}/] + "..."
+		end
+
+		title = title.gsub(/&gt;/, ">").gsub(/&lt;/, "<").gsub(/&amp;/, "&")
+
+		"#{title} [#{res["Content-Type"]}]"
+	end
+end
+
+
+tests do
+end
Index: lang/ruby/citrus/trunk/plugins/twitter_id.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/twitter_id.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/twitter_id.rb (revision 6624)
@@ -0,0 +1,92 @@
+require 'net/http'
+require 'hpricot'
+require 'htmlentities'
+
+class TwitterId < Citrus::Plugin
+	def initialize(*args)
+		super
+		@prefix = @config['prefix'] || 't '
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^#{@prefix}(.+)$/i
+			notice(channel, twitterer(Regexp.last_match[1]))
+		end
+	end
+
+	private
+	def twitterer(user)
+		return "http://twitter.com/#{user}" if user == 'home'
+
+		Net::HTTP.start('twitter.com', 80) do |http|
+			begin
+				r = Net::HTTP::Get.new("/users/show/#{user}.xml")
+				r.basic_auth @config['screen_name'], @config['password']
+				response = http.request(r)
+				log response.code.inspect
+
+				case response.code.to_i
+				when 400
+					return "くぁwせdrftgyふじこlp； http://twitter.com/#{user}"
+				when 401
+					return "見せなさいよ！ いるのは分かってるんだからねっ！ http://twitter.com/#{user}"
+				when 404
+					return 'いないわよ？'
+				end
+
+				xml = Hpricot(response.body)
+				html = HTMLEntities.new
+				name      = html.decode((xml/'name').inner_html)
+				location  = html.decode((xml/'location').inner_html)
+				uri       = (xml/'url').inner_html
+				following = (xml/'friends_count').inner_html
+				followers = (xml/'followers_count').inner_html
+				favorites = (xml/'favourites_count').inner_html
+				favotter  = favotter(user)
+				updates   = (xml/'statuses_count').inner_html
+				follow_ratio   = '%.2f' % (followers.to_f / following.to_f) || '-'
+				favotter_ratio = '%.2f' % (favotter.to_f / updates.to_f * 100)  || '-'
+
+				"#{name}@#{location} [#{format(following)}/#{format(followers)}(#{follow_ratio}), #{format(favorites)}favs, #{format(updates)}updates/#{format(favotter)}favotter(#{favotter_ratio}%)] #{uri} http://twitter.com/#{user} http://favotter.matope.com/user.php?user=#{user}"
+			rescue Exception => e
+				log e
+				"http://twitter.com/#{user}"
+			end
+		end
+	end
+
+	def favotter(user)
+		Net::HTTP.start('favotter.matope.com', 80) do |http|
+			response = http.get("/user.php?user=#{user}")
+			doc = Hpricot(response.body)
+
+			/\((\d+)\)/.match(doc.at('title').inner_html).to_a[1]
+		end
+	end
+
+	def format(i)
+		i.to_s.gsub(/(\d)(?=(\d{3})+(?!\d))/, '\1,').sub(/\.0+$/, '')
+	end
+end
+
+tests do
+	describe TwitterId do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = TwitterId.new(@core, { "TwitterId" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/http.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/http.rb (revision 6700)
+++ lang/ruby/citrus/trunk/plugins/http.rb (revision 6700)
@@ -0,0 +1,154 @@
+
+require "pathname"
+require "uri"
+
+class HTTP < Citrus::Plugin
+
+	attr_reader :handlers
+	attr_reader :config
+
+	def initialize(*args)
+		super
+
+		@handlers_dir = Pathname.new(@core.config.general["plugin_dir"]) + "http"
+		Pathname.glob(@handlers_dir + "*.rb") do |f|
+			eval(f.read)
+		end
+
+		plugin = self
+		Handler.__send__(:define_method, :log) do |*args|
+			plugin.log(*args)
+		end
+
+		@handlers = Handler.handlers.map do |h|
+			name = h.name.sub(/^.+::/, "")
+			h.new(self)
+		end
+
+		@handlers << "Default"
+	end
+
+	def on_privmsg(prefix, channel, message)
+		URI.extract(message, %w[http]) do |uri|
+			Thread.start(channel, URI(uri)) do |chan, u|
+				begin
+					response(chan, u)
+				rescue Exception => e
+					post NOTICE, chan, e.inspect
+					log e.inspect
+					e.backtrace.each do |l|
+						log l
+					end
+				end
+			end
+		end
+	end
+
+	def response(chan, uri)
+		ret = nil
+		@handlers.each do |handler|
+			ret = handler.process(uri)
+			break if ret
+		end
+		if ret
+			post NOTICE, chan, ret
+		end
+	end
+
+	class Handler
+		@@handlers = []
+
+		def self.handlers
+			@@handlers
+		end
+
+		def self.inherited(subclass)
+			@@handlers << subclass
+		end
+
+		def initialize(parent)
+			@parent = parent
+		end
+
+		def process(uri)
+		end
+	end
+end
+
+tests do
+
+	describe HTTP do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+			@handlers_dir = Pathname.tempname + "http"
+			@handlers_dir.mkpath
+
+			(@handlers_dir + "foo.rb").open("w") do |f|
+				f.puts <<-EOF
+					class Foo < Handler
+
+						def process(uri)
+							return unless uri.host == "foo"
+
+							"foo foo"
+						end
+					end
+				EOF
+			end
+
+			(@handlers_dir + "bar.rb").open("w") do |f|
+				f.puts <<-EOF
+					class Bar < Handler
+
+						def process(uri)
+							return unless uri.host == "bar"
+
+							"bar bar"
+						end
+					end
+				EOF
+			end
+
+			(@handlers_dir + "default.rb").open("w") do |f|
+				f.puts File.read("./plugins/http/default.rb")
+			end
+
+			@core.config.general["plugin_dir"] = @handlers_dir.parent.to_s
+
+			@plugin = HTTP.new(@core, { "HTTP" => {
+				"handlers" => [
+					{ "class" => "Foo"  },
+					{ "class" => "Bar" },
+				],
+				"whitelist" => [ "localhost" ],
+			} })
+		end
+
+		it "should reply correctly" do
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "http://foo/")
+			@socket.pop.to_s.should == "NOTICE #test :foo foo\r\n"
+
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "http://bar/")
+			@socket.pop.to_s.should == "NOTICE #test :bar bar\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "http://example.com/")
+			@socket.pop.to_s.should == "NOTICE #test :Example Web Page [text/html; charset=UTF-8]\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "http://www.google.co.jp/intl/ja/about.html")
+			@socket.pop.to_s.should == "NOTICE #test :Google について [text/html]\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "http://192.168.0.1/")
+			@socket.pop.to_s.should == "NOTICE #test :#<Net::HTTP::Paranoid::NotAllowedHostError: 192.168.0.1 is not allowed host>\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/aa.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/aa.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/aa.rb (revision 6624)
@@ -0,0 +1,102 @@
+
+require 'uri'
+require 'net/http'
+require 'hpricot'
+
+class Aa < Citrus::Plugin
+	def initialize(*args)
+		super
+		@prefix = @config['prefix'] || '(?i:aa) *'
+		@keyword
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^#{@prefix}(#{URI.regexp(%w[http https])})/
+			parse(Regexp.last_match[1]).each {|m| notice(channel, m.gsub(/./) {|c| c }) }
+		end
+	end
+
+	private
+
+
+	def down_color(value)
+		colors = {
+			"1"  => "000000",
+			"2"  => "000080",
+			"3"  => "008000",
+			"4"  => "ff0000",
+			"5"  => "804040",
+			"6"  => "8000ff",
+			"7"  => "808000",
+			"8"  => "FFFF00",
+			"9"  => "00FF00",
+			"10" => "008080",
+			"11" => "00FFFF",
+			"12" => "0000FF",
+			"13" => "FF00FF",
+			"14" => "808080",
+			"15" => "C0C0C0",
+			"16" => "ffffff",
+		}
+
+		r = ""
+		min_delta = nil
+		base_delta = ("0x" + value[0,2]).hex + ("0x" + value[2,2]).hex + ("0x" + value[4,2]).hex
+		colors.each {|name, color|
+			set_delta = ("0x" + color[0,2]).hex + ("0x" + color[2,2]).hex + ("0x" + color[4,2]).hex
+			delta = (base_delta - set_delta).abs
+			if !min_delta || min_delta > delta
+				r = name
+				min_delta = delta
+			end
+		}
+		return r
+	end
+
+	def parse(keyword)
+		Net::HTTP.start('ascii.techhappens.com', 80) do |http|
+			begin
+				r = http.get("/?quality=8&size=8&color=on&html=1&url=#{URI.encode(keyword, /[^-.!~*'()\w]/n)}")
+				case r.code.to_i
+				when 200
+					xml = Hpricot(r.body)
+					m = xml.at('pre').inner_html
+					m = m.gsub(/<.+?#([A-Za-z0-9]{6}).+?>/) {
+						down_color($1)
+					}
+					log m
+					log m.size
+					m
+				else
+					'?'
+				end
+			rescue
+				'?'
+			end
+		end
+	end
+
+end
+
+
+tests do
+	describe Aa do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = Aa.new(@core, { "Aa" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/js_eval.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/js_eval.rb (revision 6594)
+++ lang/ruby/citrus/trunk/plugins/js_eval.rb (revision 6594)
@@ -0,0 +1,109 @@
+require "tempfile"
+
+class JsEval < Citrus::Plugin
+
+	def initialize(*args)
+		super
+		@contexts = {}
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^\?js (.+)$/i
+			code = Regexp.last_match[1].taint
+			js_eval(code, channel)
+		end
+	end
+
+	def js_eval(code, channel)
+		begin
+			file = Tempfile.new("jseval")
+			file.close
+			pid = fork do
+				require "spidermonkey"
+				ret = ''
+				begin
+					c = SpiderMonkey::Context.new
+					c.version = "1.7"
+					c.global.function("rb_inspect") {|obj| obj.inspect }
+					c.eval(<<-EOC)
+						function inspect(ret) {
+							if (ret === null) return "null";
+							switch (typeof ret) {
+								case "null":
+								case "undefined":
+								case "number":
+								case "boolean":
+									return String(ret);
+								case "string":
+									return rb_inspect(ret);
+								case "function":
+								case "object":
+								default:
+									return (ret.toString() == "[object RubyFunction]") ? ret.toString() : ret.toSource();
+							}
+						}
+					EOC
+					ret = c.eval(code)
+					ret = c.call_function("inspect", ret)
+				rescue Exception => e
+					ret = e.inspect
+				end
+				ret = ret.to_s[/.{200}/] + "..." if ret.to_s.size > 200
+				File.open(file.path, "w") {|f| f << ret }
+				exit!
+			end
+			if pid
+				Process.detach(pid).join(@config["timeout"])
+				Process.kill(:KILL, pid)
+				notice(channel, "JS eval timeout")
+			end
+		rescue Errno::ESRCH
+			ret = file.open.read
+			notice(channel, ret)
+		end
+	end
+end
+
+tests do
+	describe JsEval do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = JsEval.new(@core, { "JsEval" => {
+				"timeout" => 1
+			} })
+		end
+
+		it "should reply correctly" do
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "?js 1")
+			@socket.pop.to_s.should == "NOTICE #test 1\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "?js for(;;);")
+			@socket.pop.to_s.should == "NOTICE #test :JS eval timeout\r\n"
+
+			class JsEval
+				def fork
+					yield
+				end
+
+				def exit!
+					raise Errno::ESRCH
+				end
+			end
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "?js 1")
+			@socket.pop.to_s.should == "NOTICE #test 1\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "?js throw 'foobar'")
+			@socket.pop.to_s.should == "NOTICE #test :#<SpiderMonkey::Error: invalid: uncaught exception: foobar>\r\n"
+		end
+	end
+
+end
Index: lang/ruby/citrus/trunk/plugins/system.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/system.rb (revision 6594)
+++ lang/ruby/citrus/trunk/plugins/system.rb (revision 6594)
@@ -0,0 +1,177 @@
+
+class System < Citrus::Plugin
+	def initialize(*args)
+		super
+
+		if @config['operator'].kind_of?(String)
+			@config['operator'] = Regexp.new(@config['operator'])
+		end
+	end
+
+	def on_privmsg(prefix, channel, message)
+		return unless @config['operator'] === prefix
+		case message
+		when /^reload(?:\s+([a-z]+))?$/i
+			log "#{prefix}: call reloading"
+			begin
+				@core.reload_config
+			rescue => e
+				log e.message
+			end
+			name = Regexp.last_match[1]
+			log name
+			if name
+				begin
+					instances = [@core.reload_plugin(name)]
+				rescue Citrus::Plugins::UnknownPlugin => e
+					notice channel, e.message
+					instances = []
+				end
+			else
+				instances = @core.reload_plugins.values
+			end
+			if instances.empty?
+				notice channel, "No plugins to reload."
+			else
+				reloaded = instances.map {|i|
+					i.class.name.sub(/^.+::/, "")
+				}.join(" ")
+				notice channel, "Reloaded: " + reloaded
+			end
+
+		when /^chokan: join to (\S+)(?: (\S+))?/
+			chan, pass = Regexp.last_match.captures
+			log "Joining to '#{chan}' with '#{pass}'"
+			join(chan.to_s, pass.to_s)
+
+		when "chokan: part"
+			part(channel, "lambda....")
+
+		when "operator?"
+			notice channel, "You are an operator for me."
+
+		when "Gem.clear_paths"
+			r = Gem.clear_paths
+			notice channel, "Gem.clear_paths #{r.inspect}"
+		end
+	end
+end
+
+tests do
+
+	describe System do
+		before do
+			@core     = DummyCore.new({
+				"plugins" => { "Foo" => nil, "Bar" => nil }
+			})
+			@pdir = Pathname.new(@core.config.general["plugin_dir"])
+
+			%w(Foo Bar).each do |name|
+				(@pdir + "#{name.downcase}.rb").open("w") do |f|
+					f << <<-EOS.unindent
+						require "thread"
+						class #{name}
+							include Net::IRC
+							include Constants
+
+							attr_reader :config
+
+							def initialize(core, config)
+								@core, @config = core, config[self.class.name.sub(/.+::/, "")] || {}
+								@messages = {}
+							end
+
+							def method_missing(method, *args)
+								@messages[method] = args
+							end
+
+							def m
+								@messages
+							end
+						end
+					EOS
+				end
+			end
+
+			@socket   = @core.socket
+			@prefix   = Net::IRC::Prefix.new("foo!foo@localhost")
+			@prefixop = Net::IRC::Prefix.new("foo!bar@localhost")
+
+			@plugin = System.new(@core, { "System" => {
+				"operator" => "foo!bar@localhost",
+			} })
+
+			@core.init_plugins
+		end
+
+		it "should response to operator" do
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "operator?")
+			@socket.should be_empty
+
+			@socket.clear
+			@plugin.on_privmsg(@prefixop, "#test", "operator?")
+			@socket.pop.to_s.should == "NOTICE #test :You are an operator for me.\r\n"
+
+			@plugin = System.new(@core, { "System" => {
+				"operator" => "foo!bar@.+",
+			} })
+
+			@socket.clear
+			@plugin.on_privmsg(@prefixop, "#test", "operator?")
+			@socket.pop.to_s.should == "NOTICE #test :You are an operator for me.\r\n"
+
+			@plugin = System.new(@core, { "System" => {
+				"operator" => /foo!bar@.+/,
+			} })
+
+			@socket.clear
+			@plugin.on_privmsg(@prefixop, "#test", "operator?")
+			@socket.pop.to_s.should == "NOTICE #test :You are an operator for me.\r\n"
+		end
+
+		it "can reload_plugins" do
+			@socket.clear
+			@plugin.on_privmsg(@prefixop, "#test", "reload")
+			@socket.pop.to_s.should match(/^NOTICE #test /)
+
+			@socket.clear
+			@plugin.on_privmsg(@prefixop, "#test", "reload Foo")
+			@socket.pop.to_s.should match(/^NOTICE #test /)
+
+			@socket.clear
+			@plugin.on_privmsg(@prefixop, "#test", "reload Unknown")
+			@socket.pop.to_s.should match(/^NOTICE #test /)
+
+			def @core.reload_config
+				raise "config error"
+			end
+
+			@socket.clear
+			@plugin.on_privmsg(@prefixop, "#test", "reload")
+			@socket.pop.to_s.should match(/^NOTICE #test /)
+		end
+
+		it "can operate join/part" do
+			@socket.clear
+			@plugin.on_privmsg(@prefixop, "#test", "chokan: part")
+			@socket.pop.to_s.should match(/^PART #test /)
+
+			@socket.clear
+			@plugin.on_privmsg(@prefixop, "#test", "chokan: join to #foobar")
+			@socket.pop.to_s.should match(/^JOIN #foobar /)
+
+			@socket.clear
+			@plugin.on_privmsg(@prefixop, "#test", "chokan: join to #foobar password")
+			@socket.pop.to_s.should match(/^JOIN #foobar password/)
+		end
+
+		it "can Gem.clear_paths" do
+			@socket.clear
+			@plugin.on_privmsg(@prefixop, "#test", "Gem.clear_paths")
+			@socket.pop.to_s.should match(/^NOTICE #test :Gem.clear_paths/)
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/lastfm.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/lastfm.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/lastfm.rb (revision 6624)
@@ -0,0 +1,60 @@
+
+require 'net/http'
+
+class Lastfm < Citrus::Plugin
+	def initialize(*args)
+		super
+		@prefix = @config['prefix'] || 'm '
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^#{@prefix.strip}(?: all| \*)?$/i
+			begin
+				@config['channel'][channel].each { |u| notice(channel, parse(u)) }
+			rescue
+				notice(channel, 'リストどこー？')
+			end
+		when /^#{@prefix}(.+)$/i
+			u = Regexp.last_match[1]
+			notice(channel, parse(u))
+		end
+	end
+
+	private
+	def parse(user)
+		Net::HTTP.start('ws.audioscrobbler.com', 80) do |http|
+			uri = "/1.0/user/#{user}/recenttracks.txt"
+			log uri
+			response = http.get(uri)
+			log response.code.inspect
+			log response.body.inspect
+			return "誰だよ＾＾" if response.code.to_i == 404
+			return "聞いてないわよ http://www.last.fm/user/#{user}"  if response.body.empty?
+
+			_, time, title = */^([\d]+?),(.+)/.match(response.body)
+			"#{Time.at(time.to_i).strftime('%X')} | #{title.gsub('–', '-')} http://www.last.fm/user/#{user}"
+		end
+	end
+end
+
+tests do
+	describe Lastfm do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = Lastfm.new(@core, { "Lastfm" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/gamer_tag.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/gamer_tag.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/gamer_tag.rb (revision 6624)
@@ -0,0 +1,54 @@
+
+require 'hpricot'
+require 'net/http'
+
+class GamerTag < Citrus::Plugin
+	def initialize(*args)
+		super
+		@prefix = @config['prefix'] || 'xbox '
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^#{@prefix}(.+)$/i
+			notice(channel, get(Regexp.last_match[1]))
+		end
+	end
+
+	private
+	def get(tag)
+		Net::HTTP.start('gamercard.xbox.com', 80) do |http|
+			uri = "/#{tag}.card"
+			log uri
+			response = http.get(uri)
+			log response.code.inspect
+			log response.body.inspect
+
+			xml = Hpricot(response.body)
+			score = (xml/'span.XbcFRAR')[1].inner_html
+			return 'だれこれ！' if score == '--'
+			"#{tag}: #{score}G http://live.xbox.com/ja-JP/profile/profile.aspx?GamerTag=#{tag}"
+		end
+	end
+end
+
+tests do
+	describe GamerTag do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = GamerTag.new(@core, { "GamerTag" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/twitter_search.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/twitter_search.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/twitter_search.rb (revision 6624)
@@ -0,0 +1,63 @@
+
+require 'net/http'
+require 'rubygems'
+require 'uri'
+require 'rexml/document'
+
+class TwitterSearch < Citrus::Plugin
+	def initialize(*args)
+		super
+		@prefix = @config['prefix'] || 'ts'
+		@number = @config['number'].nil? ? 3 : @config['number'].to_i
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^#{@prefix}\s+(.+)$/
+			result = search(Regexp.last_match[1])
+			result.each { |line| notice(channel, line) }
+		end
+	end
+
+	private
+	def search (string)
+		result   = Array.new
+		keywords = string.split(/\s+/).collect{ |item| URI.escape(item, /[^-.!~*'()\w]/n) }.join('+')
+		uri      = URI.parse("http://twitter.1x1.jp/search/?keyword=#{keywords}")
+		result   << uri.to_s
+		Net::HTTP.start(uri.host, uri.port) do |http|
+			uri.path = "/rss#{uri.path}"
+			response = http.get(uri.request_uri)
+			if response.code.to_i == 200 then
+				document = REXML::Document.new response.body
+				document.root.elements['channel'].each_element('item') { |node|
+					break if result.size > @number
+					result << "#{node.get_text('title').to_s.sub(/ .+/, '')}: #{node.get_text('description').to_s}"
+				}
+			end
+		end
+		result
+	end
+
+end
+
+tests do
+	describe TwitterSearch do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = TwitterSearch.new(@core, { "TwitterSearch" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/plusplus.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/plusplus.rb (revision 6594)
+++ lang/ruby/citrus/trunk/plugins/plusplus.rb (revision 6594)
@@ -0,0 +1,110 @@
+
+require "yaml"
+
+class Plusplus < Citrus::Plugin
+
+	def initialize(config, chokan)
+		super
+		@datafile = datafile(@config["data"] || "plusplus.yaml")
+		@least    = @config['least'] || 1
+		@excludes = @config['excludes'] || []
+
+		unless @datafile.exist?
+			@datafile.open("w") {|f| YAML.dump({}, f) }
+		end
+	end
+
+	def on_privmsg(prefix, channel, message)
+		return unless @channels.nil? || @channels.include?(channel)
+		case message
+		when /karma for (\S+)/
+			nick = Regexp.last_match[1]
+			notice_karma(channel, nick)
+
+		when /\(([^)]{#{@least},})\)(\+\+|--)/, /(\w{#{@least},})(\+\+|--)/u
+			nick, dir = Regexp.last_match.captures
+			return if @excludes.include?(nick)
+
+			@datafile.open("r+") do |f|
+				data = YAML.load(f)
+				(data[nick] ||= { "++" => 0, "--" => 0 })[dir] += 1
+				f.rewind
+				YAML.dump(data, f)
+				f.truncate(f.tell)
+			end
+
+			notice_karma(channel, nick)
+		end
+	end
+
+	def notice_karma(channel, nick)
+		plus, minus = karma_for(nick)
+		if plus
+			karma = plus - minus
+			notice(channel, "#{nick}: #{karma} (#{plus}++ #{minus}--)")
+		else
+			notice(channel, "don't know #{nick}")
+		end
+	end
+
+	def karma_for(nick)
+		data = @datafile.open {|f| YAML.load(f) }
+		if data[nick]
+			plus  = data[nick]["++"]
+			minus = data[nick]["--"]
+			[plus, minus]
+		else
+			nil
+		end
+	end
+end
+
+tests do
+
+	describe Plusplus do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = Plusplus.new(@core, { "Plusplus" => {
+			} })
+		end
+
+		it "should reply correctly" do
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "foo++")
+			@socket.pop.to_s.should == "NOTICE #test :foo: 1 (1++ 0--)\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "foo++")
+			@socket.pop.to_s.should == "NOTICE #test :foo: 2 (2++ 0--)\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "foo--")
+			@socket.pop.to_s.should == "NOTICE #test :foo: 1 (2++ 1--)\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "karma for foo")
+			@socket.pop.to_s.should == "NOTICE #test :foo: 1 (2++ 1--)\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "(C++)++")
+			@socket.pop.to_s.should == "NOTICE #test :C++: 1 (1++ 0--)\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "karma for C++")
+			@socket.pop.to_s.should == "NOTICE #test :C++: 1 (1++ 0--)\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "karma for unk")
+			@socket.pop.to_s.should == "NOTICE #test :don't know unk\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "文乃さん++")
+			@socket.pop.to_s.should == "NOTICE #test :文乃さん: 1 (1++ 0--)\r\n"
+
+		end
+	end
+
+end
Index: lang/ruby/citrus/trunk/plugins/eval.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/eval.rb (revision 6594)
+++ lang/ruby/citrus/trunk/plugins/eval.rb (revision 6594)
@@ -0,0 +1,75 @@
+
+require "rubygems"
+gem "safeeval"
+require "safe_eval"
+
+class Eval < Citrus::Plugin
+	def initialize(config, chokan)
+		super
+		@prefix = @config["prefix"] || "?rb "
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^#{Regexp.quote(@prefix)}(.+)$/i
+			code = Regexp.last_match[1].taint
+			ret = ""
+			begin
+				ret = SafeEval.eval(code).inspect
+			rescue Exception => e
+				ret = "#{e.class.name} => " + e.to_s.inspect
+			end
+			ret = ret.to_s[/.{200}/] + "..." if ret.scan(/./).size > 200
+			notice(channel, ret.gsub(/\n/, " "))
+		end
+	end
+end
+
+Lambda = Proc
+class Lambda
+	def curry
+		s = <<-EOS.unindent
+			lambda {|al|
+				args = [#{(1...self.arity).inject(""){|r,i|r<<"a#{i}, "}}al]
+				self[*args]
+			}
+		EOS
+		instance_eval (1...self.arity).inject(s) {|r,i|
+			<<-EOS.unindent
+				lambda {|a#{self.arity-1-i+1}|
+					#{r}
+				}
+			EOS
+		}
+	end
+end
+
+S = lambda {|x, y, z| x[z][y[z]] }.curry
+K = lambda {|x, y| x }.curry
+I = lambda {|x| x } # S[K][K]
+
+
+tests do
+
+	describe Eval do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = Eval.new(@core, { "SimpleReply" => {
+			} })
+		end
+
+		it "should reply correctly" do
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "?rb 1")
+			@socket.pop.to_s.should == "NOTICE #test 1\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "?rb raise 'foobar'")
+			@socket.pop.to_s.should == "NOTICE #test :RuntimeError => \"(eval):1:in `safe_eval': foobar\"\r\n"
+		end
+	end
+
+end
Index: lang/ruby/citrus/trunk/plugins/whois.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/whois.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/whois.rb (revision 6624)
@@ -0,0 +1,61 @@
+
+require 'hpricot'
+
+class Whois < Citrus::Plugin
+	def initialize(*args)
+		super
+		@prefix = @config['prefix'] || 'whois '
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^#{@prefix}(.+)$/i
+			notice(channel, search(Regexp.last_match[1]))
+		end
+	end
+
+	private
+	def search(domain)
+		Net::HTTP.start('www.trynt.com', 80) do |http|
+			r = http.get("/whois-api/v1/?h=#{domain}")
+
+			xml = Hpricot(r.body)
+
+			if xml.at('registered').inner_html == 'no'
+				_, sld, tld = */^(.+)\.(.+)$/.match(domain)
+				return "空いてるよ http://www.value-domain.com/regdom.php?action=regdom2&sld=#{sld}&tld=#{tld}"
+			end
+
+			owner = xml.at('owner > name') ? xml.at('owner > name').inner_html : '謎の人'
+			if xml.at('expires')
+				expires = xml.at('expires').inner_html
+				created = xml.at('created').inner_html
+				changed = xml.at('changed').inner_html
+				status  = " #{created}..#{expires}"
+			end
+
+			return "#{owner.to_s}が持ってるよ#{status} http://who.is/#{domain}/ http://#{domain}/"
+		end
+	end
+end
+
+tests do
+	describe Whois do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = Whois.new(@core, { "Whois" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/decode_unicode.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/decode_unicode.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/decode_unicode.rb (revision 6624)
@@ -0,0 +1,42 @@
+
+require "rubygems"
+require "charnames"
+require "open-uri"
+
+class DecodeUnicode < Citrus::Plugin
+
+	def initialize(*args)
+		super
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^U\+([0-9a-f]{4,6})$/i
+			code = Regexp.last_match[1].to_i(16)
+
+			res = Charnames.viacode(code)
+			notice(channel, "U+%X %s" % [code, res])
+		end
+	end
+end
+
+tests do
+	describe DecodeUnicode do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = DecodeUnicode.new(@core, { "DecodeUnicode" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/always_no_op.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/always_no_op.rb (revision 6594)
+++ lang/ruby/citrus/trunk/plugins/always_no_op.rb (revision 6594)
@@ -0,0 +1,35 @@
+
+
+class AlwaysNoOp < Citrus::Plugin
+	bindtextdomain("always_no_op")
+
+	def on_mode(prefix, channel, positive_mode, negative_mode)
+		if positive_mode.include? ["o", @core.prefix.nick]
+			mode(channel, "-o", @core.prefix.nick)
+			privmsg prefix.nick, _("Don't mode +o me.")
+		end
+	end
+end
+
+tests do
+
+	describe AlwaysNoOp do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+			@core.instance_variable_set(:@prefix, Net::IRC::Prefix.new("chokan!chokan@localhost"))
+
+			@plugin = AlwaysNoOp.new(@core, { "AlwaysNoOp" => {
+			} })
+		end
+
+		it "should reply correctly" do
+			@socket.clear
+			@plugin.on_mode(@prefix, "#test", [ ["o", @core.prefix.nick] ], [])
+			@socket.pop.to_s.should == "MODE #test -o chokan\r\n"
+			@socket.pop.to_s.should == "PRIVMSG foo :Don't mode +o me.\r\n"
+		end
+	end
+
+end
Index: lang/ruby/citrus/trunk/plugins/erogame_space.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/erogame_space.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/erogame_space.rb (revision 6624)
@@ -0,0 +1,66 @@
+
+require 'net/http'
+require 'uri'
+
+class ErogameSpace < Citrus::Plugin
+	def initialize(*args)
+		super
+		@prefix = @config['prefix'] || 'erg '
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^#{@prefix}(.+)$/i
+			m = Regexp.last_match[1]
+			log m
+			parse(m).each {|n| notice(channel, n)}
+		end
+	end
+
+	private
+	def parse(keyword)
+		Net::HTTP.start('erogamescape.dyndns.org', 80) do |http|
+			begin
+				n = URI.encode(keyword.gsub(" ", ".*").to_euc, /[^-.!~*'()\w]/n)
+				uri = "/~ap2/ero/toukei_kaiseki/create_csv.php?SQL=SELECT+sellday,median,shoukai,gamename+from+gamelist+where+gamename+~*+'.*#{n}.*'+order+by+sellday+desc+limit+5"
+				response = http.get(uri)
+
+				return "見つからない" if response.body.strip.empty?
+
+				o = ""
+				response.body.each_line do |line|
+					next if line.strip.empty? || line =~ /^<\/?pre>/
+					row = line.to_u8.strip.split(/,/)
+					if row[1].empty? then
+						row[1] = "--"
+					end
+					o << "#{row[3]} - 評価:#{row[1]}"
+					o << "\n#{row[2]}\n"
+				end
+				log o
+				return o
+			end
+		end
+	end
+end
+
+tests do
+	describe ErogameSpace do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = ErogameSpace.new(@core, { "ErogameSpace" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/sakage.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/sakage.rb (revision 6724)
+++ lang/ruby/citrus/trunk/plugins/sakage.rb (revision 6724)
@@ -0,0 +1,197 @@
+
+
+class Sakage < Citrus::Plugin
+
+	Morse2Alpha = {
+		".-"     => "A",
+		"-..."   => "B",
+		"-.-."   => "C",
+		"-.."    => "D",
+		"."      => "E",
+		"..-."   => "F",
+		"--."    => "G",
+		"...."   => "H",
+		".."     => "I",
+		".---"   => "J",
+		"-.-"    => "K",
+		".-.."   => "L",
+		"--"     => "M",
+		"-."     => "N",
+		"---"    => "O",
+		".--."   => "P",
+		"--.-"   => "Q",
+		".-."    => "R",
+		"..."    => "S",
+		"-"      => "T",
+		"..-"    => "U",
+		"...-"   => "V",
+		".--"    => "W",
+		"-..-"   => "X",
+		"-.--"   => "Y",
+		"--.."   => "Z",
+		"-----"  => "0",
+		".----"  => "1",
+		"..---"  => "2",
+		"...--"  => "3",
+		"....-"  => "4",
+		"....."  => "5",
+		"-...."  => "6",
+		"--..."  => "7",
+		"---.."  => "8",
+		"----."  => "9",
+		".-.-.-" => ".",
+		"--..--" => ",",
+		"..--.." => "?",
+		"-...-"  => " -- ",
+		"-....-" => "-",
+		"-..-."  => "/",
+		".--.-." => "@",}.freeze
+
+	Morse2Iroha = {
+		 ".-"     => "イ",
+		 ".-.-"   => "ロ",
+		 "-..."   => "ハ",
+		 "-.-."   => "ニ",
+		 "-.."    => "ホ",
+		 "."      => "ヘ",
+		 "..-.."  => "ト",
+		 "..-."   => "チ",
+		 "--."    => "リ",
+		 "...."   => "ヌ",
+		 "-.--."  => "ル",
+		 ".---"   => "ヲ",
+		 "-.-"    => "ワ",
+		 ".-.."   => "カ",
+		 "--"     => "ヨ",
+		 "-."     => "タ",
+		 "---"    => "レ",
+		 "---."   => "ソ",
+		 ".--."   => "ツ",
+		 "--.-"   => "ネ",
+		 ".-."    => "ナ",
+		 "..."    => "ラ",
+		 "-"      => "ム",
+		 "..-"    => "ウ",
+		 ".-..-"  => "ヰ",
+		 "..--"   => "ノ",
+		 ".-..."  => "オ",
+		 "...-"   => "ク",
+		 ".--"    => "ヤ",
+		 "-..-"   => "マ",
+		 "-.--"   => "ケ",
+		 "--.."   => "フ",
+		 "----"   => "コ",
+		 "-.---"  => "エ",
+		 ".-.--"  => "テ",
+		 "--.--"  => "ア",
+		 "-.-.-"  => "サ",
+		 "-.-.."  => "キ",
+		 "-..--"  => "ユ",
+		 "-...-"  => "メ",
+		 "..-.-"  => "ミ",
+		 "--.-."  => "シ",
+		 ".--.."  => "ヱ",
+		 "--..-"  => "ヒ",
+		 "-..-."  => "モ",
+		 ".---."  => "セ",
+		 "---.-"  => "ス",
+		 ".-.-."  => "ン",
+		 ".."     => "゛",
+		 "..--."  => "゜",
+		 ".--.-"  => "ー",
+		 ".-.-.-" => "、",
+		 ".-.-.." => "」",
+		 "-.--.-" => "（",
+		 ".-..-." => "）",
+		 "-----"  => "0",
+		 ".----"  => "1",
+		 "..---"  => "2",
+		 "...--"  => "3",
+		 "....-"  => "4",
+		 "....."  => "5",
+		 "-...."  => "6",
+		 "--..."  => "7",
+		 "---.."  => "8",
+		 "----."  => "9",}.freeze
+
+	def on_privmsg(prefix, channel, message)
+		case message
+
+		when /^([wW ]+)$/
+			ret = ""
+			source = $1.gsub(/w/, ".").gsub(/W/, "-")
+			source.split(/ /).each do |m|
+				if m.empty?
+					ret << " "
+				else
+					ret << (Morse2Alpha[m] || "")
+				end
+			end
+			notice(channel, ret)
+
+		when /^([ｗＷ　 ]+)$/, /^I([wW ]+)$/
+			ret = ""
+			source = $1.gsub(/ｗ/, ".").gsub(/Ｗ/, "-").
+			            gsub(/w/, ".").gsub(/W/, "-")
+			source.split(/[　 ]/).each do |m|
+				if m.empty?
+					ret << " "
+				else
+					ret << (Morse2Iroha[m] ||  "")
+				end
+			end
+			notice(channel, ret)
+
+		when /^sakage ([a-z,.]+)/i
+			l = $1.split(//).inject([]) { |r, i|
+				r << Morse2Alpha.invert[i.upcase].to_s.gsub(".", "w").gsub("-", "W")
+			}.join(" ")
+			notice(channel, l)
+		end
+	end
+
+end
+
+
+
+tests do
+	describe Sakage do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = Sakage.new(@core, { "Sakage" => {
+			} })
+		end
+
+		it "should reply correctly" do
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "www WWW www")
+			@socket.pop.to_s.should == "NOTICE #test SOS\r\n"
+
+			$KCODE = "u"
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "ｗｗｗ　ＷＷＷ　ｗｗｗ")
+			@socket.pop.to_s.should == "NOTICE #test ラレラ\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "www  www")
+			@socket.pop.to_s.should == "NOTICE #test :S S\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "ｗｗｗ  ｗｗｗ")
+			@socket.pop.to_s.should == "NOTICE #test :ラ ラ\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "sakage SOS")
+			@socket.pop.to_s.should == "NOTICE #test :www WWW www\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "sakage sos")
+			@socket.pop.to_s.should == "NOTICE #test :www WWW www\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/google_calc.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/google_calc.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/google_calc.rb (revision 6624)
@@ -0,0 +1,65 @@
+require 'net/http'
+require 'uri'
+
+class GoogleCalc < Citrus::Plugin
+	HYDE = 156.0000000
+
+	def initialize(*args)
+		HYDE.is_a?(Numeric) && HYDE === 156.0
+		super
+		@prefix = @config['prefix'] || 'c '
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^#{@prefix}(.+)$/
+			notice(channel, parse(Regexp.last_match[1]))
+		end
+	end
+
+	private
+	def parse(syntax)
+		in_hyde = false
+
+		syntax.gsub!(/([\d.,_]+) *?hyde/) { "#{$1.to_i * HYDE.to_i}cm" }
+		if syntax =~ /hyde/
+			in_hyde = true
+			syntax.sub!('hyde', 'cm')
+		end
+
+		Net::HTTP.start('www.google.co.jp', 80) do |http|
+			uri = "/search?q=#{URI.encode(syntax, /[^-.!~*'()\w]/n)}&oe=utf-8&num=1"
+			log uri
+			response = http.get(uri)
+			log response.code.inspect
+			if /<font size=\+1><b>(.+?)<\/b>/.match(response.body)
+				ret = Regexp.last_match[1].gsub(/<sup>/, '^').gsub(/<.*?>/, '').gsub(/&#215;/, '*')
+				ret.sub!(/= (.+) cm/) { '= ' + ($1.gsub(/[^.\d]/, '').to_f / HYDE).to_s + ' hyde' } if in_hyde
+				ret.gsub(/(\d) (\d)/, '\\1,\\2')
+			else
+				'ごっすんごっすん、分からないわ'
+			end
+		end
+	end
+end
+
+tests do
+	describe GoogleCalc do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = GoogleCalc.new(@core, { "GoogleCalc" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/hatena.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/hatena.rb (revision 6602)
+++ lang/ruby/citrus/trunk/plugins/hatena.rb (revision 6602)
@@ -0,0 +1,111 @@
+
+require "net/http"
+require "timeout"
+
+class Hatena < Citrus::Plugin
+	class APIFailed < StandardError
+		attr_reader :cause
+
+		def initialize(cause)
+			@cause = cause
+		end
+	end
+
+	def on_privmsg(prefix, channel, message)
+		re_id = /id:([a-zA-Z][\w-]{1,30}[a-zA-Z\d])/
+		case message
+		when /\bg:([a-zA-Z][a-zA-Z\d]{2,23})(?::#{re_id})?/n
+			g, id = Regexp.last_match.captures
+			id << '/' if id
+			notice(channel, "http://#{g}.g.hatena.ne.jp/#{id}")
+		when /\b(?i:([a-z][a-z\d]*):)?#{re_id}/n
+			begin
+				s, id = Regexp.last_match.captures
+				if exist?(id)
+					notice(channel, "http://#{s || "d"}.hatena.ne.jp/#{id}/")
+				end
+			rescue APIFailed => e
+				notice channel, "Hatena status API failed => #{e.cause.inspect}"
+			end
+		end
+	end
+
+	def exist?(id, t=2)
+		timeout(t) do
+			# http://www.hatena.ne.jp/#{id}/
+			# は、ユーザが存在すれば 200
+			#
+			# /help /rule など通常のページは "text/html; charset=utf-8"
+			# のようにスペースが入りユーザのページはスペースが入らない。
+			uri = URI("http://www.hatena.ne.jp/#{id}/")
+			Net::HTTP.start(uri.host, uri.port) do |http|
+				res = http.head(uri.request_uri)
+				res.code == "200" && res["Content-Type"] == "text/html;charset=utf-8"
+			end
+		end
+	rescue Exception => e
+		raise APIFailed, e
+	end
+
+end
+
+tests do
+
+	describe Hatena do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = Hatena.new(@core, { "Hatena" => {
+			} })
+		end
+
+		it "should check id existance" do
+			@plugin.exist?("jkondo").should be_true
+			proc {
+				@plugin.exist?("jkondo", Float::MIN).should be_nil
+			}.should raise_error(Hatena::APIFailed)
+			@plugin.exist?("css").should be_false
+			@plugin.exist?("theme").should be_false
+			# 1行目は本来ブラックだったID、空行以降は予約済みID
+			#%w[
+			#	chocom company config css faq guide help images info mobile rule statics tool tools
+
+			#	robots favicon theme diary_css register search conv login logout credit
+			#	hotkeyword hoturl http https asin hotasin hotentry video entrylist rakuten
+			#].each do |id|
+			#	@plugin.exist?(id).should be_false
+			#end
+		end
+
+		it "should reply correctly" do
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "id:jkondo")
+			@socket.pop.to_s.should == "NOTICE #test http://d.hatena.ne.jp/jkondo/\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "d:id:jkondo++")
+			@socket.pop.to_s.should == "NOTICE #test http://d.hatena.ne.jp/jkondo/\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "g:subtech:id:jkondo")
+			@socket.pop.to_s.should == "NOTICE #test http://subtech.g.hatena.ne.jp/jkondo/\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "hogehogeid:jitensyaaaaa")
+			@socket.should be_empty
+
+			class Hatena
+				def exist?(*)
+					raise Hatena::APIFailed, RuntimeError.new("foobar")
+				end
+			end
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "id:jkondo")
+			@socket.pop.to_s.should == "NOTICE #test :Hatena status API failed => #<RuntimeError: foobar>\r\n"
+		end
+	end
+
+end
Index: lang/ruby/citrus/trunk/plugins/google_search.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/google_search.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/google_search.rb (revision 6624)
@@ -0,0 +1,82 @@
+
+require 'net/http'
+require 'rubygems'
+require 'hpricot'
+require 'uri'
+
+class GoogleSearch < Citrus::Plugin
+	def description
+		<<-DESCRIPTION.gsub(/^\s+/, '')
+			このプラグインは Google の API を利用しておらず、真っ黒です。
+			絶対に利用しないでください。
+			<http://www.google.com/accounts/TOS>
+		DESCRIPTION
+	end
+
+	def initialize(*args)
+		super
+		@prefix = @config['prefix'] || 'g'
+		@number = @config['number'] || 3
+		@limit  = @config['limit']  || 10
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /
+			^
+			#{@prefix}
+			(?: :(\d+)            (?# 1: :N)
+			  | ((?:#{@prefix})+) (?# 2: repetition of the prefix)
+			)?
+			\s+
+			(.+)                  (?# 3: search words)
+			$
+		/x
+			words  = $3
+			number = $1 && $1.to_i ||
+			         $2 && $2.scan(@prefix).size + 1
+			result = search words, number
+			result.each { |line| notice(channel, line) }
+		end
+	end
+
+	private
+	def search (string, shu=nil)
+		result   = Array.new
+		keywords = string.split(/\s+/).collect{ |item| URI.escape(item, /[^-.!~*'()\w]/n) }.join('+')
+		number   = shu.nil? || shu.zero? ? @number : shu <= @limit ? shu : @limit
+		uri      = URI.parse("http://www.google.co.jp/search?q=#{keywords}&ie=utf-8&oe=utf-8&lr=lang_ja&num=#{number}")
+		Net::HTTP.start(uri.host, uri.port) do |http|
+			response = http.get(uri.request_uri)
+			if response.code.to_i == 200 then
+				document = Hpricot(response.body)
+				document.search("h2.r").each { |node|
+					break if result.size >= number
+					result << "[#{node.inner_text}] #{node.at('a').attributes['href']}"
+				}
+			end
+		end
+		result
+	end
+end
+
+tests do
+	describe GoogleSearch do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = GoogleSearch.new(@core, { "GoogleSearch" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/simple_reply.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/simple_reply.rb (revision 6594)
+++ lang/ruby/citrus/trunk/plugins/simple_reply.rb (revision 6594)
@@ -0,0 +1,52 @@
+
+class SimpleReply < Citrus::Plugin
+	def on_privmsg(prefix, channel, message)
+		@config["replies"].each do |r|
+			if r["words"].include?(message) &&
+			   (!r["channels"] || r["channels"].include?(channel))
+
+				notice channel, r["reply"]
+			end
+		end
+	end
+end
+
+tests do
+
+	describe SimpleReply do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = SimpleReply.new(@core, { "SimpleReply" => {
+				"replies" => [
+					{
+						"words"    => ["foo", "bar"],
+						"channels" => "#test",
+						"reply"    => "Nice boat.",
+					}
+				]
+			} })
+		end
+
+		it "should reply correctly" do
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "foo")
+			@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "bar")
+			@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "baz")
+			@socket.should be_empty
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test2", "foo")
+			@socket.should be_empty
+		end
+	end
+
+end
Index: lang/ruby/citrus/trunk/plugins/dictionary.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/dictionary.rb (revision 6723)
+++ lang/ruby/citrus/trunk/plugins/dictionary.rb (revision 6723)
@@ -0,0 +1,112 @@
+
+
+require "rubygems"
+require "hpricot"
+require "net/http"
+
+class Dictionary < Citrus::Plugin
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^e2?j\s+(.+)$/
+			Thread.start($1) do |e|
+				j = e2j(e)
+				if j.empty?
+					notice(channel, "スペルミス？")
+				else
+					j.first(5).each do |m,l|
+						l = l[/.{100}/n] if l.size > 100
+						notice(channel, l)
+					end
+					if j[1].size >= 5
+						notice(channel, "上位5個のみ表示しました。→Google「英和 #{e}」")
+					end
+				end
+			end
+		when /^j2?e\s+(.+)$/
+			Thread.start($1) do |j|
+				e = j2e(j)
+				if e.empty?
+					notice(channel, "見つからないよ？")
+				else
+					e.first(1).each do |m,l|
+						l = l[/.{100}/n] if l.size > 100
+						notice(channel, l)
+					end
+				end
+			end
+		end
+	end
+
+	private
+	def e2j(e)
+		result = []
+		uri = URI("http://eow.alc.co.jp/#{URI::escape(e)}/UTF-8/")
+		Net::HTTP.start(uri.host, uri.port) do |http|
+			res = http.get(uri.request_uri, {"User-Agent" => "Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:1.7.3) Gecko/20040913 Firefox/0.10.1"})
+			doc = Hpricot(res.body)
+			(doc/"#resultList ul li").each do |e|
+				midashi =  (e/".midashi")
+				next unless midashi
+				midashi = midashi.text
+				unless midashi.empty?
+					result << [midashi, (e/:div).text]
+				end
+			end
+		end
+		result
+	end
+
+	def j2e(j)
+		result = []
+		uri = URI("http://eow.alc.co.jp/#{URI::escape(j)}/UTF-8/")
+		Net::HTTP.start(uri.host, uri.port) do |http|
+			res = http.get(uri.request_uri, {"User-Agent" => "Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:1.7.3) Gecko/20040913 Firefox/0.10.1"})
+			doc = Hpricot(res.body)
+			(doc/"#resultList ul li").each do |e|
+				midashi =  (e/".midashi")
+				next unless midashi
+				midashi = midashi.text
+				unless midashi.empty?
+					result << [midashi, (e/:div).text]
+				end
+			end
+		end
+		result
+	end
+end
+
+
+tests do
+	describe Dictionary do
+		before do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = Dictionary.new(@core, { "Dictionary" => {
+			} })
+		end
+
+		it "should reply correctly" do
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "ej japanese")
+			@socket.pop.to_s.should match(/NOTICE #test/)
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "je 日本語")
+			@socket.pop.to_s.should match(/NOTICE #test/)
+		end
+
+		it "should reply correctly when the word it not found." do
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "ej Nice boat.")
+			@socket.pop.to_s.should match(/NOTICE #test :?スペルミス？/)
+
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "je 誠死ね")
+			@socket.pop.to_s.should match(/NOTICE #test :?見つからないよ？/)
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/wikipedia.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/wikipedia.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/wikipedia.rb (revision 6624)
@@ -0,0 +1,65 @@
+
+require 'rubygems'
+require 'uri'
+require 'net/http'
+require 'hpricot'
+
+class Wikipedia < Citrus::Plugin
+	def initialize(*args)
+		super
+		@prefix = @config['prefix'] || '\? *?'
+		@keyword
+	end
+
+	def on_privmsg(prefix, channel, message)
+		case message
+		when /^#{@prefix}(.+)$/i
+			parse(Regexp.last_match[1]).each {|m| notice(channel, m)}
+		end
+	end
+
+	private
+	def parse(keyword)
+		Net::HTTP.start('wikipedia.simpleapi.net', 80) do |http|
+			begin
+				r = http.get("/api?keyword=#{format(keyword)}")
+
+				xml = Hpricot(r.body)
+
+				m = xml.at('body').inner_html.gsub(/&lt;.+?&gt;/, '')
+				log m
+				log m.size
+				m = m[0..399] + '...'
+				m << "\n#{xml.at('url').inner_html} #{xml.at('title').inner_html}@#{Time.iso8601(xml.at('datetime').inner_html).strftime('%Y-%m-%d')}"
+			rescue
+				'知らないわよっ！'
+			end
+		end
+	end
+
+	def format(keyword)
+		keyword = keyword.capitalize if keyword.upcase!
+		URI.encode(keyword, /[^-.!~*'()\w]/n)
+	end
+end
+
+tests do
+	describe Wikipedia do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = Wikipedia.new(@core, { "Wikipedia" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/drb.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/drb.rb (revision 6407)
+++ lang/ruby/citrus/trunk/plugins/drb.rb (revision 6407)
@@ -0,0 +1,312 @@
+require "thread"
+require "drb/drb"
+
+class Drb < Citrus::Plugin
+
+	def initialize(*args)
+		super
+		@queue  = {}
+		@users  = {}
+		@log = []
+	end
+
+	# plugin loaded
+	def on_load
+		log "Loading"
+		DRb.start_service(@config["uri"], self)
+		log DRb.uri
+	end
+
+	def [](arg)
+		case arg
+		when "bridge"
+			self
+		when "core", "chokan"
+			@core
+		end
+	end
+
+	# plugin unloading
+	def on_unload
+		log "Drb stopping service"
+		DRb.thread.kill if DRb.thread
+	end
+
+	def recent(name)
+		while @queue[name] && @queue[name].num_waiting > 0
+			@queue[name].push(nil)
+		end
+		@queue[name] = Queue.new
+		@users[name] = Time.now
+		@log.select do |hash|
+			if hash.key?("channel")
+				if allow?(hash["channel"], name)
+					true
+				end
+			else
+				false
+			end
+		end
+	end
+
+	def channels(name)
+		ret = []
+		@config["perms"].each {|channel, users|
+			ret << channel if allow?(channel, name)
+		}
+		ret
+	end
+
+	def queue_pop(name)
+		@users[name] = Time.now
+		@queue[name].pop
+	end
+
+	def queue_size(name)
+		@queue[name].size
+	end
+
+	def queue_empty?(name)
+		@queue[name].empty?
+	end
+
+	alias _privmsg privmsg
+	def privmsg(id, channel, msg)
+		log id.inspect, channel, msg
+		if allow?(channel, id)
+			_privmsg(channel, msg)
+		end
+		push_queue({
+			"type"    => "priv",
+			"time"    => Time.now.to_i,
+			"nick"    => id.join("."),
+			"channel" => channel,
+			"message" => msg
+		})
+	end
+
+	# network uped
+	def on_uped
+	end
+
+	# network downed
+	def on_downed
+	end
+
+	def on_privmsg(prefix, channel, message)
+		push_queue({
+			"type"    => "priv",
+			"time"    => Time.now.to_i,
+			"nick"    => prefix.nick,
+			"channel" => channel,
+			"message" => message
+		})
+	end
+
+	def on_notice(prefix, channel, message)
+		push_queue({
+			"type"    => "notice",
+			"time"    => Time.now.to_i,
+			"nick"    => prefix.nick,
+			"channel" => channel,
+			"message" => message
+		})
+	end
+
+	def on_join(prefix, channel)
+		push_queue({
+			"type"    => "join",
+			"time"    => Time.now.to_i,
+			"nick"    => prefix.nick,
+			"channel" => channel,
+		})
+	end
+
+	def on_part(prefix, channel, message)
+		push_queue({
+			"type"    => "part",
+			"time"    => Time.now.to_i,
+			"nick"    => prefix.nick,
+			"channel" => channel,
+			"message" => message,
+		})
+	end
+
+	def on_quit(prefix, message)
+		push_queue({
+			"type"    => "quit",
+			"time"    => Time.now.to_i,
+			"nick"    => prefix.nick,
+			"message" => message,
+		})
+	end
+
+	def on_kick(prefix, channel, nick, message)
+		push_queue({
+			"type"    => "kick",
+			"time"    => Time.now.to_i,
+			"nick"    => nick,
+			"channel" => channel,
+		})
+	end
+
+	def push_queue(hash)
+		@queue.each do |k,v|
+			if hash.key?("channel")
+				if allow?(hash["channel"], k)
+					v << hash
+				end
+			else
+				v << hash
+			end
+		end
+		@users.each do |k,v|
+			if v < Time.now - 60 * 60
+				@queue[k].clear
+				@queue.delete(k)
+				@users.delete(k)
+			end
+		end
+
+		@log << hash
+		@log = @log.last(20)
+	end
+
+	def allow?(channel, id)
+		perm = @config["perms"][channel]
+		case
+		when !perm
+			false
+		when perm  == "anonymouse"
+			true
+		when perm.find {|i| id == i }
+			true
+		else
+			false
+		end
+	end
+
+end
+
+tests do
+	require "kagemusha/datetime"
+
+	describe Drb do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = Drb.new(@core, { "Drb" => {
+				"uri" => "druby://localhost:0",
+				"acl" => [
+					"deny all",
+					"allow 127.0.0.1",
+				],
+				"perms" => {
+					"#foobar" => "anonymouse",
+					"#barbaz" => [
+						["cho45", "Service"],
+						["foo", "Service"],
+						["bar", "Service"],
+					],
+				},
+			} })
+		end
+
+		it "should create and destroy threads correctly" do
+			list = Thread.list
+			@plugin.on_load
+			Thread.list.should_not == list
+			@plugin.on_unload
+			Thread.list.should == list
+		end
+
+		it "should service DRb multi object" do
+			@plugin["core"].should   == @core
+			@plugin["chokan"].should == @core
+			@plugin["bridge"].should == @plugin
+		end
+
+		it "should reject not allowed people" do
+			# anonymouse
+			@plugin.allow?("#foobar", ["foo", "Service"]).should be_true
+			@plugin.allow?("#foobar", ["bar", "Service"]).should be_true
+
+			# reject
+			@plugin.allow?("#barbaz", ["baz", "Service"]).should_not be_true
+
+			# allow
+			@plugin.allow?("#barbaz", ["foo", "Service"]).should be_true
+			@plugin.allow?("#barbaz", ["bar", "Service"]).should be_true
+
+			# unknown
+			@plugin.allow?("#unknown", ["bar", "Service"]).should_not be_true
+
+			# channels
+			@plugin.channels(["foo", "Service"]).should == ["#barbaz", "#foobar"]
+		end
+
+		it "should place message queue." do
+			user   = ["foo", "Service"]
+			@plugin.recent(user).should == []
+
+			Kagemusha::DateTime.at(2008, 1, 1, 0, 0, 0) do
+				@plugin.queue_empty?(user).should be_true
+				@plugin.queue_size(user).should == 0
+
+				@plugin.on_privmsg(@prefix, "#foobar", "foo")
+				@plugin.queue_pop(user).should == {"message"=>"foo", "time"=>1199113200, "nick"=>"foo", "type"=>"priv", "channel"=>"#foobar"}
+
+				@plugin.on_notice(@prefix, "#foobar", "foo")
+				@plugin.queue_pop(user).should == {"message"=>"foo", "time"=>1199113200, "nick"=>"foo", "type"=>"notice", "channel"=>"#foobar"}
+
+				@plugin.on_join(@prefix, "#foobar")
+				@plugin.queue_pop(user).should == {"time"=>1199113200, "nick"=>"foo", "type"=>"join", "channel"=>"#foobar"}
+
+				@plugin.on_part(@prefix, "#foobar", "message")
+				@plugin.queue_pop(user).should == {"time"=>1199113200, "nick"=>"foo", "type"=>"part", "channel"=>"#foobar", "message" => "message"}
+
+				@plugin.on_kick(@prefix, "#foobar", "bar", "message")
+				@plugin.queue_pop(user).should == {"time"=>1199113200, "nick"=>"bar", "type"=>"kick", "channel"=>"#foobar"}
+
+				@plugin.on_privmsg(@prefix, "#foobar", "foo")
+				@plugin.on_privmsg(@prefix, "#foobar", "foo")
+				@plugin.on_quit(@prefix, "message")
+
+				@plugin.recent(user).zip([
+					{"message"=>"foo", "time"=>1199113200, "nick"=>"foo", "type"=>"priv", "channel"=>"#foobar"},
+					{"message"=>"foo", "time"=>1199113200, "nick"=>"foo", "type"=>"notice", "channel"=>"#foobar"},
+					{"time"=>1199113200, "nick"=>"foo", "type"=>"join", "channel"=>"#foobar"},
+					{"time"=>1199113200, "nick"=>"foo", "type"=>"part", "channel"=>"#foobar", "message" => "message"},
+					{"time"=>1199113200, "nick"=>"bar", "type"=>"kick", "channel"=>"#foobar"},
+					{"message"=>"foo", "time"=>1199113200, "nick"=>"foo", "type"=>"priv", "channel"=>"#foobar"},
+					{"message"=>"foo", "time"=>1199113200, "nick"=>"foo", "type"=>"priv", "channel"=>"#foobar"},
+					{"message"=>"foo", "time"=>1199113200, "nick"=>"foo", "type"=>"quit", "message" => "message"},
+				]).each do |r, e|
+					r.should == e
+				end
+			end
+
+
+			Kagemusha::DateTime.at(2008, 1, 2, 0, 0, 0) do
+				@plugin.on_privmsg(@prefix, "#foobar", "foo")
+				@plugin.instance_variable_get(:@users).should == {}
+
+				@plugin.recent(user)
+
+				Thread.start do
+					@plugin.queue_pop(user)
+				end
+
+				@plugin.recent(user)
+			end
+		end
+
+		it "should have privmsg/notice method for external messaging" do
+			@plugin.privmsg(["foo", "Service"], "#foobar", "msg")
+		end
+
+	end
+
+end
Index: lang/ruby/citrus/trunk/plugins/auto_uri_escape.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/auto_uri_escape.rb (revision 6698)
+++ lang/ruby/citrus/trunk/plugins/auto_uri_escape.rb (revision 6698)
@@ -0,0 +1,149 @@
+require 'uri'
+require 'net/http'
+
+class AutoUriEscape < Citrus::Plugin
+	REVISION = '$Rev$'.gsub(/\D+/, '').to_i
+	def initialize(*args)
+		super
+		@shunirr  = @config['shunirr']   || ' x shunirr'
+		@limit    = @config['limit']     || 5
+		@ua       = @config['ua_string'] || "AutoUriEscape/#{REVISION} (chokan#{@shunirr})"
+		@multiple = @config['multiple']  || false
+	end
+
+	def on_privmsg(prefix, channel, message)
+		return unless URI.regexp(%w[http]) === message
+		res  = []
+		msgs = [message] + message.split
+		msgs.each do |m|
+			[m, m.to_euc, m.to_sjis].each do |u|
+				u   = URI.escape u
+				uri = URI.extract(u, %w[http]).first
+				if uri && (uri = follow_uri(uri))
+					res << uri
+					break
+				end
+			end if m != URI.escape(m) && !m.include?('%')
+		end
+		res.uniq!
+		return if res.empty?
+		if @multiple
+			res.each do |uri|
+				notice channel, uri
+			end
+		else
+			notice channel, res.join(' ')
+		end
+	end
+
+	def follow_uri(uri, limit = @limit.to_i)
+		return uri if limit == 0
+		uri = URI.parse uri
+		res = nil
+		Net::HTTP.start(uri.host, uri.port) do |http|
+			#http.open_timeout = 1
+			res = http.head uri.request_uri, { 'User-Agent' => @ua }
+		end
+		case res.code.to_i
+		when 200 .. 299
+			uri.to_s
+		when 300 .. 399
+			return uri.to_s unless res.key?('Location')
+			follow_uri(res['Location'], limit - 1)
+		else
+			nil
+		end
+
+	rescue => e
+		log e.inspect
+		nil
+	end
+end
+
+tests do
+	require "webrick"
+
+	describe AutoUriEscape do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = AutoUriEscape.new(@core, { "AutoUriEscape" => {
+			} })
+
+			@httpserv = WEBrick::HTTPServer.new({
+				:Logger    => WEBrick::Log.new("/dev/null", 1),
+				:AccessLog => WEBrick::Log.new("/dev/null", 1),
+				:Port      => 0,
+			})
+			trap(:INT) { exit! }
+			Thread.start do
+				@httpserv.start
+			end
+			Thread.pass
+			@port =  @httpserv.listeners.first.addr[1]
+
+			@testhttp = "http://localhost:#{@port}"
+		end
+
+		after :all do
+			@httpserv.stop
+		end
+
+		it "should follow and check exists" do
+			@httpserv.mount_proc("/foo") do |req, res|
+				res.status = 200
+				res.body   = "ok"
+			end
+			url = "#{@testhttp}/foo"
+			@plugin.follow_uri(url).should == url
+
+			@httpserv.mount_proc("/redirect_to_foo") do |req, res|
+				res.status      = 302
+				res["Location"] = "#{@testhttp}/foo"
+				res.body        = "redirect"
+			end
+			@plugin.follow_uri("#{@testhttp}/redirect_to_foo").should == url
+
+
+			@plugin.follow_uri("http://unknown_failing_host/").should be_nil
+		end
+
+		it "should reply correctly" do
+			# normal
+			@httpserv.mount_proc("/あああ") do |req, res|
+				res.status = 200
+				res.body   = "ok"
+			end
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "#{@testhttp}/あああ")
+			@socket.pop.to_s.should == "NOTICE #test #{@testhttp}/%E3%81%82%E3%81%82%E3%81%82\r\n"
+
+			# with space
+			@httpserv.mount_proc("/いいい いいい") do |req, res|
+				res.status = 200
+				res.body   = "ok"
+			end
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "#{@testhttp}/いいい いいい")
+			@socket.pop.to_s.should == "NOTICE #test #{@testhttp}/%E3%81%84%E3%81%84%E3%81%84%20%E3%81%84%E3%81%84%E3%81%84\r\n"
+
+			# 404
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "#{@testhttp}/わわわ")
+			@socket.should be_empty # これだとパスすべきじゃないときもパスする可能性がある
+
+			# redirect
+			@httpserv.mount_proc("/ほげ") do |req, res|
+				res.status      = 302
+				res["Location"] = "#{@testhttp}/%E3%81%82%E3%81%82%E3%81%82"
+				res.body        = "redirect"
+			end
+			@socket.clear
+			@plugin.on_privmsg(@prefix, "#test", "#{@testhttp}/ほげ")
+			@socket.pop.to_s.should == "NOTICE #test #{@testhttp}/%E3%81%82%E3%81%82%E3%81%82\r\n"
+		end
+	end
+end
+
Index: lang/ruby/citrus/trunk/plugins/cer.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/cer.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/cer.rb (revision 6624)
@@ -0,0 +1,111 @@
+
+require "uri"
+require "net/http"
+
+# currency exchange rate
+
+class Cer < Citrus::Plugin
+	CER = {"USD" => "アメリカドル",
+	       "GBP" => "イギリス ポンド",
+	       "INR" => "インド ルピー",
+	       "IDR" => "インドネシア ルピア",
+	       "ECS" => "エクアドル スクレ",
+	       "EGP" => "エジプト ポンド",
+	       "AUD" => "オーストラリア ドル",
+	       "CAD" => "カナダ ドル",
+	       "KRW" => "韓国 ウォン",
+	       "KWD" => "クウェート ディナール",
+	       "COP" => "コロンビア ペソ",
+	       "SAR" => "サウジ リアル",
+	       "SGD" => "シンガポール ドル",
+	       "CHF" => "スイス フラン",
+	       "SEK" => "スウェーデン クローナ",
+	       "THB" => "タイ バーツ",
+	       "TWD" => "台湾 ドル",
+	       "CLP" => "チリ ペソ",
+	       "DKK" => "デンマーク クローネ",
+	       "TRL" => "トルコ リラ",
+	       "JPY" => "日本 円",
+	       "NZD" => "ニュージーランド ドル",
+	       "NOK" => "ノルウェー クローネ",
+	       "PYG" => "パラグアイ グァラニ",
+	       "PHP" => "フィリピン ペソ",
+	       "BRL" => "ブラジル リアル",
+	       "VEB" => "ベネズエラ ボリバー",
+	       "PEN" => "ペルー ソル",
+	       "HKD" => "香港 ドル",
+	       "MYR" => "マレーシア リンギ",
+	       "ZAR" => "南アフリカ ランド",
+	       "MXN" => "メキシコ ペソ",
+	       "AED" => "UAE ダーハム",
+	       "EUR" => "欧州 ユーロ",
+	       "JOD" => "ヨルダン ディナール",
+	       "ROL" => "ルーマニア ルー",
+	       "LBP" => "レバノン ポンド",
+	       "RUB" => "ロシアン ルーブル",}.freeze
+	
+	def description
+		"This is for Currency exchange rate. React `cer <var>from-cer-code</var> [<var>to-cer-code</var>=jpy]', `cer?', `<var>cer-code</var>'"
+	end
+	
+	def on_privmsg(prefix, channel, message)
+		case message
+			when /cer\s+(...)(?:\s+(...))?/
+				begin
+					s = $1.upcase
+					t = ($2 || "JPY").upcase
+					res = cer(s, t)
+					msg = "1 #{s}(#{CER[s]}) = #{res[1]} #{t}(#{CER[t]}) @#{res[0]} - Yahoo! FINANCE"
+				rescue ArgumentError
+					msg = "不正な引数です"
+				end
+				notice(channel, msg)
+			when /^cer\?$/
+				notice(channel, CER.keys.join(" "))
+				
+			when /^(...)$/
+				if CER.include?($1.upcase)
+					s = $1.upcase
+					t = "JPY"
+					res = cer(s, t)
+					msg = "1 #{s}(#{CER[s]}) = #{res[1]} #{t}(#{CER[t]}) @#{res[0]} - Yahoo! FINANCE"
+					notice(channel, msg)
+				end
+		end
+	end
+	
+	private
+	def cer(s, t)
+		raise ArgumentError if !CER.include?(s) || !CER.include?(t)
+		ret = ""
+		uri = URI.parse("http://quote.yahoo.co.jp/m5?a=1&s=#{s}&t=#{t}")
+		Net::HTTP.start(uri.host, uri.port) do |http|
+			res = http.get(uri.request_uri, {"User-Agent" => "Mozilla/4.0 (compatible; MSIE6.0)"})
+			open("baz", "wb") {|f| f.puts res.body}
+			res.body[%r|<td>1</td><td nowrap>(\d?\d:\d\d)</td><td>([\d\.,]+?)</td><td><b>([\d\.,]+?)</b>|n]
+			ret = [$1, $2]
+		end
+		ret
+	end
+end
+
+tests do
+	describe Cer do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = Cer.new(@core, { "Cer" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/gates_point.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/gates_point.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/gates_point.rb (revision 6624)
@@ -0,0 +1,48 @@
+
+class GatesPoint < Citrus::Plugin
+	def initialize(*args)
+		super
+		@suffix = @config['suffix'] || 'gp'
+		@rate   = (@config['rate'] || 1.48).to_f
+	end
+
+	def on_privmsg(prefix, channel, message)
+		if @config['channel'].nil? || @config['channel'].include?(channel.downcase)
+			case message
+			when /^(\d+)#{@suffix}$/i
+				notice(channel, exchange(Regexp.last_match[1]))
+			end
+		end
+	end
+
+	private
+	def exchange(gp)
+		yen = gp.to_i * @rate
+		"#{format(gp)}ゲイツポイント = #{format(yen)}円"
+	end
+
+	def format(i)
+		i.to_s.gsub(/(\d)(?=(\d{3})+(?!\d))/, '\1,').sub(/\.0+$/, '')
+	end
+end
+
+tests do
+	describe GatesPoint do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = GatesPoint.new(@core, { "GatesPoint" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/plugins/dcc_cache.rb
===================================================================
--- lang/ruby/citrus/trunk/plugins/dcc_cache.rb (revision 6624)
+++ lang/ruby/citrus/trunk/plugins/dcc_cache.rb (revision 6624)
@@ -0,0 +1,86 @@
+
+
+require "digest/sha1"
+
+class DccCache < Citrus::Plugin
+	
+	def on_ctcp(prefix, message)
+		log message.inspect
+		
+		check_and_clean
+		Thread.start(prefix, message) do |prefix, message|
+			dcc, method, arg, ip, port, size = message.split(/\s+/)
+			
+			if method == "SEND"
+				filename = Digest::SHA1.hexdigest(arg) + File.extname(arg)
+				ip = [ip.to_i].pack("N").unpack("C4").join(".")
+				port = port.to_i
+				size = size.to_i
+				log "#{ip}:#{port}, #{size}byte"
+				raise "File size over" if size > @config["max_size"]
+
+				filename = "#{Time.now.strftime("%Y%m%dT%H%M%S")}.#{prefix.nick.gsub(/[^\w\d]/, "")}.#{filename}"
+				begin
+					File.open("#{@config["dir"]}/#{filename}", "wb") do |f|
+						s = timeout(10) { TCPSocket.open(ip, port) }
+						begin
+							while data = s.readpartial(4096)
+								f << data
+								raise "File size over" if f.tell > @config["max_size"]
+								if f.tell >= size
+									s.close
+									log "DCC end"
+									break
+								end
+							end
+						rescue EOFError => e
+							s.close
+							log "DCC end"
+						end
+					end
+					privmsg(prefix.nick, "#{@config["base_url"]}/#{filename}")
+				rescue TimeoutError
+					log "DCC timeout"
+				rescue Exception => e
+					privmsg(prefix.nick, e.inspect)
+					log e.inspect
+					log e.message
+					log *e.backtrace
+				end
+			end
+		end
+	end
+
+	private
+	def check_and_clean
+		files = Dir.glob("#{@config["dir"]}/*")
+		size  = files.inject(0) {|r,i| r += File.size(i) }
+		log "whole cache size: #{size}"
+		while size > @config["whole_max_size"]
+			size -= File.size(files[-1])
+			File.unlink(files.pop)
+		end
+	end
+end
+
+
+tests do
+	describe DccCache do
+		before :all do
+			@core   = DummyCore.new({})
+			@socket = @core.socket
+			@prefix = Net::IRC::Prefix.new("foo!foo@localhsot")
+
+			@plugin = DccCache.new(@core, { "DccCache" => {
+			} })
+		end
+
+		it "should reply correctly" do
+#					@socket.clear
+#					@plugin.on_privmsg(@prefix, "#test", "foo")
+#					@socket.pop.to_s.should == "NOTICE #test :Nice boat.\r\n"
+		end
+	end
+
+end
+
Index: lang/ruby/citrus/trunk/Rakefile
===================================================================
--- lang/ruby/citrus/trunk/Rakefile (revision 6628)
+++ lang/ruby/citrus/trunk/Rakefile (revision 6628)
@@ -0,0 +1,76 @@
+
+require "rubygems"
+require "rake"
+require 'spec/rake/spectask'
+require "pathname"
+
+task :default => [:spec]
+
+Spec::Rake::SpecTask.new do |t|
+	task :test => :spec
+
+	t.spec_opts = ['--options', "spec/spec.opts"]
+	t.spec_files = FileList['spec/*_spec.rb', "plugins/*.rb"]
+	t.rcov = true
+end
+
+
+namespace :core do
+	task :test => :spec
+
+	Spec::Rake::SpecTask.new do |t|
+		t.spec_opts = ['--options', "spec/spec.opts"]
+		t.spec_files = FileList['spec/*_spec.rb']
+		t.rcov = true
+	end
+end
+
+namespace :plugins do
+	task :test => :spec
+
+	desc "Run plugins spec test."
+	Spec::Rake::SpecTask.new do |t|
+		t.ruby_opts = ["-Ilib", "-rcitrus", "-rspec/spec_helper"]
+		t.spec_opts = ['--options', "spec/spec.opts"]
+		t.spec_files = "plugins/#{ENV["file"] || "*.rb"}"
+	end
+end
+
+namespace :text do
+
+	desc "Update pot/po files."
+	task :po do
+		require 'gettext/utils'
+
+		Pathname.glob("plugins/*.rb") do |f|
+			next unless f.file?
+			domain = f.basename(".rb").to_s
+			GetText.update_pofiles(domain, [f.to_s], f.basename.to_s)
+			pot = Pathname.new("po/#{domain}.pot")
+			if pot.read.scan(/msgid/).size == 1
+				pot.unlink
+			end
+		end
+	end
+
+	desc "Create mo-files"
+	task :mo do
+		require 'gettext/utils'
+		GetText.create_mofiles(true)
+	end
+
+	desc "Start translation"
+	task :init do
+		target = ENV["target"]
+		lang   = ENV["lang"]
+		if target.nil? || lang.nil?
+			puts "target=<domain> lang=<lang>"
+			exit
+		end
+		pot = Pathname.new("po/#{target}.pot")
+		po  = Pathname.new( "po/#{lang}/#{target}.po")
+		po.parent.mkpath
+		exec("msginit", "-i", pot, "-o", po)
+	end
+
+end
Index: lang/ruby/citrus/trunk/config.yaml.sample
===================================================================
--- lang/ruby/citrus/trunk/config.yaml.sample (revision 6583)
+++ lang/ruby/citrus/trunk/config.yaml.sample (revision 6583)
@@ -0,0 +1,42 @@
+---
+general:
+  host: charlotte
+  port: 6669
+  user: chokan
+  nick: chokan
+  real: chokan bot via Tiarra
+  error: "#chokan"
+  plugin_dir: plugins
+  charset:
+    default: utf-8
+
+plugins:
+  System:
+    operator: "foo!bar@localhost"
+    #operator: !ruby/regexp /^foo\w*?!bar@baz\d+\.example\.com$/i
+
+  HTTP:
+    handlers:
+      - Mixi:
+          user: hoge
+          pass: hoge
+
+  SimpleReply:
+    replies:
+      - words:
+         - "ping: chokan"
+        channels: []
+        reply: "pong"
+
+  Jitensya:
+
+  Foo:
+    disabled:
+      - #Hoge
+
+  Bar:
+    enabled:
+      - #Fuga
+      - !ruby/regexp /^#Piyo@.+$/i
+
+...
Index: lang/ruby/citrus/trunk/lib/citrus/plugin.rb
===================================================================
--- lang/ruby/citrus/trunk/lib/citrus/plugin.rb (revision 6578)
+++ lang/ruby/citrus/trunk/lib/citrus/plugin.rb (revision 6578)
@@ -0,0 +1,95 @@
+#!/usr/bin/env ruby
+
+require "rubygems"
+require "net/irc"
+
+module Citrus
+	class Plugin
+		include Net::IRC
+		include Constants
+
+		include GetText
+
+		def initialize(core, config)
+			@core, @config = core, config[self.class.name.sub(/.+::/, "")] || {}
+		end
+
+		def on_uped
+		end
+
+		def on_downed
+		end
+
+		def on_privmsg(prefix, channel, message)
+		end
+
+		def on_talk(prefix, target, message)
+		end
+
+		def on_notice(prefix, channel, message)
+		end
+
+		def on_join(prefix, channel)
+		end
+
+		def on_part(prefix, channel, message)
+		end
+
+		def on_kick(prefix, channels, nicks, message)
+		end
+
+		def on_invite(prefix, nick, channel)
+		end
+
+		def on_ctcp(prefix, target, message)
+		end
+
+		def on_mode(prefix, target, positive_mode, negative_mode)
+		end
+
+		def on_nick(prefix, new_nick)
+		end
+
+		def on_message(m)
+		end
+
+		def log(*args)
+			prefix = self.class.to_s.sub(/^#<Module:0x[0-9a-f]+>::/, "")
+			prefix = "[#{prefix}] "
+			args.each do |l|
+				@core.log "#{prefix} #{l}"
+			end
+		end
+
+		def post(command, *params)
+			m = Message.new(nil, command, params.map {|s|
+				s.gsub(/\r|\n/, " ")
+			})
+			@core.socket << m
+		end
+
+		def datafile(name)
+			myname = self.class.to_s.sub(/^.+::/, "")
+			libdir = Pathname.new(@core.config.general["data_dir"]) + myname
+			libdir.mkpath unless libdir.exist?
+			libdir + name
+		end
+
+		%w[
+			privmsg
+			notice
+			join
+			part
+			kick
+			invite
+			mode
+		].each do |command|
+			eval <<-EOS
+				def #{command}(*params)
+					post #{command.upcase}, *params
+				end
+			EOS
+		end
+	end
+end
+
Index: lang/ruby/citrus/trunk/lib/citrus/utils.rb
===================================================================
--- lang/ruby/citrus/trunk/lib/citrus/utils.rb (revision 6601)
+++ lang/ruby/citrus/trunk/lib/citrus/utils.rb (revision 6601)
@@ -0,0 +1,28 @@
+
+require "rubygems"
+require "gettext"
+
+require "nkf"
+class String
+
+	def to_jis
+		NKF.nkf('-j -m0', self)
+	end
+	def to_euc
+		NKF.nkf('-e -m0', self)
+	end
+	def to_sjis
+		NKF.nkf('-s -m0', self)
+	end
+	def to_u8
+		NKF.nkf('-w -m0', self)
+	end
+	def to_u16
+		NKF.nkf('-w16 -m0', self)
+	end
+
+	def unindent(leading=nil)
+		gsub(/^#{self[Regexp.union(leading || /\A(?:\t+| +)/)]}/, "")
+	end
+end
+
Index: lang/ruby/citrus/trunk/lib/citrus/plugins.rb
===================================================================
--- lang/ruby/citrus/trunk/lib/citrus/plugins.rb (revision 6580)
+++ lang/ruby/citrus/trunk/lib/citrus/plugins.rb (revision 6580)
@@ -0,0 +1,111 @@
+#!/usr/bin/env ruby
+
+require "pathname"
+
+module Citrus
+	class Plugins
+		class PluginsError < StandardError; end
+		class UnknownPlugin < PluginsError; end
+
+		def initialize(path)
+			@path      = Pathname.new(path)
+			@instances = {}
+			update
+		end
+
+		def plugin_names
+			@plugins.keys
+		end
+
+		def loaded_plugin_names
+			@instances.keys
+		end
+
+		def loaded_plugins
+			@instances
+		end
+
+		def [](key)
+			@instances[key]
+		end
+
+		def each(&block)
+			@instances.each(&block)
+		end
+
+		def load(class_name, *args)
+			raise UnknownPlugin, "UnknownPlugin: #{class_name}" unless @plugins.key?(class_name)
+
+			info = @plugins[class_name]
+			@instances[class_name] = info[:class].new(*args)
+			@instances[class_name].on_load if @instances[class_name].respond_to? :on_load
+
+			@instances[class_name]
+		end
+
+		def unload(class_name)
+			raise UnknownPlugin, "UnknownPlugin: #{class_name}" unless @plugins.key?(class_name)
+			@instances[class_name].on_unload if @instances[class_name].respond_to? :on_unload
+
+			@instances.delete class_name
+		end
+
+		def reload(class_name, *args)
+			if class_name == :all
+				ret = {}
+				plugins = search(@path)
+				plugins.each do |k, v|
+					if @instances.key?(k)
+						# Re-instanciate
+						if @plugins[k][:time] < v[:time]
+							@plugins[k] = v
+							self.unload(k)
+							ret[k] = self.load(k, *args)
+						end
+				end
+				end
+				ret
+			else
+				plugins = search(@path)
+				raise UnknownPlugin, "UnknownPlugin: #{class_name}" unless plugins.key?(class_name)
+				file = plugins[class_name][:file]
+				@plugins.update load_file(file)
+				self.unload(class_name)
+				self.load(class_name, *args)
+			end
+		end
+
+		def update
+			@plugins   = search(@path)
+		end
+
+		protected
+		def search(path)
+			class_table = {}
+
+			Pathname.glob("#{path}/*.rb") do |f|
+				class_table.update(load_file(f))
+			end
+
+			class_table
+		end
+
+		def load_file(f)
+			ret = {}
+			m = Module.new
+			m.module_eval(f.read)
+			m.constants.each do |name|
+				const = m.const_get(name)
+				if const.is_a? Class
+					ret[name] = {
+						:class => const,
+						:file  => f,
+						:time  => f.mtime,
+					}
+				end
+			end
+			ret
+		end
+	end
+end
+
Index: lang/ruby/citrus/trunk/lib/citrus/core.rb
===================================================================
--- lang/ruby/citrus/trunk/lib/citrus/core.rb (revision 6596)
+++ lang/ruby/citrus/trunk/lib/citrus/core.rb (revision 6596)
@@ -0,0 +1,186 @@
+#!/usr/bin/env ruby
+
+require "net/irc"
+require "ostruct"
+require "logger"
+require "yaml"
+require "monitor"
+
+module Citrus
+	class Core < Net::IRC::Client
+
+		attr_reader :socket, :plugins, :config
+
+		def initialize(config)
+			if config.is_a? Hash
+				@config = OpenStruct.new({
+					"general" => {},
+					"plugins" => {},
+				}.merge(config))
+				init_config
+			else
+				@config_file = config.to_s
+				reload_config
+			end
+
+			@logger = Logger.new(@config.general["log"] || $stdout)
+			@logger.progname = File.basename($0)
+
+			%w(host port nick user real).each do |req|
+				raise ArgumentError, "config general/#{req} is required." if @config.general[req].nil?
+			end
+
+			super(@config.general["host"], @config.general["port"], {
+				:nick   => @config.general["nick"],
+				:user   => @config.general["user"],
+				:real   => @config.general["real"],
+				:pass   => @config.general["pass"],
+				:logger => @logger,
+			})
+
+			@plugins = Plugins.new(@config.general["plugin_dir"])
+		end
+
+		def reload_config
+			raise "Passed not filename but Hash to new." unless @config_file
+			@config = OpenStruct.new(File.open(@config_file) {|f| YAML.load(f) })
+			init_config
+		end
+
+		def init_config
+			@config.general["plugin_dir"] ||= "plugins"
+			@config.general["data_dir"]   ||= "data"
+		end
+
+		def init_plugins
+			@plugins.update
+			@config.plugins.each do |name, _|
+				begin
+					unless @plugins.loaded_plugins.key?(name)
+						@plugins.load(name, self, @config.plugins)
+					end
+				rescue Plugins::UnknownPlugin => e
+					@logger.error "#{e.message}"
+				end
+			end
+		end
+
+		def load_plugin(name)
+			config = @config.plugins
+			@plugins.load(name, self, config || {})
+		end
+
+		def reload_plugin(name)
+			config = @config.plugins
+			@plugins.reload(name, self, config || {})
+		end
+
+		def reload_plugins
+			reload_plugin(:all)
+		end
+
+		def unload_plugin(name)
+			@plugins.unload(name)
+		end
+
+		def on_connected
+			super
+			init_plugins
+			call_plugins :on_uped, []
+		end
+
+		def on_disconnected
+			super
+			call_plugins :on_downed, []
+		end
+
+		def on_privmsg(m)
+			super
+			target  = m[0]
+			mes     = m[1]
+
+			case
+			when m.ctcp?
+				call_plugins :on_ctcp, [m.prefix, target, ctcp_decoding(mes)]
+			when target =~ /^[^#+&!]/
+				call_plugins :on_talk, [m.prefix, target, mes]
+			else
+				call_plugins :on_privmsg, [m.prefix, target, mes]
+			end
+		end
+
+		def on_notice(m)
+			super
+			call_plugins :on_notice, [m.prefix, m[0], m[1]]
+		end
+
+		def on_join(m)
+			super
+			call_plugins :on_join, [m.prefix, m[0]]
+		end
+
+		def on_part(m)
+			super
+			call_plugins :on_part, [m.prefix, m[0], m[1]]
+		end
+
+		def on_kick(m)
+			super
+			call_plugins :on_kick, [m.prefix, m[0].split(/,/), m[1].split(/,/), m[2]]
+		end
+
+		def on_invite(m)
+			super
+			call_plugins :on_invite, [m.prefix, m[0], m[1]]
+		end
+
+		def on_nick(m)
+			super
+			call_plugins :on_nick, [m.prefix, m[0]]
+		end
+
+		def on_mode(m)
+			negative_mode, positive_mode, = *super
+			call_plugins :on_mode, [m.prefix, m[0], positive_mode, negative_mode]
+		end
+
+		def on_message(m)
+			call_plugins :on_message, [m]
+			nil
+		end
+
+		def call_plugins(name, args)
+			@plugins.each do |n, i|
+				if args[1] && args[1][0] == ?# # when channel
+					channel  = args[1]
+					config   = @config.plugins[n] || {}
+					enabled  = config["enabled"]
+					disabled = config["disabled"]
+
+					case
+					when enabled
+						next unless enabled.find {|c| c === channel }
+					when disabled
+						next if disabled.find {|c| c === channel }
+					end
+				end
+				begin
+					i.send(name, *args)
+				rescue Exception => e
+					log "#{n} #{e.message}"
+					e.backtrace.each do |l|
+						@logger.error "#{n} \t#{l}"
+					end
+					if @config.general["error"]
+						post NOTICE, @config.general["error"], "Error: #{e.inspect}"
+					end
+				end
+			end
+		end
+
+		def log(l)
+			@logger.info(l)
+		end
+	end
+end
+
Index: lang/ruby/citrus/trunk/lib/citrus.rb
===================================================================
--- lang/ruby/citrus/trunk/lib/citrus.rb (revision 6397)
+++ lang/ruby/citrus/trunk/lib/citrus.rb (revision 6397)
@@ -0,0 +1,22 @@
+#!/usr/bin/env ruby
+
+$LOAD_PATH << "lib"
+
+require "citrus/utils"
+require "citrus/plugin"
+require "citrus/plugins"
+require "citrus/core"
+
+module Citrus
+	class << self
+		def run(config)
+			Core.new(config).start
+		end
+	end
+end
+
+
+def tests(&block)
+	# for plugin test
+end
+
Index: lang/ruby/citrus/trunk/spec/core_spec.rb
===================================================================
--- lang/ruby/citrus/trunk/spec/core_spec.rb (revision 6591)
+++ lang/ruby/citrus/trunk/spec/core_spec.rb (revision 6591)
@@ -0,0 +1,316 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/spec_helper.rb'
+require "yaml"
+require "pp"
+
+include Citrus
+include Net::IRC
+include Constants
+
+describe Citrus do
+	it "should raises ArgumentError when some configs are missing" do
+		proc { Citrus.run({}) }.should raise_error(ArgumentError)
+	end
+end
+
+describe Core, "config" do
+	it "should accept filename" do
+		proc { Core.new("config.yaml.notfound") }.should raise_error(Errno::ENOENT)
+	end
+
+	it "should accept hash" do
+		proc { Core.new({}) }.should raise_error(ArgumentError)
+	end
+end
+
+describe Core  do
+	before :all do
+		@prefix = Prefix.new("foo!bar@localhost")
+
+		@core = DummyCore.new({
+			"plugins" => {
+				"System"  => { "operator" => "foo!bar@localhost" },
+				"Foo"     => { "disabled" => "#disabled" },
+				"Bar"     => { "enabled"  => "#enabled" },
+				"Test"    => nil,
+				"Unknown" => nil,
+			}
+		})
+
+		@pdir = Pathname.new(@core.config.general["plugin_dir"])
+		@ddir = Pathname.new(@core.config.general["data_dir"])
+		@conf = Pathname.new(@core.instance_variable_get(:@config_file))
+
+		(@pdir + "system.rb").open("w") do |f|
+			f.puts File.read("plugins/system.rb")
+		end
+
+		%w(Foo Bar).each do |name|
+			(@pdir + "#{name.downcase}.rb").open("w") do |f|
+				f << <<-EOS.unindent
+					require "thread"
+					class #{name}
+						include Net::IRC
+						include Constants
+
+						attr_reader :config
+
+						def initialize(core, config)
+							@core, @config = core, config[self.class.name.sub(/.+::/, "")] || {}
+							@messages = {}
+						end
+
+						def method_missing(method, *args)
+							@messages[method] = args
+						end
+
+						def m
+							@messages
+						end
+					end
+				EOS
+			end
+		end
+
+		(@pdir + "test.rb").open("w") do |f|
+			f << <<-EOS.unindent
+				require "thread"
+				class Test < Citrus::Plugin
+
+					attr_reader :config
+
+					def on_notice(prefix, channel, message)
+						eval(message) if channel == "#eval"
+					end
+
+					def on_message(m)
+						@messages ||= SizedQueue.new(1)
+						@messages << m
+					end
+				end
+			EOS
+		end
+
+		@core.instance_variable_get(:@logger).level = Logger::FATAL
+		@core.on_connected
+		@core.instance_variable_get(:@logger).level = Logger::INFO
+
+		@core.plugins.loaded_plugins["System"].should_not be_nil
+		@core.plugins.loaded_plugins["Foo"].should_not be_nil
+		@core.plugins.loaded_plugins["Bar"].should_not be_nil
+
+		@dplug = @core.plugins.loaded_plugins["Foo"].m
+	end
+
+	it "should run initializing plugins and on_uped event." do
+		@dplug[:on_uped].should == []
+	end
+
+	it "should translate PRIVMSG to on_privmsg event." do
+		@core.on_privmsg(Message.new(@prefix, PRIVMSG, ["#channel", "message"]))
+		@dplug[:on_privmsg].should == ["foo!bar@localhost", "#channel", "message"]
+	end
+
+	it "should translate PRIVMSG to on_talk event." do
+		@core.on_privmsg(Message.new(@prefix, PRIVMSG, ["channel", "message"]))
+		@dplug[:on_talk].should == ["foo!bar@localhost", "channel", "message"]
+	end
+
+	it "should translate PRIVMSG to on_ctcp event." do
+		@core.on_privmsg(Message.new(@prefix, PRIVMSG, ["#channel", ctcp_encoding("ACTION message")]))
+		@dplug[:on_ctcp].should == ["foo!bar@localhost", "#channel", "ACTION message"]
+	end
+
+	it "should translate NOTICE to on_notice event." do
+		@core.on_notice(Message.new(@prefix, NOTICE, ["#channel", "message"]))
+		@dplug[:on_notice].should == ["foo!bar@localhost", "#channel", "message"]
+	end
+
+	it "should translate JOIN to on_join event." do
+		@core.on_join(Message.new(@prefix, JOIN, ["#channel"]))
+		@dplug[:on_join].should == ["foo!bar@localhost", "#channel"]
+	end
+
+	it "should translate PART to on_part event." do
+		@core.on_part(Message.new(@prefix, PART, ["#channel", "reason"]))
+		@dplug[:on_part].should == ["foo!bar@localhost", "#channel", "reason"]
+	end
+
+	it "should translate KICK to on_kick event." do
+		@core.on_kick(Message.new(@prefix, KICK, ["#channel", "nick", "reason"]))
+		@dplug[:on_kick].should == ["foo!bar@localhost", ["#channel"], ["nick"], "reason"]
+
+		@core.on_kick(Message.new(@prefix, KICK, ["#channel,#channel1", "nick,nick1", "reason"]))
+		@dplug[:on_kick].should == ["foo!bar@localhost", ["#channel", "#channel1"], ["nick", "nick1"], "reason"]
+	end
+
+	it "should translate INVITE to on_invite event." do
+		@core.on_invite(Message.new(@prefix, INVITE, ["nick", "#channel"]))
+		@dplug[:on_invite].should == ["foo!bar@localhost", "nick", "#channel"]
+	end
+
+	it "should translate NICK to on_nick event." do
+		@core.on_nick(Message.new(@prefix, NICK, ["nick"]))
+		@dplug[:on_nick].should == ["foo!bar@localhost", "nick"]
+	end
+
+	it "should translate MODE to on_mode event." do
+		@core.on_mode(Message.new(@prefix, MODE, ["#channel", "+ooo", "nick1", "nick2", "nick3"]))
+		@dplug[:on_mode].should == ["foo!bar@localhost", "#channel", [
+			["o", "nick1"],
+			["o", "nick2"],
+			["o", "nick3"],
+		], [
+		]]
+	end
+
+	it "should call on_message for all message" do
+		m = Message.new(@prefix, PRIVMSG, ["#channel", "message"])
+		@core.on_message(m)
+		@dplug[:on_message].should == [m]
+	end
+
+	it "should rescue plugins' exception" do
+		@core.instance_variable_get(:@logger).level = Logger::FATAL
+		proc {
+			@core.on_notice(Message.new(@prefix, NOTICE, ["#eval", "raise RuntimeError"]))
+		}.should_not raise_error
+		@core.instance_variable_get(:@logger).level = Logger::INFO
+	end
+
+	it "should run on_disconnected" do
+		@core.on_disconnected
+		@dplug[:on_downed].should == []
+	end
+
+	it "should supports enabling/disabling plugins per channel" do
+		# 1
+		@core.config.plugins["Foo"]["disabled"] = ["#foo"]
+		@core.config.plugins["Foo"]["enabled"]  = nil
+
+		@core.plugins.loaded_plugins["Foo"].m.clear
+		@core.on_privmsg(Message.new(@prefix, PRIVMSG, ["#foo", "message"]))
+		@core.plugins.loaded_plugins["Foo"].m[:on_privmsg].should_not == ["foo!bar@localhost", "#foo", "message"]
+
+		@core.plugins.loaded_plugins["Foo"].m.clear
+		@core.on_privmsg(Message.new(@prefix, PRIVMSG, ["#bar", "message"]))
+		@core.plugins.loaded_plugins["Foo"].m[:on_privmsg].should     == ["foo!bar@localhost", "#bar", "message"]
+
+		#2
+		@core.config.plugins["Bar"]["disabled"] = nil
+		@core.config.plugins["Bar"]["enabled"]  = ["#foo"]
+
+		@core.plugins.loaded_plugins["Bar"].m.clear
+		@core.on_privmsg(Message.new(@prefix, PRIVMSG, ["#foo", "message"]))
+		@core.plugins.loaded_plugins["Bar"].m[:on_privmsg].should     == ["foo!bar@localhost", "#foo", "message"]
+
+		@core.plugins.loaded_plugins["Bar"].m.clear
+		@core.on_privmsg(Message.new(@prefix, PRIVMSG, ["#bar", "message"]))
+		@core.plugins.loaded_plugins["Bar"].m[:on_privmsg].should_not == ["foo!bar@localhost", "#bar", "message"]
+
+		#3 regexp
+		@core.config.plugins["Foo"]["disabled"]        = [/-ja$/]
+		@core.config.plugins["Foo"]["enabled"]         = nil
+		@core.config.plugins["Bar"]["disabled"]        = nil
+		@core.config.plugins["Bar"]["enabled"]         = [/-ja$/]
+
+		@core.plugins.loaded_plugins["Foo"].m.clear
+		@core.on_privmsg(Message.new(@prefix, PRIVMSG, ["#foo-ja", "message"]))
+		@core.plugins.loaded_plugins["Foo"].m[:on_privmsg].should be_nil
+
+		@core.plugins.loaded_plugins["Foo"].m.clear
+		@core.on_privmsg(Message.new(@prefix, PRIVMSG, ["#foo", "message"]))
+		@core.plugins.loaded_plugins["Foo"].m[:on_privmsg].should     == ["foo!bar@localhost", "#foo", "message"]
+
+		@core.plugins.loaded_plugins["Bar"].m.clear
+		@core.on_privmsg(Message.new(@prefix, PRIVMSG, ["#foo-ja", "message"]))
+		@core.plugins.loaded_plugins["Bar"].m[:on_privmsg].should     == ["foo!bar@localhost", "#foo-ja", "message"]
+
+		@core.plugins.loaded_plugins["Bar"].m.clear
+		@core.on_privmsg(Message.new(@prefix, PRIVMSG, ["#foo", "message"]))
+		@core.plugins.loaded_plugins["Bar"].m[:on_privmsg].should be_nil
+	end
+
+	it "has plugins (re)loading features" do
+		proc { @core.reload_plugin("Unknown") }.should raise_error(Plugins::UnknownPlugin)
+
+		plugins = @core.plugins
+
+		@core.load_plugin("Foo")
+		@core.plugins.loaded_plugins["Foo"].config.should == @core.config.plugins["Foo"]
+
+		@core.reload_plugin("Foo")
+		@core.plugins.loaded_plugins["Foo"].config.should == @core.config.plugins["Foo"]
+
+		(@pdir + "foo.rb").utime(Time.now+10, Time.now+10)
+		i = @core.reload_plugins
+		i.should have_key("Foo")
+		@core.plugins.loaded_plugins["Foo"].config.should == @core.config.plugins["Foo"]
+
+		@core.unload_plugin("Foo")
+		@core.plugins.loaded_plugins["Foo"].should be_nil
+
+		@core.load_plugin("Foo")
+		@core.plugins.loaded_plugins["Foo"].should_not be_nil
+	end
+
+	it "should support reload config and load new plugin" do
+		@conf.open("w") do |f|
+			f << <<-EOS.unindent
+				---
+				general:
+				  host: charlotte
+				  port: 6669
+				  user: chokan
+				  nick: chokan
+				  real: chokan bot via Tiarra
+				  error: "#chokan"
+				  plugin_dir: #{@pdir}
+				  data_dir: #{@ddir}
+				  charset:
+				    default: utf-8
+
+				plugins:
+				  System:
+				    operator: "foo!bar@localhost"
+
+				  Foo:
+				    disabled:
+				      - "#disabled"
+
+				  Bar:
+				    enabled:
+				      - "#enabled"
+
+				  Test:
+				  Baz:
+			EOS
+		end
+
+		(@pdir + "baz.rb").open("w") do |f|
+			f << <<-EOS.unindent
+				require "thread"
+				class Baz < Citrus::Plugin
+
+					attr_reader :config
+
+					def on_notice(prefix, channel, message)
+						eval(message) if channel == "#eval"
+					end
+
+					def on_message(m)
+						@messages ||= SizedQueue.new(1)
+						@messages << m
+					end
+				end
+			EOS
+		end
+
+		@core.reload_config
+		@core.config.plugins.should have_key("Baz")
+		@core.plugins.loaded_plugins.should_not have_key("Baz")
+
+		@core.reload_plugin("Baz")
+		@core.plugins.loaded_plugins.should have_key("Baz")
+	end
+end
Index: lang/ruby/citrus/trunk/spec/plugin_spec.rb
===================================================================
--- lang/ruby/citrus/trunk/spec/plugin_spec.rb (revision 6594)
+++ lang/ruby/citrus/trunk/spec/plugin_spec.rb (revision 6594)
@@ -0,0 +1,37 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/spec_helper.rb'
+require "pathname"
+
+include Citrus
+
+
+describe Plugin  do
+	before do
+		@core   = DummyCore.new({})
+		@socket = @core.socket
+	end
+
+	it "has utility post methods" do
+		@plugin = Plugin.new(@core, {})
+		@socket.clear
+
+		@plugin.privmsg("#test", "message")
+		@plugin.privmsg("#test", "message with space")
+
+		@socket.pop.to_s.should == "PRIVMSG #test message\r\n"
+		@socket.pop.to_s.should == "PRIVMSG #test :message with space\r\n"
+	end
+
+	it "has utility log method" do
+		@plugin = Plugin.new(@core, {})
+		@plugin.log("aaa")
+	end
+
+	it "has utility datafile method" do
+		@plugin = Plugin.new(@core, {})
+		@plugin.datafile("foobar.db").to_s.should == "#{@core.config.general["data_dir"]}/Plugin/foobar.db"
+		@plugin.datafile("foobar.db").should be_a_kind_of(Pathname)
+		@plugin.datafile("foobar.db").parent.exist?.should be_true
+	end
+end
+
Index: lang/ruby/citrus/trunk/spec/spec_helper.rb
===================================================================
--- lang/ruby/citrus/trunk/spec/spec_helper.rb (revision 6630)
+++ lang/ruby/citrus/trunk/spec/spec_helper.rb (revision 6630)
@@ -0,0 +1,92 @@
+#!/usr/bin/env ruby
+
+Thread.abort_on_exception = true
+
+require "rubygems"
+require "spec"
+
+require "pathname"
+require Pathname.new(__FILE__).parent + "../lib/citrus.rb"
+
+require "tmpdir"
+
+class Pathname
+	@@tempname_number = 0
+	def self.tempname(base=$0, dir=Dir.tmpdir)
+		@@tempname_number += 1
+		name = "#{dir}/#{File.basename(base)}.#{$$}.#{@@tempname_number}"
+		path = new(name)
+		at_exit do
+			path.rmtree if path.exist?
+		end
+		path
+	end
+end
+
+require "thread"
+
+class QueueWithTimeout < Queue
+	def initialize(timeout)
+		super()
+		@timeout = timeout
+	end
+
+	def pop(*args)
+		timeout(@timeout) do
+			super
+		end
+	end
+
+	def push(*args)
+		timeout(@timeout) do
+			super
+		end
+	end
+end
+
+class DummyCore < Citrus::Core
+	def initialize(opts={})
+		@base    = Pathname.tempname
+		config   = @base + "config.yaml"
+
+		data_dir = @base + "data"
+		data_dir.mkpath
+
+		plug_dir = @base + "plugins"
+		plug_dir.mkpath
+
+		config.open("w") do |f|
+			conf = {
+				"general" => {
+					"host"       => "localhost",
+					"port"       => "6669",
+					"nick"       => "foonick",
+					"user"       => "foouser",
+					"real"       => "foo real name",
+					"plugin_dir" => plug_dir.to_s,
+					"data_dir"   => data_dir.to_s,
+					"log"        => "/dev/null",
+					"error"      => "#chokan",
+				}.merge(opts["general"] || {}),
+				"plugins" => {
+				}.merge(opts["plugins"] || {})
+			}
+			YAML.dump(conf, f)
+		end
+
+		super(config)
+
+		@socket  = QueueWithTimeout.new(5)
+	end
+
+	def start
+	end
+
+	def finish
+	end
+end
+
+def tests(&block)
+	yield
+end
+
Index: lang/ruby/citrus/trunk/spec/utils_spec.rb
===================================================================
--- lang/ruby/citrus/trunk/spec/utils_spec.rb (revision 6601)
+++ lang/ruby/citrus/trunk/spec/utils_spec.rb (revision 6601)
@@ -0,0 +1,55 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/spec_helper.rb'
+
+include Citrus
+include Net::IRC
+include Constants
+
+describe String do
+	it "should define utility method for encoding" do
+		"美乳".to_u8.should   == "\347\276\216\344\271\263"
+		"美乳".to_u16.should  == "\177\216Ns"
+		"美乳".to_euc.should  == "\310\376\306\375"
+		"美乳".to_jis.should  == "\e$BH~F}\e(B"
+		"美乳".to_sjis.should == "\224\374\223\373"
+	end
+
+	it "should define utility method for indentation" do
+		<<-EOS.unindent.should == <<-EXPECTED
+			class Tab < Indentation
+				def m; end
+			end
+		EOS
+class Tab < Indentation
+	def m; end
+end
+		EXPECTED
+
+		<<-EOS.unindent.should == <<-EXPECTED
+  two spaces leading.
+    four spaces leading.
+		EOS
+two spaces leading.
+  four spaces leading.
+		EXPECTED
+
+		<<-EOS.unindent(" ").should == <<-EXPECTED
+a
+ b
+  c
+		EOS
+a
+b
+ c
+		EXPECTED
+
+		<<-EOS.unindent(/\A(?:;c)+/).should == <<-EXPECTED
+;charset=utf-8
+;c;charset=UTF-8
+		EOS
+harset=utf-8
+;charset=UTF-8
+		EXPECTED
+	end
+end
+
Index: lang/ruby/citrus/trunk/spec/plugins_spec.rb
===================================================================
--- lang/ruby/citrus/trunk/spec/plugins_spec.rb (revision 6032)
+++ lang/ruby/citrus/trunk/spec/plugins_spec.rb (revision 6032)
@@ -0,0 +1,164 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/spec_helper.rb'
+
+include Citrus
+
+describe Plugins, "load plugins" do
+	before :all do
+		@plugins_dir = Pathname.tempname
+		@plugins_dir.mkpath
+
+		@foobarrb = @plugins_dir + "foobar.rb"
+		@foobarrb.open("w") do |f|
+			f << <<-EOF
+				class Foobar
+					def initialize(a, b)
+						@a, @b = a, b
+					end
+				end
+			EOF
+		end
+
+		@barbazrb = @plugins_dir + "barbaz.rb"
+		@barbazrb.open("w") do |f|
+			f << <<-EOF
+				class Barbaz
+					def initialize(a, b)
+						@a, @b = a, b
+					end
+				end
+			EOF
+		end
+
+		@plugins = Plugins.new(@plugins_dir)
+	end
+
+	it "should search all plugins" do
+		@plugins.plugin_names.should == ["Foobar", "Barbaz"]
+		info = @plugins.instance_variable_get(:@plugins)
+		info["Foobar"][:file].should == @foobarrb
+		info["Barbaz"][:file].should == @barbazrb
+	end
+
+	it "should load plugin correctly" do
+		instance = @plugins.load("Foobar", "a", "b")
+		instance.class.name.should match(/Foobar$/)
+
+		@plugins.loaded_plugin_names.should == ["Foobar"]
+		instance.instance_variable_get(:@a).should == "a"
+		instance.instance_variable_get(:@b).should == "b"
+
+		instance = @plugins["Foobar"]
+		instance.instance_variable_get(:@a).should == "a"
+		instance.instance_variable_get(:@b).should == "b"
+	end
+
+	it "should be able to reload plugins" do
+		@foobarrb.open("w") do |f|
+			f << <<-EOF
+				class Foobar
+					def initialize(a, b)
+						@c, @d = a, b
+					end
+				end
+			EOF
+		end
+		instance = @plugins.reload("Foobar", "a", "b")
+		instance.instance_variable_get(:@c).should == "a"
+		instance.instance_variable_get(:@d).should == "b"
+
+		instance = @plugins["Foobar"]
+		instance.instance_variable_get(:@c).should == "a"
+		instance.instance_variable_get(:@d).should == "b"
+	end
+
+	it "should be able to reload all modified plugins" do
+		@plugins.load("Foobar", "a", "b")
+		@plugins.load("Barbaz", "a", "b")
+
+		info = @plugins.instance_variable_get(:@plugins)
+		prev_Foobar_time = info["Foobar"][:time]
+		prev_Barbaz_time = info["Barbaz"][:time]
+
+		@foobarrb.utime(Time.now + 1, Time.now + 1)
+		@barbazrb.utime(Time.now + 1, Time.now + 1)
+
+		instances = @plugins.reload(:all, "a", "b")
+		instances["Foobar"].should be_a_kind_of(info["Foobar"][:class])
+		instances["Barbaz"].should be_a_kind_of(info["Barbaz"][:class])
+		instances["Barbaz"].instance_variable_get(:@a).should == "a"
+		instances["Barbaz"].instance_variable_get(:@b).should == "b"
+
+		info["Foobar"][:time].should_not == prev_Foobar_time
+		info["Barbaz"][:time].should_not == prev_Barbaz_time
+
+		@foobarrb.utime(Time.now + 2, Time.now + 2)
+		instances = @plugins.reload(:all, "a", "b")
+		instances["Foobar"].should be_a_kind_of(info["Foobar"][:class])
+	end
+
+	it "should have loaded_plugins method" do
+		@plugins.load("Foobar", "a", "b")
+		@plugins.load("Barbaz", "a", "b")
+		info = @plugins.instance_variable_get(:@plugins)
+
+		res = @plugins.loaded_plugins
+		res.should have_key("Foobar")
+		res.should have_key("Barbaz")
+		res["Foobar"].should be_a_kind_of(info["Foobar"][:class])
+		res["Barbaz"].should be_a_kind_of(info["Barbaz"][:class])
+	end
+
+	it "can iterate" do
+		@plugins.load("Foobar", "a", "b")
+		@plugins.load("Barbaz", "a", "b")
+		info = @plugins.instance_variable_get(:@plugins)
+
+		@plugins.each do |name, instance|
+			instance.should be_a_kind_of(info[name][:class])
+		end
+	end
+
+	it "should be call on_load/on_unload event correctly" do
+		@foobarrb.open("w") do |f|
+			f << <<-EOF
+				class Foobar
+					def initialize(a, b)
+						@a, @b = a, b
+					end
+
+					def on_load
+						@on_load = true
+					end
+
+					def on_unload
+						@on_unload = true
+					end
+				end
+			EOF
+		end
+		instance1 = @plugins.reload("Foobar", "a", "b")
+		instance1.instance_variable_get(:@a).should  == "a"
+		instance1.instance_variable_get(:@on_load).should be_true
+
+		@plugins.unload("Foobar")
+
+		instance1.instance_variable_get(:@on_unload).should be_true
+
+		instance2 = @plugins.load("Foobar", "b", "b")
+		instance2.instance_variable_get(:@a).should  == "b"
+		instance2.instance_variable_get(:@on_load).should be_true
+
+		instance3 = @plugins.reload("Foobar", "c", "b")
+		instance3.instance_variable_get(:@on_load).should be_true
+		instance3.instance_variable_get(:@a).should  == "c"
+
+		instance2.instance_variable_get(:@on_unload).should be_true
+	end
+
+	after :all do
+		@plugins_dir.rmtree
+	end
+end
+
+
Index: lang/ruby/citrus/trunk/spec/spec.opts
===================================================================
--- lang/ruby/citrus/trunk/spec/spec.opts (revision 5852)
+++ lang/ruby/citrus/trunk/spec/spec.opts (revision 5852)
@@ -0,0 +1,1 @@
+--color
Index: lang/ruby/citrus/trunk/citrus.rb
===================================================================
--- lang/ruby/citrus/trunk/citrus.rb (revision 6626)
+++ lang/ruby/citrus/trunk/citrus.rb (revision 6626)
@@ -0,0 +1,42 @@
+#!/usr/bin/env ruby
+$LOAD_PATH << "lib"
+ENV["GETTEXT_PATH"] = "./data/locale"
+
+require "lib/citrus"
+
+require "optparse"
+require "ostruct"
+require "yaml"
+
+opts = OpenStruct.new({
+	:file => "config.yaml"
+})
+
+OptionParser.new do |parser|
+	parser.instance_eval do
+		self.banner  = <<-EOB.unindent
+			Usage: #{$0} [opts]
+
+		EOB
+
+		separator ""
+
+		separator "Options:"
+		on("-c", "--config FILE.yaml", "Config file") do |file|
+			opts.file = file
+		end
+
+		parse!(ARGV)
+	end
+end
+
+begin
+	count = 60
+	Citrus.run(opts.file)
+rescue
+	warn "Conenction closed... will retry after #{count} sec."
+	sleep count
+	count *= 2
+	retry
+end
+
Index: lang/ruby/citrus/trunk/README
===================================================================
--- lang/ruby/citrus/trunk/README (revision 6629)
+++ lang/ruby/citrus/trunk/README (revision 6629)
@@ -0,0 +1,15 @@
+
+# Citrus - pluggable IRC bot framework
+
+
+## Test
+
+Test all:
+	$ rake test
+
+Test plugins:
+	$ rake plugins:test
+
+Test specified plugin:
+	$ rake plugins:test file=plusplus.rb
+
Index: lang/ruby/citrus/trunk/po/ja/always_no_op.po
===================================================================
--- lang/ruby/citrus/trunk/po/ja/always_no_op.po (revision 6426)
+++ lang/ruby/citrus/trunk/po/ja/always_no_op.po (revision 6426)
@@ -0,0 +1,21 @@
+# Japanese translations for cho package
+# cho パッケージに対する英訳.
+# Copyright (C) 2008 THE cho'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the cho package.
+# cho45 <cho45@lab.lowreal.net>, 2008.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: cho 45\n"
+"POT-Creation-Date: 2008-02-09 15:32+0900\n"
+"PO-Revision-Date: 2008-02-09 15:41+0900\n"
+"Last-Translator: cho45 <cho45@lab.lowreal.net>\n"
+"Language-Team: Japanese\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: plugins/always_no_op.rb:9
+msgid "Don't mode +o me."
+msgstr "オペレータにしないでください＞＜"
Index: lang/ruby/citrus/trunk/po/always_no_op.pot
===================================================================
--- lang/ruby/citrus/trunk/po/always_no_op.pot (revision 6426)
+++ lang/ruby/citrus/trunk/po/always_no_op.pot (revision 6426)
@@ -0,0 +1,21 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: always_no_op.rb\n"
+"POT-Creation-Date: 2008-02-09 15:32+0900\n"
+"PO-Revision-Date: 2008-02-09 15:32+0900\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
+
+#: plugins/always_no_op.rb:9
+msgid "Don't mode +o me."
+msgstr ""
