diff --git a/ircclient.rb b/ircclient.rb new file mode 100644 index 0000000..3658e0d --- /dev/null +++ b/ircclient.rb @@ -0,0 +1,609 @@ +module IrcClient + require 'timeout' + require "socket" + require 'thread' + require 'ircreplies' + require 'netutils' + + include IRCReplies + + # The irc class, which talks to the server and holds the main event loop + class IrcActor + include NetUtils + attr_reader :channels + #=========================================================== + #events + #=========================================================== + def initialize(client) + @client = client + @eventqueue = ConditionVariable.new + @eventlock = Mutex.new + @events = [] + @channels = {} + @store = { + :ping => + Proc.new {|server| + client.send_pong server + }, + :pong => + Proc.new {|server| + puts "pong:#{server}" + }, + :notice=> + [Proc.new {|user,channel,msg| + puts "Server:#{msg}" + #client.msg_channel channel,"message from #{user} on #{channel} : #{msg}" + }], + :privmsg => + [Proc.new {|user,channel,msg| + #puts "message from #{user} on #{channel} : #{msg}" + #client.msg_channel channel,"message from #{user} on #{channel} : #{msg}" + }], + :connect => + [Proc.new {|server,port,nick,pass| + #puts "on connect #{server}:#{port}" + client.send_pass pass + client.send_nick nick + client.send_user nick,'0','*',"#{server} Net Bot" + }], + :numeric => + [Proc.new {|server,numeric,msg,detail| + #puts "on numeric #{server}:#{numeric}" + }], + :join=> + [Proc.new {|nick,channel| + #puts "on join" + }], + :part=> + [Proc.new {|nick,channel,msg| + #puts "on part" + }], + :quit=> + [Proc.new {|nick,msg| + #puts "on quit" + }], + :unknown => + [Proc.new {|line| + puts ">unknown message #{line}" + }] + } + end + + #=========================================================== + #on_xxx appends to registered callbacks + #use [] to reset callbacks + #=========================================================== + def on_connect(&block) + raise IrcError.new('wrong arity') if block.arity != 4 + self[:connect] << block + end + + def on_ping(&block) + raise IrcError.new('wrong arity') if block.arity != 1 + self[:ping] << block + end + + def on_privmsg(&block) + raise IrcError.new('wrong arity') if block.arity != 3 + self[:privmsg] << block + end + + def on_numeric(numarray,&block) + raise IrcError.new('wrong arity') if block.arity != 4 + self[:numeric] << Proc.new {|server,numeric,msg,detail| + case numeric + when *numarray + block.call(server,numeric,msg,detail) + end + } + end + + def on_rpl(num,&block) + raise IrcError.new('wrong arity') if block.arity != 3 + self[:numeric] << Proc.new {|server,numeric,msg,detail| + block.call(server,msg,detail) if num == numeric + } + end + + def on_err(num,&block) + on_rpl(num,block) + end + + def on(method,&block) + self[method] << block + end + + #=========================================================== + def [](method) + @store[method] = [] if @store[method].nil? + return @store[method] + end + + def push(method,*args) + @eventlock.synchronize { + @events << [method,args] + @eventqueue.signal + } + end + + def send_names(channel) + @client.send_names channel + channel + end + + def send_message(user,message) + @client.msg_user user, message + user + end + + def send(message) + @client.send message + message + end + + def run + while true + begin + method,args = :unknown,'' + @eventlock.synchronize { + @eventqueue.wait(@eventlock) if @events.empty? + method,args = @events.shift + } + self[method].each {|block| block[*args] } + rescue SystemExit => e + exit 0 + rescue Exception => e + carp e + end + end + end + + def join(channel) + @client.send_join channel + @channels[channel] = Time.now + channel + end + + def part(channel) + if @channels.delete(channel) + @client.send_part channel + channel + else + 'not member' + end + end + + def nick + return @nick + end + + def names(channel) + return @client.names(channel) + end + + end + + class PrintActor < IrcActor + def initialize(client) + super(client) + on(:connect) {|server,port,nick,pass| + client.send_join '#db' + } + on(:numeric) {|server,numeric,msg,detail| + #puts "-:#{numeric}" + } + on(:join) {|nick,channel| + #puts "#{nick} join-:#{channel}" + #client.msg_channel '#markee', "heee" + } + on(:part) {|nick,channel,msg| + #puts "#{nick} part-:#{channel}" + } + on(:quit) {|nick,msg| + #puts "#{nick} quit-:#{channel}" + } + end + end + class TestActor < IrcActor + def initialize(client) + super(client) + on(:connect) {|server,port,nick,pass| + client.send_join '#db' + } + on(:numeric) {|server,numeric,msg,detail| + puts "-:#{numeric}" + } + on(:join) {|nick,channel| + puts "#{nick} join-:#{channel}" + } + on(:part) {|nick,channel,msg| + puts "#{nick} part-:#{channel}" + } + on(:quit) {|nick,msg| + puts "#{nick} quit-:#{nick}:#{msg}" + } + on(:privmsg) {|nick,channel,msg| + case msg + when /^ *!who +([^ ]+) *$/ + names = names($1) + send_message channel, "names: #{names.join(',')}" + end + } + end + end + + class IrcConnector + include IRCReplies + include NetUtils + extend NetUtils + + attr_reader :server, :port, :nick, :socket + attr_writer :actor, :socket + + def initialize(server, port, nick, pass) + @server = server + @port = port + @nick = nick + @pass = pass + @actor = IrcActor.new(self) + @readlock = Mutex.new + @writelock = Mutex.new + + + @inputlock = Mutex.new + @inputqueue = ConditionVariable.new + end + + def run + connect() + @eventloop = Thread.new { @actor.run } + listen_loop + end + + def connect() + begin + #allow socket to be handed over from elsewhere. + @socket = @socket || TCPSocket.open(@server, @port) + @actor[:connect].each{|c| c[ @server, @port, @nick, @pass]} + rescue + raise "Cannot connect #{@server}:#{@port}" + end + end + + #=========================================================== + def process(input) + input.untaint + s = input + prefix = '' + user = '' + if input =~ /^:([^ ]+) +(.*)$/ + s = $2 + prefix = $1 + user = if prefix =~ /^([^!]+)!(.+)/ + $1 + else + prefix + end + end + + cmd = s + suffix = '' + if s =~ /([^:]+):(.*)$/ + cmd = $1.strip + suffix = $2 + end + case cmd + when /^PING$/i + #dont bother about event loop here. + @actor[:ping][suffix] + when /^PONG$/i + @actor[:pong][suffix] + when /^NOTICE +(.+)$/i + @actor.push :notice, user, $1, suffix + when /^PRIVMSG +(.+)$/i + @actor.push :privmsg, user, $1, suffix + when /^JOIN$/i + #the confirmation join channel will come in suffix + @actor.push :join, user, suffix + when /^PART +(.+)$/i + #the confirmation part channel will come in cmd arg. + @actor.push :part, user, $1, suffix + when /^QUIT$/i + @actor.push :quit, user, suffix + when /^([0-9]+) +(.+)$/i + server,numeric,msg,detail = prefix, $1.to_i,$2, suffix + @actor.push :numeric, server,numeric,msg,detail if !local_numeric(numeric,msg,detail) + else + @actor.push :unknown, input + end + end + + def listen_loop() + process(gets) while !@socket.eof? + end + + #WARNING: UGLY HACK. + def local_numeric(numeric,msg,detail) + if @capture_numeric + case numeric + when ERR_NOSUCHNICK + if msg =~ / *[^ ]+ +([^ ]+)*$/ + if $1 == @capture_channel + @inputlock.synchronize { + @args << [numeric,msg,detail] + @inputqueue.signal + } + return true + end + end + when RPL_ENDOFNAMES + if msg =~ / *[^ ]+ +([^ ]+)*$/ + if $1 == @capture_channel + @inputlock.synchronize { + @args << [numeric,msg,detail] + @inputqueue.signal + } + return true + end + end + when RPL_NAMREPLY + if msg =~ / *[^ ]+ *= +([^ ]+)*$/ + if $1 == @capture_channel + @inputlock.synchronize { + @args << [numeric,msg,detail] + @inputqueue.signal + } + return true + end + end + end + end + return false #continue with processing + end + + #will be invoked from a thread different from that of the + #primary IrcConnector thread. + def names(channel) + carp "invoke names for #{channel}" + @names = [] + @args = [] + @capture_channel = channel.chomp + @capture_numeric = true + send_names channel + while true + numeric, msg, detail = 0,'','' + @inputlock.synchronize { + @inputqueue.wait(@inputlock) if @args.empty? + numeric, msg, detail = @args.shift + } + case numeric + when ERR_NOSUCHNICK + carp ERR_NOSUCHNICK + break + when RPL_ENDOFNAMES + carp "#{RPL_ENDOFNAMES} #{@names}" + break + when RPL_NAMREPLY + nicks = detail.split(/ +/) + nicks.each {|n| @names << $1.strip if n =~ /^@?([^ ]+)/ } + carp "nicks #{nicks}" + end + end + carp "returning #{@names}" + @capture_numeric = false + return @names + end + + #===================================================== + def lock_read + @readlock.lock + end + + def unlock_read + @readlock.unlock + end + + def lock_write + @writelock.lock + end + + def unlock_write + @writelock.unlock + end + #===================================================== + def send_pong(arg) + send "PONG :#{arg}" + end + + def send_pass(pass) + send "PASS #{pass}" + end + + def send_nick(nick) + send "NICK #{nick}" + end + + def send_user(user,mode,unused,real) + send "USER #{user} #{mode} #{unused} :#{real}" + end + + def send_names(channel) + send "NAMES #{channel}" + end + + def send(msg) + send "#{msg}" + end + #===================================================== + def send_join(channel) + send "JOIN #{channel}" + end + + def send_part(channel) + send "PART #{channel} :" + end + + def msg_user(user,data) + msg_channel(user, data) + end + + def msg_channel(channel, data) + send "PRIVMSG #{channel} :#{data}" + end + + def notice_channel(channel, data) + send "NOTICE #{channel} :#{data}" + end + + def gets + s = nil + @readlock.synchronize { + s = @socket.gets + } + #carp "<#{s}" + return s + end + + def send(s) + #carp ">#{s}" + @writelock.synchronize { @socket << "#{s}\n" } + end + #===================================================== + def IrcConnector.start(opts={}) + server = opts[:server] or raise 'No server defined.' + nick = opts[:nick] || '_' + Socket.gethostname.split(/\./).shift + port = opts[:port] || 6667 + pass = opts[:pass] || 'netserver' + irc = IrcConnector.new(server, port , nick, pass) + #irc.actor = PrintActor.new(irc) + irc.actor = TestActor.new(irc) + begin + irc.run + rescue SystemExit => e + puts "exiting..#{e.message()}" + exit 0 + rescue Interrupt + exit 0 + rescue Exception => e + carp e + end + end + end +#===================================================== + class ProxyConnector + attr_reader :server, :nick + attr_writer :actor + def initialize(nick, pass, server, actor) + @server = 'service' + @nick = nick + @pass = pass + @actor = actor.new(self) + @ircserver = server + end + + #===================================================== + def invoke(method, *args) + @actor[method].each{|c| c[*args]} + end + + #called during connection. + def connect + @actor[:connect].each{|c| c[ @server, @port, @nick, @pass]} + end + + def ping(arg) + @actor[:ping].each{|c| c[arg]} + end + + def privmsg(nick, channel, msg) + @actor[:privmsg].each{|c| c[nick, channel, msg]} + end + + def notice(nick, channel, msg) + @actor[:notice].each{|c| c[nick, channel, msg]} + end + + def join(nick, channel) + @actor[:join].each{|c| c[nick, channel]} + end + + def part(nick, channel, msg) + @actor[:part].each{|c| c[nick, channel, msg]} + end + + def quit(nick, msg) + @actor[:quit].each{|c| c[nick, msg]} + end + + def numeric(server,numeric,msg, detail) + @actor[:numeric].each{|c| c[server,numeric,msg,detail]} + end + + def unknown(arg) + @actor[:unknown].each{|c| c[arg]} + end + + #===================================================== + def send_pong(arg) + @ircserver.invoke :pong,arg + end + + def send_pass(arg) + @ircserver.invoke :pass,arg + end + + def send_nick(arg) + @ircserver.invoke :nick,arg + end + + def send_user(user,mode,unused,real) + @ircserver.invoke :user, user, mode, unused, real + end + + def send_names(arg) + @ircserver.invoke :names,arg + end + + def send_join(arg) + @ircserver.invoke :join,arg + end + + def send_part(channel) + @ircserver.invoke :part,arg + end + + def msg_channel(channel, data) + @ircserver.invoke :privmsg,channel, data + end + #===================================================== + def names(channel) + return @ircserver.names(channel) + end + + def msg_user(user,data) + msg_channel(user, data) + end + end +#===================================================== + if __FILE__ == $0 + server = 'localhost' + port = 6667 + nick = 'genericclient' + while arg = ARGV.shift + case arg + when /-s/ + server = ARGV.shift + when /-p/ + port = ARGV.shift + when /-n/ + nick = ARGV.shift + when /-v/ + $verbose = true + end + end + IrcConnector.start :server => server, + :port => port, + :nick => nick, + :pass => 'netserver' + end +end diff --git a/ircd.rb b/ircd.rb new file mode 100644 index 0000000..3789597 --- /dev/null +++ b/ircd.rb @@ -0,0 +1,927 @@ +#!/usr/bin/env ruby +require 'webrick' +require 'thread' +require 'ircreplies' +require 'netutils' +require 'message_server' + +include IRCReplies + +$config ||= {} +$config['version'] = '0.1' +$config['timeout'] = 10 +$config['port'] = 6667 +$config['hostname'] = Socket.gethostname.split(/\./).shift +$config['starttime'] = Time.now.to_s +$config['nick-tries'] = 5 + +$verbose = ARGV.shift || true + +CHANNEL = /^[#\$&]+/ +PREFIX = /^:[^ ]+ +(.+)$/ + +class SynchronizedStore + def initialize + @store = {} + @mutex = Mutex.new + end + + def method_missing(name,*args) + @mutex.synchronize { @store.__send__(name,*args) } + end + + def each_value + @mutex.synchronize do + @store.each_value {|u| + @mutex.unlock + yield u + @mutex.lock + } + end + end + + def keys + @mutex.synchronize{@store.keys} + end +end + +$user_store = SynchronizedStore.new +class << $user_store + def <<(client) + self[client.nick] = client + end + + alias nicks keys + alias each_user each_value +end + +$channel_store = SynchronizedStore.new +class << $channel_store + def add(c) + self[c] ||= IRCChannel.new(c) + end + + def remove(c) + self.delete[c] + end + + alias each_channel each_value + alias channels keys +end + +class IRCChannel < SynchronizedStore + include NetUtils + attr_reader :name, :topic + alias each_user each_value + + def initialize(name) + super() + + @topic = "There is no topic" + @name = name + @oper = [] + carp "create channel:#{@name}" + end + + def add(client) + @oper << client.nick if @oper.empty? and @store.empty? + self[client.nick] = client + end + + def remove(client) + delete(client.nick) + end + + def join(client) + return false if is_member? client + add client + #send join to each user in the channel + each_user {|user| + user.reply :join, client.userprefix, @name + } + return true + end + + def part(client, msg) + return false if !is_member? client + each_user {|user| + user.reply :part, client.userprefix, @name, msg + } + remove client + $channel_store.delete(@name) if self.empty? + return true + end + + def quit(client, msg) + #remove client should happen before sending notification + #to others since we dont want a notification to ourselves + #after quit. + remove client + each_user {|user| + user.reply :quit, client.userprefix, @name, msg if user!= client + } + $channel_store.delete(@name) if self.empty? + end + + def privatemsg(msg, client) + each_user {|user| + user.reply :privmsg, client.userprefix, @name, msg if user != client + } + end + + def notice(msg, client) + each_user {|user| + user.reply :notice, client.userprefix, @name, msg if user != client + } + end + + def topic(msg=nil,client=nil) + return @topic if msg.nil? + @topic = msg + each_user {|user| + user.reply :topic, client.userprefix, @name, msg + } + return @topic + end + + def nicks + return keys + end + + def mode(u) + return @oper.include?(u.nick) ? '@' : '' + end + + def is_member?(m) + values.include?(m) + end + + alias has_nick? is_member? +end + +class IRCClient + include NetUtils + + attr_reader :nick, :user, :realname, :channels, :state + + def initialize(sock, serv) + @serv = serv + @socket = sock + @channels = [] + @peername = peer() + @welcomed = false + @nick_tries = 0 + @state = {} + carp "initializing connection from #{@peername}" + end + + def host + return @peername + end + + def userprefix + return @usermsg + end + + def closed? + return @socket.nil? || @socket.closed? + end + + def ready + #check for nick and pass + return (!@pass.nil? && !@nick.nil?) ? true : false + end + + def peer + begin + sockaddr = @socket.getpeername + begin + return Socket.getnameinfo(sockaddr, Socket::NI_NAMEREQD).first + rescue + return Socket.getnameinfo(sockaddr).first + end + rescue + return @socket.peeraddr[2] + end + end + + def handle_pass(s) + carp "pass = #{s}" + @pass = s + end + + def handle_nick(s) + carp "nick => #{s}" + if $user_store[s].nil? + userlist = {} + if @nick.nil? + handle_newconnect(s) + else + userlist[s] = self if self.nick != s + $user_store.delete(@nick) + @nick = s + end + + $user_store << self + + #send the info to the world + #get unique users. + @channels.each {|c| + $channel_store[c].each_user {|u| + userlist[u.nick] = u + } + } + userlist.values.each {|user| + user.reply :nick, s + } + @usermsg = ":#{@nick}!~#{@user}@#{@peername}" + $message_server.unviewed_for(self.nick).each do |message| + reply :privmsg, ":#{message.from}!~unknown@cockatoo-server-queue", self.nick, message.content + end + $message_server.mark_as_viewed!(self.nick) + else + #check if we are just nicking ourselves. + unless $user_store[s] == self + #verify the connectivity of earlier guy + unless $user_store[s].closed? + reply :numeric, ERR_NICKNAMEINUSE,"* #{s} ","Nickname is already in use." + @nick_tries += 1 + if @nick_tries > $config['nick-tries'] + carp "kicking spurious user #{s} after #{@nick_tries} tries" + handle_abort + end + return + else + $user_store[s].handle_abort + $user_store[s] = self + end + end + end + @nick_tries = 0 + end + + def handle_user(user, mode, unused, realname) + @user = user + @mode = mode + @realname = realname + @usermsg = ":#{@nick}!~#{@user}@#{@peername}" + send_welcome if !@nick.nil? + end + + def mode + return @mode + end + + def handle_newconnect(nick) + @alive = true + @nick = nick + @host = $config['hostname'] + @ver = $config['version'] + @starttime = $config['starttime'] + send_welcome if !@user.nil? + end + + def send_welcome + if !@welcomed + repl_welcome + repl_yourhost + repl_created + repl_myinfo + repl_motd + repl_mode + @welcomed = true + end + end + + def repl_welcome + client = "#{@nick}!#{@user}@#{@peername}" + reply :numeric, RPL_WELCOME, @nick, "Welcome to Cockatoo - #{client}" + end + + def repl_yourhost + reply :numeric, RPL_YOURHOST, @nick, "Your host is #{@host}, running version #{@ver}" + end + + def repl_created + reply :numeric, RPL_CREATED, @nick, "This server was created #{@starttime}" + end + + def repl_myinfo + reply :numeric, RPL_MYINFO, @nick, "#{@host} #{@ver} #{@serv.usermodes} #{@serv.channelmodes}" + end + + def repl_bounce(sever, port) + reply :numeric, RPL_BOUNCE ,"Try server #{server}, port #{port}" + end + + def repl_ison() + #XXX TODO + reply :numeric, RPL_ISON,"notimpl" + end + + def repl_away(nick, msg) + reply :numeric, RPL_AWAY, nick, msg + end + + def repl_unaway() + reply :numeric, RPL_UNAWAY, @nick,"You are no longer marked as being away" + end + + def repl_nowaway() + reply :numeric, RPL_NOWAWAY, @nick,"You have been marked as being away" + end + + def repl_motd() + reply :numeric, RPL_MOTDSTART,'', "- Message of the Day" + File.read("motd.txt").split("\n").each do |l| + reply :numeric, RPL_MOTD,'', "- #{l}" + end + reply :numeric, RPL_ENDOFMOTD,'', "- End of /MOTD command." + end + + def repl_mode() + end + + def send_nonick(nick) + reply :numeric, ERR_NOSUCHNICK, nick, "No such nick/channel" + end + + def send_nochannel(channel) + reply :numeric, ERR_NOSUCHCHANNEL, channel, "That channel doesn't exist" + end + + def send_notonchannel(channel) + reply :numeric, ERR_NOTONCHANNEL, channel, "Not a member of that channel" + end + + def send_topic(channel) + if $channel_store[channel] + reply :numeric, RPL_TOPIC,channel, "#{$channel_store[channel].topic}" + else + send_notonchannel channel + end + end + + def names(channel) + return $channel_store[channel].nicks + end + + def send_nameslist(channel) + c = $channel_store[channel] + if c.nil? + carp "names failed :#{c}" + return + end + names = [] + c.each_user {|user| + names << c.mode(user) + user.nick if user.nick + } + reply :numeric, RPL_NAMREPLY,"= #{c.name}","#{names.join(' ')}" + reply :numeric, RPL_ENDOFNAMES,"#{c.name} ","End of /NAMES list." + end + + def send_ping() + reply :ping, "#{$config['hostname']}" + end + + def handle_join(channels) + channels.split(/,/).each {|ch| + c = ch.strip + if c !~ CHANNEL + send_nochannel(c) + carp "no such channel:#{c}" + return + end + channel = $channel_store.add(c) + if channel.join(self) + send_topic(c) + send_nameslist(c) + @channels << c + else + carp "already joined #{c}" + end + } + end + + def handle_ping(pingmsg, rest) + reply :pong, pingmsg + end + + def handle_pong(srv) + carp "got pong: #{srv}" + end + + def handle_privmsg(target, msg) + viewed = true + case target.strip + when CHANNEL + channel= $channel_store[target] + if !channel.nil? + channel.privatemsg(msg, self) + else + send_nonick(target) + end + else + user = $user_store[target] + if !user.nil? + if !user.state[:away].nil? + repl_away(user.nick,user.state[:away]) + end + user.reply :privmsg, self.userprefix, user.nick, msg + else + viewed = false + end + end + begin + $message_server.append_message(self.nick, target.strip, msg, viewed) + rescue Exception => e + carp e + end + end + + def handle_notice(target, msg) + case target.strip + when CHANNEL + channel= $channel_store[target] + if !channel.nil? + channel.notice(msg, self) + else + send_nonick(target) + end + else + user = $user_store[target] + if !user.nil? + user.reply :notice, self.userprefix, user.nick, msg + else + send_nonick(target) + end + end + end + + def handle_part(channel, msg) + if $channel_store.channels.include? channel + if $channel_store[channel].part(self, msg) + @channels.delete(channel) + else + send_notonchannel channel + end + else + send_nochannel channel + end + end + + def handle_quit(msg) + #do this to avoid double quit due to 2 threads. + return if !@alive + @alive = false + @channels.each do |channel| + $channel_store[channel].quit(self, msg) + end + $user_store.delete(self.nick) + carp "#{self.nick} #{msg}" + @socket.close if !@socket.closed? + end + + def handle_topic(channel, topic) + carp "handle topic for #{channel}:#{topic}" + if topic.nil? or topic =~ /^ *$/ + send_topic(channel) + else + begin + $channel_store[channel].topic(topic,self) + rescue Exception => e + carp e + end + end + end + + def handle_away(msg) + carp "handle away :#{msg}" + if msg.nil? or msg =~ /^ *$/ + @state.delete(:away) + repl_unaway + else + @state[:away] = msg + repl_nowaway + end + end + + def handle_list(channel) + reply :numeric, RPL_LISTSTART + case channel.strip + when /^#/ + channel.split(/,/).each {|cname| + c = $channel_store[cname.strip] + reply :numeric, RPL_LIST, c.name, c.topic if c + } + else + #older opera client sends LIST <1000 + #we wont obey the boolean after list, but allow the listing + #nonetheless + $channel_store.each_channel {|c| + reply :numeric, RPL_LIST, c.name, c.topic + } + end + reply :numeric, RPL_LISTEND + end + + def handle_whois(target,nicks) + #ignore target for now. + return reply(:numeric, RPL_NONICKNAMEGIVEN, "", "No nickname given") if nicks.strip.length == 0 + nicks.split(/,/).each {|nick| + nick.strip! + user = $user_store[nick] + if user + reply :numeric, RPL_WHOISUSER, "#{user.nick} #{user.user} #{user.host} *", "#{user.realname}" + reply :numeric, RPL_WHOISCHANNELS, user.nick, "#{user.channels.join(' ')}" + repl_away user.nick, user.state[:away] if !user.state[:away].nil? + reply :numeric, RPL_ENDOFWHOIS, user.nick, "End of /WHOIS list" + else + return send_nonick(nick) + end + } + end + + def handle_names(channels, server) + channels.split(/,/).each {|ch| send_nameslist(ch.strip) } + end + + def handle_who(mask, rest) + channel = $channel_store[mask] + hopcount = 0 + if channel.nil? + #match against all users + $user_store.each_user {|user| + reply :numeric, RPL_WHOREPLY , + "#{user.channels[0]} #{user.userprefix} #{user.host} #{$config['hostname']} #{user.nick} H" , + "#{hopcount} #{user.realname}" if File.fnmatch?(mask, "#{user.host}.#{user.realname}.#{user.nick}") + } + reply :numeric, RPL_ENDOFWHO, mask, "End of /WHO list." + else + #get all users in the channel + channel.each_user {|user| + reply :numeric, RPL_WHOREPLY , + "#{mask} #{user.userprefix} #{user.host} #{$config['hostname']} #{user.nick} H" , + "#{hopcount} #{user.realname}" + } + reply :numeric, RPL_ENDOFWHO, mask, "End of /WHO list." + end + end + + def handle_mode(target, rest) + reply :mode, target, rest + end + + def handle_userhost(nicks) + info = [] + nicks.split(/,/).each {|nick| + user = $user_store[nick] + info << user.nick + '=-' + user.nick + '@' + user.peer + } + reply :numeric, RPL_USERHOST,"", info.join(' ') + end + + def handle_reload(password) + end + + def handle_abort() + handle_quit('aborted..') + end + + def handle_version() + reply :numeric, RPL_VERSION,"#{$config['version']} Ruby IRCD", "" + end + + def handle_eval(s) + #reply :raw, eval(s) + end + + def handle_unknown(s) + carp "unknown:>#{s}<" + reply :numeric, ERR_UNKNOWNCOMMAND,s, "Unknown command" + end + + def handle_connect + reply :raw, "NOTICE AUTH :#{$config['version']} initialized, welcome." + end + + def reply(method, *args) + case method + when :raw + arg = *args + raw arg + when :ping + host = *args + raw "PING :#{host}" + when :pong + msg = *args + # according to rfc 2812 the PONG must be of + #PONG csd.bu.edu tolsun.oulu.fi + # PONG message from csd.bu.edu to tolsun.oulu.fi + # ie no host at the begining + raw "PONG #{@host} #{@peername} :#{msg}" + when :join + user,channel = args + raw "#{user} JOIN :#{channel}" + when :part + user,channel,msg = args + raw "#{user} PART #{channel} :#{msg}" + when :quit + user,msg = args + raw "#{user} QUIT :#{msg}" + when :privmsg + usermsg, channel, msg = args + raw "#{usermsg} PRIVMSG #{channel} :#{msg}" + when :notice + usermsg, channel, msg = args + raw "#{usermsg} NOTICE #{channel} :#{msg}" + when :topic + usermsg, channel, msg = args + raw "#{usermsg} TOPIC #{channel} :#{msg}" + when :nick + nick = *args + raw "#{@usermsg} NICK :#{nick}" + when :mode + nick, rest = args + raw "#{@usermsg} MODE #{nick} :#{rest}" + when :numeric + numeric,msg,detail = args + server = $config['hostname'] + raw ":#{server} #{'%03d'%numeric} #{@nick} #{msg} :#{detail}" + end + end + + def raw(arg, abrt=false) + begin + carp "--> #{arg}" + @socket.print arg.chomp + "\n" if !arg.nil? + rescue Exception => e + carp "<#{self.userprefix}>#{e.message}" + #puts e.backtrace.join("\n") + handle_abort() + raise e if abrt + end + end +end + +class ProxyClient < IRCClient + + def initialize(nick, actor, serv) + carp "Initializing service #{nick}" + @nick = nick + super(nil,serv) + @conn = IrcClient::ProxyConnector.new(nick,'pass',self,actor) + end + + def peer + return @nick + end + + def handle_connect + @conn.connect + end + + def getnick(user) + if user =~ /^:([^!]+)!.*/ + return $1 + else + return user + end + end + + def reply(method, *args) + case method + when :raw + arg = *args + @conn.invoke :unknown, arg + when :ping + host = *args + @conn.invoke :ping, host + when :pong + msg = *args + @conn.invoke :pong, msg + when :join + user,channel = args + @conn.invoke :join, getnick(user), channel + when :privmsg + user, channel, msg = args + @conn.invoke :privmsg, getnick(user), channel, msg + when :notice + user, channel, msg = args + @conn.invoke :notice, getnick(user), channel, msg + when :topic + user, channel, msg = args + @conn.invoke :topic, getnick(user), channel, msg + when :nick + nick = *args + @conn.invoke :nick, nick + when :mode + nick, rest = args + @conn.invoke :mode, nick, rest + when :numeric + numeric,msg,detail = args + server = $config['hostname'] + @conn.invoke :numeric, server, numeric, msg, detail + end + end + + #From the local services + def invoke(method, *args) + case method + when :pong + server = *args + handle_pong server + when :pass + pass = *args + handle_pass pass + when :nick + nick = *args + handle_nick nick + when :user + user, mode, vhost, realname = args + handle_user user, mode, vhost, realname + when :names + channel, serv = *args + handle_names channel, serv + when :join + channel = *args + handle_join channel + when :part + channel, msg = args + handle_part channel, msg + when :quit + msg = args + handle_quit msg + when :privmsg + channel, msg = args + handle_privmsg channel, msg + else + handle_unknown "#{method} #{args.join(',')}" + end + end +end + +class IRCServer < WEBrick::GenericServer + include NetUtils + def usermodes + return "aAbBcCdDeEfFGhHiIjkKlLmMnNopPQrRsStUvVwWxXyYzZ0123459*@" + end + + def channelmodes + return "bcdefFhiIklmnoPqstv" + end + + def run(sock) + client = IRCClient.new(sock, self) + client.handle_connect + irc_listen(sock, client) + end + + def addservice(nick,actor) + carp "Add service #{nick}" + client = ProxyClient.new(nick, actor, self) + client.handle_connect + #the client is able to call the methods directly + #so we dont need to bother about looping here. + end + + def hostname + begin + sockaddr = @socket.getsockname + begin + return Socket.getnameinfo(sockaddr, Socket::NI_NAMEREQD).first + rescue + return Socket.getnameinfo(sockaddr).first + end + rescue + return @socket.peeraddr[2] + end + end + + def irc_listen(sock, client) + begin + while !sock.closed? && !sock.eof? + s = sock.gets + handle_client_input(s.chomp, client) + end + rescue Exception => e + carp e + end + client.handle_abort() + end + + def handle_client_input(input, client) + carp "<-- #{input}" + s = if input =~ PREFIX + $1 + else + input + end + case s + when /^[ ]*$/ + return + when /^PASS +(.+)$/i + client.handle_pass($1.strip) + when /^NICK +(.+)$/i + client.handle_nick($1.strip) #done + when /^USER +([^ ]+) +([0-9]+) +([^ ]+) +:(.*)$/i + client.handle_user($1, $2, $3, $4) #done + when /^USER +([^ ]+) +([0-9]+) +([^ ]+) +:*(.*)$/i + #opera does this. + client.handle_user($1, $2, $3, $4) #done + when /^USER ([^ ]+) +[^:]*:(.*)/i + #chatzilla does this. + client.handle_user($1, '', '', $3) #done + when /^JOIN +(.+)$/i + client.handle_join($1) #done + when /^PING +([^ ]+) *(.*)$/i + client.handle_ping($1, $2) #done + when /^PONG +:(.+)$/i , /^PONG +(.+)$/i + client.handle_pong($1) + when /^PRIVMSG +([^ ]+) +:(.*)$/i + client.handle_privmsg($1, $2) #done + when /^NOTICE +([^ ]+) +(.*)$/i + client.handle_notice($1, $2) #done + when /^PART :+([^ ]+) *(.*)$/i + #some clients require this. + client.handle_part($1, $2) #done + when /^PART +([^ ]+) *(.*)$/i + client.handle_part($1, $2) #done + when /^QUIT :(.*)$/i + client.handle_quit($1) #done + when /^QUIT *(.*)$/i + client.handle_quit($1) #done + when /^TOPIC +([^ ]+) *:*(.*)$/i + client.handle_topic($1, $2) #done + when /^AWAY +:(.*)$/i + client.handle_away($1) + when /^AWAY +(.*)$/i #for opera + client.handle_away($1) + when /^:*([^ ])* *AWAY *$/i + client.handle_away(nil) + when /^LIST *(.*)$/i + client.handle_list($1) + when /^WHOIS +([^ ]+) +(.+)$/i + client.handle_whois($1,$2) + when /^WHOIS +([^ ]+)$/i + client.handle_whois(nil,$1) + when /^WHO +([^ ]+) *(.*)$/i + client.handle_who($1, $2) + when /^NAMES +([^ ]+) *(.*)$/i + client.handle_names($1, $2) + when /^MODE +([^ ]+) *(.*)$/i + client.handle_mode($1, $2) + when /^USERHOST +:(.+)$/i + #besirc does this (not accourding to RFC 2812) + client.handle_userhost($1) + when /^USERHOST +(.+)$/i + client.handle_userhost($1) + when /^RELOAD +(.+)$/i + client.handle_reload($1) + when /^VERSION *$/i + client.handle_version() + when /^EVAL (.*)$/i + #strictly for debug + client.handle_eval($1) + else + client.handle_unknown(s) + end + end + + def do_ping() + while true + sleep 300 + $user_store.each_user {|client| + client.send_ping + } + end + end +end + + +if __FILE__ == $0 + #require 'ircclient' + s = IRCServer.new( :Port => $config['port'] ) + begin + while arg = ARGV.shift + case arg + when /-v/ + $verbose = true + end + end + trap("INT"){ + s.carp "killing #{$$}" + system("kill -9 #{$$}") + s.shutdown + } + p = Thread.new { + s.do_ping() + } + + #s.addservice('serviceclient',IrcClient::TestActor) + MessageServer.start + s.start + rescue Exception => e + s.carp e + end +end diff --git a/ircreplies.rb b/ircreplies.rb new file mode 100644 index 0000000..1947d0b --- /dev/null +++ b/ircreplies.rb @@ -0,0 +1,137 @@ +module IRCReplies +RPL_WELCOME = 001 +RPL_YOURHOST = 002 +RPL_CREATED = 003 +RPL_MYINFO = 004 +RPL_BOUNCE = 005 +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_ADMINEMAIL = 259 +RPL_TRYAGAIN = 263 +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 +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 +end diff --git a/message_server.rb b/message_server.rb new file mode 100644 index 0000000..523c4de --- /dev/null +++ b/message_server.rb @@ -0,0 +1,94 @@ +require 'drb' + +class MessageServer + include NetUtils + + Message = Struct.new(:from, :target, :content, :created_at, :viewed) + + MESSAGE_LIMIT = 250 + + def initialize + @mutex = Mutex.new + @messages = {} + end + + def all_messages(limit = 100) + m = [] + @mutex.synchronize do + m = @messages.values.select { |m| m.target =~ /^[#\$&]+/ } + end + return formatted(m)[0..(limit - 1)] + end + + def messages_for(chan) + m = [] + @mutex.synchronize do + m = @messages[chan] || [] + end + formatted m + end + + def append_message(from, to, contents, viewed = true) + @mutex.synchronize do + messages = (@messages[to] ||= []) + messages.shift if messages.length == 250 + messages << ::MessageServer::Message.new(from, to, contents, Time.now, viewed) + end + end + + def messages_from(user_name) + m = [] + @mutex.synchronize do + m = @messages.values.flatten.select { |m| m.from == user_name } + end + formatted m + end + + def remote_message(from, to, text) + user_from = $user_store[from] + # from is not online, so choose the correct item to do. + if user_from.nil? + target = (to =~ /^[#\$&]+/ ? $channel_store[to] : $user_store[to]) + carp "Target: #{target.class.name}" + if !target.nil? + prefix = ":#{from}!~unknown@cockatoo-server" + if to =~ /^[#\$&]+/ + target.privatemsg text, Struct.new(:userprefix).new(prefix) + else + target.reply :privmsg, prefix, to, text + end + end + append_message from, to, text, !target.nil? + else + carp "Sending privmsg (owner) to #{to} from #{from} w/ '#{text}'" + user_from.reply :privmsg, user_from.instance_variable_get("@usermsg"), to, text + carp "Sending privmsg (self) to #{to} from #{from} w/ '#{text}'" + user_from.handle_privmsg to, text + end + return true + end + + def mark_as_viewed!(user) + @mutex.synchronize do + (@messages[user] ||= []).each { |m| m.viewed = true} + end + end + + def unviewed_for(user) + return @mutex.synchronize { (@messages[user] ||= []).select { |m| m.viewed = true} } + end + + def formatted(m = []) + return m.sort_by { |m| m.created_at }.map { |message| [message.from, message.target, message.content, message.created_at, message.viewed ] } + end + + def start + $message_server = self + DRb.start_service('druby://localhost:9000', $message_server) + end + + def self.start + self.new.start + end + +end \ No newline at end of file diff --git a/motd.txt b/motd.txt new file mode 100644 index 0000000..68c184e --- /dev/null +++ b/motd.txt @@ -0,0 +1,11 @@ +Welcome to Cockatoo 0.1 + + )/_ + <' \ + /) ) + ---/'-""--- + +Cockatoo is build on ruby-ircd +w/ a few customization + +ascii art from the intarwebs! \ No newline at end of file diff --git a/netutils.rb b/netutils.rb new file mode 100644 index 0000000..c999e38 --- /dev/null +++ b/netutils.rb @@ -0,0 +1,35 @@ +module NetUtils + def carp(arg) + if $verbose + case true + when arg.kind_of?(Exception) + puts "Error:" + arg.message + puts "#{self.class.to_s.downcase}:" + arg.message + puts arg.backtrace.collect{|s| "#{self.class.to_s.downcase}:" + s}.join("\n") + else + puts "#{self.class.to_s.downcase}:" + arg + end + end + end + + def get_resource( resource ) + $:.each do |lp| + lp << '/' unless lp =~ /\/$/ + res = "#{lp}#{resource}.rb" + if lp =~ /http:\/\//i + begin + response = Net::HTTP.get_response(URI.parse(res)) + if response.code.to_i == 200 + return response.body + else + raise response.code + end + rescue Exception => e + raise e.message() + end + else + return File.read(res) if FileTest.exist?(res) + end + end + end +end diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..2088b13 --- /dev/null +++ b/readme.txt @@ -0,0 +1,8 @@ +The ircd.rb is an irc server written in ruby. +and ircclient.rb is an irc client. The code +is pretty self explanatory and both files are +executable. + +The ircclient can also work as an integrated +service on ircd.rb. The serviceclient in ircd.rb +is an example for doing this.