#!/usr/bin/env ruby require 'open3' require 'socket' require 'openssl' require 'timeout' require 'base64' # configurable environment variables nick = ENV['OUR_NICK'] || 'our' channels = ENV['OUR_CHANNELS'] || '#tildetown,#bots' prefix = ENV['OUR_PREFIX'] || "#{nick}/" cmds_dir = ENV['OUR_CMDS_DIR'] || '/town/our' server = ENV['OUR_SERVER'] || 'localhost' use_ssl = ENV['OUR_USE_SSL'] == "true" || false port = ENV['OUR_IRC_PORT'] || 6667 sasl_user = ENV['OUR_SASL_USER'] || nil sasl_pass = ENV['OUR_SASL_PASS'] || nil module IRC class User attr_accessor :s def initialize(addr:, port:, nick:, use_ssl: false, sasl_user: nil, sasl_pass: nil) @hooks = [] sock = TCPSocket.open addr, port.to_s if use_ssl puts "connecting with SSL" ctx = OpenSSL::SSL::SSLContext.new ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER) @s = OpenSSL::SSL::SSLSocket.new(sock, ctx).tap do |socket| socket.sync_close = true socket.connect end else puts "connecting without SSL" @s = sock end if sasl_user && sasl_pass puts "connecting with SASL" s.puts "CAP REQ :sasl" s.puts "USER #{nick} m455.casa 1 :beep boop" s.puts "NICK #{nick}" s.puts "AUTHENTICATE PLAIN" plain_auth = Base64.encode64("#{sasl_user}\0#{sasl_user}\0#{sasl_pass}") s.puts "AUTHENTICATE #{plain_auth}" s.puts "CAP END" else puts "connecting without SASL" s.puts "USER #{nick} m455.casa 1 :beep boop" s.puts "NICK #{nick}" end hook do |m| next unless m.cmd == 'PING' raw "PING #{nick}" end end def raw msg @s.puts msg end def join chan raw "JOIN #{chan}" end def privmsg target, msg raw "PRIVMSG #{target} :#{msg}" end def hook &h @hooks << h end def loop while line = s.gets msg = Message.new line puts "S: #{msg.raw}" @hooks.each{|h| h.call(msg)} end end end class Message attr_accessor :prefix, :cmd, :args, :raw # TODO custom constructor def initialize msg msg = msg.delete_suffix "\r\n" @raw = msg @prefix = nil @prefix, msg = msg[1..].split(' ', 2) if msg[0] == ':' @cmd, msg = msg.split(' ', 2) @args = [] while msg and not msg.empty? if msg[0] == ':' @args << msg[1..] break end s, msg = msg.split(' ', 2) @args << s end end end end puts "starting" i = IRC::User.new(addr: server, port: port, nick: nick, use_ssl: use_ssl, sasl_user: sasl_user, sasl_pass: sasl_pass) channels.split(',').each { |channel| i.join channel } i.hook do |msg| next unless msg.cmd == 'PRIVMSG' target, content = msg.args next unless content.delete_prefix! prefix cmd, args = content.split(' ', 2) cmd = "#{cmds_dir}/#{cmd}" args ||= '' next unless File.exists? cmd if not File.executable? cmd i.privmsg target, "#{cmd} isn't executable. try chmod +x" next end begin Open3.popen2e("#{__dir__}/wrap_it.sh", cmd, args, msg.prefix, target) do |_, stdout, wait_thread| out = nil Timeout::timeout(3) do out = stdout.gets # only interested in the first line of output stdout.gets until stdout.eof? # make sure process finishes in time allotted end i.privmsg target, out if out rescue Timeout::Error Process.kill("KILL", wait_thread.pid) i.privmsg target, "[our.rb] command timed out" end rescue Exception => e i.privmsg target, "[our.rb] #{e.to_s}" end next true end i.loop