net-irc-0.0.9/0000775000175000017500000000000011604616526012726 5ustar uwabamiuwabaminet-irc-0.0.9/examples/0000775000175000017500000000000011604616526014544 5ustar uwabamiuwabaminet-irc-0.0.9/examples/2ig.rb0000755000175000017500000001346711604616526015566 0ustar uwabamiuwabami#!/usr/bin/env ruby # vim:encoding=UTF-8: $LOAD_PATH << "lib" $LOAD_PATH << "../lib" $KCODE = "u" if RUBY_VERSION < "1.9" # json use this require "rubygems" require "net/irc" require "logger" require "pathname" require "yaml" require 'uri' require 'net/http' require 'nkf' require 'stringio' require 'zlib' require "#{Pathname.new(__FILE__).parent.expand_path}/2ch.rb" Net::HTTP.version_1_2 class NiChannelIrcGateway < Net::IRC::Server::Session def server_name "2ch" end def server_version "0.0.0" end def initialize(*args) super @channels = {} end def on_disconnected @channels.each do |chan, info| begin info[:observer].kill if info[:observer] rescue end end end def on_user(m) super @real, *@opts = @real.split(/\s+/) @opts ||= [] end def on_join(m) channels = m.params.first.split(/,/) channels.each do |channel| @channels[channel] = { :topic => "", :dat => nil, :interval => nil, :observer => nil, } unless @channels.key?(channel) post @prefix, JOIN, channel post nil, RPL_NAMREPLY, @prefix.nick, "=", channel, "@#{@prefix.nick}" post nil, RPL_ENDOFNAMES, @prefix.nick, channel, "End of NAMES list" end end def on_part(m) channel = m.params[0] if @channels.key?(channel) info = @channels.delete(channel) info[:observer].kill if info[:observer] post @prefix, PART, channel end end def on_privmsg(m) target, mesg = *m.params m.ctcps.each {|ctcp| on_ctcp(target, ctcp) } if m.ctcp? end def on_ctcp(target, mesg) type, mesg = mesg.split(" ", 2) method = "on_ctcp_#{type.downcase}".to_sym send(method, target, mesg) if respond_to? method, true end def on_ctcp_action(target, mesg) command, *args = mesg.split(" ") command.downcase! case command when 'next' if @channels.key?(target) guess_next_thread(target) end end rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end def on_topic(m) channel, topic, = m.params p m.params if @channels.key?(channel) info = @channels[channel] unless topic post nil, '332', channel, info[:topic] return end uri, interval = *topic.split(/\s/) interval = interval.to_i post @prefix, TOPIC, channel, topic case when !info[:dat], uri != info[:dat].uri post @prefix, NOTICE, channel, "Thread URL has been changed." info[:dat] = ThreadData.new(uri) create_observer(channel) when info[:interval] != interval post @prefix, NOTICE, channel, "Interval has been changed." create_observer(channel) end info[:topic] = topic info[:interval] = interval > 0 ? interval : 90 end end def guess_next_thread(channel) info = @channels[channel] post server_name, NOTICE, channel, "Current Thread: #{info[:dat].subject}" threads = info[:dat].guess_next_thread threads.first(3).each do |t| if t[:continuous_num] && t[:appear_recent] post server_name, NOTICE, channel, "#{t[:uri]} \003%d#{t[:subject]}\017" % 10 else post server_name, NOTICE, channel, "#{t[:uri]} #{t[:subject]}" end end threads end def create_observer(channel) info = @channels[channel] info[:observer].kill if info[:observer] @log.debug "create_observer %s, interval %d" % [channel, info[:interval]] info[:observer] = Thread.start(info, channel) do |info, channel| Thread.pass loop do begin sleep info[:interval] @log.debug "retrieving (interval %d) %s..." % [info[:interval], info[:dat].uri] info[:dat].retrieve.last(100).each do |line| priv_line channel, line end if info[:dat].length >= 1000 post server_name, NOTICE, channel, "Thread is over 1000. Guessing next thread..." guess_next_thread(channel) break end rescue UnknownThread # pass rescue Exception => e @log.error "Error: #{e.inspect}" e.backtrace.each do |l| @log.error "\t#{l}" end end end end end def priv_line(channel, line) post "%d{%s}" % [line.n, line.id], PRIVMSG, channel, line.aa?? encode_aa(line.body) : line.body end def encode_aa(aa) uri = URI('http://tinyurl.com/api-create.php') uri.query = 'url=' + URI.escape(<<-EOS.gsub(/[\n\t]/, '')) data:text/html,
#{aa.gsub(/\n/, '
')}
EOS Net::HTTP.get(uri.host, uri.request_uri, uri.port) end end if __FILE__ == $0 require "optparse" opts = { :port => 16701, :host => "localhost", :log => nil, :debug => false, :foreground => false, } OptionParser.new do |parser| parser.instance_eval do self.banner = <<-EOB.gsub(/^\t+/, "") Usage: #{$0} [opts] EOB separator "" separator "Options:" on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port| opts[:port] = port end on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host| opts[:host] = host end on("-l", "--log LOG", "log file") do |log| opts[:log] = log end on("--debug", "Enable debug mode") do |debug| opts[:log] = $stdout opts[:debug] = true end on("-f", "--foreground", "run foreground") do |foreground| opts[:log] = $stdout opts[:foreground] = true end parse!(ARGV) end end opts[:logger] = Logger.new(opts[:log], "daily") opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO def daemonize(foreground=false) trap("SIGINT") { exit! 0 } trap("SIGTERM") { exit! 0 } trap("SIGHUP") { exit! 0 } return yield if $DEBUG || foreground Process.fork do Process.setsid Dir.chdir "/" File.open("/dev/null") {|f| STDIN.reopen f STDOUT.reopen f STDERR.reopen f } yield end exit! 0 end daemonize(opts[:debug] || opts[:foreground]) do Net::IRC::Server.new(opts[:host], opts[:port], NiChannelIrcGateway, opts).start end end net-irc-0.0.9/examples/sig.rb0000755000175000017500000000721711604616526015663 0ustar uwabamiuwabami#!/usr/bin/env ruby # vim:encoding=UTF-8: =begin # sig.rb ServerLog IRC Gateway # Usage * Connect. * Join a channel (you can name it as you like) * Set topic "filename regexp" * You will see the log at the channel only matching the regexp. =end $LOAD_PATH << "lib" $LOAD_PATH << "../lib" $KCODE = "u" unless defined? ::Encoding require "rubygems" require "net/irc" require "logger" require "pathname" require "yaml" class ServerLogIrcGateway < Net::IRC::Server::Session def server_name "serverlog" end def server_version "0.0.0" end def initialize(*args) super @channels = {} @config = Pathname.new(ENV["HOME"]) + ".sig" end def on_disconnected @channels.each do |chan, info| begin info[:observer].kill if info[:observer] rescue end end end def on_user(m) super @real, *@opts = @real.split(/\s+/) @opts ||= [] end def on_join(m) channels = m.params.first.split(/,/) channels.each do |channel| @channels[channel] = { :topic => "", :observer => nil, } unless @channels.key?(channel) post @prefix, JOIN, channel post nil, RPL_NAMREPLY, @prefix.nick, "=", channel, "@#{@prefix.nick}" post nil, RPL_ENDOFNAMES, @prefix.nick, channel, "End of NAMES list" end end def on_topic(m) channel, topic, = m.params p m.params if @channels.key?(channel) post @prefix, TOPIC, channel, topic @channels[channel][:topic] = topic create_observer(channel) end end def create_observer(channel) return unless @channels.key?(channel) info = @channels[channel] @log.debug "create_observer<#{channel}>#{info.inspect}" begin info[:observer].kill if info[:observer] rescue end info[:observer] = Thread.start(channel, info) do |chan, i| file, reg, = i[:topic].split(/\s+/) name = File.basename(file) grep = Regexp.new(reg.to_s) @log.info "#{file} / grep => #{grep.inspect}" File.open(file) do |f| size = File.size(f) f.seek size loop do sleep 1 nsize = File.size(f) if nsize > size @log.debug "follow up log" while l = f.gets if grep === l post name, PRIVMSG, chan, l end end end size = nsize end end end end end if __FILE__ == $0 require "optparse" opts = { :port => 16700, :host => "localhost", :log => nil, :debug => false, :foreground => false, } OptionParser.new do |parser| parser.instance_eval do self.banner = <<-EOB.gsub(/^\t+/, "") Usage: #{$0} [opts] EOB separator "" separator "Options:" on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port| opts[:port] = port end on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host| opts[:host] = host end on("-l", "--log LOG", "log file") do |log| opts[:log] = log end on("--debug", "Enable debug mode") do |debug| opts[:log] = $stdout opts[:debug] = true end on("-f", "--foreground", "run foreground") do |foreground| opts[:log] = $stdout opts[:foreground] = true end parse!(ARGV) end end opts[:logger] = Logger.new(opts[:log], "daily") opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO def daemonize(foreground=false) trap("SIGINT") { exit! 0 } trap("SIGTERM") { exit! 0 } trap("SIGHUP") { exit! 0 } return yield if $DEBUG || foreground Process.fork do Process.setsid Dir.chdir "/" File.open("/dev/null") {|f| STDIN.reopen f STDOUT.reopen f STDERR.reopen f } yield end exit! 0 end daemonize(opts[:debug] || opts[:foreground]) do Net::IRC::Server.new(opts[:host], opts[:port], ServerLogIrcGateway, opts).start end end net-irc-0.0.9/examples/lig.rb0000755000175000017500000003444711604616526015661 0ustar uwabamiuwabami#!/usr/bin/env ruby =begin # lig.rb Lingr IRC Gateway - IRC Gateway to Lingr ( http://www.lingr.com/ ) ## Launch $ ruby lig.rb # daemonized If you want to help: $ ruby lig.rb --help Usage: examples/lig.rb [opts] Options: -p, --port [PORT=16669] port number to listen -h, --host [HOST=localhost] host name or IP address to listen -l, --log LOG log file -a, --api_key API_KEY Your api key on Lingr --debug Enable debug mode ## Configuration Configuration example for Tiarra ( http://coderepos.org/share/wiki/Tiarra ). lingr { host: localhost port: 16669 name: username@example.com (Email on Lingr) password: password on Lingr in-encoding: utf8 out-encoding: utf8 } Set your email as IRC 'real name' field, and password as server password. This does not allow anonymous connection to Lingr. You must create a account on Lingr and get API key (ask it first time). ## Client This gateway sends multibyte nicknames at Lingr rooms as-is. So you should use a client which treats it correctly. Recommended: * LimeChat for OSX ( http://limechat.sourceforge.net/ ) * Irssi ( http://irssi.org/ ) * (gateway) Tiarra ( http://coderepos.org/share/wiki/Tiarra ) ## Nickname/Mask nick -> nickname in a room. o_id -> occupant_id (unique id in a room) u_id -> user_id (unique user id in Lingr) * Anonymous User: |!anon@lingr.com * Logged-in User: |!@lingr.com * Your: |!@lingr.com So you can see some nicknames in same user, but it is needed for nickname management on client. (Lingr allows different nicknames between rooms in a same user, but IRC not) ## Licence Ruby's by cho45 ## 備考 このクライアントで 1000speakers への応募はできません。lingr.com から行ってください。 =end $LOAD_PATH << File.dirname(__FILE__) $LOAD_PATH << "lib" $LOAD_PATH << "../lib" require "rubygems" require "lingr" require "net/irc" require "pit" require "mutex_m" class LingrIrcGateway < Net::IRC::Server::Session def server_name "lingrgw" end def server_version "0.0.0" end def initialize(*args) super @channels = {} @channels.extend(Mutex_m) end def on_user(m) super @real, *@copts = @real.split(/\s+/) @copts ||= [] # Tiarra sends prev nick when reconnects. @nick.sub!(/\|.+$/, "") log "Hello #{@nick}, this is Lingr IRC Gateway." log "Client Option: #{@copts.join(", ")}" @log.info "Client Option: #{@copts.join(", ")}" @log.info "Client initialization is completed." @lingr = Lingr::Client.new(@opts.api_key) @lingr.create_session('human') @lingr.login(@real, @pass) @session_observer = Thread.start do loop do begin @log.info "Verifying session..." @log.info "Verifed session => #{@lingr.verify_session.inspect}" rescue Lingr::Client::APIError => e @log.info "Verify session raised APIError<#{e.code}:#{e.message}>. Try to re-create session." @lingr.create_session('human') @lingr.login(@real, @pass) rescue Exception => e @log.info "Error on verify_session: #{e.inspect}" end sleep 9 * 60 end end @user_info = @lingr.get_user_info prefix = make_ids(@user_info) @user_info["prefix"] = prefix post @prefix, NICK, prefix.nick rescue Lingr::Client::APIError => e case e.code when 105 post nil, ERR_PASSWDMISMATCH, @nick, "Password incorrect" else log "Error: #{e.code}: #{e.message}" end finish end def on_privmsg(m) target, message = *m.params if @channels.key?(target.downcase) @lingr.say(@channels[target.downcase][:ticket], message) else post nil, ERR_NOSUCHNICK, @user_info["prefix"].nick, target, "No such nick/channel" end rescue Lingr::Client::APIError => e log "Error: #{e.code}: #{e.message}" log "Coundn't say to #{target}." on_join(Message.new(nil, "JOIN", [target])) if e.code == 102 # invalid session end def on_notice(m) on_privmsg(m) end def on_whois(m) nick = m.params[0] chan = nil info = nil @channels.each do |k, v| if v[:users].key?(nick) chan = k info = v[:users][nick] break end end if chan prefix = info["prefix"] real_name = info["description"].to_s server_info = "Lingr: type:#{info["client_type"]} source:#{info["source"]}" channels = [info["client_type"] == "human" ? "@#{chan}" : chan] me = @user_info["prefix"] post nil, RPL_WHOISUSER, me.nick, prefix.nick, prefix.user, prefix.host, "*", real_name post nil, RPL_WHOISSERVER, me.nick, prefix.nick, prefix.host, server_info # post nil, RPL_WHOISOPERATOR, me.nick, prefix.nick, "is an IRC operator" # post nil, RPL_WHOISIDLE, me.nick, prefix.nick, idle, "seconds idle" post nil, RPL_WHOISCHANNELS, me.nick, prefix.nick, channels.join(" ") post nil, RPL_ENDOFWHOIS, me.nick, prefix.nick, "End of WHOIS list" else post nil, ERR_NOSUCHNICK, me.nick, nick, "No such nick/channel" end rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end def on_who(m) channel = m.params[0] return unless channel info = @channels.synchronize { @channels[channel.downcase] } me = @user_info["prefix"] res = @lingr.get_room_info(info[:chan_id], nil, info[:password]) res["occupants"].each do |o| next unless o["nickname"] u_id, o_id, prefix = *make_ids(o, true) op = (o["client_type"] == "human") ? "@" : "" post nil, RPL_WHOREPLY, me.nick, channel, o_id, "lingr.com", "lingr.com", prefix.nick, "H*#{op}", "0 #{o["description"].to_s.gsub(/\s+/, " ")}" end post nil, RPL_ENDOFWHO, me.nick, channel rescue Lingr::Client::APIError => e log "Maybe gateway don't know password for channel #{channel}. Please part and join." end def on_join(m) channels = m.params[0].split(/\s*,\s*/) password = m.params[1] channels.each do |channel| next if @channels.key? channel.downcase begin @log.debug "Enter room -> #{channel}" res = @lingr.enter_room(channel.sub(/^#/, ""), @nick, password) res["password"] = password @channels.synchronize do create_observer(channel, res) end rescue Lingr::Client::APIError => e log "Error: #{e.code}: #{e.message}" log "Coundn't join to #{channel}." if e.code == 102 log "Invalid session... prompt the client to reconnect" finish end rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end end end def on_part(m) channel = m.params[0] info = @channels[channel.downcase] prefix = @user_info["prefix"] if info info[:observer].kill @lingr.exit_room(info[:ticket]) @channels.delete(channel.downcase) post prefix, PART, channel, "Parted" else post nil, ERR_NOSUCHCHANNEL, prefix.nick, channel, "No such channel" end rescue Lingr::Client::APIError => e unless e.code == 102 log "Error: #{e.code}: #{e.message}" log "Coundn't say to #{target}." @channels.delete(channel.downcase) post prefix, PART, channel, "Parted" end end def on_disconnected @channels.each do |k, info| info[:observer].kill end @session_observer.kill rescue nil begin @lingr.destroy_session rescue end end private def create_observer(channel, response) Thread.start(channel, response) do |chan, res| myprefix = @user_info["prefix"] if @channels[chan.downcase] @channels[chan.downcase][:observer].kill rescue nil end @channels[chan.downcase] = { :ticket => res["ticket"], :counter => res["room"]["counter"], :o_id => res["occupant_id"], :chan_id => res["room"]["id"], :password => res["password"], :users => res["occupants"].reject {|i| i["nickname"].nil? }.inject({}) {|r,i| i["prefix"] = make_ids(i) r.update(i["prefix"].nick => i) }, :hcounter => 0, :observer => Thread.current, } post server_name, TOPIC, chan, "#{res["room"]["url"]} #{res["room"]["description"]}" post myprefix, JOIN, channel post server_name, MODE, channel, "+o", myprefix.nick post nil, RPL_NAMREPLY, myprefix.nick, "=", chan, @channels[chan.downcase][:users].map{|k,v| v["client_type"] == "human" ? "@#{k}" : k }.join(" ") post nil, RPL_ENDOFNAMES, myprefix.nick, chan, "End of NAMES list" info = @channels[chan.downcase] while true begin @log.debug "observe_room<#{info[:counter]}><#{chan}> start <- #{myprefix}" res = @lingr.observe_room info[:ticket], info[:counter] info[:counter] = res["counter"] if res["counter"] (res["messages"] || []).each do |m| next if m["id"].to_i <= info[:hcounter] u_id, o_id, prefix = *make_ids(m, true) case m["type"] when "user" # Don't send my messages. unless info[:o_id] == o_id post prefix, PRIVMSG, chan, m["text"] end when "private" # TODO not sent from lingr? post prefix, PRIVMSG, chan, ctcp_encoding("ACTION Sent private: #{m["text"]}") # system:{enter,leave,nickname_changed} should not be used for nick management. # when "system:enter" # post prefix, PRIVMSG, chan, ctcp_encoding("ACTION #{m["text"]}") # when "system:leave" # post prefix, PRIVMSG, chan, ctcp_encoding("ACTION #{m["text"]}") # when "system:nickname_change" # post prefix, PRIVMSG, chan, ctcp_encoding("ACTION #{m["text"]}") when "system:broadcast" post "system.broadcast", NOTICE, chan, m["text"] end info[:hcounter] = m["id"].to_i if m["id"] end if res["occupants"] enter = [], leave = [] newusers = res["occupants"].reject {|i| i["nickname"].nil? }.inject({}) {|r,i| i["prefix"] = make_ids(i) r.update(i["prefix"].nick => i) } nickchange = newusers.inject({:new => [], :old => []}) {|r,(k,new)| old = info[:users].find {|l,old| # same occupant_id and different nickname # when nickname was changed and when un-authed user promoted to authed user. new["prefix"] != old["prefix"] && new["id"] == old["id"] } if old old = old[1] post old["prefix"], NICK, new["prefix"].nick r[:old] << old["prefix"].nick r[:new] << new["prefix"].nick end r } entered = newusers.keys - info[:users].keys - nickchange[:new] leaved = info[:users].keys - newusers.keys - entered - nickchange[:old] leaved.each do |leave| leave = info[:users][leave] post leave["prefix"], PART, chan, "" end entered.each do |enter| enter = newusers[enter] prefix = enter["prefix"] post prefix, JOIN, chan if enter["client_type"] == "human" post server_name, MODE, chan, "+o", prefix.nick end end info[:users] = newusers end rescue Lingr::Client::APIError => e case e.code when 100 @log.fatal "BUG: API returns invalid HTTP method" exit 1 when 102 @log.error "BUG: API returns invalid session. Prompt the client to reconnect." finish when 104 @log.fatal "BUG: API returns invalid response format. JSON is unsupported?" exit 1 when 109 @log.error "Error: API returns invalid ticket. Rejoin this channel..." on_part(Message.new(nil, PART, [chan, res["error"]["message"]])) on_join(Message.new(nil, JOIN, [chan, info["password"]])) when 114 @log.fatal "BUG: API returns no counter parameter." exit 1 when 120 @log.error "Error: API returns invalid encoding. But continues." when 122 @log.error "Error: API returns repeated counter. But continues." info[:counter] += 10 log "Error: repeated counter. Some message may be ignored..." else # may be socket error? @log.debug "observe failed : #{res.inspect}" log "Error: #{e.code}: #{e.message}" end rescue Timeout::Error # pass rescue JSON::ParserError => e @log.error e info[:counter] += 10 log "Error: JSON::ParserError Some message may be ignored..." rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep 1 end end end def log(str) str.gsub!(/\s/, " ") begin post nil, NOTICE, @user_info["prefix"].nick, str rescue post nil, NOTICE, @nick, str end end def make_ids(o, ext=false) u_id = o["user_id"] || "anon" o_id = o["occupant_id"] || o["id"] nick = (o["default_nickname"] || o["nickname"]).gsub(/\s+/, "") if o["user_id"] == @user_info["user_id"] nick << "|#{o["user_id"]}" else nick << "|#{o["user_id"] ? o_id : "_"+o_id}" end pref = Prefix.new("#{nick}!#{u_id}@lingr.com") ext ? [u_id, o_id, pref] : pref end end if __FILE__ == $0 require "rubygems" require "optparse" require "pit" opts = { :port => 16669, :host => "localhost", :log => nil, :debug => false, :foreground => false, } OptionParser.new do |parser| parser.instance_eval do self.banner = <<-EOB.gsub(/^\t+/, "") Usage: #{$0} [opts] EOB separator "" separator "Options:" on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port| opts[:port] = port end on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host| opts[:host] = host end on("-l", "--log LOG", "log file") do |log| opts[:log] = log end on("-a", "--api_key API_KEY", "Your api key on Lingr") do |key| opts[:api_key] = key end on("--debug", "Enable debug mode") do |debug| opts[:log] = $stdout opts[:debug] = true end on("-f", "--foreground", "run foreground") do |foreground| opts[:log] = $stdout opts[:foreground] = true end parse!(ARGV) end end opts[:logger] = Logger.new(opts[:log], "daily") opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO def daemonize(foreground=false) [:INT, :TERM, :HUP].each do |sig| Signal.trap sig, "EXIT" end return yield if $DEBUG || foreground Process.fork do Process.setsid Dir.chdir "/" File.open("/dev/null") {|f| STDIN.reopen f STDOUT.reopen f, "w" STDERR.reopen f, "w" } yield end exit! 0 end opts[:api_key] = Pit.get("lig.rb", :require => { "api_key" => "API key of Lingr" })["api_key"] unless opts[:api_key] daemonize(opts[:debug] || opts[:foreground]) do Net::IRC::Server.new(opts[:host], opts[:port], LingrIrcGateway, opts).start end end net-irc-0.0.9/examples/lingr.rb0000644000175000017500000002312711604616526016207 0ustar uwabamiuwabami# Ruby client for the Lingr[http://www.lingr.com] API. For more details and tutorials, see the # {Lingr API Reference}[http://wiki.lingr.com/dev/show/API+Reference] pages on the {Lingr Developer Wiki}[http://wiki.lingr.com]. # # All methods return a hash with two keys: # * :succeeded - true if the method succeeded, false otherwise # * :response - a Hash version of the response document received from the server # # = api_client.rb # # Lingr API client # # # Original written by Lingr. # Modified by cho45 # * Use json gem instead of gsub/eval. # * Raise APIError when api fails. # * Rename class name to Lingr::Client. $KCODE = 'u' # used by json require "rubygems" require "net/http" require "json" require "uri" require "timeout" module Lingr class Client class ClientError < StandardError; end class APIError < ClientError def initialize(error) @error = error || { "message" => "socket error", "code" => 0, } super(@error["message"]) end def code @error["code"] end end attr_accessor :api_key # 0 = quiet, 1 = some debug info, 2 = more debug info attr_accessor :verbosity attr_accessor :session attr_accessor :timeout def initialize(api_key, verbosity=0, hostname='www.lingr.com') @api_key = api_key @host = hostname @verbosity = verbosity @timeout = 60 end # Create a new API session # def create_session(client_type='automaton') if @session @error_info = nil raise ClientError, "already in a session" end ret = do_api :post, 'session/create', { :api_key => @api_key, :client_type => client_type }, false @session = ret["session"] ret end # Verify a session id. If no session id is passed, verifies the current session id for this ApiClient # def verify_session(session_id=nil) do_api :get, 'session/verify', { :session => session_id || @session }, false end # Destroy the current API session # def destroy_session ret = do_api :post, 'session/destroy', { :session => @session } @session = nil ret end # Get a list of the currently hot rooms # def get_hot_rooms(count=nil) do_api :get, 'explore/get_hot_rooms', { :api_key => @api_key }.merge(count ? { :count => count} : {}), false end # Get a list of the newest rooms # def get_new_rooms(count=nil) do_api :get, 'explore/get_new_rooms', { :api_key => @api_key }.merge(count ? { :count => count} : {}), false end # Get a list of the currently hot tags # def get_hot_tags(count=nil) do_api :get, 'explore/get_hot_tags', { :api_key => @api_key }.merge(count ? { :count => count} : {}), false end # Get a list of all tags # def get_all_tags(count=nil) do_api :get, 'explore/get_all_tags', { :api_key => @api_key }.merge(count ? { :count => count} : {}), false end # Search room name, description, and tags for keywords. Keywords can be a String or an Array. # def search(keywords) do_api :get, 'explore/search', { :api_key => @api_key, :q => keywords.is_a?(Array) ? keywords.join(',') : keywords }, false end # Search room tags. Tagnames can be a String or an Array. # def search_tags(tagnames) do_api :get, 'explore/search_tags', { :api_key => @api_key, :q => tagnames.is_a?(Array) ? tagnames.join(',') : tagnames }, false end # Search archives. If room_id is non-nil, the search is limited to the archives of that room. # def search_archives(query, room_id=nil) params = { :api_key => @api_key, :q => query } params.merge!({ :id => room_id }) if room_id do_api :get, 'explore/search_archives', params, false end # Authenticate a user within the current API session # def login(email, password) do_api :post, 'auth/login', { :session => @session, :email => email, :password => password } end # Log out the currently-authenticated user in the session, if any # def logout do_api :post, 'auth/logout', { :session => @session } end # Get information about the currently-authenticated user # def get_user_info do_api :get, 'user/get_info', { :session => @session } end # Start observing the currently-authenticated user # def start_observing_user do_api :post, 'user/start_observing', { :session => @session } end # Observe the currently-authenticated user, watching for profile changes # def observe_user(ticket, counter) do_api :get, 'user/observe', { :session => @session, :ticket => ticket, :counter => counter } end # Stop observing the currently-authenticated user # def stop_observing_user(ticket) do_api :post, 'user/stop_observing', { :session => @session, :ticket =>ticket } end # Get information about a chatroom, including room description, current occupants, recent messages, etc. # def get_room_info(room_id, counter=nil, password=nil) params = { :api_key => @api_key, :id => room_id } params.merge!({ :counter => counter }) if counter params.merge!({ :password => password }) if password do_api :get, 'room/get_info', params, false end # Create a chatroom # # options is a Hash containing any of the parameters allowed for room.create. If the :image key is present # in options, its value must be a hash with the keys :filename, :mime_type, and :io # def create_room(options) do_api :post, 'room/create', options.merge({ :session => @session }) end # Change the settings for a chatroom # # options is a Hash containing any of the parameters allowed for room.create. If the :image key is present # in options, its value must be a hash with the keys :filename, :mime_type, and :io. To change the id for # a room, use the key :new_id # def change_settings(room_id, options) do_api :post, 'room/change_settings', options.merge({ :session => @session }) end # Delete a chatroom # def delete_room(room_id) do_api :post, 'room/delete', { :id => room_id, :session => @session } end # Enter a chatroom # def enter_room(room_id, nickname=nil, password=nil, idempotent=false) params = { :session => @session, :id => room_id } params.merge!({ :nickname => nickname }) if nickname params.merge!({ :password => password }) if password params.merge!({ :idempotent => 'true' }) if idempotent do_api :post, 'room/enter', params end # Poll for messages in a chatroom # def get_messages(ticket, counter, user_messages_only=false) do_api :get, 'room/get_messages', { :session => @session, :ticket => ticket, :counter => counter, :user_messages_only => user_messages_only } end # Observe a chatroom, waiting for events to occur in the room # def observe_room(ticket, counter) do_api :get, 'room/observe', { :session => @session, :ticket => ticket, :counter => counter } end # Set your nickname in a chatroom # def set_nickname(ticket, nickname) do_api :post, 'room/set_nickname', { :session => @session, :ticket => ticket, :nickname => nickname } end # Say something in a chatroom. If target_occupant_id is not nil, a private message # is sent to the indicated occupant. # def say(ticket, msg, target_occupant_id = nil) params = { :session => @session, :ticket => ticket, :message => msg } params.merge!({ :occupant_id => target_occupant_id}) if target_occupant_id do_api :post, 'room/say', params end # Exit a chatroom # def exit_room(ticket) do_api :post, 'room/exit', { :session => @session, :ticket => ticket } end private def do_api(method, path, parameters, require_session=true) if require_session and !@session raise ClientError, "not in a session" end response = Timeout.timeout(@timeout) { JSON.parse(self.send(method, url_for(path), parameters.merge({ :format => 'json' }))) } unless success?(response) raise APIError, response["error"] end response end def url_for(method) "http://#{@host}/#{@@PATH_BASE}#{method}" end def get(url, params) uri = URI.parse(url) path = uri.path q = params.inject("?") {|s, p| s << "#{p[0].to_s}=#{URI.encode(p[1].to_s, /./)}&"}.chop path << q if q.length > 0 Net::HTTP.start(uri.host, uri.port) do |http| http.read_timeout = @timeout req = Net::HTTP::Get.new(path) req.basic_auth(uri.user, uri.password) if uri.user parse_result http.request(req) end end def post(url, params) if !params.find {|p| p[1].is_a?(Hash)} params = params.inject({}){|hash,(k,v)| hash[k.to_s] = v; hash} parse_result Net::HTTP.post_form(URI.parse(url), params) else boundary = 'lingr-api-client' + (0x1000000 + rand(0x1000000).to_s(16)) query = params.collect { |p| ret = ["--#{boundary}"] if p[1].is_a?(Hash) ret << "Content-Disposition: form-data; name=\"#{URI.encode(p[0].to_s)}\"; filename=\"#{p[1][:filename]}\"" ret << "Content-Transfer-Encoding: binary" ret << "Content-Type: #{p[1][:mime_type]}" ret << "" ret << p[1][:io].read else ret << "Content-Disposition: form-data; name=\"#{URI.encode(p[0].to_s)}\"" ret << "" ret << p[1] end ret.join("\r\n") }.join('') + "--#{boundary}--\r\n" uri = URI.parse(url) Net::HTTP.start(uri.host, uri.port) do |http| http.read_timeout = @timeout parse_result http.post2(uri.path, query, "Content-Type" => "multipart/form-data; boundary=#{boundary}") end end end def parse_result(result) return nil if !result || result.code != '200' || (!result['Content-Type'] || result['Content-Type'].index('text/javascript') != 0) # puts # puts # puts result.body # puts # puts result.body end def success?(response) return false if !response response["status"] and response["status"] == 'ok' end @@PATH_BASE = 'api/' end end net-irc-0.0.9/examples/client.rb0000755000175000017500000000052511604616526016352 0ustar uwabamiuwabami#!/usr/bin/env ruby # vim:fileencoding=UTF-8: $LOAD_PATH << "lib" $LOAD_PATH << "../lib" require "rubygems" require "net/irc" require "pp" class SimpleClient < Net::IRC::Client def initialize(*args) super end end SimpleClient.new("foobar", "6667", { :nick => "foobartest", :user => "foobartest", :real => "foobartest", }).start net-irc-0.0.9/examples/mixi.rb0000755000175000017500000001210311604616526016035 0ustar uwabamiuwabami#!/usr/bin/env ruby # vim:encoding=UTF-8: =begin ## Licence Ruby's by cho45 =end $LOAD_PATH << "lib" $LOAD_PATH << "../lib" $KCODE = "u" unless defined? ::Encoding # json use this require "rubygems" require "json" require "net/irc" require "mechanize" # Mixi from mixi.vim by ujihisa! class Mixi def initialize(email, password, mixi_premium = false, image_dir = '~/.vim/mixi_images') require 'kconv' require 'rubygems' require 'mechanize' @image_dir = File.expand_path image_dir @email, @password, @mixi_premium = email, password, mixi_premium end def post(title, body, images) @agent = WWW::Mechanize.new @agent.user_agent_alias = 'Mac Safari' page = @agent.get 'http://mixi.jp/home.pl' form = page.forms[0] form.email = @email form.password = @password @agent.submit form page = @agent.get "http://mixi.jp/home.pl" #page = @agent.get page.links[18].uri page = @agent.get page.links[14].uri form = page.forms[1] #form = page.forms[(@mixi_premium ? 1 : 0)] form.diary_title = title form.diary_body = self.class.magic_body(body) get_image images images[0, 3].each_with_index do |img, i| if /darwin/ =~ RUBY_PLATFORM && /\.png$/i =~ img imgjpg = '/tmp/mixi-vim-' << File.basename(img).sub(/\.png$/i, '.jpg') system "sips -s format jpeg --out #{imgjpg} #{img} > /dev/null 2>&1" img = imgjpg end form.file_uploads[i].file_name = img end page = @agent.submit form page = @agent.submit page.forms[1] end def get_latest page = @agent.get 'http://mixi.jp/list_diary.pl' ["http://mixi.jp/" << page.links[33].uri.to_s.toutf8, page.links[33].text.toutf8] end def self.magic_body(body) body.gsub(/^( )+/) {|i| ' '.toeuc * (i.length/2) } end def get_image(images) images.each_with_index do |img, i| if img =~ %r{^http://} path = File.join @image_dir, i.to_s + File.extname(img) unless File.exist? @image_dir Dir.mkdir @image_dir else Dir.chdir(@image_dir) do Dir.entries(@image_dir). each {|f| File.unlink f if File.file? f } end end system "wget -O #{path} #{img} > /dev/null 2>&1" if File.exist? path and !File.zero? path images[i] = path else images.delete_at i end end end end end class MixiDiary < Net::IRC::Server::Session def server_name "mixi" end def server_version "0.0.0" end def main_channel "#mixi" end def initialize(*args) super @ua = WWW::Mechanize.new end def on_user(m) super post @prefix, JOIN, main_channel post server_name, MODE, main_channel, "+o", @prefix.nick @real, *@opts = @opts.name || @real.split(/\s+/) @opts ||= [] @mixi = Mixi.new(@real, @pass) @cont = [] end def on_disconnected @observer.kill rescue nil end def on_privmsg(m) super # CTCP にしたほうがよくないか? case m[1] when "." title, *body = *@cont @mixi.post ">_<× < #{title}".toeuc, body.join("\n").toeuc, [] @mixi.get_latest.each do |line| post server_name, NOTICE, main_channel, line.chomp end when "c" @cont.clear post server_name, NOTICE, main_channel, "cleared." when "p" @cont.each do |l| post server_name, NOTICE, main_channel, l end post server_name, NOTICE, main_channel, "^^end" when "d" post server_name, NOTICE, main_channel, "Deleted last line: #{@cont.pop}" else @cont << m[1] if @cont.size == 1 post server_name, NOTICE, main_channel, "start with title: #{@cont.first}" else end end end def on_ctcp(target, message) end def on_whois(m) end def on_who(m) end def on_join(m) end def on_part(m) end end if __FILE__ == $0 require "optparse" opts = { :port => 16701, :host => "localhost", :log => nil, :debug => false, :foreground => false, } OptionParser.new do |parser| parser.instance_eval do self.banner = <<-EOB.gsub(/^\t+/, "") Usage: #{$0} [opts] EOB separator "" separator "Options:" on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port| opts[:port] = port end on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host| opts[:host] = host end on("-l", "--log LOG", "log file") do |log| opts[:log] = log end on("--debug", "Enable debug mode") do |debug| opts[:log] = $stdout opts[:debug] = true end on("-f", "--foreground", "run foreground") do |foreground| opts[:log] = $stdout opts[:foreground] = true end parse!(ARGV) end end opts[:logger] = Logger.new(opts[:log], "daily") opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO def daemonize(foreground=false) trap("SIGINT") { exit! 0 } trap("SIGTERM") { exit! 0 } trap("SIGHUP") { exit! 0 } return yield if $DEBUG || foreground Process.fork do Process.setsid Dir.chdir "/" File.open("/dev/null") {|f| STDIN.reopen f STDOUT.reopen f STDERR.reopen f } yield end exit! 0 end daemonize(opts[:debug] || opts[:foreground]) do Net::IRC::Server.new(opts[:host], opts[:port], MixiDiary, opts).start end end # Local Variables: # coding: utf-8 # End: net-irc-0.0.9/examples/tig.rb0000755000175000017500000016414511604616526015670 0ustar uwabamiuwabami#!/usr/bin/env ruby # vim:encoding=UTF-8: $KCODE = "u" unless defined? ::Encoding # json use this =begin # tig.rb Ruby version of TwitterIrcGateway ## Launch $ ruby tig.rb If you want to help: $ ruby tig.rb --help ## Configuration Options specified by after IRC realname. Configuration example for Tiarra . general { server-in-encoding: utf8 server-out-encoding: utf8 client-in-encoding: utf8 client-out-encoding: utf8 } networks { name: tig } tig { server: localhost 16668 password: password on Twitter # Recommended name: username mentions tid # Same as TwitterIrcGateway.exe.config.sample # (90, 360 and 300 seconds) #name: username dm ratio=4:1 maxlimit=50 #name: username dm ratio=20:5:6 maxlimit=62 mentions # # # (60, 360 and 150 seconds) #name: username dm ratio=30:5:12 maxlimit=94 mentions # # # (36, 360 and 150 seconds) #name: username dm ratio=50:5:12 maxlimit=134 mentions # # for Jabber #name: username jabber=username@example.com:jabberpasswd } ### athack If `athack` client option specified, all nick in join message is leading with @. So if you complemente nicks (e.g. Irssi), it's good for Twitter like reply command (@nick). In this case, you will see torrent of join messages after connected, because NAMES list can't send @ leading nick (it interpreted op.) ### tid[=[,]] Apply ID to each message for make favorites by CTCP ACTION. /me fav [ID...] and can be 0 => white 1 => black 2 => blue navy 3 => green 4 => red 5 => brown maroon 6 => purple 7 => orange olive 8 => yellow 9 => lightgreen lime 10 => teal 11 => lightcyan cyan aqua 12 => lightblue royal 13 => pink lightpurple fuchsia 14 => grey 15 => lightgrey silver ### jabber=: If `jabber=:` option specified, use Jabber to get friends timeline. You must setup im notifing settings in the site and install "xmpp4r-simple" gem. $ sudo gem install xmpp4r-simple Be careful for managing password. ### alwaysim Use IM instead of any APIs (e.g. post) ### ratio=:[:] "121:6:20" by default. /me ratios Ratio | Timeline | DM | Mentions | ---------+----------+-------+----------| 1 | 24s | N/A | N/A | 141:6 | 26s | 10m OR N/A | 135:12 | 27s | 5m OR N/A | 135:6:6 | 27s | 10m | 10m | ---------+----------+-------+----------| 121:6:20 | 30s | 10m | 3m | ---------+----------+-------+----------| 4:1 | 31s | 2m1s | N/A | 50:5:12 | 49s | 8m12s | 3m25s | 20:5:6 | 57s | 3m48s | 3m10s | 30:5:12 | 58s | 5m45s | 2m24s | 1:1:1 | 1m13s | 1m13s | 1m13s | ---------------------------------------+ (Hourly limit: 150) ### dm[=] ### mentions[=] ### maxlimit= ### clientspoofing ### httpproxy=[[:]@]
[:] ### main_channel= ### api_source= ### check_friends_interval= ### check_updates_interval= Set 0 to disable checking. ### old_style_reply ### tmap_size= ### strftime= ### untiny_whole_urls ### bitlify=:: ### unuify ### shuffled_tmap ### ll=, ### with_retweets ## Extended commands through the CTCP ACTION ### list (ls) /me list NICK [NUMBER] ### fav (favorite, favourite, unfav, unfavorite, unfavourite) /me fav [ID...] /me unfav [ID...] /me fav! [ID...] /me fav NICK ### link (ln, url, u) /me link ID [ID...] ### destroy (del, delete, miss, oops, remove, rm) /me destroy [ID...] ### in (location) /me in Sugamo, Tokyo, Japan ### reply (re, mention) /me reply ID blah, blah... ### retweet (rt) /me retweet ID (blah, blah...) ### utf7 (utf-7) /me utf7 ### name /me name My Name ### description (desc) /me description blah, blah... ### spoof /me spoof /me spoo[o...]f /me spoof tigrb twitterircgateway twitt web mobileweb ### bot (drone) /me bot NICK [NICK...] ## Feed ## License Ruby's by cho45 =end case when File.directory?("lib") $LOAD_PATH << "lib" when File.directory?(File.expand_path("lib", "..")) $LOAD_PATH << File.expand_path("lib", "..") end require "rubygems" require "net/irc" require "net/https" require "uri" require "time" require "logger" require "yaml" require "pathname" require "ostruct" require "json" begin require "iconv" require "punycode" rescue LoadError end module Net::IRC::Constants; RPL_WHOISBOT = "335"; RPL_CREATEONTIME = "329"; end class TwitterIrcGateway < Net::IRC::Server::Session @@ctcp_action_commands = [] class << self def ctcp_action(*commands, &block) name = "+ctcp_action_#{commands.inspect}" define_method(name, block) commands.each do |command| @@ctcp_action_commands << [command, name] end end end def server_name "twittergw" end def server_version head = `git rev-parse HEAD 2>/dev/null` head.empty?? "unknown" : head end def available_user_modes "o" end def available_channel_modes "mnti" end def main_channel @opts.main_channel || "#twitter" end def api_base(secure = true) URI("http#{"s" if secure}://twitter.com/") end def api_source "#{@opts.api_source || "tigrb"}" end def jabber_bot_id "twitter@twitter.com" end def hourly_limit 150 end class APIFailed < StandardError; end MAX_MODE_PARAMS = 3 WSP_REGEX = Regexp.new("\\r\\n|[\\r\\n\\t#{"\\u00A0\\u1680\\u180E\\u2002-\\u200D\\u202F\\u205F\\u2060\\uFEFF" if "\u0000" == "\000"}]") def initialize(*args) super @groups = {} @channels = [] # joined channels (groups) @nicknames = {} @drones = [] @config = Pathname.new(ENV["HOME"]) + ".tig" ### TODO マルチユーザに対応してない @etags = {} @consums = [] @limit = hourly_limit @friends = @sources = @rsuffix_regex = @im = @im_thread = @utf7 = @httpproxy = nil load_config end def on_user(m) super @real, *@opts = (@opts.name || @real).split(" ") @opts = @opts.inject({}) do |r, i| key, value = i.split("=", 2) key = "mentions" if key == "replies" # backcompat r.update key => case value when nil then true when /\A\d+\z/ then value.to_i when /\A(?:\d+\.\d*|\.\d+)\z/ then value.to_f else value end end @opts = OpenStruct.new(@opts) @opts.httpproxy.sub!(/\A(?:([^:@]+)(?::([^@]+))?@)?([^:]+)(?::(\d+))?\z/) do @httpproxy = OpenStruct.new({ :user => $1, :password => $2, :address => $3, :port => $4.to_i, }) $&.sub(/[^:@]+(?=@)/, "********") end if @opts.httpproxy retry_count = 0 begin @me = api("account/update_profile") #api("account/verify_credentials") rescue APIFailed => e @log.error e.inspect sleep 1 retry_count += 1 retry if retry_count < 3 log "Failed to access API 3 times." << " Please check your username/email and password combination, " << " Twitter Status and try again later." finish end @prefix = prefix(@me) @user = @prefix.user @host = @prefix.host #post NICK, @me.screen_name if @nick != @me.screen_name post server_name, MODE, @nick, "+o" post @prefix, JOIN, main_channel post server_name, MODE, main_channel, "+mto", @nick post server_name, MODE, main_channel, "+q", @nick if @me.status @me.status.user = @me post @prefix, TOPIC, main_channel, generate_status_message(@me.status.text) end if @opts.jabber jid, pass = @opts.jabber.split(":", 2) @opts.jabber.replace("jabber=#{jid}:********") if jabber_bot_id begin require "xmpp4r-simple" start_jabber(jid, pass) rescue LoadError log "Failed to start Jabber." log 'Installl "xmpp4r-simple" gem or check your ID/pass.' finish end else @opts.delete_field :jabber log "This gateway does not support Jabber bot." end end log "Client options: #{@opts.marshal_dump.inspect}" @log.info "Client options: #{@opts.inspect}" @opts.tid = begin c = @opts.tid # expect: 0..15, true, "0,1" b = nil c, b = c.split(",", 2).map {|i| i.to_i } if c.respond_to? :split c = 10 unless (0 .. 15).include? c # 10: teal if (0 .. 15).include?(b) "\003%.2d,%.2d[%%s]\017" % [c, b] else "\003%.2d[%%s]\017" % c end end if @opts.tid @ratio = (@opts.ratio || "121").split(":") @ratio = Struct.new(:timeline, :dm, :mentions).new(*@ratio) @ratio.dm ||= @opts.dm == true ? @opts.mentions ? 6 : 26 : @opts.dm @ratio.mentions ||= @opts.mentions == true ? @opts.dm ? 20 : 26 : @opts.mentions @check_friends_thread = Thread.start do loop do begin check_friends rescue APIFailed => e @log.error e.inspect rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep @opts.check_friends_interval || 3600 end end return if @opts.jabber @timeline = TypableMap.new(@opts.tmap_size || 10_404, @opts.shuffled_tmap || false) if @opts.clientspoofing update_sources else @sources = [api_source] end update_redundant_suffix @check_updates_thread = Thread.start do sleep 30 loop do begin @log.info "check_updates" check_updates rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep 0.01 * (90 + rand(21)) * (@opts.check_updates_interval || 86400) # 0.9 ... 1.1 day end sleep @opts.check_updates_interval || 86400 end @check_timeline_thread = Thread.start do sleep 2 * (@me.friends_count / 100.0).ceil loop do begin check_timeline rescue APIFailed => e @log.error e.inspect rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep interval(@ratio.timeline) end end @check_dms_thread = Thread.start do loop do begin check_direct_messages rescue APIFailed => e @log.error e.inspect rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep interval(@ratio.dm) end end if @opts.dm @check_mentions_thread = Thread.start do sleep interval(@ratio.timeline) / 2 loop do begin check_mentions rescue APIFailed => e @log.error e.inspect rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep interval(@ratio.mentions) end end if @opts.mentions end def on_disconnected @check_friends_thread.kill rescue nil @check_timeline_thread.kill rescue nil @check_mentions_thread.kill rescue nil @check_dms_thread.kill rescue nil @check_updates_thread.kill rescue nil @im_thread.kill rescue nil @im.disconnect rescue nil end def on_privmsg(m) target, mesg = *m.params m.ctcps.each {|ctcp| on_ctcp(target, ctcp) } if m.ctcp? return if mesg.empty? return on_ctcp_action(target, mesg) if mesg.sub!(/\A +/, "") #and @opts.direct_action command, params = mesg.split(" ", 2) case command.downcase # TODO: escape recursive when "d", "dm" screen_name, mesg = params.split(" ", 2) unless screen_name or mesg log 'Send "d NICK message" to send a direct (private) message.' << " You may reply to a direct message the same way." return end m.params[0] = screen_name.sub(/\A@/, "") m.params[1] = mesg #.rstrip return on_privmsg(m) # TODO #when "f", "follow" #when "on" #when "off" # BUG if no args #when "g", "get" #when "w", "whois" #when "n", "nudge" # BUG if no args #when "*", "fav" #when "delete" #when "stats" # no args #when "leave" #when "invite" end unless command.nil? mesg = escape_http_urls(mesg) mesg = @opts.unuify ? unuify(mesg) : bitlify(mesg) mesg = Iconv.iconv("UTF-7", "UTF-8", mesg).join.encoding!("ASCII-8BIT") if @utf7 ret = nil retry_count = 3 begin case when target.ch? if @opts.alwaysim and @im and @im.connected? # in Jabber mode, using Jabber post ret = @im.deliver(jabber_bot_id, mesg) post @prefix, TOPIC, main_channel, mesg else previous = @me.status if previous and ((Time.now - Time.parse(previous.created_at)).to_i < 60 rescue true) and mesg.strip == previous.text log "You can't submit the same status twice in a row." return end q = { :status => mesg, :source => source } if @opts.old_style_reply and mesg[/\A@(?>([A-Za-z0-9_]{1,15}))[^A-Za-z0-9_]/] if user = friend($1) || api("users/show/#{$1}") unless user.status user = api("users/show/#{user.id}", {}, { :authenticate => user.protected }) end if user.status q.update :in_reply_to_status_id => user.status.id end end end if @opts.ll lat, long = @opts.ll.split(",", 2) q.update :lat => lat.to_f q.update :long => long.to_f end ret = api("statuses/update", q) log oops(ret) if ret.truncated ret.user.status = ret @me = ret.user log "Status updated" end when target.screen_name? # Direct message ret = api("direct_messages/new", { :screen_name => target, :text => mesg }) post server_name, NOTICE, @nick, "Your direct message has been sent to #{target}." else post server_name, ERR_NOSUCHNICK, target, "No such nick/channel" end rescue => e @log.error [retry_count, e.inspect].inspect if retry_count > 0 retry_count -= 1 @log.debug "Retry to setting status..." retry end log "Some Error Happened on Sending #{mesg}. #{e}" end end def on_whois(m) nick = m.params[0] unless nick.screen_name? post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" return end unless user = user(nick) if api("users/username_available", { :username => nick }).valid # TODO: 404 suspended post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" return end user = api("users/show/#{nick}", {}, { :authenticate => false }) end prefix = prefix(user) desc = user.name desc = "#{desc} / #{user.description}".gsub(/\s+/, " ") if user.description and not user.description.empty? signon_at = Time.parse(user.created_at).to_i rescue 0 idle_sec = (Time.now - (user.status ? Time.parse(user.status.created_at) : signon_at)).to_i rescue 0 location = user.location location = "SoMa neighborhood of San Francisco, CA" if location.nil? or location.empty? post server_name, RPL_WHOISUSER, @nick, nick, prefix.user, prefix.host, "*", desc post server_name, RPL_WHOISSERVER, @nick, nick, api_base.host, location post server_name, RPL_WHOISIDLE, @nick, nick, "#{idle_sec}", "#{signon_at}", "seconds idle, signon time" post server_name, RPL_ENDOFWHOIS, @nick, nick, "End of WHOIS list" if @drones.include?(user.id) post server_name, RPL_WHOISBOT, @nick, nick, "is a \002Bot\002 on #{server_name}" end end def on_who(m) channel = m.params[0] whoreply = Proc.new do |ch, user| # " # ( "H" / "G" > ["*"] [ ( "@" / "+" ) ] # : " prefix = prefix(user) server = api_base.host mode = case prefix.nick when @nick then "~" #when @drones.include?(user.id) then "%" # FIXME else "+" end hop = prefix.host.count("/") real = user.name post server_name, RPL_WHOREPLY, @nick, ch, prefix.user, prefix.host, server, prefix.nick, "H*#{mode}", "#{hop} #{real}" end case when channel.casecmp(main_channel).zero? users = [@me] users.concat @friends.reverse if @friends users.each {|friend| whoreply.call channel, friend } post server_name, RPL_ENDOFWHO, @nick, channel when (@groups.key?(channel) and @friends) @groups[channel].each do |nick| whoreply.call channel, friend(nick) end post server_name, RPL_ENDOFWHO, @nick, channel else post server_name, ERR_NOSUCHNICK, @nick, "No such nick/channel" end end def on_join(m) channels = m.params[0].split(/ *, */) channels.each do |channel| channel = channel.split(" ", 2).first next if channel.casecmp(main_channel).zero? @channels << channel @channels.uniq! post @prefix, JOIN, channel post server_name, MODE, channel, "+mtio", @nick post server_name, MODE, channel, "+q", @nick save_config end end def on_part(m) channel = m.params[0] return if channel.casecmp(main_channel).zero? @channels.delete(channel) post @prefix, PART, channel, "Ignore group #{channel}, but setting is alive yet." end def on_invite(m) nick, channel = *m.params if not nick.screen_name? or @nick.casecmp(nick).zero? post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" # or yourself return end friend = friend(nick) case when channel.casecmp(main_channel).zero? case when friend #TODO when api("users/username_available", { :username => nick }).valid post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" else user = api("friendships/create/#{nick}") join main_channel, [user] @friends << user if @friends @me.friends_count += 1 end when friend ((@groups[channel] ||= []) << friend.screen_name).uniq! join channel, [friend] save_config else post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" end end def on_kick(m) channel, nick, msg = *m.params if channel.casecmp(main_channel).zero? @friends.delete_if do |friend| if friend.screen_name.casecmp(nick).zero? user = api("friendships/destroy/#{friend.id}") if user.is_a? User post prefix(user), PART, main_channel, "Removed: #{msg}" @me.friends_count -= 1 end end end if @friends else friend = friend(nick) if friend (@groups[channel] ||= []).delete(friend.screen_name) post prefix(friend), PART, channel, "Removed: #{msg}" save_config else post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" end end end #def on_nick(m) # @nicknames[@nick] = m.params[0] #end def on_topic(m) channel = m.params[0] return if not channel.casecmp(main_channel).zero? or @me.status.nil? return if not @opts.mesautofix begin require "levenshtein" topic = m.params[1] previous = @me.status return unless previous distance = Levenshtein.normalized_distance(previous.text, topic) return if distance.zero? status = api("statuses/update", { :status => topic, :source => source }) log oops(ret) if status.truncated status.user.status = status @me = status.user if distance < 0.5 deleted = api("statuses/destroy/#{previous.id}") @timeline.delete_if {|tid, s| s.id == deleted.id } log "Similar update in previous. Conclude that it has error." log "And overwrite previous as new status: #{status.text}" else log "Status updated" end rescue LoadError end end def on_mode(m) channel = m.params[0] unless m.params[1] case when channel.ch? mode = "+mt" mode += "i" unless channel.casecmp(main_channel).zero? post server_name, RPL_CHANNELMODEIS, @nick, channel, mode #post server_name, RPL_CREATEONTIME, @nick, channel, 0 when channel.casecmp(@nick).zero? post server_name, RPL_UMODEIS, @nick, @nick, "+o" end end end private def on_ctcp(target, mesg) type, mesg = mesg.split(" ", 2) method = "on_ctcp_#{type.downcase}".to_sym send(method, target, mesg) if respond_to? method, true end def on_ctcp_action(target, mesg) #return unless main_channel.casecmp(target).zero? command, *args = mesg.split(" ") if command command.downcase! @@ctcp_action_commands.each do |define, name| if define === command send(name, target, mesg, Regexp.last_match || command, args) break end end else commands = @@ctcp_action_commands.map {|define, name| define }.select {|define| define.is_a? String } log "[tig.rb] CTCP ACTION COMMANDS:" commands.each_slice(5) do |c| log c.join(" ") end end rescue APIFailed => e log e.inspect rescue Exception => e log e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end ctcp_action "call" do |target, mesg, command, args| if args.size < 2 log "/me call as " return end screen_name = args[0] nickname = args[2] || args[1] # allow omitting "as" if nickname == "is" and deleted_nick = @nicknames.delete(screen_name) log %Q{Removed the nickname "#{deleted_nick}" for #{screen_name}} else @nicknames[screen_name] = nickname log "Call #{screen_name} as #{nickname}" end #save_config end ctcp_action "debug" do |target, mesg, command, args| code = args.join(" ") begin log instance_eval(code).inspect rescue Exception => e log e.inspect end end ctcp_action "utf-7", "utf7" do |target, mesg, command, args| unless defined? ::Iconv log "Can't load iconv." return end @utf7 = !@utf7 log "UTF-7 mode: #{@utf7 ? 'on' : 'off'}" end ctcp_action "list", "ls" do |target, mesg, command, args| if args.empty? log "/me list []" return end nick = args.first if not nick.screen_name? or api("users/username_available", { :username => nick }).valid post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" return end id = nick authenticate = false if user = friend(nick) id = user.id nick = user.screen_name authenticate = user.protected end unless (1..200).include?(count = args[1].to_i) count = 20 end begin res = api("statuses/user_timeline/#{id}", { :count => count }, { :authenticate => authenticate }) rescue APIFailed #log "#{nick} has protected their updates." return end res.reverse_each do |s| message(s, target, nil, nil, NOTICE) end end ctcp_action %r/\A(un)?fav(?:ou?rite)?(!)?\z/ do |target, mesg, command, args| # fav, unfav, favorite, unfavorite, favourite, unfavourite method = command[1].nil? ? "create" : "destroy" force = !!command[2] entered = command[0].capitalize statuses = [] if args.empty? if method == "create" if status = @timeline.last statuses << status else #log "" return end else @favorites ||= api("favorites").reverse if @favorites.empty? log "You've never favorite yet. No favorites to unfavorite." return end statuses.push @favorites.last end else args.each do |tid_or_nick| case when status = @timeline[tid = tid_or_nick] statuses.push status when friend = friend(nick = tid_or_nick) if friend.status statuses.push friend.status else log "#{tid_or_nick} has no status." end else # PRIVMSG: fav nick log "No such ID/NICK #{@opts.tid % tid_or_nick}" end end end @favorites ||= [] statuses.each do |s| if not force and method == "create" and @favorites.find {|i| i.id == s.id } log "The status is already favorited! <#{permalink(s)}>" next end res = api("favorites/#{method}/#{s.id}") log "#{entered}: #{res.user.screen_name}: #{generate_status_message(res.text)}" if method == "create" @favorites.push res else @favorites.delete_if {|i| i.id == res.id } end end end ctcp_action "link", "ln", /\Au(?:rl)?\z/ do |target, mesg, command, args| args.each do |tid| if status = @timeline[tid] log "#{@opts.tid % tid}: #{permalink(status)}" else log "No such ID #{@opts.tid % tid}" end end end ctcp_action "ratio", "ratios" do |target, mesg, command, args| unless args.empty? args = args.first.split(":") if args.size == 1 case when @opts.dm && @opts.mentions && args.size < 3 log "/me ratios " return when @opts.dm && args.size < 2 log "/me ratios " return when @opts.mentions && args.size < 2 log "/me ratios " return end ratios = args.map {|ratio| ratio.to_f } if ratios.any? {|ratio| ratio <= 0.0 } log "Ratios must be greater than 0.0 and fractional values are permitted." return end @ratio.timeline = ratios[0] case when @opts.dm @ratio.dm = ratios[1] @ratio.mentions = ratios[2] if @opts.mentions when @opts.mentions @ratio.mentions = ratios[1] end end log "Intervals: " + @ratio.zip([:timeline, :dm, :mentions]).map {|ratio, name| [name, "#{interval(ratio).round}sec"] }.inspect end ctcp_action "rm", %r/\A(?:de(?:stroy|l(?:ete)?)|miss|oops|r(?:emove|m))\z/ do |target, mesg, command, args| # destroy, delete, del, remove, rm, miss, oops statuses = [] if args.empty? and @me.status statuses.push @me.status else args.each do |tid| if status = @timeline[tid] if status.user.id == @me.id statuses.push status else log "The status you specified by the ID #{@opts.tid % tid} is not yours." end else log "No such ID #{@opts.tid % tid}" end end end b = false statuses.each do |st| res = api("statuses/destroy/#{st.id}") @timeline.delete_if {|tid, s| s.id == res.id } b = @me.status && @me.status.id == res.id log "Destroyed: #{res.text}" end Thread.start do sleep 2 @me = api("account/update_profile") #api("account/verify_credentials") if @me.status @me.status.user = @me msg = generate_status_message(@me.status.text) @timeline.any? do |tid, s| if s.id == @me.status.id msg << " " << @opts.tid % tid end end post @prefix, TOPIC, main_channel, msg end end if b end ctcp_action "name" do |target, mesg, command, args| name = mesg.split(" ", 2)[1] unless name.nil? @me = api("account/update_profile", { :name => name }) @me.status.user = @me if @me.status log "You are named #{@me.name}." end end ctcp_action "email" do |target, mesg, command, args| # FIXME email = args.first unless email.nil? @me = api("account/update_profile", { :email => email }) @me.status.user = @me if @me.status end end ctcp_action "url" do |target, mesg, command, args| # FIXME url = args.first || "" @me = api("account/update_profile", { :url => url }) @me.status.user = @me if @me.status end ctcp_action "in", "location" do |target, mesg, command, args| location = mesg.split(" ", 2)[1] || "" @me = api("account/update_profile", { :location => location }) @me.status.user = @me if @me.status location = (@me.location and @me.location.empty?) ? "nowhere" : "in #{@me.location}" log "You are #{location} now." end ctcp_action %r/\Adesc(?:ription)?\z/ do |target, mesg, command, args| # FIXME description = mesg.split(" ", 2)[1] || "" @me = api("account/update_profile", { :description => description }) @me.status.user = @me if @me.status end ctcp_action %r/\A(?:mention|re(?:ply)?)\z/ do |target, mesg, command, args| # reply, re, mention tid = args.first if status = @timeline[tid] text = mesg.split(" ", 3)[2] screen_name = "@#{status.user.screen_name}" if text.nil? or not text.include?(screen_name) text = "#{screen_name} #{text}" end ret = api("statuses/update", { :status => text, :source => source, :in_reply_to_status_id => status.id }) log oops(ret) if ret.truncated msg = generate_status_message(status.text) url = permalink(status) log "Status updated (In reply to #{@opts.tid % tid}: #{msg} <#{url}>)" ret.user.status = ret @me = ret.user end end ctcp_action %r/\Aspoo(o+)?f\z/ do |target, mesg, command, args| if args.empty? Thread.start do update_sources(command[1].nil?? 0 : command[1].size) end return end names = [] @sources = args.map do |arg| names << "=#{arg}" case arg.upcase when "WEB" then "" when "API" then nil else arg end end log(names.inject([]) do |r, name| s = r.join(", ") if s.size < 400 r << name else log s [name] end end.join(", ")) end ctcp_action "bot", "drone" do |target, mesg, command, args| if args.empty? log "/me bot [...]" return end args.each do |bot| user = friend(bot) unless user post server_name, ERR_NOSUCHNICK, bot, "No such nick/channel" next end if @drones.delete(user.id) mode = "-#{mode}" log "#{bot} is no longer a bot." else @drones << user.id mode = "+#{mode}" log "Marks #{bot} as a bot." end end save_config end ctcp_action "home", "h" do |target, mesg, command, args| if args.empty? log "/me home " return end nick = args.first if not nick.screen_name? or api("users/username_available", { :username => nick }).valid post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" return end log "http://twitter.com/#{nick}" end ctcp_action "retweet", "rt" do |target, mesg, command, args| if args.empty? log "/me #{command} blah blah" return end tid = args.first if status = @timeline[tid] if args.size >= 2 comment = mesg.split(" ", 3)[2] + " " else comment = "" end screen_name = "@#{status.user.screen_name}" rt_message = generate_status_message(status.text) text = "#{comment}RT #{screen_name}: #{rt_message}" ret = api("statuses/update", { :status => text, :source => source }) log oops(ret) if ret.truncated log "Status updated (RT to #{@opts.tid % tid}: #{text})" ret.user.status = ret @me = ret.user end end def on_ctcp_clientinfo(target, msg) if user = user(target) post prefix(user), NOTICE, @nick, ctcp_encode("CLIENTINFO :CLIENTINFO USERINFO VERSION TIME") end end def on_ctcp_userinfo(target, msg) user = user(target) if user and not user.description.empty? post prefix(user), NOTICE, @nick, ctcp_encode("USERINFO :#{user.description}") end end def on_ctcp_version(target, msg) user = user(target) if user and user.status source = user.status.source version = source.gsub(/<[^>]*>/, "").strip version << " <#{$1}>" if / href="([^"]+)/ === source post prefix(user), NOTICE, @nick, ctcp_encode("VERSION :#{version}") end end def on_ctcp_time(target, msg) if user = user(target) offset = user.utc_offset post prefix(user), NOTICE, @nick, ctcp_encode("TIME :%s%s (%s)" % [ (Time.now + offset).utc.iso8601[0, 19], "%+.2d:%.2d" % (offset/60).divmod(60), user.time_zone, ]) end end def check_friends if @friends.nil? @friends = page("statuses/friends/#{@me.id}", @me.friends_count) if @opts.athack join main_channel, @friends else rest = @friends.map do |i| prefix = "+" #@drones.include?(i.id) ? "%" : "+" # FIXME ~&% "#{prefix}#{i.screen_name}" end.reverse.inject("~#{@nick}") do |r, nick| if r.size < 400 r << " " << nick else post server_name, RPL_NAMREPLY, @nick, "=", main_channel, r nick end end post server_name, RPL_NAMREPLY, @nick, "=", main_channel, rest post server_name, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list" end else new_ids = page("friends/ids/#{@me.id}", @me.friends_count) friend_ids = @friends.reverse.map {|friend| friend.id } (friend_ids - new_ids).each do |id| @friends.delete_if do |friend| if friend.id == id post prefix(friend), PART, main_channel, "" @me.friends_count -= 1 end end end new_ids -= friend_ids unless new_ids.empty? new_friends = page("statuses/friends/#{@me.id}", new_ids.size) join main_channel, new_friends.delete_if {|friend| @friends.any? {|i| i.id == friend.id } }.reverse @friends.concat new_friends @me.friends_count += new_friends.size end end end def check_timeline cmd = PRIVMSG path = "statuses/#{@opts.with_retweets ? "home" : "friends"}_timeline" q = { :count => 200 } @latest_id ||= nil case when @latest_id q.update(:since_id => @latest_id) when is_first_retrieve = !@me.statuses_count.zero? && !@me.friends_count.zero? # cmd = NOTICE # デバッグするときめんどくさいので q.update(:count => 20) end api(path, q).reverse_each do |status| id = @latest_id = status.id next if @timeline.any? {|tid, s| s.id == id } status.user.status = status user = status.user tid = @timeline.push(status) tid = nil unless @opts.tid @log.debug [id, user.screen_name, status.text].inspect if user.id == @me.id mesg = generate_status_message(status.text) mesg << " " << @opts.tid % tid if tid post @prefix, TOPIC, main_channel, mesg @me = user else if @friends b = false @friends.each_with_index do |friend, i| if b = friend.id == user.id if friend.screen_name != user.screen_name post prefix(friend), NICK, user.screen_name end @friends[i] = user break end end unless b join main_channel, [user] @friends << user @me.friends_count += 1 end end message(status, main_channel, tid, nil, cmd) end @groups.each do |channel, members| next unless members.include?(user.screen_name) message(status, channel, tid, nil, cmd) end end end def check_direct_messages @prev_dm_id ||= nil q = @prev_dm_id ? { :count => 200, :since_id => @prev_dm_id } \ : { :count => 1 } api("direct_messages", q).reverse_each do |mesg| unless @prev_dm_id &&= mesg.id @prev_dm_id = mesg.id next end id = mesg.id user = mesg.sender tid = nil text = mesg.text @log.debug [id, user.screen_name, text].inspect message(user, @nick, tid, text) end end def check_mentions return if @timeline.empty? @prev_mention_id ||= @timeline.last.id api("statuses/mentions", { :count => 200, :since_id => @prev_mention_id }).reverse_each do |mention| id = @prev_mention_id = mention.id next if @timeline.any? {|tid, s| s.id == id } mention.user.status = mention user = mention.user tid = @timeline.push(mention) tid = nil unless @opts.tid @log.debug [id, user.screen_name, mention.text].inspect message(mention, main_channel, tid) @friends.each_with_index do |friend, i| if friend.id == user.id @friends[i] = user break end end if @friends end end def check_updates update_redundant_suffix uri = URI("http://github.com/api/v1/json/cho45/net-irc/commits/master") @log.debug uri.inspect res = http(uri).request(http_req(:get, uri)) latest = JSON.parse(res.body)['commits'][0]['id'] unless server_version == latest log "\002New version is available.\017 run 'git pull'." end rescue Errno::ECONNREFUSED, Timeout::Error => e @log.error "Failed to get the latest revision of tig.rb from #{uri.host}: #{e.inspect}" end def interval(ratio) now = Time.now max = @opts.maxlimit || 0 limit = 0.98 * @limit # 98% of the rate limit i = 3600.0 # an hour in seconds i *= @ratio.inject {|sum, r| sum.to_f + r.to_f } + @consums.delete_if {|t| t < now }.size i /= ratio.to_f i /= (0 < max && max < limit) ? max : limit i = 60 * 30 if i > 60 * 30 # 30分以上止まらないように。 i rescue => e @log.error e.inspect 100 end def join(channel, users) params = [] users.each do |user| prefix = prefix(user) post prefix, JOIN, channel params << prefix.nick if user.protected next if params.size < MAX_MODE_PARAMS post server_name, MODE, channel, "+#{"v" * params.size}", *params params = [] end post server_name, MODE, channel, "+#{"v" * params.size}", *params unless params.empty? users end def start_jabber(jid, pass) @log.info "Logging-in with #{jid} -> jabber_bot_id: #{jabber_bot_id}" @im = Jabber::Simple.new(jid, pass) @im.add(jabber_bot_id) @im_thread = Thread.start do require "cgi" loop do begin @im.received_messages.each do |msg| @log.debug [msg.from, msg.body].inspect if msg.from.strip == jabber_bot_id # Twitter -> 'id: msg' body = msg.body.sub(/\A(.+?)(?:\(([^()]+)\))?: /, "") body = decode_utf7(body) if Regexp.last_match nick, id = Regexp.last_match.captures body = untinyurl(CGI.unescapeHTML(body)) user = nick nick = id || nick nick = @nicknames[nick] || nick post "#{nick}!#{user}@#{api_base.host}", PRIVMSG, main_channel, body end end end rescue Exception => e @log.error "Error on Jabber loop: #{e.inspect}" e.backtrace.each do |l| @log.error "\t#{l}" end end sleep 1 end end end def save_config config = { :groups => @groups, :channels => @channels, #:nicknames => @nicknames, :drones => @drones, } @config.open("w") {|f| YAML.dump(config, f) } end def load_config @config.open do |f| config = YAML.load(f) @groups = config[:groups] || {} @channels = config[:channels] || [] #@nicknames = config[:nicknames] || {} @drones = config[:drones] || [] end rescue Errno::ENOENT end def require_post?(path) %r{ \A (?: status(?:es)?/update \z | direct_messages/new \z | friendships/create/ | account/(?: end_session \z | update_ ) | favou?ri(?: ing | tes )/create/ | notifications/ | blocks/create/ ) }x === path end #def require_put?(path) # %r{ \A status(?:es)?/retweet (?:/|\z) }x === path #end def api(path, query = {}, opts = {}) path.sub!(%r{\A/+}, "") query = query.to_query_str authenticate = opts.fetch(:authenticate, true) uri = api_base(authenticate) uri.path += path uri.path += ".json" if path != "users/username_available" uri.query = query unless query.empty? header = {} credentials = authenticate ? [@real, @pass] : nil req = case when path.include?("/destroy/") http_req :delete, uri, header, credentials when require_post?(path) http_req :post, uri, header, credentials #when require_put?(path) # http_req :put, uri, header, credentials else http_req :get, uri, header, credentials end @log.debug [req.method, uri.to_s] ret = http(uri, 30, 30).request req #@etags[uri.to_s] = ret["ETag"] case when authenticate hourly_limit = ret["X-RateLimit-Limit"].to_i unless hourly_limit.zero? if @limit != hourly_limit msg = "The rate limit per hour was changed: #{@limit} to #{hourly_limit}" log msg @log.info msg @limit = hourly_limit end #if req.is_a?(Net::HTTP::Get) and not %w{ if not %w{ statuses/friends_timeline direct_messages statuses/mentions }.include?(path) and not ret.is_a?(Net::HTTPServerError) expired_on = Time.parse(ret["Date"]) rescue Time.now expired_on += 3636 # 1.01 hours in seconds later @consums << expired_on end end when ret["X-RateLimit-Remaining"] @limit_remaining_for_ip = ret["X-RateLimit-Remaining"].to_i @log.debug "IP based limit: #{@limit_remaining_for_ip}" end case ret when Net::HTTPOK # 200 # Avoid Twitter's invalid JSON json = ret.body.strip.sub(/\A(?:false|true)\z/, "[\\&]") res = JSON.parse json if res.is_a?(Hash) and res["error"] # and not res["response"] if @error != res["error"] @error = res["error"] log @error end raise APIFailed, res["error"] end res.to_tig_struct when Net::HTTPNoContent, # 204 Net::HTTPNotModified # 304 [] when Net::HTTPBadRequest # 400: exceeded the rate limitation if ret.key?("X-RateLimit-Reset") s = ret["X-RateLimit-Reset"].to_i - Time.now.to_i if s > 0 log "RateLimit: #{(s / 60.0).ceil} min remaining to get timeline" sleep (s > 60 * 10) ? 60 * 10 : s # 10 分に一回はとってくるように end end raise APIFailed, "#{ret.code}: #{ret.message}" when Net::HTTPUnauthorized # 401 raise APIFailed, "#{ret.code}: #{ret.message}" else raise APIFailed, "Server Returned #{ret.code} #{ret.message}" end rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e raise APIFailed, e.inspect end def page(path, max_count, authenticate = false) @limit_remaining_for_ip ||= 52 limit = 0.98 * @limit_remaining_for_ip # 98% of IP based rate limit r = [] cpp = nil # counts per page 1.upto(limit) do |num| ret = api(path, { :page => num }, { :authenticate => authenticate }) cpp ||= ret.size r.concat ret break if ret.empty? or num >= max_count / cpp.to_f or ret.size != cpp or r.size >= max_count end r end def generate_status_message(mesg) mesg = decode_utf7(mesg) mesg.delete!("\000\001") mesg.gsub!(">", ">") mesg.gsub!("<", "<") mesg.gsub!(WSP_REGEX, " ") mesg = untinyurl(mesg) mesg.sub!(@rsuffix_regex, "") if @rsuffix_regex mesg.strip end def friend(id) return nil unless @friends if id.is_a? String @friends.find {|i| i.screen_name.casecmp(id).zero? } else @friends.find {|i| i.id == id } end end def user(id) if id.is_a? String @nick.casecmp(id).zero? ? @me : friend(id) else @me.id == id ? @me : friend(id) end end def prefix(u) nick = u.screen_name nick = "@#{nick}" if @opts.athack user = "id=%.9d" % u.id host = api_base.host host += "/protected" if u.protected host += "/bot" if @drones.include?(u.id) Prefix.new("#{nick}!#{user}@#{host}") end def message(struct, target, tid = nil, str = nil, command = PRIVMSG) unless str status = struct.is_a?(Status) ? struct : struct.status str = status.text if command != PRIVMSG time = Time.parse(status.created_at) rescue Time.now str = "#{time.strftime(@opts.strftime || "%m-%d %H:%M")} #{str}" # TODO: color end end user = (struct.is_a?(User) ? struct : struct.user).dup screen_name = user.screen_name user.screen_name = @nicknames[screen_name] || screen_name prefix = prefix(user) str = generate_status_message(str) str = "#{str} #{@opts.tid % tid}" if tid post prefix, command, target, str end def log(str) post server_name, NOTICE, main_channel, str.gsub(/\r\n|[\r\n]/, " ") end def decode_utf7(str) return str unless defined? ::Iconv and str.include?("+") str.sub!(/\A(?:.+ > |.+\z)/) { Iconv.iconv("UTF-8", "UTF-7", $&).join } #FIXME str = "[utf7]: #{str}" if str =~ /[^a-z0-9\s]/i str rescue Iconv::IllegalSequence str rescue => e @log.error e str end def untinyurl(text) text.gsub(@opts.untiny_whole_urls ? URI.regexp(%w[http https]) : %r{ http:// (?: (?: bit\.ly | (?: tin | rub) yurl\.com | is\.gd | cli\.gs | tr\.im | u\.nu | airme\.us | ff\.im | twurl.nl | bkite\.com | tumblr\.com | pic\.gd | sn\.im | digg\.com ) / [0-9a-z=-]+ | blip\.fm/~ (?> [0-9a-z]+) (?! /) | flic\.kr/[a-z0-9/]+ ) }ix) {|url| "#{resolve_http_redirect(URI(url)) || url}" } end def bitlify(text) login, key, len = @opts.bitlify.split(":", 3) if @opts.bitlify len = (len || 20).to_i longurls = URI.extract(text, %w[http https]).uniq.map do |url| URI.rstrip url end.reject do |url| url.size < len end return text if longurls.empty? bitly = URI("http://api.bit.ly/shorten") if login and key bitly.path = "/shorten" bitly.query = { :version => "2.0.1", :format => "json", :longUrl => longurls, }.to_query_str(";") @log.debug bitly req = http_req(:get, bitly, {}, [login, key]) res = http(bitly, 5, 10).request(req) res = JSON.parse(res.body) res = res["results"] longurls.each do |longurl| text.gsub!(longurl) do res[$&] && res[$&]["shortUrl"] || $& end end else bitly.path = "/api" longurls.each do |longurl| bitly.query = { :url => longurl }.to_query_str @log.debug bitly req = http_req(:get, bitly) res = http(bitly, 5, 5).request(req) text.gsub!(longurl, res.body) end end text rescue => e @log.error e text end def unuify(text) unu_url = "http://u.nu/" unu = URI("#{unu_url}unu-api-simple") size = unu_url.size text.gsub(URI.regexp(%w[http https])) do |url| url = URI.rstrip url if url.size < size + 5 or url[0, size] == unu_url return url end unu.query = { :url => url }.to_query_str @log.debug unu res = http(unu, 5, 5).request(http_req(:get, unu)).body if res[0, 12] == unu_url res else raise res.split("|") end end rescue => e @log.error e text end def escape_http_urls(text) original_text = text.encoding!("UTF-8").dup if defined? ::Punycode # TODO: Nameprep text.gsub!(%r{(https?://)([^\x00-\x2C\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+)}) do domain = $2 # Dots: # * U+002E (full stop) * U+3002 (ideographic full stop) # * U+FF0E (fullwidth full stop) * U+FF61 (halfwidth ideographic full stop) # => /[.\u3002\uFF0E\uFF61] # Ruby 1.9 /x $1 + domain.split(/\.|\343\200\202|\357\274\216|\357\275\241/).map do |label| break [domain] if /\A-|[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]|-\z/ === label next label unless /[^-A-Za-z0-9]/ === label punycode = Punycode.encode(label) break [domain] if punycode.size > 59 "xn--#{punycode}" end.join(".") end if text != original_text log "Punycode encoded: #{text}" original_text = text.dup end end urls = [] text.split(/[\s<>]+/).each do |str| next if /%[0-9A-Fa-f]{2}/ === str # URI::UNSAFE + "#" escaped_str = URI.escape(str, %r{[^-_.!~*'()a-zA-Z0-9;/?:@&=+$,\[\]#]}) URI.extract(escaped_str, %w[http https]).each do |url| uri = URI(URI.rstrip(url)) if not urls.include?(uri.to_s) and exist_uri?(uri) urls << uri.to_s end end if escaped_str != str end urls.each do |url| unescaped_url = URI.unescape(url).encoding!("UTF-8") text.gsub!(unescaped_url, url) end log "Percent encoded: #{text}" if text != original_text text.encoding!("ASCII-8BIT") rescue => e @log.error e text end def exist_uri?(uri, limit = 1) ret = nil #raise "Not supported." unless uri.is_a?(URI::HTTP) return ret if limit.zero? or uri.nil? or not uri.is_a?(URI::HTTP) @log.debug uri.inspect req = http_req :head, uri http(uri, 3, 2).request(req) do |res| ret = case res when Net::HTTPSuccess true when Net::HTTPRedirection uri = resolve_http_redirect(uri) exist_uri?(uri, limit - 1) when Net::HTTPClientError false #when Net::HTTPServerError # nil else nil end end ret rescue => e @log.error e.inspect ret end def resolve_http_redirect(uri, limit = 3) return uri if limit.zero? or uri.nil? @log.debug uri.inspect req = http_req :head, uri http(uri, 3, 2).request(req) do |res| break if not res.is_a?(Net::HTTPRedirection) or not res.key?("Location") begin location = URI(res["Location"]) rescue URI::InvalidURIError end unless location.is_a? URI::HTTP begin location = URI.join(uri.to_s, res["Location"]) rescue URI::InvalidURIError, URI::BadURIError # FIXME end end uri = resolve_http_redirect(location, limit - 1) end uri rescue => e @log.error e.inspect uri end def update_sources(n = 0) if @sources and @sources.size > 1 and n.zero? log "tig.rb" @sources = [api_source] return @sources end uri = URI("http://wedata.net/databases/TwitterSources/items.json") @log.debug uri.inspect json = http(uri).request(http_req(:get, uri)).body sources = JSON.parse json sources.map! {|item| [item["data"]["source"], item["name"]] } sources.push ["", "web"] sources.push [nil, "API"] sources = Array.new(n) do sources.delete_at(rand(sources.size)) end if (1 ... sources.size).include?(n) log(sources.inject([]) do |r, src| s = r.join(", ") if s.size < 400 r << src[1] else log s [src[1]] end end.join(", ")) if @sources @sources = sources.map {|src| src[0] } rescue => e @log.error e.inspect log "An error occured while loading #{uri.host}." @sources ||= [api_source] end def update_redundant_suffix uri = URI("http://svn.coderepos.org/share/platform/twitterircgateway/suffixesblacklist.txt") @log.debug uri.inspect res = http(uri).request(http_req(:get, uri)) @etags[uri.to_s] = res["ETag"] return if res.is_a? Net::HTTPNotModified source = res.body source.encoding!("UTF-8") if source.respond_to?(:encoding) and source.encoding == Encoding::BINARY @rsuffix_regex = /#{Regexp.union(*source.split)}\z/ rescue Errno::ECONNREFUSED, Timeout::Error => e @log.error "Failed to get the redundant suffix blacklist from #{uri.host}: #{e.inspect}" end def http(uri, open_timeout = nil, read_timeout = 60) http = case when @httpproxy Net::HTTP.new(uri.host, uri.port, @httpproxy.address, @httpproxy.port, @httpproxy.user, @httpproxy.password) when ENV["HTTP_PROXY"], ENV["http_proxy"] proxy = URI(ENV["HTTP_PROXY"] || ENV["http_proxy"]) Net::HTTP.new(uri.host, uri.port, proxy.host, proxy.port, proxy.user, proxy.password) else Net::HTTP.new(uri.host, uri.port) end http.open_timeout = open_timeout if open_timeout # nil by default http.read_timeout = read_timeout if read_timeout # 60 by default if uri.is_a? URI::HTTPS http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE end http rescue => e @log.error e end def http_req(method, uri, header = {}, credentials = nil) accepts = ["*/*;q=0.1"] #require "mime/types"; accepts.unshift MIME::Types.of(uri.path).first.simplified types = { "json" => "application/json", "txt" => "text/plain" } ext = uri.path[/[^.]+\z/] accepts.unshift types[ext] if types.key?(ext) user_agent = "#{self.class}/#{server_version} (#{File.basename(__FILE__)}; net-irc) Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})" header["User-Agent"] ||= user_agent header["Accept"] ||= accepts.join(",") header["Accept-Charset"] ||= "UTF-8,*;q=0.0" if ext != "json" #header["Accept-Language"] ||= @opts.lang # "en-us,en;q=0.9,ja;q=0.5" header["If-None-Match"] ||= @etags[uri.to_s] if @etags[uri.to_s] req = case method.to_s.downcase.to_sym when :get Net::HTTP::Get.new uri.request_uri, header when :head Net::HTTP::Head.new uri.request_uri, header when :post Net::HTTP::Post.new uri.path, header when :put Net::HTTP::Put.new uri.path, header when :delete Net::HTTP::Delete.new uri.request_uri, header else # raise "" end if req.request_body_permitted? req["Content-Type"] ||= "application/x-www-form-urlencoded" req.body = uri.query end req.basic_auth(*credentials) if credentials req rescue => e @log.error e end def oops(status) "Oops! Your update was over 140 characters. We sent the short version" << " to your friends (they can view the entire update on the Web <" << permalink(status) << ">)." end def permalink(struct) path = struct.is_a?(Status) ? "#{struct.user.screen_name}/statuses/#{struct.id}" \ : struct.screen_name "http://twitter.com/#{path}" end def source @sources[rand(@sources.size)] end def initial_message super post server_name, RPL_ISUPPORT, @nick, "PREFIX=(qov)~@%+", "CHANTYPES=#", "CHANMODES=#{available_channel_modes}", "MODES=#{MAX_MODE_PARAMS}", "NICKLEN=15", "TOPICLEN=420", "CHANNELLEN=50", "NETWORK=Twitter", "are supported by this server" end User = Struct.new(:id, :name, :screen_name, :location, :description, :url, :following, :notifications, :protected, :time_zone, :utc_offset, :created_at, :friends_count, :followers_count, :statuses_count, :favourites_count, :verified, :geo_enabled, :profile_image_url, :profile_background_color, :profile_text_color, :profile_link_color, :profile_sidebar_fill_color, :profile_sidebar_border_color, :profile_background_image_url, :profile_background_tile, :status) Status = Struct.new(:id, :text, :source, :created_at, :truncated, :favorited, :geo, :in_reply_to_status_id, :in_reply_to_user_id, :in_reply_to_screen_name, :user) DM = Struct.new(:id, :text, :created_at, :sender_id, :sender_screen_name, :sender, :recipient_id, :recipient_screen_name, :recipient) Geo = Struct.new(:type, :coordinates, :geometries, :geometry, :properties, :id, :crs, :name, :href, :bbox, :features) class TypableMap < Hash #Roman = %w[ # k g ky gy s z sh j t d ch n ny h b p hy by py m my y r ry w v q #].unshift("").map do |consonant| # case consonant # when "h", "q" then %w|a i e o| # when /[hy]$/ then %w|a u o| # else %w|a i u e o| # end.map {|vowel| "#{consonant}#{vowel}" } #end.flatten Roman = %w[ a i u e o ka ki ku ke ko sa shi su se so ta chi tsu te to na ni nu ne no ha hi fu he ho ma mi mu me mo ya yu yo ra ri ru re ro wa wo n ga gi gu ge go za ji zu ze zo da de do ba bi bu be bo pa pi pu pe po kya kyu kyo sha shu sho cha chu cho nya nyu nyo hya hyu hyo mya myu myo rya ryu ryo gya gyu gyo ja ju jo bya byu byo pya pyu pyo ].freeze def initialize(size = nil, shuffle = false) if shuffle @seq = Roman.dup if @seq.respond_to?(:shuffle!) @seq.shuffle! else @seq = Array.new(@seq.size) { @seq.delete_at(rand(@seq.size)) } end @seq.freeze else @seq = Roman end @n = 0 @size = size || @seq.size end def generate(n) ret = [] begin n, r = n.divmod(@seq.size) ret << @seq[r] end while n > 0 ret.reverse.join #.gsub(/n(?=[bmp])/, "m") end def push(obj) id = generate(@n) self[id] = obj @n += 1 @n %= @size id end alias :<< :push def clear @n = 0 super end def first @size.times do |i| id = generate((@n + i) % @size) return self[id] if key? id end unless empty? nil end def last @size.times do |i| id = generate((@n - 1 - i) % @size) return self[id] if key? id end unless empty? nil end private :[]= undef update, merge, merge!, replace end end class Array def to_tig_struct map do |v| v.respond_to?(:to_tig_struct) ? v.to_tig_struct : v end end end class Hash def to_tig_struct if empty? #warn "" if $VERBOSE #raise Error return nil end struct = case when struct_of?(TwitterIrcGateway::User) TwitterIrcGateway::User.new when struct_of?(TwitterIrcGateway::Status) TwitterIrcGateway::Status.new when struct_of?(TwitterIrcGateway::DM) TwitterIrcGateway::DM.new when struct_of?(TwitterIrcGateway::Geo) TwitterIrcGateway::Geo.new else members = keys members.concat TwitterIrcGateway::User.members members.concat TwitterIrcGateway::Status.members members.concat TwitterIrcGateway::DM.members members.concat TwitterIrcGateway::Geo.members members.map! {|m| m.to_sym } members.uniq! Struct.new(*members).new end each do |k, v| struct[k.to_sym] = v.respond_to?(:to_tig_struct) ? v.to_tig_struct : v end struct end # { :f => "v" } #=> "f=v" # { "f" => [1, 2] } #=> "f=1&f=2" # { "f" => "" } #=> "f=" # { "f" => nil } #=> "f" def to_query_str separator = "&" inject([]) do |r, (k, v)| k = URI.encode_component k.to_s (v.is_a?(Array) ? v : [v]).each do |i| if i.nil? r << k else r << "#{k}=#{URI.encode_component i.to_s}" end end r end.join separator end private def struct_of? struct (keys - struct.members.map {|m| m.to_s }).size.zero? end end class String def ch? /\A[&#+!][^ \007,]{1,50}\z/ === self end def screen_name? /\A[A-Za-z0-9_]{1,15}\z/ === self end def encoding! enc return self unless respond_to? :force_encoding force_encoding enc end end module URI::Escape alias :_orig_escape :escape if defined? ::RUBY_REVISION and RUBY_REVISION < 24544 # URI.escape("あ1") #=> "%E3%81%82\xEF\xBC\x91" # URI("file:///4") #=> # # "\\d" -> "[0-9]" for Ruby 1.9 def escape str, unsafe = %r{[^-_.!~*'()a-zA-Z0-9;/?:@&=+$,\[\]]} _orig_escape(str, unsafe) end alias :encode :escape end def encode_component str, unsafe = /[^-_.!~*'()a-zA-Z0-9 ]/ _orig_escape(str, unsafe).tr(" ", "+") end def rstrip str str.sub(%r{ (?: ( / [^/?#()]* (?: \( [^/?#()]* \) [^/?#()]* )* ) \) [^/?#()]* | \. ) \z }x, "\\1") end end if __FILE__ == $0 require "optparse" opts = { :port => 16668, :host => "localhost", :log => nil, :debug => false, :foreground => false, } OptionParser.new do |parser| parser.instance_eval do self.banner = <<-EOB.gsub(/^\t+/, "") Usage: #{$0} [opts] EOB separator "" separator "Options:" on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port| opts[:port] = port end on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host| opts[:host] = host end on("-l", "--log LOG", "log file") do |log| opts[:log] = log end on("--debug", "Enable debug mode") do |debug| opts[:log] = $stdout opts[:debug] = true end on("-f", "--foreground", "run foreground") do |foreground| opts[:log] = $stdout opts[:foreground] = true end on("-n", "--name [user name or email address]") do |name| opts[:name] = name end parse!(ARGV) end end opts[:logger] = Logger.new(opts[:log], "daily") opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO #def daemonize(foreground = false) # [:INT, :TERM, :HUP].each do |sig| # Signal.trap sig, "EXIT" # end # return yield if $DEBUG or foreground # Process.fork do # Process.setsid # Dir.chdir "/" # STDIN.reopen "/dev/null" # STDOUT.reopen "/dev/null", "a" # STDERR.reopen STDOUT # yield # end # exit! 0 #end #daemonize(opts[:debug] || opts[:foreground]) do Net::IRC::Server.new(opts[:host], opts[:port], TwitterIrcGateway, opts).start #end end net-irc-0.0.9/examples/2ch.rb0000755000175000017500000001151611604616526015552 0ustar uwabamiuwabami#!/usr/bin/env ruby # vim:encoding=UTF-8: $KCODE = "u" if RUBY_VERSION < "1.9" # json use this require 'uri' require 'net/http' require 'stringio' require 'zlib' require 'nkf' class ThreadData class UnknownThread < StandardError; end attr_accessor :uri attr_accessor :last_modified, :size Line = Struct.new(:n, :name, :mail, :misc, :body, :opts, :id) do def aa? body = self.body return false if body.count("\n") < 3 significants = body.scan(/[>\n0-9a-z0-9A-Za-zA-Zぁ-んァ-ン一-龠]/u).size.to_f body_length = body.scan(/./u).size is_aa = 1 - significants / body_length is_aa > 0.6 end end def initialize(thread_uri) @uri = URI(thread_uri) _, _, _, @board, @num, = *@uri.path.split('/') @dat = [] end def length @dat.length end def subject retrieve(true) if @dat.size.zero? self[1].opts || "" end def [](n) l = @dat[n - 1] return nil unless l name, mail, misc, body, opts = * l.split(/<>/) id = misc[/ID:([^\s]+)/, 1] body.gsub!(/
/, "\n") body.gsub!(/<[^>]+>/, "") body.gsub!(/^\s+|\s+$/, "") body.gsub!(/&(gt|lt|amp|nbsp);/) {|s| { 'gt' => ">", 'lt' => "<", 'amp' => "&", 'nbsp' => " " }[$1] } Line.new(n, name, mail, misc, body, opts, id) end def dat @num end def retrieve(force=false) @dat = [] if @force res = Net::HTTP.start(@uri.host, @uri.port) do |http| req = Net::HTTP::Get.new('/%s/dat/%d.dat' % [@board, @num]) req['User-Agent'] = 'Monazilla/1.00 (2ig.rb/0.0e)' req['Accept-Encoding'] = 'gzip' unless @size unless force req['If-Modified-Since'] = @last_modified if @last_modified req['Range'] = "bytes=%d-" % @size if @size end http.request(req) end ret = nil case res.code.to_i when 200, 206 body = res.body if res['Content-Encoding'] == 'gzip' body = StringIO.open(body, 'rb') {|io| Zlib::GzipReader.new(io).read } end @last_modified = res['Last-Modified'] if res.code == '206' @size += body.size else @size = body.size end body = NKF.nkf('-w', body) curr = @dat.size + 1 @dat.concat(body.split(/\n/)) last = @dat.size (curr..last).map {|n| self[n] } when 416 # たぶん削除が発生 p ['416'] retrieve(true) [] when 304 # Not modified [] when 302 # dat 落ち p ['302', res['Location']] raise UnknownThread else p ['Unknown Status:', res.code] [] end end def canonicalize_subject(subject) subject.gsub(/[A-Za-z0-9]/u) {|c| c.unpack("U*").map {|i| i - 65248 }.pack("U*") } end def guess_next_thread res = Net::HTTP.start(@uri.host, @uri.port) do |http| req = Net::HTTP::Get.new('/%s/subject.txt' % @board) req['User-Agent'] = 'Monazilla/1.00 (2ig.rb/0.0e)' http.request(req) end recent_posted_threads = (900..999).inject({}) {|r,i| line = self[i] line.body.scan(%r|ttp://#{@uri.host}/test/read.cgi/[^/]+/\d+/|).each do |uri| r["h#{uri}"] = i end if line r } current_subject = canonicalize_subject(self.subject) current_thread_rev = current_subject.scan(/\d+/).map {|d| d.to_i } current = current_subject.scan(/./u) body = NKF.nkf('-w', res.body) threads = body.split(/\n/).map {|l| dat, rest = *l.split(/<>/) dat.sub!(/\.dat$/, "") uri = "http://#{@uri.host}/test/read.cgi/#{@board}/#{dat}/" subject, n = */(.+?) \((\d+)\)/.match(rest).captures canonical_subject = canonicalize_subject(subject) thread_rev = canonical_subject[/\d+/].to_i distance = (dat == self.dat) ? Float::MAX : (subject == self.subject) ? 0 : levenshtein(canonical_subject.scan(/./u), current) continuous_num = current_thread_rev.find {|rev| rev == thread_rev - 1 } appear_recent = recent_posted_threads[uri] score = distance score -= 10 if continuous_num score -= 10 if appear_recent score += 10 if dat.to_i < self.dat.to_i { :uri => uri, :dat => dat, :subject => subject, :distance => distance, :continuous_num => continuous_num, :appear_recent => appear_recent, :score => score.to_f } }.sort_by {|o| o[:score] } threads end def levenshtein(a, b) case when a.empty? b.length when b.empty? a.length when a == b 0 else d = Array.new(a.length + 1) { |s| Array.new(b.length + 1, 0) } (0..a.length).each do |i| d[i][0] = i end (0..b.length).each do |j| d[0][j] = j end (1..a.length).each do |i| (1..b.length).each do |j| cost = (a[i - 1] == b[j - 1]) ? 0 : 1 d[i][j] = [ d[i-1][j ] + 1, d[i ][j-1] + 1, d[i-1][j-1] + cost ].min end end d[a.length][b.length] end end end if __FILE__ == $0 require 'pp' thread = ThreadData.new(ARGV[0]) pp thread.guess_next_thread.reverse p thread.subject end net-irc-0.0.9/examples/hatena-star-stream.rb0000755000175000017500000001470011604616526020574 0ustar uwabamiuwabami#!/usr/bin/env ruby # vim:encoding=UTF-8: =begin ## Licence Ruby's by cho45 =end $LOAD_PATH << "lib" $LOAD_PATH << "../lib" $KCODE = "u" unless defined? ::Encoding # json use this require "rubygems" require "json" require "net/irc" require "mechanize" require "sdbm" require "tmpdir" require "nkf" require "hpricot" WWW::Mechanize.html_parser = Hpricot class HatenaStarStream < Net::IRC::Server::Session def server_name "hatenastarstream" end def server_version "0.0.0" end def main_channel "#star" end def initialize(*args) super @ua = WWW::Mechanize.new @ua.max_history = 1 end def on_user(m) super post @prefix, JOIN, main_channel post server_name, MODE, main_channel, "+o", @prefix.nick @real, *@opts = @opts.name || @real.split(/\s+/) @opts = @opts.inject({}) {|r,i| key, value = i.split("=") r.update(key => value) } start_observer end def on_disconnected @observer.kill rescue nil end def on_privmsg(m) @ua.instance_eval do get "http://h.hatena.ne.jp/" form = page.forms.find {|f| f.action == "/entry" } form["body"] = m[1] submit form end post server_name, NOTICE, main_channel, "posted" rescue Exception => e log e.inspect end def on_ctcp(target, message) end def on_whois(m) end def on_who(m) end def on_join(m) end def on_part(m) end private def start_observer @observer = Thread.start do Thread.abort_on_exception = true loop do begin login @log.info "getting report..." @ua.get("http://s.hatena.ne.jp/#{@real}/report") entries = @ua.page.root.search("#main span.entry-title a").map {|a| a['href'] } @log.info "getting stars... #{entries.length}" stars = retrive_stars(entries) db = SDBM.open("#{Dir.tmpdir}/#{@real}.db", 0666) entries.reverse_each do |entry| next if stars[entry].empty? i = 0 s = stars[entry].select {|star| id = "#{entry}::#{i}" i += 1 if db.include?(id) false else db[id] = "1" true end } post server_name, NOTICE, main_channel, "#{entry} #{title(entry)}" if s.length > 0 if @opts.key?("metadata") post "metadata", NOTICE, main_channel, JSON.generate({ "uri" => entry }) end s.each do |star| post server_name, NOTICE, main_channel, "id:%s \x03%d%s%s\x030 %s" % [ star.name, Star::Colors[star.color], ((star.color == "normal") ? "☆" : "★") * ([star.count, 10].min), (star.count > 10) ? "(...#{star.count})" : "", star.quote ] end end rescue Exception => e @log.error e.inspect @log.error e.backtrace ensure db.close rescue nil end sleep 60 * 5 end end end def retrive_stars(entries, n=0) uri = "http://s.hatena.ne.jp/entries.json?" while uri.length < 1800 and n < entries.length uri << "uri=#{URI.escape(entries[n], /[^-.!~*'()\w]/n)}&" n += 1 end ret = JSON.load(@ua.get(uri).body)["entries"].inject({}) {|r,i| if i["stars"].any? {|star| star.kind_of? Numeric } i = JSON.load(@ua.get("http://s.hatena.ne.jp/entry.json?uri=#{URI.escape(i["uri"])}").body)["entries"].first end stars = [] if i["colored_stars"] i["colored_stars"].each do |s| s["stars"].each do |j| stars << Star.new(j, s["color"]) end end end i["stars"].each do |j| star = Star.new(j) if star.quote.empty? && stars.last && stars.last.name == star.name && stars.last.color == "normal" stars.last.count += 1 else stars << star end end r.update(i["uri"] => stars) } if n < entries.length ret.update retrive_stars(entries, n) end ret end def title(url) uri = URI(url) @ua.get(uri) text = "" case when uri.fragment fragment = @ua.page.root.at("//*[@name = '#{uri.fragment}']") || @ua.page.root.at("//*[@id = '#{uri.fragment}']") text = fragment.inner_text + fragment.following.text + fragment.parent.following.text when uri.to_s =~ %r|^http://h.hatena.ne.jp/[^/]+/\d+| text = @ua.page.root.at("#main .entries .entry .list-body div.body").inner_text else text = @ua.page.root.at("//title").inner_text end text.gsub!(/\s+/, " ") text.strip! NKF.nkf("-w", text).split(//)[0..30].join rescue Exception => e @log.debug ["title:", e.inspect] "" end def login @log.info "logging in as #{@real}" @ua.get "https://www.hatena.ne.jp/login?backurl=http%3A%2F%2Fd.hatena.ne.jp%2F" return if @ua.page.forms.empty? form = @ua.page.forms.first form["name"] = @real form["password"] = @pass @ua.submit(form) unless @ua.page.forms.empty? post server_name, ERR_PASSWDMISMATCH, ":Password incorrect" finish end end class Star < OpenStruct Colors = { "purple" => 6, "blue" => 2, "green" => 3, "red" => 4, "normal" => 8, } def initialize(obj, col="normal") super(obj) self.count = obj["count"].to_i + 1 self.color = col end end end if __FILE__ == $0 require "optparse" opts = { :port => 16702, :host => "localhost", :log => nil, :debug => false, :foreground => false, } OptionParser.new do |parser| parser.instance_eval do self.banner = <<-EOB.gsub(/^\t+/, "") Usage: #{$0} [opts] EOB separator "" separator "Options:" on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port| opts[:port] = port end on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host| opts[:host] = host end on("-l", "--log LOG", "log file") do |log| opts[:log] = log end on("--debug", "Enable debug mode") do |debug| opts[:log] = $stdout opts[:debug] = true end on("-f", "--foreground", "run foreground") do |foreground| opts[:log] = $stdout opts[:foreground] = true end parse!(ARGV) end end opts[:logger] = Logger.new(opts[:log], "daily") opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO def daemonize(foreground=false) trap("SIGINT") { exit! 0 } trap("SIGTERM") { exit! 0 } trap("SIGHUP") { exit! 0 } return yield if $DEBUG || foreground Process.fork do Process.setsid Dir.chdir "/" File.open("/dev/null") {|f| STDIN.reopen f STDOUT.reopen f STDERR.reopen f } yield end exit! 0 end daemonize(opts[:debug] || opts[:foreground]) do Net::IRC::Server.new(opts[:host], opts[:port], HatenaStarStream, opts).start end end # Local Variables: # coding: utf-8 # End: net-irc-0.0.9/examples/echo_bot.rb0000755000175000017500000000067011604616526016657 0ustar uwabamiuwabami#!/usr/bin/env ruby # vim:encoding=UTF-8: $LOAD_PATH << "lib" $LOAD_PATH << "../lib" require "rubygems" require "net/irc" require "pp" class EchoBot < Net::IRC::Client def initialize(*args) super end def on_rpl_welcome(m) post JOIN, "#bot_test" end def on_privmsg(m) post NOTICE, m[0], m[1] end end EchoBot.new("foobar", "6667", { :nick => "foobartest", :user => "foobartest", :real => "foobartest", }).start net-irc-0.0.9/examples/iig.rb0000755000175000017500000005045211604616526015650 0ustar uwabamiuwabami#!/usr/bin/env ruby # vim:encoding=UTF-8: $KCODE = "u" unless defined? ::Encoding # json use this =begin # iig.rb Identi.ca/Laconi.ca IRC gateway ## Launch $ ruby iig.rb If you want to help: $ ruby iig.rb --help ## Configuration Options specified by after IRC realname. Configuration example for Tiarra . identica { host: localhost port: 16672 name: username@example.com athack tid ratio=77:1:12 replies password: password on Identi.ca in-encoding: utf8 out-encoding: utf8 } ### athack If `athack` client option specified, all nick in join message is leading with @. So if you complemente nicks (e.g. Irssi), it's good for Identi.ca like reply command (@nick). In this case, you will see torrent of join messages after connected, because NAMES list can't send @ leading nick (it interpreted op.) ### tid[=] Apply ID to each message for make favorites by CTCP ACTION. /me fav ID [ID...] can be 0 => white 1 => black 2 => blue navy 3 => green 4 => red 5 => brown maroon 6 => purple 7 => orange olive 8 => yellow 9 => lightgreen lime 10 => teal 11 => lightcyan cyan aqua 12 => lightblue royal 13 => pink lightpurple fuchsia 14 => grey 15 => lightgrey silver ### jabber=: If `jabber=:` option specified, use jabber to get friends timeline. You must setup im notifing settings in the site and install "xmpp4r-simple" gem. $ sudo gem install xmpp4r-simple Be careful for managing password. ### alwaysim Use IM instead of any APIs (e.g. post) ### ratio=: ### replies[=] ### maxlimit= ### checkrls= ## Feed ## License Ruby's by cho45 =end $LOAD_PATH << "lib" << "../lib" require "rubygems" require "net/irc" require "net/http" require "uri" require "socket" require "time" require "logger" require "yaml" require "pathname" require "cgi" require "json" class IdenticaIrcGateway < Net::IRC::Server::Session def server_name; "identicagw" end def server_version; "0.0.0" end def main_channel; "#Identi.ca" end def api_base; URI("http://identi.ca/api/") end def api_source; "iig.rb" end def jabber_bot_id; "update@identi.ca" end def hourly_limit; 100 end class APIFailed < StandardError; end def initialize(*args) super @groups = {} @channels = [] # joined channels (groups) @user_agent = "#{self.class}/#{server_version} (#{File.basename(__FILE__)})" @config = Pathname.new(ENV["HOME"]) + ".iig" load_config end def on_user(m) super post @prefix, JOIN, main_channel post server_name, MODE, main_channel, "+o", @prefix.nick @real, *@opts = @opts.name || @real.split(/\s+/) @opts = @opts.inject({}) {|r,i| key, value = i.split("=") r.update(key => value) } @tmap = TypableMap.new if @opts["jabber"] jid, pass = @opts["jabber"].split(":", 2) @opts["jabber"].replace("jabber=#{jid}:********") if jabber_bot_id begin require "xmpp4r-simple" start_jabber(jid, pass) rescue LoadError log "Failed to start Jabber." log 'Installl "xmpp4r-simple" gem or check your ID/pass.' finish end else @opts.delete("jabber") log "This gateway does not support Jabber bot." end end log "Client Options: #{@opts.inspect}" @log.info "Client Options: #{@opts.inspect}" @hourly_limit = hourly_limit @ratio = Struct.new(:timeline, :friends, :replies).new(*(@opts["ratio"] || "10:3").split(":").map {|ratio| ratio.to_f }) @ratio[:replies] = @opts.key?("replies") ? (@opts["replies"] || 5).to_f : 0.0 footing = @ratio.inject {|sum, ratio| sum + ratio } @ratio.each_pair {|m, v| @ratio[m] = v / footing } @timeline = [] @check_friends_thread = Thread.start do loop do begin check_friends rescue APIFailed => e @log.error e.inspect rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep freq(@ratio[:friends]) end end return if @opts["jabber"] sleep 3 @check_timeline_thread = Thread.start do loop do begin check_timeline # check_direct_messages rescue APIFailed => e @log.error e.inspect rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep freq(@ratio[:timeline]) end end return unless @opts.key?("replies") sleep 10 @check_replies_thread = Thread.start do loop do begin check_replies rescue APIFailed => e @log.error e.inspect rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep freq(@ratio[:replies]) end end end def on_disconnected @check_friends_thread.kill rescue nil @check_replies_thread.kill rescue nil @check_timeline_thread.kill rescue nil @im_thread.kill rescue nil @im.disconnect rescue nil end def on_privmsg(m) return m.ctcps.each {|ctcp| on_ctcp(m[0], ctcp) } if m.ctcp? retry_count = 3 ret = nil target, message = *m.params begin if target =~ /^#/ if @opts.key?("alwaysim") && @im && @im.connected? # in jabber mode, using jabber post ret = @im.deliver(jabber_bot_id, message) post "#{nick}!#{nick}@#{api_base.host}", TOPIC, main_channel, untinyurl(message) else ret = api("statuses/update", {"status" => message}) end else # direct message ret = api("direct_messages/new", {"user" => target, "text" => message}) end raise APIFailed, "API failed" unless ret log "Status Updated" rescue => e @log.error [retry_count, e.inspect].inspect if retry_count > 0 retry_count -= 1 @log.debug "Retry to setting status..." retry else log "Some Error Happened on Sending #{message}. #{e}" end end end def on_ctcp(target, message) _, command, *args = message.split(/\s+/) case command when "list", "ls" nick = args[0] unless (1..200).include?(count = args[1].to_i) count = 20 end @log.debug([ nick, message ]) res = api("statuses/user_timeline", {"id" => nick, "count" => "#{count}"}).reverse_each do |s| @log.debug(s) post nick, NOTICE, main_channel, "#{generate_status_message(s)}" end unless res post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" end when /^(un)?fav(?:ou?rite)?$/ method, pfx = $1.nil? ? ["create", "F"] : ["destroy", "Unf"] args.each_with_index do |tid, i| st = @tmap[tid] if st sleep 1 if i > 0 res = api("favorites/#{method}/#{st["id"]}") post server_name, NOTICE, main_channel, "#{pfx}av: #{res["user"]["screen_name"]}: #{res["text"]}" else post server_name, NOTICE, main_channel, "No such ID #{tid}" end end when "link", "ln" args.each do |tid| st = @tmap[tid] if st st["link"] = "http://#{api_base.host}/notice/#{st["id"]}" unless st["link"] post server_name, NOTICE, main_channel, st["link"] else post server_name, NOTICE, main_channel, "No such ID #{tid}" end end # when /^ratios?$/ # if args[1].nil? || # @opts.key?("replies") && args[2].nil? # return post server_name, NOTICE, main_channel, "/me ratios [ ]" # end # ratios = args.map {|ratio| ratio.to_f } # if ratios.any? {|ratio| ratio <= 0.0 } # return post server_name, NOTICE, main_channel, "Ratios must be greater than 0." # end # footing = ratios.inject {|sum, ratio| sum + ratio } # @ratio[:timeline] = ratios[0] # @ratio[:friends] = ratios[1] # @ratio[:replies] = ratios[2] || 0.0 # @ratio.each_pair {|m, v| @ratio[m] = v / footing } # intervals = @ratio.map {|ratio| freq ratio } # post server_name, NOTICE, main_channel, "Intervals: #{intervals.join(", ")}" when /^(?:de(?:stroy|l(?:ete)?)|remove|miss)$/ args.each_with_index do |tid, i| st = @tmap[tid] if st sleep 1 if i > 0 res = api("statuses/destroy/#{st["id"]}") post server_name, NOTICE, main_channel, "Destroyed: #{res["text"]}" else post server_name, NOTICE, main_channel, "No such ID #{tid}" end end when "in", "location" location = args.join(" ") api("account/update_location", {"location" => location}) location = location.empty? ? "nowhere" : "in #{location}" post server_name, NOTICE, main_channel, "You are #{location} now." end rescue APIFailed => e log e.inspect end; private :on_ctcp def on_whois(m) nick = m.params[0] f = (@friends || []).find {|i| i["screen_name"] == nick } if f post server_name, RPL_WHOISUSER, @nick, nick, nick, api_base.host, "*", "#{f["name"]} / #{f["description"]}" post server_name, RPL_WHOISSERVER, @nick, nick, api_base.host, api_base.to_s post server_name, RPL_WHOISIDLE, @nick, nick, "0", "seconds idle" post server_name, RPL_ENDOFWHOIS, @nick, nick, "End of WHOIS list" else post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" end end def on_who(m) channel = m.params[0] case when channel == main_channel # " # ( "H" / "G" > ["*"] [ ( "@" / "+" ) ] # : " @friends.each do |f| user = nick = f["screen_name"] host = serv = api_base.host real = f["name"] post server_name, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}" end post server_name, RPL_ENDOFWHO, @nick, channel when @groups.key?(channel) @groups[channel].each do |name| f = @friends.find {|i| i["screen_name"] == name } user = nick = f["screen_name"] host = serv = api_base.host real = f["name"] post server_name, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}" end post server_name, RPL_ENDOFWHO, @nick, channel else post server_name, ERR_NOSUCHNICK, @nick, nick, "No such nick/channel" end end def on_join(m) channels = m.params[0].split(/\s*,\s*/) channels.each do |channel| next if channel == main_channel @channels << channel @channels.uniq! post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, channel post server_name, MODE, channel, "+o", @nick save_config end end def on_part(m) channel = m.params[0] return if channel == main_channel @channels.delete(channel) post @nick, PART, channel, "Ignore group #{channel}, but setting is alive yet." end def on_invite(m) nick, channel = *m.params return if channel == main_channel if (@friends || []).find {|i| i["screen_name"] == nick } ((@groups[channel] ||= []) << nick).uniq! post "#{nick}!#{nick}@#{api_base.host}", JOIN, channel post server_name, MODE, channel, "+o", nick save_config else post ERR_NOSUCHNICK, nil, nick, "No such nick/channel" end end def on_kick(m) channel, nick, mes = *m.params return if channel == main_channel if (@friends || []).find {|i| i["screen_name"] == nick } (@groups[channel] ||= []).delete(nick) post nick, PART, channel save_config else post ERR_NOSUCHNICK, nil, nick, "No such nick/channel" end end private def check_timeline api("statuses/friends_timeline", {:count => "117"}).reverse_each do |s| id = s["id"] next if id.nil? || @timeline.include?(id) @timeline << id nick = s["user"]["screen_name"] mesg = generate_status_message(s) tid = @tmap.push(s) @log.debug [id, nick, mesg] if nick == @nick # 自分のときは TOPIC に post "#{nick}!#{nick}@#{api_base.host}", TOPIC, main_channel, untinyurl(mesg) else if @opts.key?("tid") mesg = "%s \x03%s [%s]" % [mesg, @opts["tid"] || 10, tid] end message(nick, main_channel, mesg) end @groups.each do |channel, members| next unless members.include?(nick) if @opts.key?("tid") mesg = "%s \x03%s [%s]" % [mesg, @opts["tid"] || 10, tid] end message(nick, channel, mesg) end end @log.debug "@timeline.size = #{@timeline.size}" @timeline = @timeline.last(117) end def generate_status_message(status) s = status mesg = s["text"] @log.debug(mesg) # time = Time.parse(s["created_at"]) rescue Time.now m = {""" => "\"", "<" => "<", ">" => ">", "&" => "&", "\n" => " "} mesg.gsub!(/#{m.keys.join("|")}/) { m[$&] } mesg end def check_replies time = @prev_time_r || Time.now @prev_time_r = Time.now api("statuses/replies").reverse_each do |s| id = s["id"] next if id.nil? || @timeline.include?(id) created_at = Time.parse(s["created_at"]) rescue next next if created_at < time nick = s["user"]["screen_name"] mesg = generate_status_message(s) tid = @tmap.push(s) @log.debug [id, nick, mesg] if @opts.key?("tid") mesg = "%s \x03%s [%s]" % [mesg, @opts["tid"] || 10, tid] end message nick, main_channel, mesg end end def check_direct_messages time = @prev_time_d || Time.now @prev_time_d = Time.now api("direct_messages", {"since" => time.httpdate}).reverse_each do |s| nick = s["sender_screen_name"] mesg = s["text"] time = Time.parse(s["created_at"]) @log.debug [nick, mesg, time].inspect message(nick, @nick, mesg) end end def check_friends first = true unless @friends @friends ||= [] friends = api("statuses/friends") if first && !@opts.key?("athack") @friends = friends post server_name, RPL_NAMREPLY, @nick, "=", main_channel, @friends.map{|i| "@#{i["screen_name"]}" }.join(" ") post server_name, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list" else prv_friends = @friends.map {|i| i["screen_name"] } now_friends = friends.map {|i| i["screen_name"] } # Twitter API bug? return if !first && (now_friends.length - prv_friends.length).abs > 10 (now_friends - prv_friends).each do |join| join = "@#{join}" if @opts.key?("athack") post "#{join}!#{join}@#{api_base.host}", JOIN, main_channel end (prv_friends - now_friends).each do |part| part = "@#{part}" if @opts.key?("athack") post "#{part}!#{part}@#{api_base.host}", PART, main_channel, "" end @friends = friends end end def check_downtime @prev_downtime ||= nil schedule = api("help/downtime_schedule", {}, {:avoid_error => true})["error"] if @prev_downtime != schedule && @prev_downtime = schedule msg = schedule.gsub(%r{[\r\n]|]*)?>.*?}m, "") uris = URI.extract(msg) uris.each do |uri| msg << " #{uri}" end msg.gsub!(/<[^>]+>/, "") log "\002\037#{msg}\017" # TODO: sleeping for the downtime end end def freq(ratio) max = (@opts["maxlimit"] || 100).to_i limit = @hourly_limit < max ? @hourly_limit : max f = 3600 / (limit * ratio).round @log.debug "Frequency: #{f}" f end def start_jabber(jid, pass) @log.info "Logging-in with #{jid} -> jabber_bot_id: #{jabber_bot_id}" @im = Jabber::Simple.new(jid, pass) @im.add(jabber_bot_id) @im_thread = Thread.start do loop do begin @im.received_messages.each do |msg| @log.debug [msg.from, msg.body] if msg.from.strip == jabber_bot_id # Twitter -> 'id: msg' body = msg.body.sub(/^(.+?)(?:\((.+?)\))?: /, "") if Regexp.last_match nick, id = Regexp.last_match.captures body = CGI.unescapeHTML(body) message(id || nick, main_channel, body) end end end rescue Exception => e @log.error "Error on Jabber loop: #{e.inspect}" e.backtrace.each do |l| @log.error "\t#{l}" end end sleep 1 end end end def save_config config = { :channels => @channels, :groups => @groups, } @config.open("w") do |f| YAML.dump(config, f) end end def load_config @config.open do |f| config = YAML.load(f) @channels = config[:channels] @groups = config[:groups] end rescue Errno::ENOENT end def require_post?(path) [ %r{^statuses/(?:update$|destroy/)}, "direct_messages/new", "account/update_location", %r{^favorites/}, ].any? {|i| i === path } end def api(path, q = {}, opt = {}) ret = {} headers = {"User-Agent" => @user_agent} headers["If-Modified-Since"] = q["since"] if q.key?("since") q["source"] ||= api_source q = q.inject([]) {|r,(k,v)| v ? r << "#{k}=#{URI.escape(v, /[^-.!~*'()\w]/n)}" : r }.join("&") path = path.sub(%r{^/+}, "") uri = api_base.dup uri.path += "#{path}.json" if require_post? path req = Net::HTTP::Post.new(uri.request_uri, headers) req.body = q else uri.query = q req = Net::HTTP::Get.new(uri.request_uri, headers) end req.basic_auth(@real, @pass) @log.debug uri.inspect ret = Net::HTTP.start(uri.host, uri.port) {|http| http.request(req) } case ret when Net::HTTPOK # 200 ret = JSON.parse(ret.body.gsub(/'(y(?:es)?|no?|true|false|null)'/, '"\1"')) if ret.kind_of?(Hash) && !opt[:avoid_error] && ret["error"] raise APIFailed, "Server Returned Error: #{ret["error"]}" end ret when Net::HTTPNotModified # 304 [] when Net::HTTPBadRequest # 400 # exceeded the rate limitation raise APIFailed, "#{ret.code}: #{ret.message}" else raise APIFailed, "Server Returned #{ret.code} #{ret.message}" end rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e raise APIFailed, e.inspect end def message(sender, target, str) # str.gsub!(/&#(x)?([0-9a-f]+);/i) do # [$1 ? $2.hex : $2.to_i].pack("U") # end str = untinyurl(str) sender = "#{sender}!#{sender}@#{api_base.host}" post sender, PRIVMSG, target, str end def log(str) str.gsub!(/\r\n|[\r\n]/, " ") post server_name, NOTICE, main_channel, str end def untinyurl(text) text.gsub(%r|http://(preview\.)?tinyurl\.com/[0-9a-z=]+|i) {|m| uri = URI(m) uri.host = uri.host.sub($1, "") if $1 Net::HTTP.start(uri.host, uri.port) {|http| http.open_timeout = 3 begin http.head(uri.request_uri, {"User-Agent" => @user_agent})["Location"] || m rescue Timeout::Error m end } } end class TypableMap < Hash Roman = %w[ k g ky gy s z sh j t d ch n ny h b p hy by py m my y r ry w v q ].unshift("").map do |consonant| case consonant when "y", /\A.{2}/ then %w|a u o| when "q" then %w|a i e o| else %w|a i u e o| end.map {|vowel| "#{consonant}#{vowel}" } end.flatten def initialize(size = 1) @seq = Roman @n = 0 @size = size end def generate(n) ret = [] begin n, r = n.divmod(@seq.size) ret << @seq[r] end while n > 0 ret.reverse.join end def push(obj) id = generate(@n) self[id] = obj @n += 1 @n %= @seq.size ** @size id end alias << push def clear @n = 0 super end private :[]= undef update, merge, merge!, replace end end if __FILE__ == $0 require "optparse" opts = { :port => 16672, :host => "localhost", :log => nil, :debug => false, :foreground => false, } OptionParser.new do |parser| parser.instance_eval do self.banner = <<-EOB.gsub(/^\t+/, "") Usage: #{$0} [opts] EOB separator "" separator "Options:" on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port| opts[:port] = port end on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host| opts[:host] = host end on("-l", "--log LOG", "log file") do |log| opts[:log] = log end on("--debug", "Enable debug mode") do |debug| opts[:log] = $stdout opts[:debug] = true end on("-f", "--foreground", "run foreground") do |foreground| opts[:log] = $stdout opts[:foreground] = true end on("-n", "--name [user name or email address]") do |name| opts[:name] = name end parse!(ARGV) end end opts[:logger] = Logger.new(opts[:log], "daily") opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO #def daemonize(foreground = false) # trap("SIGINT") { exit! 0 } # trap("SIGTERM") { exit! 0 } # trap("SIGHUP") { exit! 0 } # return yield if $DEBUG || foreground # Process.fork do # Process.setsid # Dir.chdir "/" # File.open("/dev/null") {|f| # STDIN.reopen f # STDOUT.reopen f # STDERR.reopen f # } # yield # end # exit! 0 #end #daemonize(opts[:debug] || opts[:foreground]) do Net::IRC::Server.new(opts[:host], opts[:port], IdenticaIrcGateway, opts).start #end end # Local Variables: # coding: utf-8 # End: net-irc-0.0.9/examples/hig.rb0000755000175000017500000004511411604616526015646 0ustar uwabamiuwabami#!/usr/bin/env ruby # vim:encoding=UTF-8: =begin # hig.rb ## Launch $ ruby hig.rb If you want to help: $ ruby hig.rb --help ## Configuration Options specified by after irc realname. Configuration example for Tiarra ( http://coderepos.org/share/wiki/Tiarra ). haiku { host: localhost port: 16679 name: username@example.com athack jabber=username@example.com:jabberpasswd tid=10 ratio=10:3:5 password: password on Haiku in-encoding: utf8 out-encoding: utf8 } ### athack If `athack` client option specified, all nick in join message is leading with @. So if you complemente nicks (e.g. Irssi), it's good for Twitter like reply command (@nick). In this case, you will see torrent of join messages after connected, because NAMES list can't send @ leading nick (it interpreted op.) ### tid= Apply id to each message for make favorites by CTCP ACTION. /me fav id can be 0 => white 1 => black 2 => blue navy 3 => green 4 => red 5 => brown maroon 6 => purple 7 => orange olive 8 => yellow 9 => lightgreen lime 10 => teal 11 => lightcyan cyan aqua 12 => lightblue royal 13 => pink lightpurple fuchsia 14 => grey 15 => lightgrey silver ### jabber=: If `jabber=:` option specified, use jabber to get friends timeline. You must setup im notifing settings in the site and install "xmpp4r-simple" gem. $ sudo gem install xmpp4r-simple Be careful for managing password. ### alwaysim Use IM instead of any APIs (e.g. post) ### ratio=:: ## License Ruby's by cho45 =end $LOAD_PATH << "lib" $LOAD_PATH << "../lib" $KCODE = "u" unless defined? ::Encoding # json use this require "rubygems" require "net/irc" require "net/http" require "uri" require "json" require "socket" require "time" require "logger" require "yaml" require "pathname" require "cgi" require "digest/md5" Net::HTTP.version_1_2 class HaikuIrcGateway < Net::IRC::Server::Session def server_name "haikugw" end def server_version "0.0.0" end def main_channel "#haiku" end def api_base URI(ENV["HAIKU_BASE"] || "http://h.hatena.ne.jp/api/") end def api_source "hig.rb" end def jabber_bot_id nil end def hourly_limit 60 end class ApiFailed < StandardError; end def initialize(*args) super @channels = {} @user_agent = "#{self.class}/#{server_version} (hig.rb)" @counters = {} # for jabber fav end def on_user(m) super post @prefix, JOIN, main_channel post server_name, MODE, main_channel, "+o", @prefix.nick @real, *@opts = @opts.name || @real.split(/\s+/) @opts = @opts.inject({}) {|r,i| key, value = i.split("=") r.update(key => value) } @tmap = TypableMap.new if @opts["jabber"] jid, pass = @opts["jabber"].split(":", 2) @opts["jabber"].replace("jabber=#{jid}:********") if jabber_bot_id begin require "xmpp4r-simple" start_jabber(jid, pass) rescue LoadError log "Failed to start Jabber." log 'Installl "xmpp4r-simple" gem or check your id/pass.' finish end else @opts.delete("jabber") log "This gateway does not support Jabber bot." end end log "Client Options: #{@opts.inspect}" @log.info "Client Options: #{@opts.inspect}" timeline_ratio, friends_ratio, channel_ratio = (@opts["ratio"] || "10:3:5").split(":").map {|ratio| ratio.to_i } footing = (timeline_ratio + friends_ratio + channel_ratio).to_f @timeline = [] @check_follows_thread = Thread.start do loop do begin check_friends check_keywords rescue ApiFailed => e @log.error e.inspect rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep freq(friends_ratio / footing) end end return if @opts["jabber"] @check_timeline_thread = Thread.start do sleep 10 loop do begin check_timeline rescue ApiFailed => e @log.error e.inspect rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep freq(timeline_ratio / footing) end end end def on_disconnected @check_follows_thread.kill rescue nil @check_timeline_thread.kill rescue nil @im_thread.kill rescue nil @im.disconnect rescue nil end def on_privmsg(m) return m.ctcps.each {|ctcp| on_ctcp(m[0], ctcp) } if m.ctcp? retry_count = 3 ret = nil target, message = *m.params begin channel = target.sub(/^#/, "") reply = message[/\s+>(.+)$/, 1] if !reply && @opts.key?("alwaysim") && @im && @im.connected? # in jabber mode, using jabber post message = "##{channel} #{message}" unless "##{channel}" == main_channel ret = @im.deliver(jabber_bot_id, message) post "#{nick}!#{nick}@#{api_base.host}", TOPIC, channel, message else channel = "" if "##{channel}" == main_channel rid = rid_for(reply) if reply if @typo log "typo mode. requesting..." message.gsub!(/\\n/, "\n") file = Net::HTTP.get(URI("http://lab.lowreal.net/test/haiku.rb/?text=" + URI.escape(message))) ret = api("statuses/update", {"file" => file, "in_reply_to_status_id" => rid, "keyword" => channel}) else ret = api("statuses/update", {"status" => message, "in_reply_to_status_id" => rid, "keyword" => channel}) end log "Status Updated via API" end raise ApiFailed, "API failed" unless ret check_timeline rescue => e @log.error [retry_count, e.message, e.inspect, e.backtrace].inspect if retry_count > 0 retry_count -= 1 @log.debug "Retry to setting status..." # retry else log "Some Error Happened on Sending #{message}. #{e}" end end end def on_ctcp(target, message) _, command, *args = message.split(/\s+/) case command when "list" nick = args[0] @log.debug([ nick, message ]) res = api("statuses/user_timeline", { "id" => nick }).reverse_each do |s| @log.debug(s) post nick, NOTICE, main_channel, s end unless res post nil, ERR_NOSUCHNICK, nick, "No such nick/channel" end when "fav" target = args[0] st = @tmap[target] id = rid_for(target) if st || id unless id if @im && @im.connected? # IM のときはいろいろめんどうなことする nick, count = *st pos = @counters[nick] - count @log.debug "%p %s %d/%d => %d" % [ st, nick, count, @counters[nick], pos ] res = api("statuses/user_timeline", { "id" => nick }) raise ApiFailed, "#{nick} may be private mode" if res.empty? if res[pos] id = res[pos]["id"] else raise ApiFailed, "#{pos} of #{nick} is not found." end else id = st["id"] end end res = api("favorites/create/#{id}", {}) post nil, NOTICE, main_channel, "Fav: #{res["screen_name"]}: #{res["text"].gsub(URI.regexp(%w|http https|), "http...")}" else post nil, NOTICE, main_channel, "No such id or status #{target}" end when "link" tid = args[0] st = @tmap[tid] if st st["link"] = (api_base + "/#{st["user"]["screen_name"]}/#{st["id"]}").to_s unless st["link"] post nil, NOTICE, main_channel, st["link"] else post nil, NOTICE, main_channel, "No such id #{tid}" end when "typo" @typo = !@typo post nil, NOTICE, main_channel, "typo mode: #{@typo}" end rescue ApiFailed => e log e.inspect end; private :on_ctcp def on_whois(m) nick = m.params[0] f = (@friends || []).find {|i| i["screen_name"] == nick } if f post nil, RPL_WHOISUSER, @nick, nick, nick, api_base.host, "*", "#{f["name"]} / #{f["description"]}" post nil, RPL_WHOISSERVER, @nick, nick, api_base.host, api_base.to_s post nil, RPL_WHOISIDLE, @nick, nick, "0", "seconds idle" post nil, RPL_ENDOFWHOIS, @nick, nick, "End of WHOIS list" else post nil, ERR_NOSUCHNICK, nick, "No such nick/channel" end end def on_join(m) return ### なんかしらんけど何度も入ってしまってうざいので…… channels = m.params[0].split(/\s*,\s*/) channels.each do |channel| next if channel == main_channel begin api("keywords/create/#{URI.escape(channel.sub(/^#/, ""))}") @channels[channel] = { :read => [] } post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, channel rescue => e @log.debug e.inspect post nil, ERR_NOSUCHNICK, nick, "No such nick/channel" end end end def on_part(m) channel = m.params[0] return if channel == main_channel @channels.delete(channel) api("keywords/destroy/#{URI.escape(channel.sub(/^#/, ""))}") post "#{@nick}!#{@nick}@#{api_base.host}", PART, channel end def on_who(m) channel = m.params[0] case when channel == main_channel # " # ( "H" / "G" > ["*"] [ ( "@" / "+" ) ] # : " @friends.each do |f| user = nick = f["screen_name"] host = serv = api_base.host real = f["name"] post nil, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}" end post nil, RPL_ENDOFWHO, @nick, channel when @groups.key?(channel) @groups[channel].each do |name| f = @friends.find {|i| i["screen_name"] == name } user = nick = f["screen_name"] host = serv = api_base.host real = f["name"] post nil, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}" end post nil, RPL_ENDOFWHO, @nick, channel else post nil, ERR_NOSUCHNICK, @nick, nick, "No such nick/channel" end end private def check_timeline api("statuses/friends_timeline").reverse_each do |s| begin id = s["id"] next if id.nil? || @timeline.include?(id) @timeline << id nick = s["user"]["id"] mesg = generate_status_message(s) tid = @tmap.push(s) @log.debug [id, nick, mesg] channel = "##{s["keyword"]}" case when s["keyword"].match(/^id:/) channel = main_channel when !@channels.keys.include?(channel) channel = main_channel mesg = "%s = %s" % [s["keyword"], mesg] end if nick == @nick # 自分のときは topic に post "#{nick}!#{nick}@#{api_base.host}", TOPIC, channel, mesg else if @opts["tid"] message(nick, channel, "%s \x03%s [%s]" % [mesg, @opts["tid"], tid]) else message(nick, channel, "%s" % [mesg, tid]) end if @opts.key?("metadata") post "metadata", NOTICE, channel, JSON.generate({ "uri" => (api_base + "/#{s["user"]["screen_name"]}/#{s["id"]}").to_s }) end end rescue => e @log.debug "Error: %p" % e end end @log.debug "@timeline.size = #{@timeline.size}" @timeline = @timeline.last(100) end def generate_status_message(s) mesg = s["text"] mesg.sub!("#{s["keyword"]}=", "") unless s["keyword"] =~ /^id:/ mesg << " > #{s["in_reply_to_user_id"]}" unless s["in_reply_to_user_id"].empty? @log.debug(mesg) mesg end def check_friends first = true unless @friends @friends ||= [] friends = api("statuses/friends") if first && !@opts.key?("athack") @friends = friends post nil, RPL_NAMREPLY, @nick, "=", main_channel, @friends.map{|i| "@#{i["screen_name"]}" }.join(" ") post nil, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list" else prv_friends = @friends.map {|i| i["screen_name"] } now_friends = friends.map {|i| i["screen_name"] } # Twitter API bug? return if !first && (now_friends.length - prv_friends.length).abs > 10 (now_friends - prv_friends).each do |join| join = "@#{join}" if @opts.key?("athack") post "#{join}!#{join}@#{api_base.host}", JOIN, main_channel end (prv_friends - now_friends).each do |part| part = "@#{part}" if @opts.key?("athack") post "#{part}!#{part}@#{api_base.host}", PART, main_channel, "" end @friends = friends end end def check_keywords keywords = api("statuses/keywords").map {|i| "##{i["title"]}" } current = @channels.keys current.delete main_channel (current - keywords).each do |part| @channels.delete(part) post "#{@nick}!#{@nick}@#{api_base.host}", PART, part end (keywords - current).each do |join| @channels[join] = { :read => [] } post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, join end end def freq(ratio) ret = 3600 / (hourly_limit * ratio).round @log.debug "Frequency: #{ret}" ret end def start_jabber(jid, pass) @log.info "Logging-in with #{jid} -> jabber_bot_id: #{jabber_bot_id}" @im = Jabber::Simple.new(jid, pass) @im.add(jabber_bot_id) @im_thread = Thread.start do loop do begin @im.received_messages.each do |msg| @log.debug [msg.from, msg.body] if msg.from.strip == jabber_bot_id # Haiku -> 'nick(id): msg' body = msg.body.sub(/^(.+?)(?:\((.+?)\))?: /, "") if Regexp.last_match nick, id = Regexp.last_match.captures body = CGI.unescapeHTML(body) case when nick == "投稿完了" log "#{nick}: #{body}" when nick == "チャンネル投稿完了" log "#{nick}: #{body}" when body =~ /^#([a-z_]+)\s+(.+)$/i # channel message or not message(id || nick, "##{Regexp.last_match[1]}", Regexp.last_match[2]) when nick == "photo" && body =~ %r|^http://haiku\.jp/user/([^/]+)/| nick = Regexp.last_match[1] message(nick, main_channel, body) else @counters[nick] ||= 0 @counters[nick] += 1 tid = @tmap.push([nick, @counters[nick]]) message(nick, main_channel, "%s \x03%s [%s]" % [body, @opts["tid"], tid]) end end end end rescue Exception => e @log.error "Error on Jabber loop: #{e.inspect}" e.backtrace.each do |l| @log.error "\t#{l}" end end sleep 1 end end end def require_post?(path) [ %r|/update|, %r|/create|, %r|/destroy|, ].any? {|i| i === path } end def api(path, q={}) ret = {} q["source"] ||= api_source uri = api_base.dup uri.path = "/api/#{path}.json" uri.query = q.inject([]) {|r,(k,v)| v ? r << "#{k}=#{URI.escape(v, /[^:,-.!~*'()\w]/n)}" : r }.join("&") req = nil if require_post?(path) req = Net::HTTP::Post.new(uri.path) if q["file"] boundary = (rand(0x1_00_00_00_00_00) + 0x1_00_00_00_00_00).to_s(16) @log.info boundary req["content-type"] = "multipart/form-data; boundary=#{boundary}" body = "" q.each do |k, v| body << "--#{boundary}\r\n" if k == "file" body << "Content-Disposition: form-data; name=\"#{k}\"; filename=\"temp.png\";\r\n" body << "Content-Transfer-Encoding: binary\r\n" body << "Content-Type: image/png\r\n" else body << "Content-Disposition: form-data; name=\"#{k}\";\r\n" end body << "\r\n" body << v.to_s body << "\r\n" end body << "--#{boundary}--\r\n" req.body = body uri.query = "" else req.body = uri.query end else req = Net::HTTP::Get.new(uri.request_uri) end req.basic_auth(@real, @pass) req["User-Agent"] = @user_agent req["If-Modified-Since"] = q["since"] if q.key?("since") @log.debug uri.inspect ret = Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) } case ret when Net::HTTPOK # 200 ret = JSON.parse(ret.body.gsub(/:'/, ':"').gsub(/',/, '",').gsub(/'(y(?:es)?|no?|true|false|null)'/, '"\1"')) raise ApiFailed, "Server Returned Error: #{ret["error"]}" if ret.kind_of?(Hash) && ret["error"] ret when Net::HTTPNotModified # 304 [] when Net::HTTPBadRequest # 400 # exceeded the rate limitation raise ApiFailed, "#{ret.code}: #{ret.message}" else raise ApiFailed, "Server Returned #{ret.code} #{ret.message}" end rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e raise ApiFailed, e.inspect end def message(sender, target, str) sender = "#{sender}!#{sender}@#{api_base.host}" post sender, PRIVMSG, target, str.gsub(/\s+/, " ") end def log(str) str.gsub!(/\n/, " ") post server_name, NOTICE, main_channel, str end # return rid of most recent matched status with text def rid_for(text) target = Regexp.new(Regexp.quote(text.strip), "i") status = api("statuses/friends_timeline").find {|i| next false if i["user"]["name"] == @nick # 自分は除外 i["text"] =~ target } @log.debug "Looking up status contains #{text.inspect} -> #{status.inspect}" status ? status["id"] : nil end class TypableMap < Hash Roman = %w[ k g ky gy s z sh j t d ch n ny h b p hy by py m my y r ry w v q ].unshift("").map do |consonant| case consonant when "y", /\A.{2}/ then %w|a u o| when "q" then %w|a i e o| else %w|a i u e o| end.map {|vowel| "#{consonant}#{vowel}" } end.flatten def initialize(size = 1) @seq = Roman @n = 0 @size = size end def generate(n) ret = [] begin n, r = n.divmod(@seq.size) ret << @seq[r] end while n > 0 ret.reverse.join end def push(obj) id = generate(@n) self[id] = obj @n += 1 @n %= @seq.size ** @size id end alias << push def clear @n = 0 super end private :[]= undef update, merge, merge!, replace end end if __FILE__ == $0 require "optparse" opts = { :port => 16679, :host => "localhost", :log => nil, :debug => false, :foreground => false, } OptionParser.new do |parser| parser.instance_eval do self.banner = <<-EOB.gsub(/^\t+/, "") Usage: #{$0} [opts] EOB separator "" separator "Options:" on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port| opts[:port] = port end on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host| opts[:host] = host end on("-l", "--log LOG", "log file") do |log| opts[:log] = log end on("--debug", "Enable debug mode") do |debug| opts[:log] = $stdout opts[:debug] = true end on("-f", "--foreground", "run foreground") do |foreground| opts[:log] = $stdout opts[:foreground] = true end on("-n", "--name [user name or email address]") do |name| opts[:name] = name end parse!(ARGV) end end opts[:logger] = Logger.new(opts[:log], "daily") opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO # def daemonize(foreground=false) # trap("SIGINT") { exit! 0 } # trap("SIGTERM") { exit! 0 } # trap("SIGHUP") { exit! 0 } # return yield if $DEBUG || foreground # Process.fork do # Process.setsid # Dir.chdir "/" # File.open("/dev/null") {|f| # STDIN.reopen f # STDOUT.reopen f # STDERR.reopen f # } # yield # end # exit! 0 # end # daemonize(opts[:debug] || opts[:foreground]) do Net::IRC::Server.new(opts[:host], opts[:port], HaikuIrcGateway, opts).start # end end # Local Variables: # coding: utf-8 # End: net-irc-0.0.9/examples/ircd.rb0000755000175000017500000001756611604616526016032 0ustar uwabamiuwabami#!/usr/bin/env ruby # vim:encoding=UTF-8: require 'rubygems' require 'net/irc' class NetIrcServer < Net::IRC::Server::Session def server_name "net-irc" end def server_version "0.0.0" end def available_user_modes "iosw" end def default_user_modes "" end def available_channel_modes "om" end def default_channel_modes "" end def initialize(*args) super @@channels ||= {} @@users ||= {} @ping = false end def on_pass(m) end def on_user(m) @user, @real = m.params[0], m.params[3] @host = @socket.peeraddr[2] @prefix = Prefix.new("#{@nick}!#{@user}@#{@host}") @joined_on = @updated_on = Time.now.to_i post @socket, @prefix, NICK, nick @nick = nick @prefix = "#{@nick}!#{@user}@#{@host}" time = Time.now.to_i @@users[@nick.downcase] = { :nick => @nick, :user => @user, :host => @host, :real => @real, :prefix => @prefix, :socket => @socket, :joined_on => time, :updated_on => time } initial_message start_ping end def on_join(m) channels = m.params[0].split(/\s*,\s*/) password = m.params[1] channels.each do |channel| unless channel.downcase =~ /^#/ post @socket, server_name, ERR_NOSUCHCHANNEL, @nick, channel, "No such channel" next end unless @@channels.key?(channel.downcase) channel_create(channel) else return if @@channels[channel.downcase][:users].key?(@nick.downcase) @@channels[channel.downcase][:users][@nick.downcase] = [] end mode = @@channels[channel.downcase][:mode].empty? ? "" : "+" + @@channels[channel.downcase][:mode] post @socket, server_name, RPL_CHANNELMODEIS, @nick, @@channels[channel.downcase][:alias], mode channel_users = "" @@channels[channel.downcase][:users].each do |nick, m| post @@users[nick][:socket], @prefix, JOIN, @@channels[channel.downcase][:alias] case when m.index("@") f = "@" when m.index("+") f = "+" else f = "" end channel_users += "#{f}#{@@users[nick.downcase][:nick]} " end post @socket, server_name, RPL_NAMREPLY, @@users[nick][:nick], "=", @@channels[channel.downcase][:alias], "#{channel_users.strip}" post @socket, server_name, RPL_ENDOFNAMES, @@users[nick][:nick], @@channels[channel.downcase][:alias], "End of /NAMES list" end end def on_part(m) channel, message = *m.params @@channels[channel.downcase][:users].each do |nick, f| post @@users[nick][:socket], @prefix, PART, @@channels[channel.downcase][:alias], message end channel_part(channel) end def on_quit(m) message = m.params[0] @@channels.each do |channel, f| if f[:users].key?(@nick.downcase) channel_part(channel) f[:users].each do |nick, m| post @@users[nick][:socket], @prefix, QUIT, message end end end finish end def on_disconnected super @@channels.each do |channel, f| if f[:users].key?(@nick.downcase) channel_part(channel) f[:users].each do |nick, m| post @@users[nick][:socket], @prefix, QUIT, "disconnect" end end end channel_part_all @@users.delete(@nick.downcase) end def on_who(m) channel = m.params[0] return unless channel c = channel.downcase case when @@channels.key?(c) @@channels[c][:users].each do |nickname, m| nick = @@users[nickname][:nick] user = @@users[nickname][:user] host = @@users[nickname][:host] real = @@users[nickname][:real] case when m.index("@") f = "@" when m.index("+") f = "+" else f = "" end post @socket, server_name, RPL_WHOREPLY, @nick, @@channels[c][:alias], user, host, server_name, nick, "H#{f}", "0 #{real}" end post @socket, server_name, RPL_ENDOFWHO, @nick, @@channels[c][:alias], "End of /WHO list" end end def on_mode(m) end def on_privmsg(m) while (Time.now.to_i - @updated_on < 2) sleep 2 end idle_update return on_ctcp(m[0], ctcp_decoding(m[1])) if m.ctcp? target, message = *m.params t = target.downcase case when @@channels.key?(t) if @@channels[t][:users].key?(@nick.downcase) @@channels[t][:users].each do |nick, m| post @@users[nick][:socket], @prefix, PRIVMSG, @@channels[t][:alias], message unless nick == @nick.downcase end else post @socket, nil, ERR_CANNOTSENDTOCHAN, @nick, target, "Cannot send to channel" end when @@users.key?(t) post @@users[nick][:socket], @prefix, PRIVMSG, @@users[t][:nick], message else post @socket, nil, ERR_NOSUCHNICK, @nick, target, "No such nick/channel" end end def on_ping(m) post @socket, server_name, PONG, m.params[0] end def on_pong(m) @ping = true end def idle_update @updated_on = Time.now.to_i if logged_in? @@users[@nick.downcase][:updated_on] = @updated_on end end def channel_create(channel) @@channels[channel.downcase] = { :alias => channel, :topic => "", :mode => default_channel_modes, :users => {@nick.downcase => ["@"]}, } end def channel_part(channel) @@channels[channel.downcase][:users].delete(@nick.downcase) channel_delete(channel.downcase) if @@channels[channel.downcase][:users].size == 0 end def channel_part_all @@channels.each do |c| channel_part(c) end end def channel_delete(channel) @@channels.delete(channel.downcase) end def post(socket, prefix, command, *params) m = Message.new(prefix, command, params.map{|s| s.gsub(/[\r\n]/, "") }) socket << m rescue finish end def start_ping Thread.start do loop do @ping = false time = Time.now.to_i if @ping == false && (time - @updated_on > 60) post @socket, server_name, PING, @prefix loop do sleep 1 if @ping break end if 60 < Time.now.to_i - time Thread.stop finish end end end sleep 60 end end end # Call when client connected. # Send RPL_WELCOME sequence. If you want to customize, override this method at subclass. def initial_message post @socket, server_name, RPL_WELCOME, @nick, "Welcome to the Internet Relay Network #{@prefix}" post @socket, server_name, RPL_YOURHOST, @nick, "Your host is #{server_name}, running version #{server_version}" post @socket, server_name, RPL_CREATED, @nick, "This server was created #{Time.now}" post @socket, server_name, RPL_MYINFO, @nick, "#{server_name} #{server_version} #{available_user_modes} #{available_channel_modes}" end end if __FILE__ == $0 require "optparse" opts = { :port => 6969, :host => "localhost", :log => nil, :debug => false, :foreground => false, } OptionParser.new do |parser| parser.instance_eval do self.banner = <<-EOB.gsub(/^\t+/, "") Usage: #{$0} [opts] EOB separator "" separator "Options:" on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port| opts[:port] = port end on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host| opts[:host] = host end on("-l", "--log LOG", "log file") do |log| opts[:log] = log end on("--debug", "Enable debug mode") do |debug| opts[:log] = $stdout opts[:debug] = true end on("-f", "--foreground", "run foreground") do |foreground| opts[:log] = $stdout opts[:foreground] = true end on("-n", "--name [user name or email address]") do |name| opts[:name] = name end parse!(ARGV) end end opts[:logger] = Logger.new(opts[:log], "daily") opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO #def daemonize(foreground = false) # [:INT, :TERM, :HUP].each do |sig| # Signal.trap sig, "EXIT" # end # return yield if $DEBUG or foreground # Process.fork do # Process.setsid # Dir.chdir "/" # STDIN.reopen "/dev/null" # STDOUT.reopen "/dev/null", "a" # STDERR.reopen STDOUT # yield # end # exit! 0 #end #daemonize(opts[:debug] || opts[:foreground]) do Net::IRC::Server.new(opts[:host], opts[:port], NetIrcServer, opts).start #end end net-irc-0.0.9/examples/wig.rb0000755000175000017500000005065711604616526015675 0ustar uwabamiuwabami#!/usr/bin/env ruby # vim:encoding=UTF-8: =begin # wig.rb wig.rb channel: http://wassr.jp/channel/wigrb ## Launch $ ruby wig.rb If you want to help: $ ruby wig.rb --help ## Configuration Options specified by after irc realname. Configuration example for Tiarra ( http://coderepos.org/share/wiki/Tiarra ). wassr { host: localhost port: 16670 name: username@example.com athack jabber=username@example.com:jabberpasswd tid=10 ratio=10:3:5 password: password on Wassr in-encoding: utf8 out-encoding: utf8 } ### athack If `athack` client option specified, all nick in join message is leading with @. So if you complemente nicks (e.g. Irssi), it's good for Twitter like reply command (@nick). In this case, you will see torrent of join messages after connected, because NAMES list can't send @ leading nick (it interpreted op.) ### tid= Apply id to each message for make favorites by CTCP ACTION. /me fav id can be 0 => white 1 => black 2 => blue navy 3 => green 4 => red 5 => brown maroon 6 => purple 7 => orange olive 8 => yellow 9 => lightgreen lime 10 => teal 11 => lightcyan cyan aqua 12 => lightblue royal 13 => pink lightpurple fuchsia 14 => grey 15 => lightgrey silver ### jabber=: If `jabber=:` option specified, use jabber to get friends timeline. You must setup im notifing settings in the site and install "xmpp4r-simple" gem. $ sudo gem install xmpp4r-simple Be careful for managing password. ### alwaysim Use IM instead of any APIs (e.g. post) ### ratio=:: ## License Ruby's by cho45 =end $LOAD_PATH << "lib" $LOAD_PATH << "../lib" $KCODE = "u" unless defined? ::Encoding # json use this require "rubygems" require "net/irc" require "net/http" require "uri" require "json" require "socket" require "time" require "logger" require "yaml" require "pathname" require "cgi" require "digest/md5" Net::HTTP.version_1_2 class WassrIrcGateway < Net::IRC::Server::Session def server_name "wassrgw" end def server_version "0.0.0" end def main_channel "#wassr" end def api_base URI("http://api.wassr.jp/") end def api_source "wig.rb" end def jabber_bot_id "wassr-bot@wassr.jp" end def hourly_limit 60 end class ApiFailed < StandardError; end def initialize(*args) super @channels = {} @user_agent = "#{self.class}/#{server_version} (wig.rb)" @counters = {} # for jabber fav end def on_user(m) super post @prefix, JOIN, main_channel post server_name, MODE, main_channel, "+o", @prefix.nick @real, *@opts = @opts.name || @real.split(/\s+/) @opts = @opts.inject({}) {|r,i| key, value = i.split("=") r.update(key => value) } @tmap = TypableMap.new if @opts["jabber"] jid, pass = @opts["jabber"].split(":", 2) @opts["jabber"].replace("jabber=#{jid}:********") if jabber_bot_id begin require "xmpp4r-simple" start_jabber(jid, pass) rescue LoadError log "Failed to start Jabber." log 'Installl "xmpp4r-simple" gem or check your id/pass.' finish end else @opts.delete("jabber") log "This gateway does not support Jabber bot." end end log "Client Options: #{@opts.inspect}" @log.info "Client Options: #{@opts.inspect}" @ratio = Struct.new(:timeline, :friends, :channel).new(*(@opts["ratio"] || "10:3:5").split(":").map {|ratio| ratio.to_f }) @footing = @ratio.inject {|r,i| r + i } @timeline = [] @check_friends_thread = Thread.start do loop do begin check_friends rescue ApiFailed => e @log.error e.inspect rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep freq(@ratio[:friends] / @footing) end end return if @opts["jabber"] @check_timeline_thread = Thread.start do sleep 3 loop do begin check_timeline # check_direct_messages rescue ApiFailed => e @log.error e.inspect rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep freq(@ratio[:timeline] / @footing) end end @check_channel_thread = Thread.start do sleep 5 Thread.abort_on_exception= true loop do begin check_channel # check_direct_messages rescue ApiFailed => e @log.error e.inspect rescue Exception => e @log.error e.inspect e.backtrace.each do |l| @log.error "\t#{l}" end end sleep freq(@ratio[:channel] / @footing) end end end def on_disconnected @check_friends_thread.kill rescue nil @check_timeline_thread.kill rescue nil @check_channel_thread.kill rescue nil @im_thread.kill rescue nil @im.disconnect rescue nil end def on_privmsg(m) return m.ctcps.each {|ctcp| on_ctcp(m[0], ctcp) } if m.ctcp? retry_count = 3 ret = nil target, message = *m.params begin if target =~ /^#(.+)/ channel = Regexp.last_match[1] reply = message[/\s+>(.+)$/, 1] reply = reply.force_encoding("UTF-8") if reply && reply.respond_to?(:force_encoding) if @utf7 message = Iconv.iconv("UTF-7", "UTF-8", message).join message = message.force_encoding("ASCII-8BIT") if message.respond_to?(:force_encoding) end if !reply && @opts.key?("alwaysim") && @im && @im.connected? # in jabber mode, using jabber post message = "##{channel} #{message}" unless "##{channel}" == main_channel ret = @im.deliver(jabber_bot_id, message) post "#{nick}!#{nick}@#{api_base.host}", TOPIC, channel, untinyurl(message) else if "##{channel}" == main_channel rid = rid_for(reply) if reply ret = api("statuses/update", {"status" => message, "reply_status_rid" => rid}) else ret = api("channel_message/update", {"name_en" => channel, "body" => message}) end log "Status Updated via API" end else # direct message ret = api("direct_messages/new", {"user" => target, "text" => message}) end raise ApiFailed, "API failed" unless ret rescue => e @log.error [retry_count, e.inspect].inspect if retry_count > 0 retry_count -= 1 @log.debug "Retry to setting status..." retry else log "Some Error Happened on Sending #{message}. #{e}" end end end def on_ctcp(target, message) _, command, *args = message.split(/\s+/) case command when "utf7" begin require "iconv" @utf7 = !@utf7 log "utf7 mode: #{@utf7 ? 'on' : 'off'}" rescue LoadError => e log "Can't load iconv." end when "list" nick = args[0] @log.debug([ nick, message ]) res = api("statuses/user_timeline", { "id" => nick }).reverse_each do |s| @log.debug(s) post nick, NOTICE, main_channel, "#{generate_status_message(s)}" end unless res post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" end when "fav" target = args[0] st = @tmap[target] id = rid_for(target) if st || id unless id if @im && @im.connected? # IM のときはいろいろめんどうなことする nick, count = *st pos = @counters[nick] - count @log.debug "%p %s %d/%d => %d" % [ st, nick, count, @counters[nick], pos ] res = api("statuses/user_timeline", { "id" => nick }) raise ApiFailed, "#{nick} may be private mode" if res.empty? if res[pos] id = res[pos]["rid"] else raise ApiFailed, "#{pos} of #{nick} is not found." end else id = st["id"] || st["rid"] end end res = api("favorites/create/#{id}", {}) post server_name, NOTICE, main_channel, "Fav: #{res["screen_name"]}: #{res["text"]}" else post server_name, NOTICE, main_channel, "No such id or status #{target}" end when "link" tid = args[0] st = @tmap[tid] if st st["link"] = (api_base + "/#{st["user"]["screen_name"]}/statuses/#{st["id"]}").to_s unless st["link"] post server_name, NOTICE, main_channel, st["link"] else post server_name, NOTICE, main_channel, "No such id #{tid}" end end rescue ApiFailed => e log e.inspect end; private :on_ctcp def on_whois(m) nick = m.params[0] f = (@friends || []).find {|i| i["screen_name"] == nick } if f post server_name, RPL_WHOISUSER, @nick, nick, nick, api_base.host, "*", "#{f["name"]} / #{f["description"]}" post server_name, RPL_WHOISSERVER, @nick, nick, api_base.host, api_base.to_s post server_name, RPL_WHOISIDLE, @nick, nick, "0", "seconds idle" post server_name, RPL_ENDOFWHOIS, @nick, nick, "End of WHOIS list" else post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" end end def on_join(m) channels = m.params[0].split(/\s*,\s*/) channels.each do |channel| next if channel == main_channel res = api("channel/exists", { "name_en" => channel.sub(/^#/, "") }) if res["exists"] @channels[channel] = { :read => [] } post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, channel else post server_name, ERR_NOSUCHNICK, channel, "No such nick/channel" end end end def on_part(m) channel = m.params[0] return if channel == main_channel @channels.delete(channel) post "#{@nick}!#{@nick}@#{api_base.host}", PART, channel end def on_who(m) channel = m.params[0] case when channel == main_channel # " # ( "H" / "G" > ["*"] [ ( "@" / "+" ) ] # : " @friends.each do |f| user = nick = f["screen_name"] host = serv = api_base.host real = f["name"] post server_name, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}" end post server_name, RPL_ENDOFWHO, @nick, channel when @groups.key?(channel) @groups[channel].each do |name| f = @friends.find {|i| i["screen_name"] == name } user = nick = f["screen_name"] host = serv = api_base.host real = f["name"] post server_name, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}" end post server_name, RPL_ENDOFWHO, @nick, channel else post server_name, ERR_NOSUCHNICK, @nick, nick, "No such nick/channel" end end private def check_timeline @prev_time ||= Time.at(0) api("statuses/friends_timeline", {"since" => @prev_time.httpdate}).reverse_each do |s| id = s["id"] || s["rid"] next if id.nil? || @timeline.include?(id) @timeline << id nick = s["user_login_id"] mesg = generate_status_message(s) tid = @tmap.push(s) @log.debug [id, nick, mesg] if nick == @nick # 自分のときは topic に post "#{nick}!#{nick}@#{api_base.host}", TOPIC, main_channel, untinyurl(mesg) else if @opts["tid"] message(nick, main_channel, "%s \x03%s [%s]" % [mesg, @opts["tid"], tid]) else message(nick, main_channel, "%s" % [mesg, tid]) end end end @log.debug "@timeline.size = #{@timeline.size}" @timeline = @timeline.last(100) @prev_time = Time.now end def check_channel @channels.keys.each do |channel| @log.debug "getting channel -> #{channel}..." api("channel_message/list", { "name_en" => channel.sub(/^#/, "") }).reverse_each do |s| begin id = Digest::MD5.hexdigest(s["user"]["login_id"] + s["body"]) next if @channels[channel][:read].include?(id) @channels[channel][:read] << id nick = s["user"]["login_id"] mesg = s["body"] if nick == @nick post nick, NOTICE, channel, mesg else message(nick, channel, mesg) end rescue Execepton => e post server_name, NOTICE, channel, e.inspect end end @channels[channel][:read] = @channels[channel][:read].last(100) end end def generate_status_message(status) s = status mesg = s["text"] @log.debug(mesg) begin require 'iconv' mesg = mesg.sub(/^.+ > |^.+/) {|str| Iconv.iconv("UTF-8", "UTF-7", str).join } mesg = "[utf7]: #{mesg}" if mesg =~ /[^a-z0-9\s]/i rescue LoadError rescue Iconv::IllegalSequence end # added @user in no use @user reply message (Wassr only) if s.has_key?("reply_status_url") and s["reply_status_url"] and s["text"] !~ /^@.*/ and %r{([^/]+)/statuses/[^/]+}.match(s["reply_status_url"]) reply_user_id = $1 mesg = "@#{reply_user_id} #{mesg}" end # display area name (Wassr only) if s.has_key?("areaname") and s["areaname"] mesg += " L: #{s["areaname"]}" end # display photo URL (Wassr only) if s.has_key?("photo_url") and s["photo_url"] mesg += " #{s["photo_url"]}" end # time = Time.parse(s["created_at"]) rescue Time.now m = { """ => "\"", "<"=> "<", ">"=> ">", "&"=> "&", "\n" => " "} mesg.gsub!(/(#{m.keys.join("|")})/) { m[$1] } mesg end def check_direct_messages @prev_time_d ||= Time.now api("direct_messages", {"since" => @prev_time_d.httpdate}).reverse_each do |s| nick = s["sender_screen_name"] mesg = s["text"] time = Time.parse(s["created_at"]) @log.debug [nick, mesg, time].inspect message(nick, @nick, mesg) end @prev_time_d = Time.now end def check_friends first = true unless @friends @friends ||= [] friends = [] 1.upto(5) do |i| f = api("statuses/friends", {"page" => i.to_s}) friends += f break if f.length < 100 end if first && !@opts.key?("athack") @friends = friends post server_name, RPL_NAMREPLY, @nick, "=", main_channel, @friends.map{|i| "@#{i["screen_name"]}" }.join(" ") post server_name, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list" else prv_friends = @friends.map {|i| i["screen_name"] } now_friends = friends.map {|i| i["screen_name"] } # Twitter API bug? return if !first && (now_friends.length - prv_friends.length).abs > 10 (now_friends - prv_friends).each do |join| join = "@#{join}" if @opts.key?("athack") post "#{join}!#{join}@#{api_base.host}", JOIN, main_channel end (prv_friends - now_friends).each do |part| part = "@#{part}" if @opts.key?("athack") post "#{part}!#{part}@#{api_base.host}", PART, main_channel, "" end @friends = friends end end def freq(ratio) ret = 3600 / (hourly_limit * ratio).round @log.debug "Frequency: #{ret}" ret end def start_jabber(jid, pass) @log.info "Logging-in with #{jid} -> jabber_bot_id: #{jabber_bot_id}" @im = Jabber::Simple.new(jid, pass) @im.add(jabber_bot_id) @im_thread = Thread.start do loop do begin @im.received_messages.each do |msg| @log.debug [msg.from, msg.body] if msg.from.strip == jabber_bot_id # Wassr -> 'nick(id): msg' body = msg.body.sub(/^(.+?)(?:\((.+?)\))?: /, "") if Regexp.last_match nick, id = Regexp.last_match.captures body = CGI.unescapeHTML(body) begin require 'iconv' body = body.sub(/^.+ > |^.+/) {|str| Iconv.iconv("UTF-8", "UTF-7", str).join } body = "[utf7]: #{body}" if body =~ /[^a-z0-9\s]/i rescue LoadError rescue Iconv::IllegalSequence end case when nick == "投稿完了" log "#{nick}: #{body}" when nick == "チャンネル投稿完了" log "#{nick}: #{body}" when body =~ /^#([a-z_]+)\s+(.+)$/i # channel message or not message(id || nick, "##{Regexp.last_match[1]}", Regexp.last_match[2]) when nick == "photo" && body =~ %r|^http://wassr\.jp/user/([^/]+)/| nick = Regexp.last_match[1] message(nick, main_channel, body) else @counters[nick] ||= 0 @counters[nick] += 1 tid = @tmap.push([nick, @counters[nick]]) message(nick, main_channel, "%s \x03%s [%s]" % [body, @opts["tid"], tid]) end end end end rescue Exception => e @log.error "Error on Jabber loop: #{e.inspect}" e.backtrace.each do |l| @log.error "\t#{l}" end end sleep 1 end end end def require_post?(path) [ "statuses/update", "direct_messages/new", "channel_message/update", %r|^favorites/create|, ].any? {|i| i === path } end def api(path, q={}) ret = {} q["source"] ||= api_source uri = api_base.dup uri.path = "/#{path}.json" uri.query = q.inject([]) {|r,(k,v)| v ? r << "#{k}=#{URI.escape(v, /[^-.!~*'()\w]/n)}" : r }.join("&") req = nil if require_post?(path) req = Net::HTTP::Post.new(uri.path) req.body = uri.query else req = Net::HTTP::Get.new(uri.request_uri) end req.basic_auth(@real, @pass) req["User-Agent"] = @user_agent req["If-Modified-Since"] = q["since"] if q.key?("since") @log.debug uri.inspect ret = Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) } case ret when Net::HTTPOK # 200 ret = JSON.parse(ret.body) raise ApiFailed, "Server Returned Error: #{ret["error"]}" if ret.kind_of?(Hash) && ret["error"] ret when Net::HTTPNotModified # 304 [] when Net::HTTPBadRequest # 400 # exceeded the rate limitation raise ApiFailed, "#{ret.code}: #{ret.message}" else raise ApiFailed, "Server Returned #{ret.code} #{ret.message}" end rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e raise ApiFailed, e.inspect end def message(sender, target, str) str = untinyurl(str) sender = "#{sender}!#{sender}@#{api_base.host}" post sender, PRIVMSG, target, str end def log(str) str.gsub!(/\n/, " ") post server_name, NOTICE, main_channel, str end def untinyurl(text) text.gsub(%r|http://(preview\.)?tinyurl\.com/[0-9a-z=]+|i) {|m| uri = URI(m) uri.host = uri.host.sub($1, "") if $1 expanded = Net::HTTP.start(uri.host, uri.port) {|http| http.open_timeout = 3 begin http.head(uri.request_uri, { "User-Agent" => @user_agent })["Location"] || m rescue Timeout::Error m end } expanded = URI(expanded) if %w|http https|.include? expanded.scheme expanded.to_s else "#{expanded.scheme}: #{uri}" end } end # return rid of most recent matched status with text def rid_for(text) target = Regexp.new(Regexp.quote(text.strip), "i") status = api("statuses/friends_timeline").find {|i| next false if i["user_login_id"] == @nick # 自分は除外 i["text"] =~ target } @log.debug "Looking up status contains #{text.inspect} -> #{status.inspect}" status ? status["rid"] : nil end class TypableMap < Hash Roman = %w[ k g ky gy s z sh j t d ch n ny h b p hy by py m my y r ry w v q ].unshift("").map do |consonant| case consonant when "y", /\A.{2}/ then %w|a u o| when "q" then %w|a i e o| else %w|a i u e o| end.map {|vowel| "#{consonant}#{vowel}" } end.flatten def initialize(size = 1) @seq = Roman @n = 0 @size = size end def generate(n) ret = [] begin n, r = n.divmod(@seq.size) ret << @seq[r] end while n > 0 ret.reverse.join end def push(obj) id = generate(@n) self[id] = obj @n += 1 @n %= @seq.size ** @size id end alias << push def clear @n = 0 super end private :[]= undef update, merge, merge!, replace end end if __FILE__ == $0 require "optparse" opts = { :port => 16670, :host => "localhost", :log => nil, :debug => false, :foreground => false, } OptionParser.new do |parser| parser.instance_eval do self.banner = <<-EOB.gsub(/^\t+/, "") Usage: #{$0} [opts] EOB separator "" separator "Options:" on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port| opts[:port] = port end on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host| opts[:host] = host end on("-l", "--log LOG", "log file") do |log| opts[:log] = log end on("--debug", "Enable debug mode") do |debug| opts[:log] = $stdout opts[:debug] = true end on("-f", "--foreground", "run foreground") do |foreground| opts[:log] = $stdout opts[:foreground] = true end on("-n", "--name [user name or email address]") do |name| opts[:name] = name end parse!(ARGV) end end opts[:logger] = Logger.new(opts[:log], "daily") opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO # def daemonize(foreground=false) # trap("SIGINT") { exit! 0 } # trap("SIGTERM") { exit! 0 } # trap("SIGHUP") { exit! 0 } # return yield if $DEBUG || foreground # Process.fork do # Process.setsid # Dir.chdir "/" # File.open("/dev/null") {|f| # STDIN.reopen f # STDOUT.reopen f # STDERR.reopen f # } # yield # end # exit! 0 # end # daemonize(opts[:debug] || opts[:foreground]) do Net::IRC::Server.new(opts[:host], opts[:port], WassrIrcGateway, opts).start # end end # Local Variables: # coding: utf-8 # End: net-irc-0.0.9/examples/gmail.rb0000755000175000017500000000760611604616526016174 0ustar uwabamiuwabami#!/usr/bin/env ruby # vim:encoding=UTF-8: $LOAD_PATH << "lib" $LOAD_PATH << "../lib" $KCODE = "u" unless defined? ::Encoding require "rubygems" require "net/irc" require "sdbm" require "tmpdir" require "uri" require "mechanize" require "rexml/document" class GmailNotifier < Net::IRC::Server::Session def server_name "gmail" end def server_version "0.0.0" end def main_channel "#gmail" end def initialize(*args) super @agent = WWW::Mechanize.new end def on_user(m) super post @prefix, JOIN, main_channel post server_name, MODE, main_channel, "+o", @prefix.nick @real, *@opts = @opts.name || @real.split(/\s+/) @opts ||= [] start_observer end def on_disconnected @observer.kill rescue nil end def on_privmsg(m) super case m[1] when 'list' check_mail end end def on_ctcp(target, message) end def on_whois(m) end def on_who(m) end def on_join(m) end def on_part(m) end private def start_observer @observer = Thread.start do Thread.abort_on_exception = true loop do begin @agent.auth(@real, @pass) page = @agent.get(URI.parse("https://gmail.google.com/gmail/feed/atom")) feed = REXML::Document.new page.body db = SDBM.open("#{Dir.tmpdir}/#{@real}.db", 0666) feed.get_elements('/feed/entry').reverse.each do |item| id = item.text('id') if db.include?(id) next else db[id] = "1" end post server_name, PRIVMSG, main_channel, "Subject: #{item.text('title')} From: #{item.text('author/name')}" post server_name, PRIVMSG, main_channel, "#{item.text('summary')}" end rescue Exception => e @log.error e.inspect ensure db.close rescue nil end sleep 60 * 5 end end end def check_mail begin @agent.auth(@real, @pass) page = @agent.get(URI.parse("https://gmail.google.com/gmail/feed/atom")) feed = REXML::Document.new page.body db = SDBM.open("#{Dir.tmpdir}/#{@real}.db", 0666) feed.get_elements('/feed/entry').reverse.each do |item| id = item.text('id') if db.include?(id) #next else db[id] = "1" end post server_name, PRIVMSG, main_channel, "Subject: #{item.text('title')} From: #{item.text('author/name')}" post server_name, PRIVMSG, main_channel, "#{item.text('summary')}" end rescue Exception => e @log.error e.inspect ensure db.close rescue nil end end end if __FILE__ == $0 require "optparse" opts = { :port => 16800, :host => "localhost", :log => nil, :debug => false, :foreground => false, } OptionParser.new do |parser| parser.instance_eval do self.banner = <<-EOB.gsub(/^\t+/, "") Usage: #{$0} [opts] EOB separator "" separator "Options:" on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port| opts[:port] = port end on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host| opts[:host] = host end on("-l", "--log LOG", "log file") do |log| opts[:log] = log end on("--debug", "Enable debug mode") do |debug| opts[:log] = $stdout opts[:debug] = true end on("-f", "--foreground", "run foreground") do |foreground| opts[:log] = $stdout opts[:foreground] = true end parse!(ARGV) end end opts[:logger] = Logger.new(opts[:log], "daily") opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO def daemonize(foreground=false) trap("SIGINT") { exit! 0 } trap("SIGTERM") { exit! 0 } trap("SIGHUP") { exit! 0 } return yield if $DEBUG || foreground Process.fork do Process.setsid Dir.chdir "/" File.open("/dev/null") {|f| STDIN.reopen f STDOUT.reopen f STDERR.reopen f } yield end exit! 0 end daemonize(opts[:debug] || opts[:foreground]) do Net::IRC::Server.new(opts[:host], opts[:port], GmailNotifier, opts).start end end # Local Variables: # coding: utf-8 # End: net-irc-0.0.9/README0000644000175000017500000000317511604616526013612 0ustar uwabamiuwabami = net-irc == Description IRC library. This is mostly conform to RFC 1459 but partly not for convenience. == Installation === Archive Installation rake install === Gem Installation gem install net-irc == Features/Problems * IRC client (for bot) * IRC server (for gateway to webservices) == Synopsis === Client require "net/irc" class SimpleClient < Net::IRC::Client def on_privmsg(m) super channel, message = *m if message =~ /Hello/ post NOTICE, channel, "Hello!" end end end Net::IRC::Client manages channel status and the information is set in @channels. So, be careful to use @channels instance variable and call super surely. === Server see example/tig.rb == IRC Gateways There are some gateways connecting to webservices. * Twitter * Wassr * Hatena Haiku * Hatena Star If you want to run it, type following: $ cd `ruby -rubygems -e 'print Gem.searcher.find("net/irc").full_gem_path+"/examples"'` Twitter: $ ./tig.rb -f >> /dev/null 2>&1 Wassr: $ ./wig.rb Run as daemon in default. If you want to help: $ ./tig.rb --help Usage: tig.rb [opts] Options: -p, --port [PORT=16668] port number to listen -h, --host [HOST=localhost] host name or IP address to listen -l, --log LOG log file --debug Enable debug mode -f, --foreground run foreground -n [user name or email address] --name == Copyright This library is based on RICE written by akira yamada. Author:: cho45 Copyright:: Copyright (c) 2008-2009 cho45 License:: Ruby's net-irc-0.0.9/ChangeLog0000644000175000017500000000324211604616526014477 0ustar uwabamiuwabami2009-10-11 SATOH Hiroh * [new] Implemented Server#sessions which returns all sessions connected to the server. * Released 0.0.9 2009-08-08 SATOH Hiroh * [bug]: Fixed to work on ruby1.9.1 (now can send iso-2022-jp) * [new] Implemented Message#ctcps returns embedded all ctcp messages (drry). * Released 0.0.8 2009-02-19 SATOH Hiroh * [bug]: Fixed net/irc.rb permission. * Released 0.0.7 2009-02-01 SATOH Hiroh * [bug]: Fixed to work on ruby1.9.1 * [release]: Released 0.0.6 2008-07-06 SATOH Hiroh * [interface]: Removed around @channels and separeted to Net::IRC::Client::ChannelManager as just a sample of managing channels. * [release]: Released 0.0.5 2008-07-06 Satoshi Nakagawa * [new]: Added a mode parser which can be configured automatically from 005 replies. 2008-06-28 cho45 * [interface]: Change mode character to symbol. * [new]: Seperate each class to some files. * [release]: Released 0.0.4 2008-06-14 cho45 * [bug]: Fixed examples. (twitter, wassr, lingr gateways) * [release]: Released 0.0.3 2008-02-06 cho45 * [release] @5832: Released 0.0.2 2008-02-01 cho45 * [bug] @5986: Fixed to destroy closed stream. 2008-01-31 cho45 * [new] @5939: Added client example. * [new] @5929: Updated tests. Made allow lame prefix in RPL_WELCOME (like freenode) 2008-01-29 cho45 * [bug] @5846: athack つかわないときの処理がもろに間違ってた。 * [bug] @5843: Net::IRC::Server の修正に追従できていなかった * [release] @5832: Released 0.0.1 net-irc-0.0.9/spec/0000775000175000017500000000000011604616526013660 5ustar uwabamiuwabaminet-irc-0.0.9/spec/modeparser_spec.rb0000644000175000017500000001427211604616526017364 0ustar uwabamiuwabami $LOAD_PATH << "lib" $LOAD_PATH << "../lib" require "net/irc" include Net::IRC include Constants describe Message::ModeParser do it "should parse RFC2812+ correctly" do parser = Message::ModeParser.new parser.parse("#Finish +im")[:positive].should == [[:i, nil], [:m, nil]] parser.parse("#Finish +o Kilroy")[:positive].should == [[:o, "Kilroy"]] parser.parse("#Finish +v Kilroy")[:positive].should == [[:v, "Kilroy"]] parser.parse("#Fins -s")[:negative].should == [[:s, nil]] parser.parse("#42 +k oulu")[:positive].should == [[:k, "oulu"]] parser.parse("#eu-opers +l 10")[:positive].should == [[:l, "10"]] parser.parse("&oulu +b")[:positive].should == [[:b, nil]] parser.parse("&oulu +b *!*@*")[:positive].should == [[:b, "*!*@*"]] parser.parse("&oulu +b *!*@*.edu")[:positive].should == [[:b, "*!*@*.edu"]] parser.parse("#oulu +e")[:positive].should == [[:e, nil]] parser.parse("#oulu +e *!*@*.edu")[:positive].should == [[:e, "*!*@*.edu"]] parser.parse("#oulu +I")[:positive].should == [[:I, nil]] parser.parse("#oulu +I *!*@*.edu")[:positive].should == [[:I, "*!*@*.edu"]] parser.parse("#oulu +R")[:positive].should == [[:R, nil]] parser.parse("#oulu +R *!*@*.edu")[:positive].should == [[:R, "*!*@*.edu"]] parser.parse("#foo +ooo foo bar baz").should == { :positive => [[:o, "foo"], [:o, "bar"], [:o, "baz"]], :negative => [], } parser.parse("#foo +oo-o foo bar baz").should == { :positive => [[:o, "foo"], [:o, "bar"]], :negative => [[:o, "baz"]], } parser.parse("#foo -oo+o foo bar baz").should == { :positive => [[:o, "baz"]], :negative => [[:o, "foo"], [:o, "bar"]], } parser.parse("#foo +imv foo").should == { :positive => [[:i, nil], [:m, nil], [:v, "foo"]], :negative => [], } parser.parse("#foo +lk 10 foo").should == { :positive => [[:l, "10"], [:k, "foo"]], :negative => [], } parser.parse("#foo -l+k foo").should == { :positive => [[:k, "foo"]], :negative => [[:l, nil]], } parser.parse("#foo +ao foo").should == { :positive => [[:a, nil], [:o, "foo"]], :negative => [], } end it "should parse modes of Hyperion ircd correctly" do parser = Message::ModeParser.new parser.set(:CHANMODES, 'bdeIq,k,lfJD,cgijLmnPQrRstz') parser.parse("#Finish +im")[:positive].should == [[:i, nil], [:m, nil]] parser.parse("#Finish +o Kilroy")[:positive].should == [[:o, "Kilroy"]] parser.parse("#Finish +v Kilroy")[:positive].should == [[:v, "Kilroy"]] parser.parse("#Fins -s")[:negative].should == [[:s, nil]] parser.parse("#42 +k oulu")[:positive].should == [[:k, "oulu"]] parser.parse("#eu-opers +l 10")[:positive].should == [[:l, "10"]] parser.parse("&oulu +b")[:positive].should == [[:b, nil]] parser.parse("&oulu +b *!*@*")[:positive].should == [[:b, "*!*@*"]] parser.parse("&oulu +b *!*@*.edu")[:positive].should == [[:b, "*!*@*.edu"]] parser.parse("#oulu +e")[:positive].should == [[:e, nil]] parser.parse("#oulu +e *!*@*.edu")[:positive].should == [[:e, "*!*@*.edu"]] parser.parse("#oulu +I")[:positive].should == [[:I, nil]] parser.parse("#oulu +I *!*@*.edu")[:positive].should == [[:I, "*!*@*.edu"]] parser.parse("#foo +ooo foo bar baz").should == { :positive => [[:o, "foo"], [:o, "bar"], [:o, "baz"]], :negative => [], } parser.parse("#foo +oo-o foo bar baz").should == { :positive => [[:o, "foo"], [:o, "bar"]], :negative => [[:o, "baz"]], } parser.parse("#foo -oo+o foo bar baz").should == { :positive => [[:o, "baz"]], :negative => [[:o, "foo"], [:o, "bar"]], } parser.parse("#foo +imv foo").should == { :positive => [[:i, nil], [:m, nil], [:v, "foo"]], :negative => [], } parser.parse("#foo +lk 10 foo").should == { :positive => [[:l, "10"], [:k, "foo"]], :negative => [], } parser.parse("#foo -l+k foo").should == { :positive => [[:k, "foo"]], :negative => [[:l, nil]], } parser.parse("#foo +cv foo").should == { :positive => [[:c, nil], [:v, "foo"]], :negative => [], } end it "should parse modes of Unreal ircd correctly" do parser = Message::ModeParser.new parser.set(:PREFIX, '(qaohv)~&@%+') parser.set(:CHANMODES, 'beI,kfL,lj,psmntirRcOAQKVCuzNSMTG') parser.parse("#Finish +im")[:positive].should == [[:i, nil], [:m, nil]] parser.parse("#Finish +o Kilroy")[:positive].should == [[:o, "Kilroy"]] parser.parse("#Finish +v Kilroy")[:positive].should == [[:v, "Kilroy"]] parser.parse("#Fins -s")[:negative].should == [[:s, nil]] parser.parse("#42 +k oulu")[:positive].should == [[:k, "oulu"]] parser.parse("#eu-opers +l 10")[:positive].should == [[:l, "10"]] parser.parse("&oulu +b")[:positive].should == [[:b, nil]] parser.parse("&oulu +b *!*@*")[:positive].should == [[:b, "*!*@*"]] parser.parse("&oulu +b *!*@*.edu")[:positive].should == [[:b, "*!*@*.edu"]] parser.parse("#oulu +e")[:positive].should == [[:e, nil]] parser.parse("#oulu +e *!*@*.edu")[:positive].should == [[:e, "*!*@*.edu"]] parser.parse("#oulu +I")[:positive].should == [[:I, nil]] parser.parse("#oulu +I *!*@*.edu")[:positive].should == [[:I, "*!*@*.edu"]] parser.parse("#foo +ooo foo bar baz").should == { :positive => [[:o, "foo"], [:o, "bar"], [:o, "baz"]], :negative => [], } parser.parse("#foo +oo-o foo bar baz").should == { :positive => [[:o, "foo"], [:o, "bar"]], :negative => [[:o, "baz"]], } parser.parse("#foo -oo+o foo bar baz").should == { :positive => [[:o, "baz"]], :negative => [[:o, "foo"], [:o, "bar"]], } parser.parse("#foo +imv foo").should == { :positive => [[:i, nil], [:m, nil], [:v, "foo"]], :negative => [], } parser.parse("#foo +lk 10 foo").should == { :positive => [[:l, "10"], [:k, "foo"]], :negative => [], } parser.parse("#foo -l+k foo").should == { :positive => [[:k, "foo"]], :negative => [[:l, nil]], } parser.parse("#foo -q+ah foo bar baz").should == { :positive => [[:a, "bar"], [:h, "baz"]], :negative => [[:q, "foo"]], } parser.parse("#foo +Av foo").should == { :positive => [[:A, nil], [:v, "foo"]], :negative => [], } end end net-irc-0.0.9/spec/net-irc_spec.rb0000755000175000017500000002130611604616526016563 0ustar uwabamiuwabami#!spec # coding: ASCII-8BIT # vim:encoding=UTF-8: $LOAD_PATH << "lib" $LOAD_PATH << "../lib" require "rubygems" require "spec" require "net/irc" include Net::IRC include Constants describe Net::IRC::Message, "construct" do it "should generate message correctly" do m = Message.new("foo", "PRIVMSG", ["#channel", "message"]) m.to_s.should == ":foo PRIVMSG #channel message\r\n" m = Message.new("foo", "PRIVMSG", ["#channel", "message with space"]) m.to_s.should == ":foo PRIVMSG #channel :message with space\r\n" m = Message.new(nil, "PRIVMSG", ["#channel", "message"]) m.to_s.should == "PRIVMSG #channel message\r\n" m = Message.new(nil, "PRIVMSG", ["#channel", "message with space"]) m.to_s.should == "PRIVMSG #channel :message with space\r\n" m = Message.new(nil, "MODE", [ "#channel", "+ooo", "nick1", "nick2", "nick3" ]) m.to_s.should == "MODE #channel +ooo nick1 nick2 nick3\r\n" m = Message.new(nil, "KICK", [ "#channel,#channel1", "nick1,nick2", ]) m.to_s.should == "KICK #channel,#channel1 nick1,nick2\r\n" end it "should have ctcp? method" do m = Message.new("foo", "PRIVMSG", ["#channel", "\x01ACTION foo\x01"]) m.ctcp?.should be_true end it "should behave as Array contains params" do m = Message.new("foo", "PRIVMSG", ["#channel", "message"]) m[0].should == m.params[0] m[1].should == m.params[1] m.to_a.should == ["#channel", "message"] channel, message = *m channel.should == "#channel" message.should == "message" end it "#to_a should return duplicated array" do m = Message.new("foo", "PRIVMSG", ["#channel", "message"]) m[0].should == m.params[0] m[1].should == m.params[1] m.to_a.should == ["#channel", "message"] m.to_a.clear m.to_a.should == ["#channel", "message"] end end describe Net::IRC::Message, "parse" do it "should parse correctly following RFC." do m = Message.parse("PRIVMSG #channel message\r\n") m.prefix.should == "" m.command.should == "PRIVMSG" m.params.should == ["#channel", "message"] m = Message.parse("PRIVMSG #channel :message leading :\r\n") m.prefix.should == "" m.command.should == "PRIVMSG" m.params.should == ["#channel", "message leading :"] m = Message.parse("PRIVMSG #channel middle :message leading :\r\n") m.prefix.should == "" m.command.should == "PRIVMSG" m.params.should == ["#channel", "middle", "message leading :"] m = Message.parse("PRIVMSG #channel middle message with middle\r\n") m.prefix.should == "" m.command.should == "PRIVMSG" m.params.should == ["#channel", "middle", "message", "with", "middle"] m = Message.parse(":prefix PRIVMSG #channel message\r\n") m.prefix.should == "prefix" m.command.should == "PRIVMSG" m.params.should == ["#channel", "message"] m = Message.parse(":prefix PRIVMSG #channel :message leading :\r\n") m.prefix.should == "prefix" m.command.should == "PRIVMSG" m.params.should == ["#channel", "message leading :"] end it "should allow multibyte " do m = Message.parse(":てすと PRIVMSG #channel :message leading :\r\n") m.prefix.should == "てすと" m.command.should == "PRIVMSG" m.params.should == ["#channel", "message leading :"] end it "should allow space at end" do m = Message.parse("JOIN #foobar \r\n") m.prefix.should == "" m.command.should == "JOIN" m.params.should == ["#foobar"] end end describe Net::IRC::Constants, "lookup" do it "should lookup numeric replies from Net::IRC::COMMANDS" do welcome = Net::IRC::Constants.const_get("RPL_WELCOME") welcome.should == "001" Net::IRC::COMMANDS[welcome].should == "RPL_WELCOME" end end describe Net::IRC::Prefix, "" do it "should be kind of String" do Prefix.new("").should be_kind_of(String) end it "should parse prefix correctly." do prefix = Prefix.new("foo!bar@localhost") prefix.extract.should == ["foo", "bar", "localhost"] prefix = Prefix.new("foo!-bar@localhost") prefix.extract.should == ["foo", "-bar", "localhost"] prefix = Prefix.new("foo!+bar@localhost") prefix.extract.should == ["foo", "+bar", "localhost"] prefix = Prefix.new("foo!~bar@localhost") prefix.extract.should == ["foo", "~bar", "localhost"] end it "should allow multibyte in nick." do prefix = Prefix.new("あああ!~bar@localhost") prefix.extract.should == ["あああ", "~bar", "localhost"] end it "should allow lame prefix." do prefix = Prefix.new("nick") prefix.extract.should == ["nick", nil, nil] end it "has nick method" do prefix = Prefix.new("foo!bar@localhost") prefix.nick.should == "foo" end it "has user method" do prefix = Prefix.new("foo!bar@localhost") prefix.user.should == "bar" end it "has host method" do prefix = Prefix.new("foo!bar@localhost") prefix.host.should == "localhost" end end describe Net::IRC, "utilities" do it "has ctcp_encode method" do message = ctcp_encode "ACTION hehe" message.should == "\x01ACTION hehe\x01" message = ctcp_encode "ACTION \x01 \x5c " message.should == "\x01ACTION \x5c\x61 \x5c\x5c \x01" message = ctcp_encode "ACTION \x00 \x0a \x0d \x10 " message.should == "\x01ACTION \x100 \x10n \x10r \x10\x10 \x01" end it "has ctcp_decode method" do message = ctcp_decode "\x01ACTION hehe\x01" message.should == "ACTION hehe" message = ctcp_decode "\x01ACTION \x5c\x61 \x5c\x5c \x01" message.should == "ACTION \x01 \x5c " message = ctcp_decode "\x01ACTION \x100 \x10n \x10r \x10\x10 \x01" message.should == "ACTION \x00 \x0a \x0d \x10 " end end class TestServerSession < Net::IRC::Server::Session @@testq = SizedQueue.new(1) @@instance = nil def self.testq @@testq end def self.instance @@instance end def initialize(*args) super @@instance = self end def on_message(m) @@testq << m end end class TestClient < Net::IRC::Client @@testq = SizedQueue.new(1) def self.testq @@testq end def on_message(m) @@testq << m end end describe Net::IRC, "server and client" do before :all do @port = nil @server, @client = nil, nil Thread.abort_on_exception = true @tserver = Thread.start do @server = Net::IRC::Server.new("localhost", @port, TestServerSession, { :logger => Logger.new(nil), }) @server.start end Thread.pass true until @server.instance_variable_get(:@serv) @port = @server.instance_variable_get(:@serv).addr[1] @tclient = Thread.start do @client = TestClient.new("localhost", @port, { :nick => "foonick", :user => "foouser", :real => "foo real name", :pass => "foopass", :logger => Logger.new(nil), }) @client.start end Thread.pass true until @client end server_q = TestServerSession.testq client_q = TestClient.testq it "client should send pass/nick/user sequence." do server_q.pop.to_s.should == "PASS foopass\r\n" server_q.pop.to_s.should == "NICK foonick\r\n" server_q.pop.to_s.should == "USER foouser 0 * :foo real name\r\n" end it "server should send 001,002,003 numeric replies." do client_q.pop.to_s.should match(/^:net-irc 001 foonick :Welcome to the Internet Relay Network \S+!\S+@\S+/) client_q.pop.to_s.should match(/^:net-irc 002 foonick :Your host is .+?, running version /) client_q.pop.to_s.should match(/^:net-irc 003 foonick :This server was created /) end it "client posts PRIVMSG and server receives it." do @client.instance_eval do post PRIVMSG, "#channel", "message a b c" end message = server_q.pop message.should be_a_kind_of(Net::IRC::Message) message.to_s.should == "PRIVMSG #channel :message a b c\r\n" end if defined? Encoding it "dummy encoding: client posts PRIVMSG and server receives it." do @client.instance_eval do s = "てすと".force_encoding("UTF-8") post PRIVMSG, "#channel", s end message = server_q.pop message.should be_a_kind_of(Net::IRC::Message) message.to_s.should == "PRIVMSG #channel てすと\r\n" end it "dummy encoding: client posts PRIVMSG and server receives it." do @client.instance_eval do s = "てすと".force_encoding("UTF-8") s.encode!("ISO-2022-JP") post PRIVMSG, "#channel", s end message = server_q.pop message.should be_a_kind_of(Net::IRC::Message) message.to_s.should == "PRIVMSG #channel \e$B$F$9$H\e(B\r\n" end end it "should allow lame RPL_WELCOME (not prefix but nick)" do client = @client TestServerSession.instance.instance_eval do Thread.exclusive do post "server", RPL_WELCOME, client.prefix.nick, "Welcome to the Internet Relay Network #{client.prefix.nick}" post nil, NOTICE, "#test", "sep1" end end Thread.pass true until client_q.pop.to_s == "NOTICE #test sep1\r\n" client.prefix.should == "foonick" end # it "should destroy closed session" do # end after :all do @server.finish @client.finish @tserver.kill @tclient.kill @server = @client = @tserver = @tclient = nil end end net-irc-0.0.9/spec/spec.opts0000644000175000017500000000001011604616526015506 0ustar uwabamiuwabami--color net-irc-0.0.9/spec/channel_manager_spec.rb0000755000175000017500000001217511604616526020330 0ustar uwabamiuwabami#!spec $LOAD_PATH << "lib" $LOAD_PATH << "../lib" require "rubygems" require "spec" require "thread" require "net/irc" require "net/irc/client/channel_manager" include Net::IRC include Constants class ChannelManagerTestServerSession < Net::IRC::Server::Session @@testq = SizedQueue.new(1) @@instance = nil def self.testq @@testq end def self.instance @@instance end def initialize(*args) super @@instance = self end def on_message(m) @@testq << m end end class ChannelManagerTestClient < Net::IRC::Client include Net::IRC::Client::ChannelManager @@testq = SizedQueue.new(1) def self.testq @@testq end def on_message(m) @@testq << m end end describe Net::IRC, "server and client" do before :all do @port = nil @server, @client = nil, nil Thread.abort_on_exception = true Thread.start do @server = Net::IRC::Server.new("localhost", @port, ChannelManagerTestServerSession, { :logger => Logger.new(nil), }) @server.start end Thread.pass true until @server.instance_variable_get(:@serv) @port = @server.instance_variable_get(:@serv).addr[1] Thread.start do @client = ChannelManagerTestClient.new("localhost", @port, { :nick => "foonick", :user => "foouser", :real => "foo real name", :pass => "foopass", :logger => Logger.new(nil), }) @client.start end Thread.pass true until @client end server_q = ChannelManagerTestServerSession.testq client_q = ChannelManagerTestClient.testq it "client should manage channel mode/users correctly" do client = @client client.instance_variable_set(:@prefix, Prefix.new("foonick!foouser@localhost")) true until ChannelManagerTestServerSession.instance ChannelManagerTestServerSession.instance.instance_eval do Thread.exclusive do post client.prefix, JOIN, "#test" post nil, NOTICE, "#test", "sep1" end end true until client_q.pop.to_s == "NOTICE #test sep1\r\n" c = @client.instance_variable_get(:@channels) c.synchronize do c.should be_a_kind_of(Hash) c["#test"].should be_a_kind_of(Hash) c["#test"][:modes].should be_a_kind_of(Array) c["#test"][:users].should be_a_kind_of(Array) c["#test"][:users].should == ["foonick"] end ChannelManagerTestServerSession.instance.instance_eval do Thread.exclusive do post "test1!test@localhost", JOIN, "#test" post "test2!test@localhost", JOIN, "#test" post nil, NOTICE, "#test", "sep2" end end true until client_q.pop.to_s == "NOTICE #test sep2\r\n" c.synchronize do c["#test"][:users].should == ["foonick", "test1", "test2"] end ChannelManagerTestServerSession.instance.instance_eval do Thread.exclusive do post nil, RPL_NAMREPLY, client.prefix.nick, "@", "#test", "foo1 foo2 foo3 @foo4 +foo5" post nil, NOTICE, "#test", "sep3" end end true until client_q.pop.to_s == "NOTICE #test sep3\r\n" c.synchronize do c["#test"][:users].should == ["foonick", "test1", "test2", "foo1", "foo2", "foo3", "foo4", "foo5"] c["#test"][:modes].should include([:s, nil]) c["#test"][:modes].should include([:o, "foo4"]) c["#test"][:modes].should include([:v, "foo5"]) end ChannelManagerTestServerSession.instance.instance_eval do Thread.exclusive do post nil, RPL_NAMREPLY, client.prefix.nick, "@", "#test1", "foo1 foo2 foo3 @foo4 +foo5" post "foo4!foo@localhost", QUIT, "message" post "foo5!foo@localhost", PART, "#test1", "message" post client.prefix, KICK, "#test", "foo1", "message" post client.prefix, MODE, "#test", "+o", "foo2" post nil, NOTICE, "#test", "sep4" end end true until client_q.pop.to_s == "NOTICE #test sep4\r\n" c.synchronize do c["#test"][:users].should == ["foonick", "test1", "test2", "foo2", "foo3", "foo5"] c["#test1"][:users].should == ["foo1", "foo2", "foo3"] c["#test"][:modes].should_not include([:o, "foo4"]) c["#test"][:modes].should include([:v, "foo5"]) c["#test1"][:modes].should_not include([:v, "foo5"]) c["#test"][:modes].should include([:o, "foo2"]) end ChannelManagerTestServerSession.instance.instance_eval do Thread.exclusive do post "foonick!test@localhost", NICK, "foonick2" post "foonick2!test@localhost", NICK, "foonick" post "foo2!test@localhost", NICK, "bar2" post "foo3!test@localhost", NICK, "bar3" post nil, NOTICE, "#test", "sep5" end end true until client_q.pop.to_s == "NOTICE #test sep5\r\n" c.synchronize do c["#test"][:users].should == ["foonick", "test1", "test2", "bar2", "bar3", "foo5"] c["#test1"][:users].should == ["foo1", "bar2", "bar3"] c["#test"][:modes].should_not include([:o, "foo4"]) c["#test"][:modes].should include([:v, "foo5"]) c["#test1"][:modes].should_not include([:v, "foo5"]) c["#test"][:modes].should_not include([:o, "foo2"]) c["#test"][:modes].should include([:o, "bar2"]) end end after :all do @server.finish @client.finish end end net-irc-0.0.9/metadata.yml0000664000175000017500000000351711604616526015237 0ustar uwabamiuwabami--- !ruby/object:Gem::Specification name: net-irc version: !ruby/object:Gem::Version version: 0.0.9 platform: ruby authors: - cho45 autorequire: "" bindir: bin cert_chain: [] date: 2009-10-11 00:00:00 +09:00 default_executable: dependencies: [] description: library for implementing IRC server and client email: cho45@lowreal.net executables: [] extensions: [] extra_rdoc_files: - README - ChangeLog files: - README - ChangeLog - Rakefile - spec/channel_manager_spec.rb - spec/modeparser_spec.rb - spec/net-irc_spec.rb - spec/spec.opts - lib/net/irc/client/channel_manager.rb - lib/net/irc/client.rb - lib/net/irc/constants.rb - lib/net/irc/message/modeparser.rb - lib/net/irc/message/serverconfig.rb - lib/net/irc/message.rb - lib/net/irc/pattern.rb - lib/net/irc/server.rb - lib/net/irc.rb - examples/2ch.rb - examples/2ig.rb - examples/client.rb - examples/echo_bot.rb - examples/gmail.rb - examples/hatena-star-stream.rb - examples/hig.rb - examples/iig.rb - examples/ircd.rb - examples/lig.rb - examples/lingr.rb - examples/mixi.rb - examples/sig.rb - examples/tig.rb - examples/wig.rb has_rdoc: true homepage: http://cho45.stfuawsc.com/net-irc/ licenses: [] post_install_message: rdoc_options: - --title - net-irc documentation - --charset - utf-8 - --opname - index.html - --line-numbers - --main - README - --inline-source - --exclude - ^(examples|extras)/ require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: "0" version: required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: "0" version: requirements: [] rubyforge_project: rubygems_version: 1.3.5 signing_key: specification_version: 3 summary: library for implementing IRC server and client test_files: [] net-irc-0.0.9/lib/0000775000175000017500000000000011604616526013474 5ustar uwabamiuwabaminet-irc-0.0.9/lib/net/0000775000175000017500000000000011604616526014262 5ustar uwabamiuwabaminet-irc-0.0.9/lib/net/irc/0000775000175000017500000000000011604616526015037 5ustar uwabamiuwabaminet-irc-0.0.9/lib/net/irc/pattern.rb0000644000175000017500000000537411604616526017050 0ustar uwabamiuwabami# coding: ASCII-8BIT module Net::IRC::PATTERN # :nodoc: # letter = %x41-5A / %x61-7A ; A-Z / a-z # digit = %x30-39 ; 0-9 # hexdigit = digit / "A" / "B" / "C" / "D" / "E" / "F" # special = %x5B-60 / %x7B-7D # ; "[", "]", "\", "`", "_", "^", "{", "|", "}" LETTER = 'A-Za-z' DIGIT = '0-9' HEXDIGIT = "#{DIGIT}A-Fa-f" SPECIAL = '\x5B-\x60\x7B-\x7D' # shortname = ( letter / digit ) *( letter / digit / "-" ) # *( letter / digit ) # ; as specified in RFC 1123 [HNAME] # hostname = shortname *( "." shortname ) SHORTNAME = "[#{LETTER}#{DIGIT}](?:[-#{LETTER}#{DIGIT}]*[#{LETTER}#{DIGIT}])?" HOSTNAME = "#{SHORTNAME}(?:\\.#{SHORTNAME})*" # servername = hostname SERVERNAME = HOSTNAME # nickname = ( letter / special ) *8( letter / digit / special / "-" ) #NICKNAME = "[#{LETTER}#{SPECIAL}#{DIGIT}_][-#{LETTER}#{DIGIT}#{SPECIAL}]*" NICKNAME = "\\S+" # for multibytes # user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF ) # ; any octet except NUL, CR, LF, " " and "@" USER = '[\x01-\x09\x0B-\x0C\x0E-\x1F\x21-\x3F\x41-\xFF]+' # ip4addr = 1*3digit "." 1*3digit "." 1*3digit "." 1*3digit IP4ADDR = "[#{DIGIT}]{1,3}(?:\\.[#{DIGIT}]{1,3}){3}" # ip6addr = 1*hexdigit 7( ":" 1*hexdigit ) # ip6addr =/ "0:0:0:0:0:" ( "0" / "FFFF" ) ":" ip4addr IP6ADDR = "(?:[#{HEXDIGIT}]+(?::[#{HEXDIGIT}]+){7}|0:0:0:0:0:(?:0|FFFF):#{IP4ADDR})" # hostaddr = ip4addr / ip6addr HOSTADDR = "(?:#{IP4ADDR}|#{IP6ADDR})" # host = hostname / hostaddr HOST = "(?:#{HOSTNAME}|#{HOSTADDR})" # prefix = servername / ( nickname [ [ "!" user ] "@" host ] ) PREFIX = "(?:#{NICKNAME}(?:(?:!#{USER})?@#{HOST})?|#{SERVERNAME})" # nospcrlfcl = %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF # ; any octet except NUL, CR, LF, " " and ":" NOSPCRLFCL = '\x01-\x09\x0B-\x0C\x0E-\x1F\x21-\x39\x3B-\xFF' # command = 1*letter / 3digit COMMAND = "(?:[#{LETTER}]+|[#{DIGIT}]{3})" # SPACE = %x20 ; space character # middle = nospcrlfcl *( ":" / nospcrlfcl ) # trailing = *( ":" / " " / nospcrlfcl ) # params = *14( SPACE middle ) [ SPACE ":" trailing ] # =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ] MIDDLE = "[#{NOSPCRLFCL}][:#{NOSPCRLFCL}]*" TRAILING = "[: #{NOSPCRLFCL}]*" PARAMS = "(?:((?: #{MIDDLE}){0,14})(?: :(#{TRAILING}))?|((?: #{MIDDLE}){14}):?(#{TRAILING}))" # crlf = %x0D %x0A ; "carriage return" "linefeed" # message = [ ":" prefix SPACE ] command [ params ] crlf CRLF = '\x0D\x0A' MESSAGE = "(?::(#{PREFIX}) )?(#{COMMAND})#{PARAMS}\s*#{CRLF}" CLIENT_PATTERN = /\A#{NICKNAME}(?:(?:!#{USER})?@#{HOST})\z/on MESSAGE_PATTERN = /\A#{MESSAGE}\z/on end # PATTERN net-irc-0.0.9/lib/net/irc/client.rb0000644000175000017500000000464711604616526016653 0ustar uwabamiuwabamiclass Net::IRC::Client include Net::IRC include Constants attr_reader :host, :port, :opts attr_reader :prefix, :channels def initialize(host, port, opts={}) @host = host @port = port @opts = OpenStruct.new(opts) @log = @opts.logger || Logger.new($stdout) @server_config = Message::ServerConfig.new @channels = { # "#channel" => { # :modes => [], # :users => [], # } } @channels.extend(MonitorMixin) end # Connect to server and start loop. def start # reset config @server_config = Message::ServerConfig.new @socket = TCPSocket.open(@host, @port) on_connected post PASS, @opts.pass if @opts.pass post NICK, @opts.nick post USER, @opts.user, "0", "*", @opts.real while l = @socket.gets begin @log.debug "RECEIVE: #{l.chomp}" m = Message.parse(l) next if on_message(m) === true name = "on_#{(COMMANDS[m.command.upcase] || m.command).downcase}" send(name, m) if respond_to?(name) rescue Exception => e warn e warn e.backtrace.join("\r\t") raise rescue Message::InvalidMessage @log.error "MessageParse: " + l.inspect end end rescue IOError ensure finish end # Close connection to server. def finish begin @socket.close rescue end on_disconnected end # Catch all messages. # If this method return true, aother callback will not be called. def on_message(m) end # Default RPL_WELCOME callback. # This sets @prefix from the message. def on_rpl_welcome(m) @prefix = Prefix.new(m[1][/\S+\z/]) end # Default RPL_ISUPPORT callback. # This detects server's configurations. def on_rpl_isupport(m) @server_config.set(m) end # Default PING callback. Response PONG. def on_ping(m) post PONG, @prefix ? @prefix.nick : "" end # Do nothing. # This is for avoiding error on calling super. # So you can always call super at subclass. def method_missing(name, *args) end # Call when socket connected. def on_connected end # Call when socket closed. def on_disconnected end private # Post message to server. # # include Net::IRC::Constants # post PRIVMSG, "#channel", "foobar" def post(command, *params) m = Message.new(nil, command, params.map {|s| if s s.force_encoding("ASCII-8BIT") if s.respond_to? :force_encoding #s.gsub(/\r\n|[\r\n]/, " ") s.tr("\r\n", " ") else "" end }) @log.debug "SEND: #{m.to_s.chomp}" @socket << m end end # Client net-irc-0.0.9/lib/net/irc/message.rb0000644000175000017500000000410611604616526017007 0ustar uwabamiuwabamiclass Net::IRC::Message include Net::IRC class InvalidMessage < Net::IRC::IRCException; end attr_reader :prefix, :command, :params # Parse string and return new Message. # If the string is invalid message, this method raises Net::IRC::Message::InvalidMessage. def self.parse(str) _, prefix, command, *rest = *PATTERN::MESSAGE_PATTERN.match(str) raise InvalidMessage, "Invalid message: #{str.dump}" unless _ case when rest[0] && !rest[0].empty? middle, trailer, = *rest when rest[2] && !rest[2].empty? middle, trailer, = *rest[2, 2] when rest[1] params = [] trailer = rest[1] when rest[3] params = [] trailer = rest[3] else params = [] end params ||= middle.split(/ /)[1..-1] params << trailer if trailer new(prefix, command, params) end def initialize(prefix, command, params) @prefix = Prefix.new(prefix.to_s) @command = command @params = params end # Same as @params[n]. def [](n) @params[n] end # Iterate params. def each(&block) @params.each(&block) end # Stringfy message to raw IRC message. def to_s str = "" str << ":#{@prefix} " unless @prefix.empty? str << @command if @params f = false @params.each do |param| f = !f && (param.empty? || param[0] == ?: || param.include?(" ")) str << " " str << ":" if f str << param end end str << "\x0D\x0A" str end alias to_str to_s # Same as params. def to_a @params.dup end # If the message is CTCP, return true. def ctcp? #message = @params[1] #message[0] == ?\01 && message[-1] == ?\01 /\x01(?>[^\x00\x01\r\n]*)\x01/ === @params[1] end def ctcps messages = [] @params[1].gsub!(/\x01(?>[^\x00\x01\r\n]*)\x01/) do messages << ctcp_decode($&) "" end messages end def inspect "#<%s:0x%x prefix:%s command:%s params:%s>" % [ self.class, self.object_id, @prefix, @command, @params.inspect ] end autoload :ModeParser, "net/irc/message/modeparser" autoload :ServerConfig, "net/irc/message/serverconfig" #autoload :ISupportModeParser, "net/irc/message/isupportmodeparser" end # Message net-irc-0.0.9/lib/net/irc/constants.rb0000644000175000017500000001501711604616526017402 0ustar uwabamiuwabamimodule Net::IRC::Constants # :nodoc: RPL_WELCOME = '001' RPL_YOURHOST = '002' RPL_CREATED = '003' RPL_MYINFO = '004' RPL_ISUPPORT = '005' RPL_USERHOST = '302' RPL_ISON = '303' RPL_AWAY = '301' RPL_UNAWAY = '305' RPL_NOWAWAY = '306' RPL_WHOISUSER = '311' RPL_WHOISSERVER = '312' RPL_WHOISOPERATOR = '313' RPL_WHOISIDLE = '317' RPL_ENDOFWHOIS = '318' RPL_WHOISCHANNELS = '319' RPL_WHOWASUSER = '314' RPL_ENDOFWHOWAS = '369' RPL_LISTSTART = '321' RPL_LIST = '322' RPL_LISTEND = '323' RPL_UNIQOPIS = '325' RPL_CHANNELMODEIS = '324' RPL_NOTOPIC = '331' RPL_TOPIC = '332' RPL_INVITING = '341' RPL_SUMMONING = '342' RPL_INVITELIST = '346' RPL_ENDOFINVITELIST = '347' RPL_EXCEPTLIST = '348' RPL_ENDOFEXCEPTLIST = '349' RPL_VERSION = '351' RPL_WHOREPLY = '352' RPL_ENDOFWHO = '315' RPL_NAMREPLY = '353' RPL_ENDOFNAMES = '366' RPL_LINKS = '364' RPL_ENDOFLINKS = '365' RPL_BANLIST = '367' RPL_ENDOFBANLIST = '368' RPL_INFO = '371' RPL_ENDOFINFO = '374' RPL_MOTDSTART = '375' RPL_MOTD = '372' RPL_ENDOFMOTD = '376' RPL_YOUREOPER = '381' RPL_REHASHING = '382' RPL_YOURESERVICE = '383' RPL_TIME = '391' RPL_USERSSTART = '392' RPL_USERS = '393' RPL_ENDOFUSERS = '394' RPL_NOUSERS = '395' RPL_TRACELINK = '200' RPL_TRACECONNECTING = '201' RPL_TRACEHANDSHAKE = '202' RPL_TRACEUNKNOWN = '203' RPL_TRACEOPERATOR = '204' RPL_TRACEUSER = '205' RPL_TRACESERVER = '206' RPL_TRACESERVICE = '207' RPL_TRACENEWTYPE = '208' RPL_TRACECLASS = '209' RPL_TRACERECONNECT = '210' RPL_TRACELOG = '261' RPL_TRACEEND = '262' RPL_STATSLINKINFO = '211' RPL_STATSCOMMANDS = '212' RPL_ENDOFSTATS = '219' RPL_STATSUPTIME = '242' RPL_STATSOLINE = '243' RPL_UMODEIS = '221' RPL_SERVLIST = '234' RPL_SERVLISTEND = '235' RPL_LUSERCLIENT = '251' RPL_LUSEROP = '252' RPL_LUSERUNKNOWN = '253' RPL_LUSERCHANNELS = '254' RPL_LUSERME = '255' RPL_ADMINME = '256' RPL_ADMINLOC1 = '257' RPL_ADMINLOC2 = '258' RPL_ADMINEMAIL = '259' RPL_TRYAGAIN = '263' ERR_NOSUCHNICK = '401' ERR_NOSUCHSERVER = '402' ERR_NOSUCHCHANNEL = '403' ERR_CANNOTSENDTOCHAN = '404' ERR_TOOMANYCHANNELS = '405' ERR_WASNOSUCHNICK = '406' ERR_TOOMANYTARGETS = '407' ERR_NOSUCHSERVICE = '408' ERR_NOORIGIN = '409' ERR_NORECIPIENT = '411' ERR_NOTEXTTOSEND = '412' ERR_NOTOPLEVEL = '413' ERR_WILDTOPLEVEL = '414' ERR_BADMASK = '415' ERR_UNKNOWNCOMMAND = '421' ERR_NOMOTD = '422' ERR_NOADMININFO = '423' ERR_FILEERROR = '424' ERR_NONICKNAMEGIVEN = '431' ERR_ERRONEUSNICKNAME = '432' ERR_NICKNAMEINUSE = '433' ERR_NICKCOLLISION = '436' ERR_UNAVAILRESOURCE = '437' ERR_USERNOTINCHANNEL = '441' ERR_NOTONCHANNEL = '442' ERR_USERONCHANNEL = '443' ERR_NOLOGIN = '444' ERR_SUMMONDISABLED = '445' ERR_USERSDISABLED = '446' ERR_NOTREGISTERED = '451' ERR_NEEDMOREPARAMS = '461' ERR_ALREADYREGISTRED = '462' ERR_NOPERMFORHOST = '463' ERR_PASSWDMISMATCH = '464' ERR_YOUREBANNEDCREEP = '465' ERR_YOUWILLBEBANNED = '466' ERR_KEYSET = '467' ERR_CHANNELISFULL = '471' ERR_UNKNOWNMODE = '472' ERR_INVITEONLYCHAN = '473' ERR_BANNEDFROMCHAN = '474' ERR_BADCHANNELKEY = '475' ERR_BADCHANMASK = '476' ERR_NOCHANMODES = '477' ERR_BANLISTFULL = '478' ERR_NOPRIVILEGES = '481' ERR_CHANOPRIVSNEEDED = '482' ERR_CANTKILLSERVER = '483' ERR_RESTRICTED = '484' ERR_UNIQOPPRIVSNEEDED = '485' ERR_NOOPERHOST = '491' ERR_UMODEUNKNOWNFLAG = '501' ERR_USERSDONTMATCH = '502' RPL_SERVICEINFO = '231' RPL_ENDOFSERVICES = '232' RPL_SERVICE = '233' RPL_NONE = '300' RPL_WHOISCHANOP = '316' RPL_KILLDONE = '361' RPL_CLOSING = '362' RPL_CLOSEEND = '363' RPL_INFOSTART = '373' RPL_MYPORTIS = '384' RPL_STATSCLINE = '213' RPL_STATSNLINE = '214' RPL_STATSILINE = '215' RPL_STATSKLINE = '216' RPL_STATSQLINE = '217' RPL_STATSYLINE = '218' RPL_STATSVLINE = '240' RPL_STATSLLINE = '241' RPL_STATSHLINE = '244' RPL_STATSSLINE = '244' RPL_STATSPING = '246' RPL_STATSBLINE = '247' RPL_STATSDLINE = '250' ERR_NOSERVICEHOST = '492' PASS = 'PASS' NICK = 'NICK' USER = 'USER' OPER = 'OPER' MODE = 'MODE' SERVICE = 'SERVICE' QUIT = 'QUIT' SQUIT = 'SQUIT' JOIN = 'JOIN' PART = 'PART' TOPIC = 'TOPIC' NAMES = 'NAMES' LIST = 'LIST' INVITE = 'INVITE' KICK = 'KICK' PRIVMSG = 'PRIVMSG' NOTICE = 'NOTICE' MOTD = 'MOTD' LUSERS = 'LUSERS' VERSION = 'VERSION' STATS = 'STATS' LINKS = 'LINKS' TIME = 'TIME' CONNECT = 'CONNECT' TRACE = 'TRACE' ADMIN = 'ADMIN' INFO = 'INFO' SERVLIST = 'SERVLIST' SQUERY = 'SQUERY' WHO = 'WHO' WHOIS = 'WHOIS' WHOWAS = 'WHOWAS' KILL = 'KILL' PING = 'PING' PONG = 'PONG' ERROR = 'ERROR' AWAY = 'AWAY' REHASH = 'REHASH' DIE = 'DIE' RESTART = 'RESTART' SUMMON = 'SUMMON' USERS = 'USERS' WALLOPS = 'WALLOPS' USERHOST = 'USERHOST' ISON = 'ISON' end Net::IRC::COMMANDS = Net::IRC::Constants.constants.inject({}) {|r, i| # :nodoc: r.update(Net::IRC::Constants.const_get(i).to_s => i.to_s.freeze) } net-irc-0.0.9/lib/net/irc/server.rb0000644000175000017500000001004711604616526016672 0ustar uwabamiuwabamiclass Net::IRC::Server # Server global state for accessing Server::Session attr_accessor :state attr_accessor :sessions def initialize(host, port, session_class, opts={}) @host = host @port = port @session_class = session_class @opts = OpenStruct.new(opts) @sessions = [] @state = {} end # Start server loop. def start @serv = TCPServer.new(@host, @port) @log = @opts.logger || Logger.new($stdout) @log.info "Host: #{@host} Port:#{@port}" @accept = Thread.start do loop do Thread.start(@serv.accept) do |s| begin @log.info "Client connected, new session starting..." s = @session_class.new(self, s, @log, @opts) @sessions << s s.start rescue Exception => e puts e puts e.backtrace ensure @sessions.delete(s) end end end end @accept.join end # Close all sessions. def finish Thread.exclusive do @accept.kill begin @serv.close rescue end @sessions.each do |s| s.finish end end end class Session include Net::IRC include Constants attr_reader :prefix, :nick, :real, :host # Override subclass. def server_name "net-irc" end # Override subclass. def server_version "0.0.0" end # Override subclass. def available_user_modes "eixwy" end # Override subclass. def available_channel_modes "spknm" end def initialize(server, socket, logger, opts={}) @server, @socket, @log, @opts = server, socket, logger, opts end def self.start(*args) new(*args).start end # Start session loop. def start on_connected while l = @socket.gets begin @log.debug "RECEIVE: #{l.chomp}" m = Message.parse(l) next if on_message(m) === true name = "on_#{(COMMANDS[m.command.upcase] || m.command).downcase}" send(name, m) if respond_to?(name) break if m.command == QUIT rescue Message::InvalidMessage @log.error "MessageParse: " + l.inspect end end rescue IOError ensure finish end # Close this session. def finish begin @socket.close rescue end on_disconnected end # Default PASS callback. # Set @pass. def on_pass(m) @pass = m.params[0] end # Default NICK callback. # Set @nick. def on_nick(m) @nick = m.params[0] @prefix = Prefix.new("#{@nick}!#{@user}@#{@host}") if defined? @prefix end # Default USER callback. # Set @user, @real, @host and call initial_message. def on_user(m) @user, @real = m.params[0], m.params[3] @nick ||= @user @host = @socket.peeraddr[2] @prefix = Prefix.new("#{@nick}!#{@user}@#{@host}") initial_message end # Call when socket connected. def on_connected end # Call when socket closed. def on_disconnected end # Catch all messages. # If this method return true, aother callback will not be called. def on_message(m) end # Default PING callback. Response PONG. def on_ping(m) post server_name, PONG, m.params[0] end # Do nothing. # This is for avoiding error on calling super. # So you can always call super at subclass. def method_missing(name, *args) end private # Post message to server. # # include Net::IRC::Constants # post prefix, PRIVMSG, "#channel", "foobar" def post(prefix, command, *params) m = Message.new(prefix, command, params.map {|s| #s.gsub(/\r\n|[\r\n]/, " ") s.tr("\r\n", " ") }) @log.debug "SEND: #{m.to_s.chomp}" @socket << m rescue IOError finish end # Call when client connected. # Send RPL_WELCOME sequence. If you want to customize, override this method at subclass. def initial_message post server_name, RPL_WELCOME, @nick, "Welcome to the Internet Relay Network #{@prefix}" post server_name, RPL_YOURHOST, @nick, "Your host is #{server_name}, running version #{server_version}" post server_name, RPL_CREATED, @nick, "This server was created #{Time.now}" post server_name, RPL_MYINFO, @nick, "#{server_name} #{server_version} #{available_user_modes} #{available_channel_modes}" end end end # Server net-irc-0.0.9/lib/net/irc/message/0000775000175000017500000000000011604616526016463 5ustar uwabamiuwabaminet-irc-0.0.9/lib/net/irc/message/modeparser.rb0000644000175000017500000000326511604616526021155 0ustar uwabamiuwabamiclass Net::IRC::Message::ModeParser ONE_PARAM_MASK = 0 ONE_PARAM = 1 ONE_PARAM_FOR_POSITIVE = 2 NO_PARAM = 3 def initialize @modes = {} @op_to_mark_map = {} @mark_to_op_map = {} # Initialize for ircd 2.11 (RFC2812+) set(:CHANMODES, 'beIR,k,l,imnpstaqr') set(:PREFIX, '(ov)@+') end def mark_to_op(mark) mark.empty? ? nil : @mark_to_op_map[mark.to_sym] end def set(key, value) case key when :PREFIX if value =~ /\A\(([a-zA-Z]+)\)(.+)\z/ @op_to_mark_map = {} key, value = Regexp.last_match[1], Regexp.last_match[2] key.scan(/./).zip(value.scan(/./)) {|pair| @op_to_mark_map[pair[0].to_sym] = pair[1].to_sym } @mark_to_op_map = @op_to_mark_map.invert end when :CHANMODES @modes = {} value.split(",").each_with_index do |s, kind| s.scan(/./).each {|c| @modes[c.to_sym] = kind } end end end def parse(arg) params = arg.kind_of?(Net::IRC::Message) ? arg.to_a : arg.split(" ") params.shift ret = { :positive => [], :negative => [], } current = ret[:positive] until params.empty? s = params.shift s.scan(/./).each do |c| c = c.to_sym case c when :+ current = ret[:positive] when :- current = ret[:negative] else case @modes[c] when ONE_PARAM_MASK,ONE_PARAM current << [c, params.shift] when ONE_PARAM_FOR_POSITIVE if current.equal?(ret[:positive]) current << [c, params.shift] else current << [c, nil] end when NO_PARAM current << [c, nil] else if @op_to_mark_map[c] current << [c, params.shift] end end end end end ret end end net-irc-0.0.9/lib/net/irc/message/serverconfig.rb0000644000175000017500000000115011604616526021477 0ustar uwabamiuwabamiclass Net::IRC::Message::ServerConfig attr_reader :mode_parser def initialize @config = {} @mode_parser = Net::IRC::Message::ModeParser.new end def set(arg) params = arg.kind_of?(Net::IRC::Message) ? arg.to_a : arg.split(" ") params[1..-1].each do |s| case s when /\A:?are supported by this server\z/ # Ignore when /\A([^=]+)=(.*)\z/ key = Regexp.last_match[1].to_sym value = Regexp.last_match[2] @config[key] = value @mode_parser.set(key, value) if key == :CHANMODES || key == :PREFIX else @config[s] = true end end end def [](key) @config[key] end end net-irc-0.0.9/lib/net/irc/client/0000775000175000017500000000000011604616526016315 5ustar uwabamiuwabaminet-irc-0.0.9/lib/net/irc/client/channel_manager.rb0000644000175000017500000000511511604616526021744 0ustar uwabamiuwabami module Net::IRC::Client::ChannelManager # For managing channel def on_rpl_namreply(m) type = m[1] channel = m[2] init_channel(channel) @channels.synchronize do m[3].split(" ").each do |u| _, mode, nick = *u.match(/\A([@+]?)(.+)/) @channels[channel][:users] << nick @channels[channel][:users].uniq! op = @server_config.mode_parser.mark_to_op(mode) if op @channels[channel][:modes] << [op, nick] end end case type when "@" # secret @channels[channel][:modes] << [:s, nil] when "*" # private @channels[channel][:modes] << [:p, nil] when "=" # public end @channels[channel][:modes].uniq! end end # For managing channel def on_part(m) nick = m.prefix.nick channel = m[0] init_channel(channel) @channels.synchronize do info = @channels[channel] if info info[:users].delete(nick) info[:modes].delete_if {|u| u[1] == nick } end end end # For managing channel def on_quit(m) nick = m.prefix.nick @channels.synchronize do @channels.each do |channel, info| info[:users].delete(nick) info[:modes].delete_if {|u| u[1] == nick } end end end # For managing channel def on_kick(m) users = m[1].split(",") @channels.synchronize do m[0].split(",").each do |chan| init_channel(chan) info = @channels[chan] if info users.each do |nick| info[:users].delete(nick) info[:modes].delete_if {|u| u[1] == nick } end end end end end # For managing channel def on_join(m) nick = m.prefix.nick channel = m[0] @channels.synchronize do init_channel(channel) @channels[channel][:users] << nick @channels[channel][:users].uniq! end end # For managing channel def on_nick(m) oldnick = m.prefix.nick newnick = m[0] @channels.synchronize do @channels.each do |channel, info| info[:users].map! {|i| (i == oldnick) ? newnick : i } info[:modes].map! {|mode, target| (target == oldnick) ? [mode, newnick] : [mode, target] } end end end # For managing channel def on_mode(m) channel = m[0] @channels.synchronize do init_channel(channel) modes = @server_config.mode_parser.parse(m) modes[:negative].each do |mode| @channels[channel][:modes].delete(mode) end modes[:positive].each do |mode| @channels[channel][:modes] << mode end @channels[channel][:modes].uniq! [modes[:negative], modes[:positive]] end end # For managing channel def init_channel(channel) @channels[channel] ||= { :modes => [], :users => [], } end end net-irc-0.0.9/lib/net/irc.rb0000644000175000017500000000274611604616526015373 0ustar uwabamiuwabami#!ruby require "ostruct" require "socket" require "logger" require "monitor" module Net; end module Net::IRC VERSION = "0.0.9".freeze class IRCException < StandardError; end require "net/irc/constants" require "net/irc/pattern" autoload :Message, "net/irc/message" autoload :Client, "net/irc/client" autoload :Server, "net/irc/server" class Prefix < String def nick extract[0] end def user extract[1] end def host extract[2] end # Extract Prefix String to [nick, user, host] Array. def extract _, *ret = *self.match(/\A([^\s!]+)(?:!([^\s@]+)@(\S+))?\z/) ret end end # Encode to CTCP message. Prefix and postfix \x01. def ctcp_encode(str) "\x01#{ctcp_quote(str)}\x01" end #alias :ctcp_encoding :ctcp_encode module_function :ctcp_encode #, :ctcp_encoding # Decode from CTCP message delimited with \x01. def ctcp_decode(str) ctcp_dequote(str.delete("\x01")) end #alias :ctcp_decoding :ctcp_decode module_function :ctcp_decode #, :ctcp_decoding def ctcp_quote(str) low_quote(str.gsub("\\", "\\\\\\\\").gsub("\x01", "\\a")) end module_function :ctcp_quote def ctcp_dequote(str) low_dequote(str).gsub("\\a", "\x01").gsub(/\\(.|\z)/m, "\\1") end module_function :ctcp_dequote private def low_quote(str) str.gsub("\x10", "\x10\x10").gsub("\x00", "\x10\x30").gsub("\r", "\x10r").gsub("\n", "\x10n") end def low_dequote(str) str.gsub("\x10n", "\n").gsub("\x10r", "\r").gsub("\x10\x30", "\x00").gsub("\x10\x10", "\x10") end end net-irc-0.0.9/Rakefile0000644000175000017500000000572111604616526014376 0ustar uwabamiuwabamirequire 'rubygems' require "shipit" require 'rake' require 'rake/clean' require 'rake/packagetask' require 'rake/gempackagetask' require 'rake/rdoctask' require 'rake/contrib/sshpublisher' require 'fileutils' require 'spec/rake/spectask' include FileUtils $LOAD_PATH.unshift "lib" require "net/irc" NAME = "net-irc" AUTHOR = "cho45" EMAIL = "cho45@lowreal.net" DESCRIPTION = "library for implementing IRC server and client" HOMEPATH = "http://cho45.stfuawsc.com/net-irc/" BIN_FILES = %w( ) VERS = Net::IRC::VERSION.dup REV = File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil CLEAN.include ['**/.*.sw?', '*.gem', '.config'] RDOC_OPTS = [ '--title', "#{NAME} documentation", "--charset", "utf-8", "--opname", "index.html", "--line-numbers", "--main", "README", "--inline-source", ] task :default => [:spec] task :package => [:clean] Spec::Rake::SpecTask.new do |t| t.spec_opts = ['--options', "spec/spec.opts"] t.spec_files = FileList['spec/*_spec.rb'] #t.rcov = true end spec = Gem::Specification.new do |s| s.name = NAME s.version = VERS s.platform = Gem::Platform::RUBY s.has_rdoc = true s.extra_rdoc_files = ["README", "ChangeLog"] s.rdoc_options += RDOC_OPTS + ['--exclude', '^(examples|extras)/'] s.summary = DESCRIPTION s.description = DESCRIPTION s.author = AUTHOR s.email = EMAIL s.homepage = HOMEPATH s.executables = BIN_FILES s.bindir = "bin" s.require_path = "lib" s.autorequire = "" #s.add_dependency('activesupport', '>=1.3.1') #s.required_ruby_version = '>= 1.8.2' s.files = %w(README ChangeLog Rakefile) + Dir.glob("{bin,doc,spec,test,lib,templates,generator,extras,website,script}/**/*") + Dir.glob("ext/**/*.{h,c,rb}") + Dir.glob("examples/**/*.rb") + Dir.glob("tools/*.rb") s.extensions = FileList["ext/**/extconf.rb"].to_a end Rake::GemPackageTask.new(spec) do |p| p.need_tar = true p.gem_spec = spec end task :install do name = "#{NAME}-#{VERS}.gem" sh %{rake package} sh %{sudo gem install pkg/#{name}} end task :uninstall => [:clean] do sh %{sudo gem uninstall #{NAME}} end task :upload_doc => [:rdoc] do sh %{rsync --update -avptr html/ lowreal@cho45.stfuawsc.com:/virtual/lowreal/public_html/cho45.stfuawsc.com/net-irc} end Rake::RDocTask.new do |rdoc| rdoc.rdoc_dir = 'html' rdoc.options += RDOC_OPTS rdoc.template = "resh" #rdoc.template = "#{ENV['template']}.rb" if ENV['template'] if ENV['DOC_FILES'] rdoc.rdoc_files.include(ENV['DOC_FILES'].split(/,\s*/)) else rdoc.rdoc_files.include('README', 'ChangeLog') rdoc.rdoc_files.include('lib/**/*.rb') rdoc.rdoc_files.include('ext/**/*.c') end end Rake::ShipitTask.new do |s| s.ChangeVersion "lib/net/irc.rb", "VERSION" s.Commit s.Task :clean, :package, :upload_doc s.Step.new { }.and { system("gem", "push", "pkg/net-irc-#{VERS}.gem") } s.Tag s.Twitter end