#!/usr/local/bin/ruby ############################################################################ ### FRuby Client 1.1 by Retnur ### A big thanks goes out to Kiasyn. ############################################################################ # Copyright (c) 2009, Jeffrey Heath Basurto <bigng22@gmail.com> # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ############################################################################ require "net/telnet.rb" require 'thread' require "eventmachine" require 'strscan' # For standalone colors. $color_table = { "#z" => "\e[0;30m", "#Z" => "\e[1;30m", "#r" => "\e[0;31m", "#R" => "\e[1;31m", "#g" => "\e[0;32m", "#G" => "\e[1;32m", "#y" => "\e[0;33m", "#Y" => "\e[1;33m", "#b" => "\e[0;34m", "#B" => "\e[1;34m", "#m" => "\e[0;35m", "#M" => "\e[1;35m", "#c" => "\e[0;36m", "#C" => "\e[1;36m", "#w" => "\e[0;37m", "#W" => "\e[1;37m", "#n" => "\e[0m", } $comp_tab = Regexp.union *$color_table.keys def render_color(data) last_code = nil data.gsub($comp_tab) do |s| if last_code != nil && last_code.eql?(s) "" else last_code = s $color_table[s] end end end ### String extensions ### class String # Return the first line in a string def pop_line if (pos = index("\n")) != nil then return slice!(0..pos).chomp end nil end def encapsulate s = self s = s.reverse s.concat '"' s = s.reverse s.concat '"' return s end end ### Net::Telnet extensions ### class Net::Telnet def read_from_telnet # :yield: recvdata time_out = @options["Timeout"] waittime = @options["Waittime"] if time_out == false time_out = nil end line = '' buf = '' rest = '' until(not IO::select([@sock], nil, nil, waittime)) unless IO::select([@sock], nil, nil, time_out) raise TimeoutError, "timed out while waiting for more data" end begin c = @sock.readpartial(1024 * 1024) @dumplog.log_dump('<', c) if @options.has_key?("Dump_log") buf = c buf.gsub!(/#{EOL}/no, "\n") unless @options["Binmode"] rest = '' @log.print(buf) if @options.has_key?("Output_log") line += buf yield buf if block_given? rescue # End of file reached if line == '' line = nil yield nil if block_given? end break end end line end end ### Look for usage below for examples. ### You should only need to instantiate this a single time. class IMCclient ### This is called when IMCclient is instantiated. def initialize(name, pw) @connection = Net::Telnet::new( "Host" => "www.mudbytes.net", "Port" => 5000, "Telnetmode" => false, "Prompt" => /\n/) @pw = pw # Start authenticating. Will autosetup if IMC server does not have configuration. @connection.puts "PW #{name} #{pw} version=2 autosetup #{pw}2" # Set the sequence, which is a number associated with each packet @sequence = Time.now.to_i @myname = name end ### call this in some event. It really should called at least twice a second. ### This continually checks for incoming packets def accept_data # empty string s= "" ### read from connection and put it in s @connection.read_from_telnet do |c| break if c == nil s << c end ### If s is empty we return. return if s.chomp.empty? ### Search for new packets line by line and process. while ( (line = s.pop_line) != nil ) ### Does something with each line. Probably a chat message. handle_server_input line end end ### Handles raw input ### Directs towards packets. def handle_server_input(s) # <sender>@<origin> <sequence> <route> <packet-type> <target>@<destination> <data...> case s.strip #On initial client connection: #SERVER Sends: autosetup <servername> accept <networkname> (SHA256-SET) when /^autosetup (\S+) accept (\S+)$/ puts "Autosetup complete. Connected to #{$1} on network #{$2}\n" when /^PW Server\d+ #{@pw}2 version=2 (\S+)$/i puts "IMC Authentication complete\n" send_isalive when /^(\S+) \d+ \S+ (\S+) (\S+)$/i handle_packet( $1, $2, $3 ) when /^(\S+) \d+ \S+ (\S+) (\S+) (.*)$/i handle_packet( $1, $2, $3, $4 ) else puts "Not found! #{s}" end end ### Processes all packets and directs it. def handle_packet( sender, type, target, data=nil ) if data != nil and data.is_a?( String ) then new_data = Hash.new data.strip! data = StringScanner.new(data) while !data.eos? key = data.scan(/\w+/) ### grab a key data.skip(/=/) ### skip the = if data.peek(1).eql?('"') val = data.scan(/"([^"\\]*(\\.[^"\\]*)*)"/) val.slice!(0) val.slice!(-1) else val = "" while (!data.eos? && !data.peek(1).eql?(' ')) val << data.getch end end data.skip(/\s+/) new_data[key] = val end data = new_data end return if sender.include?("@#{@myname}") return if data != nil and data.include? 'sender' and data['sender'].include? "@#{@myname}" case type when "keepalive-request" send_isalive when "ice-msg-b" puts render_color("#R[#{data['channel']}] #B#{sender}: #b#{data['text']}") end end ### Sends a packet in the right format. def packet_send( sender, type, target, destination, data) data_out = "" if data.is_a?( Hash ) then hash = data.to_hash hash.each do |k, v| if v.to_s.include? " " then v = v.encapsulate end data_out.concat "#{k}=#{v} " end end packet = "#{sender}@#{@myname} #{@sequence} #{@myname} #{type} #{target}@#{destination} #{data_out}" @sequence += 1 @connection.puts packet end ### This formats for a specific channel. ### channel_send "Retnur", Server01:ichat, "example string" def channel_send( sender, channel, message ) packet_send( sender, 'ice-msg-b', '*', '*', {:channel => channel, :text => message, :emote => 0, :echo => 1 }) end ### Sends info to IMC server about mud. ### host is your muds address, port is its port. url is your web site. ### This isn't required. def send_isalive packet_send( "*", "is-alive", "*", "*", { :versionid => "FRuby Client1.0", :url => "", :host => "", :port => 0} ) end ### Shut the IMCclient down. def shutdown puts "Connection closed." @connection.close end end ### A list of all known IMC channels. $imc_channel_list = {"icode"=>"Server02:icode", "igame"=>"Server02:igame", "ichat"=>"Server01:ichat", "inews"=>"Server02:inews", "imudnews"=>"Server02:imudnews", "ibuild"=>"Server01:ibuild", "imusic"=>"Server02:imusic"} $chan_compiled = Regexp.union *$imc_channel_list.keys ### Get your name. puts "Type quit to exit." print "What is your name? " name = gets name = name.chomp ### Beginning of actual execution. ### You ned to change this name and password. ### create our client and a lock for it. client = IMCclient.new("RClient_Test", "RClient") imclock = Mutex.new ### Run EventMachine in a new thread. ### This lets it autonomously receive data ### even when the application stops waiting on user input. t = Thread.new() do EventMachine::run do EM::add_periodic_timer(0.01) do ### takes control of the lock for our IMC client. imclock.synchronize do client.accept_data end end end end ### Defaults to ichat $cur_channel = "Server01:ichat" ### This loop for all processing of input. ### accept input forever loop do print "\n>>>" ### Gets a full line of input, and stops this thread while we wait. s = gets s = s.strip next if s.empty? ### Type quit to end the stand alone client. break if s =~ /quit|shutdown/i if s =~ $chan_compiled ### Channel found so set our chan to this $cur_channel = $imc_channel_list[s] ### Should set our current channel to this puts "Channel set to " + s + "." next end print "\n" ### Syncs our thread just in case we do something that isn't totally thread safe there. imclock.synchronize do client.channel_send("#{name.capitalize}", $cur_channel, s) puts render_color("#R[#{$cur_channel}] #b#{name.capitalize}:#C #{s}#n") end end client.shutdown