#!/usr/bin/env ruby require 'rubygems' require 'yaml' require 'telegram/bot' require 'pp' require 'time' require_relative 'commands.rb' require_relative 'callbacks.rb' require_relative 'colors.rb' def timestamp Time.now.strftime("%F %H:%M:%S").yellow end #conf = YAML.load(File.read("bot_config.yaml")) @conf = YAML.load_file("bot_config.yaml") @botname = @conf['botname'] @tmpdir = @conf['tmpdir'] @token = @conf['token'] admin = @conf['admin'] @auth_chat = @conf['authorized_chats'] @allowed_sources = @conf['allowed_sources'] @probes = @conf['probes'] @tunit = @conf['units'] ## Non-configurable @simplehot = 24 @simplecold = 10 @ip_provider = "ip.skyfall.tech" ### Begin sanity check ### STDOUT.sync = true errcount = 0 puts "Checking if environment is sane...\n\n" print "Checking system utilities ............... " if !system("which nmap 2>&1 >/dev/null") print "FAIL!\n\n".red.bold puts "nmap".yellow.bold + " not found. This utility requires the 'nmap' command (for testing connectivity to temperature probes)" puts "THIS IS REQUIRED!".red.bold + " Bot initialization failed; exiting..." exit(1) elsif !system("which curl 2>&1 >/dev/null") print "FAIL!\n\n".red.bold puts "curl".yellow.bold + " not found. This utility requires the 'curl' command (for providing IP information to users upon request)" puts "THIS IS REQUIRED!".red.bold + " Bot initialization failed; exiting..." exit(1) else print "OK\n".green.bold end print "Checking bot token ...................... " if @token.nil? print "FAIL!\n\n".red.bold puts "No bot @token defined in bot_config.yaml!\n" + "THIS IS REQUIRED!".red.bold + " Bot initialization failed; exiting..." exit(1) else print "OK\n".green.bold end print "Checking configured bot name ............ " if @botname.nil? errcount += 1 print "FAIL!\n\n".red.bold puts "Error(#{errcount.to_s}): No bot name defined. This is superficial. We'll call him Bob.\n\n" @botname = "Bob" else print "OK\n".green.bold end ### Temporary directory check print "Checking configured tmp directory ....... " def is_tmp_writable? system("mkdir -p #{@tmpdir} >/dev/null 2>&1") if system("touch #{@tmpdir}/test.file >/dev/null 2>&1") system("rm #{@tmpdir}/test.file >/dev/null 2>&1") return true else system("rm #{@tmpdir}/test.file >/dev/null 2>&1") #Attempt to clean up anyway return false end end if @tmpdir.nil? errcount += 1 print "FAIL!\n\n".red.bold puts "Error(#{errcount.to_s}): No temporary directory defined. Using '/tmp/.skyfall/egs-bot'.\n" @tmpdir = "/tmp/.skyfall/egs-bot" if is_tmp_writable? puts "Default temporary directory is writable. Continuing...\n\n" else errcount += 1 puts "Error(#{errcount.to_s}): Temporary directory [" + @tmpdir.red.bold + "] is not writable!\n" + "THIS IS REQUIRED!".red.bold + " Bot initialization failed; exiting..." exit(1) end else if is_tmp_writable? print "OK\n".green.bold else errcount += 1 print "FAIL!\n\n".red.bold puts "Error(#{errcount.to_s}): Temporary directory [" + @tmpdir.red.bold + "] is not writable!\n" + "THIS IS REQUIRED!".red.bold + " Bot initialization failed; exiting..." exit(1) end end ### End tmpdir check print "Checking administrators ................. " if admin.nil? errcount += 1 print "FAIL!\n\n".red.bold puts "Error(#{errcount.to_s}): No admin Telegram IDs provided in bot_config.yaml.\nThis is required for many functions.\n" + "THIS SHOULD BE ADDRESSED. Continuing. (some commands will not be available)\n\n" admin = ["0"] else print "OK\n".green.bold end print "Checking authorized chats ............... " if @auth_chat.nil? errcount += 1 print "FAIL!\n\n".red.bold puts "Error(#{errcount.to_s}): No authorized Telegram group IDs provided in bot_config.yaml.\nThis is required for most Empyrion-related " + "functions.\nTHIS SHOULD BE ADDRESSED. Continuing. (some commands will not be available)\n\n" @auth_chat = ["0"] else print "OK\n".green.bold end print "Checking temperature units .............. " if @tunit.nil? errcount += 1 print "FAIL!\n\n".red.bold puts "Error(#{errcount.to_s}): Temperature units not configured! Defaulting to Rankine. Continuing...\n\n" @tunit = 'R' else case @tunit.upcase when "C", "F", "K", "R", "RA" @tunit = @tunit.capitalize print "OK\n".green.bold else errcount += 1 print "FAIL!\n\n".red.bold puts "Error(#{errcount.to_s}): Temperature unit '#{@tunit}' not recognized! Defaulting to Rankine. Continuing...\n\n" @tunit = 'R' end end print "Checking probes list .................... " if @probes.nil? errcount += 1 print "FAIL!\n\n".red.bold puts "Error(#{errcount.to_s}): No temperature probes configured! Bot will serve no purpose. Continuing anyway...\n\n" else print "OK\n".green.bold end puts "Errors found: #{errcount.to_s}\n\n" if errcount > 0 print "Environment is grinning and holding a spatula. Please review your configuration.\n\n".red.bold else print "Environment appears sane.\n\n".green.bold end STDOUT.sync = false ### End sanity check ### puts "Starting [#{@botname}]...\n\n" puts "Authorized administrator IDs: #{admin}" puts "Authorized chat IDs: #{@auth_chat}" puts "Bot token: #{@token}" puts "Temporary direcotry: #{@tmpdir}" puts "Temperature unit: #{@tunit}" puts "Probes loaded:" @probes.each do |k,v| #puts " [#{k}] #{v["loc"].to_s}" puts " [" + "#{k}: ".bold + v['port'].to_s.green.bold + "] #{v['loc'].to_s}" end puts "Start time: " + timestamp + "\n\n\n\n" STDOUT.flush ## Constant collection model (may be scrapped) #last_collection = 0 #loop do # collection_time = Time.now # if collection_time - last_collection >= 10 # puts timestamp + ": [#{collection_time}] Updating records //Not really" # last_collection = collection_time # STDOUT.flush # end #end def host_lookup(select_loc) return @probes.select { |k,v| v['loc'] == select_loc } end class String def is_integer? /\A[-+]?\d+\z/ === self end def is_float? /^\s*[+-]?((\d+_?)*\d+(\.(\d+_?)*\d+)?|\.(\d+_?)*\d+)(\s*|([eE][+-]?(\d+_?)*\d+)\s*)$/ === self end end def process_tdata(host, port, simple=false) print "Processing #{host}: " STDOUT.flush if system("nmap #{host} -p #{port} 2>&1 | grep #{port} | grep open >/dev/null") tdata = `ssh -p #{port} -oBatchMode=yes #{host} heatbot_gettemp`.chomp #if tdata.is_integer? if tdata.is_float? tdf = tdata.to_f case @tunit.upcase when "C" #Do nothing; expected input is Celsius when "F" tdf = (tdf * 1.8) + 32 when "K" tdf = tdf + 273.15 when "R", "RA" tdf = (tdf * 1.8) + 491.67 else puts "#{@tunit} not valid temperature unit! Submitting unmodified output!" end ctdata = tdf.round.to_s print ctdata + "°#{@tunit}" + " (" + "Raw: ".bold + "#{tdata})" if simple #This is mostly just an Easter egg if tdata.to_i > @simplehot sdata = "Hot" elsif tdata.to_i < @simplecold sdata = "Cold" else sdata = "Fair" end print " [#{sdata}]\n" STDOUT.flush return sdata else print "\n" STDOUT.flush return ctdata + "°#{@tunit}" end else puts "Unexpected output from [".red.bold + host.bold + "]: ".red.bold + tdata STDOUT.flush return "CHECK PROBE" end else print "OFFLINE\n" STDOUT.flush return "OFFLINE" end end def ack_callback(message, display_message = true) #Delete message and notify user that we got the request begin Telegram::Bot::Client.run(@token) do |bot| if display_message == true bot.api.editMessageText(chat_id: message.message.chat.id, message_id: message.message.message_id, text: "Request received. Please wait...", reply_markup: "") #Removes buttons. Changes text bot.api.answerCallbackQuery(callback_query_id: message.id, show_alert: false, text: "Request received. Please wait...") #Sends a pop-up notification else bot.api.deleteMessage(chat_id: message.message.chat.id, message_id: message.message.message_id) #Deletes message and buttons end end rescue puts "Error handling callback query. Error: " + $!.message end STDOUT.flush end def delete_message(message) #Deletes a message referred to by message_id begin Telegram::Bot::Client.run(@token) do |bot| bot.api.deleteMessage(chat_id: message.message.chat.id, message_id: message.message.message_id) #Deletes message and buttons end rescue puts "Error deleting message. Error: " + $!.message end STDOUT.flush end def delete_confirmation(message) #Deletes a message referred to by message_id begin Telegram::Bot::Client.run(@token) do |bot| bot.api.deleteMessage(chat_id: message.chat.id, message_id: message.message_id) end rescue puts "Error deleting message. Error: " + $!.message end STDOUT.flush end def send_message(chatid, message_text, imageurl = nil) if imageurl != nil #Send message with text as html link to image message = Telegram::Bot::Client.run(@token) {|bot| message = bot.api.send_message(chat_id: chatid, text: "#{message_text}.", parse_mode: "HTML") } puts timestamp + ": Sent: #{message_text.inspect}\n\n" STDOUT.flush #puts timestamp + ": Sent: #{message_text.inspect}\n\n" return message else #Send a plain-text message message = Telegram::Bot::Client.run(@token) {|bot| bot.api.send_message(chat_id: chatid, text: message_text) } puts timestamp + ": Sent: #{message_text.inspect}\n\n" STDOUT.flush #puts message #message = message["results"] #puts message return message end end def send_message_markdown(chatid, message_text) #Send a plain-text message Telegram::Bot::Client.run(@token) {|bot| bot.api.send_message(chat_id: chatid, text: "```#{message_text}```", parse_mode: 'Markdown') } puts timestamp + ": Sent: #{message_text.inspect}\n\n" STDOUT.flush end def send_question(chatid, question_text, answers = [ ]) if ! answers.empty? then begin keyboard = Telegram::Bot::Types::InlineKeyboardMarkup.new(inline_keyboard: answers) Telegram::Bot::Client.run(@token) {|bot| bot.api.send_message(chat_id: chatid, text: question_text, reply_markup: keyboard) } rescue puts timestamp + ": " + "ERROR".red.bold + ": " + $!.message end else puts timestamp + "send_question called without any possible answers provided" end STDOUT.flush end def handle_message(message) if ! message.reply_to_message.nil? then #drop message. Someone's replying to a message sent by our bot message.text = nil return end if message.text.nil? # Find out if user(s) joined the group. If so, welcome them if ! message.new_chat_members.nil? handle_user_join(message) else #Handle non-messages and non-joins here end return #so that we don't try to process this as a command (below) end #Format sender name if ! message.from.username.nil? message.from.username = "@" + message.from.username elsif ! message.from.first_name.nil? message.from.username = message.from.first_name end #Format command command = message.text.split(" ")[0].split("@")[0].downcase #Strip command from arguments and @tags reply = 'Empty String' adm = @conf['admin'] puts timestamp + ": Received command from " + "#{message.from.username}".cyan.bold + " [" + "#{message.from.id}".cyan + "]: " + "#{command}".magenta.bold STDOUT.flush case command when '/start', '/help' process_command_start(message, command, adm) when '/patch', '/patchnotes', '/version', '/versioninfo' process_command_patchnotes(message, command, adm) when '/location', '/whereareyou' process_command_location(message, command, adm) when '/whoami', '/chatinfo' process_command_chatinfo(message) when '/check', '/c' process_command_check(message, command, adm) when '/report', '/r' process_command_report(message, command, adm) #when '/simple', '/s', '/sc' # process_command_check(message, command, adm, true) #when '/sreport', '/simpler', '/simplereport', '/sr' when '/simple', '/s', '/sreport', '/simpler', '/simplereport', '/sr' process_command_report(message, command, adm, true) when '/pp', '/debug' pp message send_message(message.chat.id"Confirmation: Message debug information sent to console.") else send_message(message.chat.id,"Sorry, #{command} is not a valid command.") end rescue => e handle_exception(e, message, true) # Verbose output: #print timestamp + ": Sending #{reply.inspect} ..... " STDOUT.flush return reply end def handle_callback_query(message) #callbacks that start with a "!" ("!DMS|tt123456") can ONLY be submitted #by an admin. Ignore if normal user presses #Get "DLM" from "DLM|abc123" command = message.data.split("|")[0].upcase if command.start_with?('!') then #verify an admin pressed this button if ! message_from_admin?(message) Telegram::Bot::Client.run(@token) {|bot| bot.api.answerCallbackQuery(callback_query_id: message.id, show_alert: false, text: "Requires admin approval")} return end command = command.split("!")[1] elsif command.start_with?('#') then #is this an ignored command return end ack_callback(message) #Change the selection message to "Request received..." case command when "ZONE" #Get temp info from location process_callback_zone(message) end delete_message(message) #Delete the "Request received..." message rescue => e handle_exception(e, message, true) end def handle_exception(e, message, notify_users) puts "=" * 60 puts "EXCEPTION OCCURRED!".red.bold puts "=" * 60 puts "PRINTING INSPECT...".yellow.bold puts e.inspect puts "=" * 60 puts "PRINTING BACKTRACE...".yellow.bold puts e.backtrace puts "=" * 60 STDOUT.flush if notify_users == true then #is this a callback query or a message case message when Telegram::Bot::Types::Message send_message(message.chat.id, "I have run into an issue while processing a command.\n\nPlease notify an administrator.") when Telegram::Bot::Types::CallbackQuery send_message(message.message.chat.id, "I have run into an issue while processing a request.\n\nPlease notify an administrator.") end end end Telegram::Bot::Client.run(@token) do |bot| bot.listen do |message| #pp message validation = validate_incoming_data(message) #puts "DEBUG: #{validation}" if validation #Change message.from.username to something we can call the user. This makes referring to the user in replies much easier. if ! message.from.username.nil? #Username -> @Username #message.from.username = "@" + message.from.username elsif ! message.from.first_name.nil? #Username -> John message.from.username = message.from.first_name end case message when Telegram::Bot::Types::Message handle_message(message) #entrypoint for all messages when Telegram::Bot::Types::CallbackQuery handle_callback_query(message) #entrypoint for all callback queries end else puts "Received bad data! [#{message.chat.type}]" puts validation STDOUT.flush end end end