module Potato module IRC # A single client that intends to use dAmn. class Client include Events # The connection to the IRC user associated with this instance. # @return [TCPSocket] attr_reader :socket # The dAmn connection associated with this instance. # @return [DAmn::Client] attr_reader :client # Configuration value store # @return [OpenStruct] attr_reader :config # Have we logged in? # @return [Boolean] attr_accessor :logged_in def initialize sock @config = OpenStruct.new({:last_line => ""}) @socket = sock @client = Potato::DAmn::Client.new(self) @logged_in = false @users = {} end # Triggered when a user first connects. # @return [void] def welcome! send_packet :cmd => "localhost", :args => ["001", @config.nick], :content => "Welcome to potato #{@config.nick}!#{@config.nick}@chat.deviantart.com" send_packet :cmd => "localhost", :args => ["002", @config.nick], :content => "Your host is localhost, running version potato0.1" send_packet :cmd => "localhost", :args => ["004", @config.nick, "iowghraAsORTVSxNCWqBzvdHtGp lvhopsmntikrRcaqOALQbSeIKVfMCuzNTGj"] send_packet :cmd => "localhost", :args => ["005", @config.nick, %w[UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=20 CHANLIMIT=#:20 MAXLIST=b:60,e:60,I:60 NICKLEN=20 CHANNELLEN=100 TOPICLEN=8192 KICKLEN=8192 AWAYLEN=8192 MAXTARGETS=20]].flatten, :content => "are supported by this server" send_packet :cmd => "localhost", :args => ["005", @config.nick, %w[MODES=12 CHANTYPES=# PREFIX=(qaohv)~&@%+ NETWORK=dAmn ELIST=MNUCT STATUSMSG=~&@%+]].flatten, :content => "are supported by this server" send_packet :cmd => @config.nick, :args => ["MODE", @config.nick], :content => "+i" greeting = <<-EOG Welcome to dAmn, #{@config.nick}! To connect you need to give me some information. Type /pass yourpassword to authenticate. EOG greeting.split(/\n/).each do |g| send_packet :colon => false, :cmd => "NOTICE", :args => %w[AUTH], :content => "*** #{g}" end end # Returns an IRC hostname. # @param [String] user user to generate a hostname for # @return [String] def host_for user "%s!%s@chat.deviantart.com" % [user, user] end # Rejects one or more join requests if the user isn't logged in. # @param [Array] chans channels to notify for # @return [void] def cannot_join *chans chans.each do |chan| send_packet :cmd => "localhost", :args => [473, @config.nick, chan], :content => "Not authenticated" end end # Notify that you have joined a room # @param [String] room the joined room # @return [void] def join room send_packet :cmd => @config.host, :args => "JOIN", :content => "##{room}" end # Notify that you have parted a room # @param [String] room the parted room # @return [void] def part room send_packet :cmd => @config.host, :args => "PART", :content => "##{room}" end # Notify that another user has joined a room # @param [String] user the user that has joined # @param [String] room the room that the user has joined # @param [Integer] conns the number of connections the user has in which they are joined to this channel # @param [String] privclass the name of the privclass in which the user can be found # @return [void] def joins user, room, conns, privclass, reason if conns < 2 @users[room] << User.new(user, privclass, sym(@client.privclasses[room][privclass]), @client.privclasses[room][privclass]) send_packet :cmd => host_for(user), :args => "JOIN", :content => "##{room}" send_packet :cmd => "localhost", :args => ["MODE", "##{room}", "+#{sym_to_level(sym(@client.privclasses[room][privclass]))}", user] else chan_notice(room, "#{user} has joined again (now joined #{conns} times)") end end # Notify that a user has parted a room # @param [String] user the user that has parted # @param [String] room the room that the user has parted # @param [Integer] conns the number of connections the user has in which they are joined to this channel # @param [String] privclass # @return [void] def parts user, room, conns, privclass, reason if conns < 1 @users[room].delete_if{|x|x.username == user} send_packet :cmd => host_for(user), :args => ["PART", "##{room}"], :content => reason else chan_notice(room, "#{user} has parted (now joined #{conns} time#{"s" unless conns == 1})") end end # A /names list for the current room. # @param [String] room the room to query # @param [Array] packets # @return [void] def names room, packets @users[room] = packets.map{|x|User.new(x.param, x.args[:pc], sym(@client.privclasses[room][x.args[:pc]]), @client.privclasses[room][x.args[:pc]])} send_packet :cmd => "localhost", :args => [353, @config.nick, "=", "##{room}"], :content => @users[room].reject{|k|k.username == @config.nick}.map{|k|k.symbol + k.username}.join(" ") send_packet :cmd => "localhost", :args => [366, @config.nick, "##{room}"], :content => "End of /NAMES list." me_mode = sym_to_level(sym(@client.privclasses[room][packets.find{|x|x.param == @config.nick}.args[:pc]])) unless me_mode.empty? send_packet :cmd => "localhost", :args => ["MODE", "##{room}", "+#{me_mode}", @config.nick] end end # A /whois query for a user. # @param [String] name the queried user # @param [DAmn::Packet] packet # @return [void] def whois name, packet send_packet :cmd => "localhost", :args => [311, @config.nick, name, name, "chat.deviantart.com", "*"], :content => packet.subpkts[0].args[:realname] send_packet :cmd => "localhost", :args => [307, @config.nick, name], :content => "is a registered nick" send_packet :cmd => "localhost", :args => [319, @config.nick, name], :content => packet.subpkts.select{|pk|pk.cmd == "ns"}.map{|x|x.param.sub("chat:", "#")}.uniq.join(" ") send_packet :cmd => "localhost", :args => [312, @config.nick, name, "localhost"], :content => "potato" send_packet :cmd => "localhost", :args => [317, @config.nick, name, packet.subpkts.find{|pk|pk.cmd == "conn"}[:idle], (Time.now - packet.subpkts.find{|pk|pk.cmd == "conn"}[:online].to_i).to_i], :content => "seconds idle, signon time" send_packet :cmd => "localhost", :args => [318, @config.nick, name], :content => "End of /WHOIS list." end # Convert a privclass level to a ~&@%+ symbol # @param [#to_i] level the privclass level # @return [String] def sym level case level.to_i when 0..30 "" when 31..70 "+" when 71..80 "%" when 81..98 "@" when 99 "~" end end # Convert a ~&@%+ symbol to a mode qaohv # @param [String] sym the symbol to convert # @return [String] def sym_to_level sym case sym when "" "" when "~" "q" when "@" "o" when "+" "v" when "%" "h" end end # Notify that the topic has been set in the current room # @param [String] room the room in which the topic has been set # @param [String] topic the topic set # @param [String] by the user who set the topic # @param [String] time the time in seconds (since epoch) at which the topic was set # @return [void] def topic room, topic, by, time send_packet :cmd => "localhost", :args => [332, @config.nick, "##{room}"], :content => topic send_packet :cmd => "localhost", :args => [333, @config.nick, "##{room}", by, time] end # Display a message from a user in a room. # @param [String] room the receiving room # @param [String] user the sayer of the line # @param [String] line the line said # @return [void] def msg room, user, line line.gsub("
", "\n").split(/\n/).each do |l| send_packet :cmd => host_for(user), :args => ["PRIVMSG", "##{room}"], :content => l end end # Display an action from a user in a room. # @param [String] room the receiving room # @param [String] user the perpetrator of the action # @param [String] line the action perpetrated # @return [void] def action room, user, line line.gsub("
", "\n").split(/\n/).each do |l| send_packet :cmd => host_for(user), :args => ["PRIVMSG", "##{room}"], :content => "\x01ACTION #{l}\x01" end end # Notify that a user has been kicked from a room. # @param [String] room the room from which somebody has been kicked # @param [String] user the kickee # @param [String] by the kicker # @param [String] line the reason for kicking # @return [void] def kick room, user, by, line send_packet :cmd => host_for(by), :args => ["KICK", "##{room}", user], :content => line || by end # Notify that a privclass has been created in a room. # @param [String] room the room in which has been added the privclass # @param [String] privclass the name of the created privclass # @param [String] privs the privileges of the created privclass # @param [String] by the creator of the privclass # @return [void] def create_privclass room, privclass, privs, by chan_notice room, "Privilege class \x16#{privclass}\x0F has been created by #{by} with {#{privs.split(" ").map{|x|x.split("=").join(": ")}.join(", ")}}" end # Notify that a privclass has been updated in a room. # @param [String] room the room in which has been updated the privclass # @param [String] privclass the name of the updated privclass # @param [String] privs the privileges of the created privclass # @param [String] by the updator of the privclass # @return [void] def update_privclass room, privclass, privs, by chan_notice room, "Privilege class \x16#{privclass}\x0F has been updated by #{by} with {#{privs.split(" ").map{|x|x.split("=").join(": ")}.join(", ")}}" users = @users[room].select{|x|x.privclass == privclass} mode = "" count = 0 unless users[0].symbol == sym(@client.privclasses[room][privclass]) unless users[0].symbol.empty? mode << "-#{sym_to_level(users[0].symbol) * users.size}" count += 1 end unless sym(@client.privclasses[room][privclass]).empty? mode << "+#{sym_to_level(sym(@client.privclasses[room][privclass])) * users.size}" count += 1 end send_packet :cmd => "localhost", :args => ["MODE", "##{room}", mode, *(users.map(&:username) * count)] users.each do |user| user.symbol = sym(@client.privclasses[room][privclass]) user.privclass = privclass user.level = @client.privclasses[room][privclass] end end end # Notify that a privclass has been renamed in a room. # @param [String] room the room in which has been renamed the privclass # @param [String] prev the previous name of the privclass # @param [String] priv the new name of the privclass # @param [String] by the renamer of the privclass # @return [void] def rename_privclass room, prev, priv, by chan_notice room, "Privclass \x16#{prev}\x0F has been renamed to \x16#{priv}\x0F by #{by}" end # Notify that a privclass has been removed from a room. # @param [String] room the room from which the privclass has been removed # @param [String] privclass the name of the removed privclass # @param [String] by the remover of the privclass # @return [void] def remove_privclass room, privclass, by chan_notice room, "Privclass \x16#{privclass}\x0F has been removed by #{by}" end # Notify that a user has been moved from one privclass to another. # @param [String] room the room in which the user has been moved # @param [String] user the name of the moved user # @param [String] privclass the privclass to which the user has been moved # @param [String] by the mover of the user # @return [void] def move_user room, user, privclass, by chan_notice room, "#{user} has been moved to \x16#{privclass}\x0F by #{by}" us = @users[room].find{|x|x.username == user} return if us.nil? # the user is not in this room mode = "" count = 0 unless us.symbol == sym(@client.privclasses[room][privclass]) unless us.symbol.empty? mode << "-#{sym_to_level(us.symbol)}" count += 1 end unless sym(@client.privclasses[room][privclass]).empty? mode << "+#{sym_to_level(sym(@client.privclasses[room][privclass]))}" count += 1 end send_packet :cmd => "localhost", :args => ["MODE", "##{room}", mode, *([us.username] * count)] us.symbol = sym(@client.privclasses[room][privclass]) us.privclass = privclass us.level = @client.privclasses[room][privclass] end end # Sends you an auth notice (in the server window) # @param [String] line the line to display # @return [void] def notice line send_packet :colon => false, :cmd => "NOTICE", :args => "AUTH", :content => "*** #{line}" end # Sends you a channel notice # @param [String] room the room to notice # @param [String] line the line to display # @return [void] def chan_notice room, line send_packet :cmd => "localhost", :args => ["NOTICE", "##{room}"], :content => line end # Compiles a packet and sends it to the server # @param [Hash] opts a list of options # @option opts [String] :cmd # @option opts [Array] :args # @option opts [String, nil] :content # @option opts [Boolean] :colon whether to put a colon in front or not # @see IRC::Packet # @return [void] def send_packet opts = {} opts = ({:colon => true, :cmd => "", :args => [], :content => nil}).merge(opts) str = opts[:colon] ? ":" : "" str << opts[:cmd] << " " << Array(opts[:args]).join(" ") str << " :#{opts[:content]}" if opts[:content] @socket.write(str + "\r\n") debug str end # Sends debug messages # @param [Array] strs all messages to send # @return [void] def debug *strs Server.debug *[strs, :irc_out] end end end end