#!/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'] telnet = @conf['telnet'] admin = @conf['admin'] @auth_chat = @conf['authorized_chats'] @allowed_sources = @conf['allowed_sources'] @ip_provider = "ip.skyfall.tech" #@gif_url_prefix = "https://img.skyfalltech.net/togra/" @gif_url_prefix = @conf['gif_url_prefix'] @voice_chat_enabled = @conf['voice_chat_enabled'] @vchat_info = @conf['voice_chat_info'] ### 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") if !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 telnet configuration ........... " if telnet.nil? errcount += 1 print "FAIL!\n\n".red.bold puts "Error(#{errcount.to_s}): No telnet information provided in bot_config.yaml.\nThis is required for nearly all Empyrion-related " + "functions.\nTHIS SHOULD BE ADDRESSED. Continuing. (some commands will return broken messages)\n\n" else print "OK\n".green.bold end 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 GIF URL prefix ................. " if @gif_url_prefix.nil? errcount += 1 print "FAIL!\n\n".red.bold puts "Error(#{errcount.to_s}): No GIF URL prefix provided in bot_config.yaml.\nThis is required only for a couple of Easter egg features. " + "Using the Skyfall/Togra GIF prefix instead. Continuing.\n\n" @gif_url_prefix = "https://img.skyfalltech.net/togra/" else print "OK\n".green.bold end if @voice_chat_enabled == "true" print "Checking voice chat info ................ " if @vchat_info.nil? errcount += 1 print "FAIL!\n\n".red.bold puts "Error(#{errcount.to_s}): Voice chat features are enabled, but no voice chat application info is configured in bot_config.yaml!\n" + "THIS SHOULD BE ADDRESSED. Some functions may fail, but the core functionality should be unaffected. Continuing.\n\n" else print "OK\n".green.bold end 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 "Empyrion Host: #{telnet['host']}" puts "Empyrion Telnet Port: #{telnet['port']}" puts "Authorized administrator IDs: #{admin}" puts "Authorized chat IDs: #{@auth_chat}" puts "Bot token: #{@token}" puts "Temporary direcotry: #{@tmpdir}" puts "IP Provider: #{@ip_provider}" puts "GIF URL Prefix: #{@gif_url_prefix}" puts "Start time: " + timestamp + "\n\n\n\n" STDOUT.flush #def process_command_srvstart(message) # puts "Received command: srvstart" # reply = `./srvstart` #end # #def process_command_srvstop(message) # puts "Received command: srvstop" # reply = `./srvstop` #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 print timestamp + ": Sending ............ " STDOUT.flush message = Telegram::Bot::Client.run(@token) {|bot| message = bot.api.send_message(chat_id: chatid, text: "#{message_text}.", parse_mode: "HTML") } print "OK\n".green.bold STDOUT.flush puts "Sent: #{message_text.inspect}\n\n" STDOUT.flush #puts timestamp + ": Sent: #{message_text.inspect}\n\n" return message else #Send a plain-text message print timestamp + ": Sending ............ " STDOUT.flush message = Telegram::Bot::Client.run(@token) {|bot| bot.api.send_message(chat_id: chatid, text: message_text) } print "OK\n".green.bold puts "Sent: #{message_text.inspect}\n\n" STDOUT.flush #puts message #message = message["results"] #puts message return message end end def send_gif(chatid, cmd) #message = Telegram::Bot::Client.run(@token) {|bot| message = bot.api.send_message(chat_id: chatid, text: "#{message_text}.", parse_mode: "HTML") } gif_url = "#{@gif_url_prefix}#{cmd}.gif" print timestamp + ": Sending ............ " message = Telegram::Bot::Client.run(@token) {|bot| message = bot.api.sendVideo(chat_id: chatid, video: gif_url) } print "OK\n".green.bold puts "Sent GIF: #{gif_url}\n\n" STDOUT.flush return message 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' telnet = @conf['telnet'] adm = @conf['admin'] puts Time.now.strftime("%F %H:%M:%S").yellow + ": Received command from " + "#{message.from.username}".cyan.bold + " [" + "#{message.from.id}".cyan + "]: " + "#{command}".magenta.bold case command when '/start', '/help' process_command_start(message, command, adm) when '/chat', '/voice', '/mumble', '/teamspeak', '/discord', '/vox', '/voicechat' if @voice_chat_enabled == "true" process_command_voicechat(message, command, adm) else send_message(message.chat.id, "Refusal: My master has disabled this feature. Perhaps voice chat service does not exist for this group?") end when '/srvstart' reply = process_command_srvstart(message, command, adm) send_message(message.chat.id, reply) when '/srvstop' reply = process_command_srvstop(message, command, adm) send_message(message.chat.id, reply) when '/status', '/stat', '/stats', '/list', '/check' reply = process_command_srvstatus(message, command, adm) if command == '/check' reply = "Hi Matt, here's the /status.\n\n" + reply end send_message(message.chat.id, reply) when '/patch', '/patchnotes' reply = process_command_patchnotes(message, command, adm) send_message(message.chat.id, reply) when '/location', '/whereareyou' if message_from_admin?(message, adm) || is_chat_authorized?(message, @auth_chat) reply = "I am currently located at:\n\nHost: #{`head -n1 /etc/hostname`}ExtIP: #{`curl #{@ip_provider} 2>/dev/null`}" else reply = "Refusal: I am not authorized to provide this information here." end send_message(message.chat.id, reply) when '/whoami', '/chatinfo' #reply = "Answer: You are a meatbag named #{message.from.username}\n\nUser ID: #{message.from.id}\n\nChat ID: #{message.chat.id}" reply = "User ID: #{message.from.id}\nChat ID: #{message.chat.id}" if command == '/whoami' reply = "Answer: You are a meatbag named #{message.from.username}\n\n" + reply end if command == '/chatinfo' reply = reply + "\n\nInterjection: If you were intending to find information about how to connect to voice chat, try using the /voice command instead." end send_message(message.chat.id, reply) when '/pp', '/debug' pp message send_message(message.chat.id,"Confirmation: Message debug information sent to console.") when '/dance' if is_chat_authorized?(message, @auth_chat) || message_from_admin?(message, adm) send_gif(message.chat.id, 'dance') else send_message(message.chat.id,"Refusal: I am not authorized to bust a move in this location.") end when '/flex', '/unclemike' if is_chat_authorized?(message, @auth_chat) || message_from_admin?(message, adm) send_gif(message.chat.id, 'flex') else send_message(message.chat.id,"Refusal: I am not authorized to bring the gun show to this location.") end else send_message(message.chat.id,"Mockery: My name is #{message.from.username}, I am a meatbag, and I think #{command} is a valid command.") end rescue => e handle_exception(e, message, true) # Verbose output: #print timestamp + ": Sending #{reply.inspect} ..... " STDOUT.flush #puts "End of case" 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 #reply = handle_message(message) #print timestamp + ": Sending ............ " if ! message.text.nil? #if reply == "dance" || reply == "flex" # bot.api.sendVideo(chat_id: message.chat.id, video: "https://img.skyfalltech.net/togra/#{reply}.gif") #else ## bot.api.send_message(chat_id: message.chat.id, text: "#{reply}") ## print "OK\n".green.bold ## puts "Sent: #{reply.inspect}\n\n" 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 print 'FAIL'.red.bold print " (Message is NULL)\n\n" end STDOUT.flush else puts "Received bad data! [#{message.chat.type}]" puts validation STDOUT.flush end end end