diff --git a/.gitignore b/.gitignore index e85ebed..86a1311 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /bot_config.yaml /.ignore +/static_text/custom/ diff --git a/bot_config.yaml.example b/bot_config.yaml.example index af7fa4e..1692c16 100644 --- a/bot_config.yaml.example +++ b/bot_config.yaml.example @@ -13,4 +13,11 @@ allowed_sources: - 'private' - 'group' - 'supergroup' +#gif_url_prefix: "" +voice_chat_enabled: 'true' +voice_chat_info: + type: 'Mumble' + address: 'chat.example.com' + port: 64738 + password: 'ilikebirds' diff --git a/commands.rb b/commands.rb index a9c3b14..2ac7f14 100644 --- a/commands.rb +++ b/commands.rb @@ -31,6 +31,67 @@ def is_chat_authorized?(message, auth_chat) end end +#def send_gif(chat, cmd) +# bot.api.sendVideo(chat_id: chat, video: "#{@gif_url_prefix}#{cmd}.gif") +#end + +def process_command_start(message, command, adm) + reply = "Introduction: I am #{@botname}, and I am here to make life easier for meatbag admins like you. Currently I can retrieve information " + + "from an Empyrion server as well as kill the server process. I also have some pretty sweet dance moves.\n\n" + + "Commands available:\n/start or /help (Shows this message)\n/voice or /chat (Shows voice chat info)\n/srvstart (Currently non-functional)\n/srvstop\n/status\n/whoami or /chatinfo\n/whereareyou or /location\n\n" + + "Check again later to see if any new functions have been added, or use /patchnotes to learn about recent updates.\n" + + "You can also view the source code at the following location:\nhttps://git.skyfall.tech/skyfall/empyrion-bot" + if message_from_admin?(message, adm) + msg_from_admin = true + end + if is_chat_authorized?(message, @auth_chat) + chat_authorized = true + end + if ! msg_from_admin && ! chat_authorized + if message.from.id == message.chat.id + reply = reply + "\n\nWARNING: I am not authorized to work with you directly. My functionality is limited." + else + reply = reply + "\n\nWARNING: I am not authorized to participate with this group. My functionality is limited." + end + elsif msg_from_admin && ! chat_authorized + reply = reply + "\n\nWARNING: Although you are my master, I have not been authorized to participate in this group. My functionality is limited." + end + send_message(message.chat.id, reply) +end + +def process_command_voicechat(message, command, adm, vchat_info = @vchat_info) + if is_chat_authorized?(message, @auth_chat) || message_from_admin?(message, adm) + print "Checking connectivity to #{vchat_info['type']} service... " + STDOUT.flush + if vchat_info['type'].downcase != 'discord' + if system("nc -zvw3 #{vchat_info['address']} #{vchat_info['port']} >/dev/null 2>&1") + print "OK\n".green.bold + STDOUT.flush + reply = "#{vchat_info['type']} service is online.\n\n" + else + print "FAIL\n".red.bold + STDOUT.flush + reply = "#{vchat_info['type']} service is not responding!\n\n" + end + else + reply = "" + end + reply = reply + "Voice Chat Connection Information\n" + + "-------------------------------------------------\n" + + "Application: #{vchat_info['type']}\n" + + "Address: #{vchat_info['address']}\n" + + "Port: #{vchat_info['port']}\n" + + "Password: #{vchat_info['password']}\n" + if system("test -f static_text/custom/voicechat.txt") + reply = reply + File.read("static_text/custom/voicechat.txt") #Add custom info + end + STDOUT.flush + else + reply = "Refusal: I am not authorized to provide this information here." + end + send_message(message.chat.id, reply) +end + def process_command_srvstart(message, command, adm) #puts "Received command: " + "srvstart".green #pp message @@ -84,7 +145,7 @@ def process_command_srvstatus(message, command, adm) reply = `./scripts/srvstatus #{telnet['host']} #{telnet['port']} #{telnet['pass']} #{@tmpdir}` else print "Server did not respond!\nSending failure message...\n".red.bold - reply = "Server is not responding!" + reply = "Exclamation: Server is not responding!" end STDOUT.flush else @@ -95,11 +156,10 @@ end def process_command_patchnotes(message, command, adm) if is_chat_authorized?(message, @auth_chat) || message_from_admin?(message, adm) - telnet = @conf['telnet'] - reply = `cat patchnotes.txt` + #telnet = @conf['telnet'] + reply = File.read("static_text/patchnotes.txt") else reply = "Refusal: I am not authorized to provide this information here." end return reply end - diff --git a/run.rb b/run.rb index d3f443c..763e593 100755 --- a/run.rb +++ b/run.rb @@ -6,6 +6,7 @@ require 'telegram/bot' require 'pp' require 'time' require_relative 'commands.rb' +#require_relative 'callbacks.rb' require_relative 'colors.rb' def timestamp @@ -16,18 +17,38 @@ end @conf = YAML.load_file("bot_config.yaml") @botname = @conf['botname'] @tmpdir = @conf['tmpdir'] -token = @conf['token'] +@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? +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) @@ -108,6 +129,29 @@ if @auth_chat.nil? 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 @@ -121,8 +165,10 @@ 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 "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 @@ -136,10 +182,100 @@ STDOUT.flush # 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 + 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_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" + message = Telegram::Bot::Client.run(@token) {|bot| message = bot.api.sendVideo(chat_id: chatid, video: gif_url) } + puts timestamp + ": 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 + #drop message. Someone's replying to a message sent by our bot message.text = nil return end @@ -154,102 +290,166 @@ def handle_message(message) 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 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 - #command = message.text reply = 'Empty String' - #conf = YAML.load_file("bot_config.yaml") 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' - reply = "Introduction: I am #{@botname}, and I am here to make life easier for meatbag admins like you. Currently I can retrieve information " + - "from an Empyrion server as well as kill the server process. I also have some pretty sweet dance moves.\n\n" + - "Commands available:\n/start (Shows this message)\n/srvstart (Currently non-functional)\n/srvstop\n/status\n/whoami or /chatinfo\n/whereareyou or /location\n\n" + - "Check again later to see if any new functions have been added, or use /patchnotes to learn about recent updates.\n" + - "You can also view the source code at the following location:\nhttps://git.skyfall.tech/skyfall/empyrion-bot" - if message_from_admin?(message, adm) - msg_from_admin = true - end - if is_chat_authorized?(message, @auth_chat) - chat_authorized = true - end - if ! msg_from_admin && ! chat_authorized - if message.from.id == message.chat.id - reply = reply + "\n\nWARNING: I am not authorized to work with you directly. My functionality is limited." - else - reply = reply + "\n\nWARNING: I am not authorized to participate with this group. My functionality is limited." - end - elsif msg_from_admin && ! chat_authorized - reply = reply + "\n\nWARNING: Although you are my master, I have not been authorized to participate in this group. My functionality is limited." + 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' reply = process_command_srvstatus(message, command, adm) + 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 icanhazip.com 2>/dev/null`}" + 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 - reply = "Confirmation: Message debug information sent to console." + 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) - reply = "dance" + send_gif(message.chat.id, 'dance') else - reply = "Refusal: I am not authorized to bust a move in this location." + 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) - reply = "flex" + send_gif(message.chat.id, 'flex') else - reply = "Refusal: I am not authorized to bring the gun show to this location." + send_message(message.chat.id,"Refusal: I am not authorized to bring the gun show to this location.") end else - reply = "Mockery: My name is #{message.from.username}, I am a meatbag, and I think #{command} is a valid command." + 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 -Telegram::Bot::Client.run(token) do |bot| +### 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 - reply = handle_message(message) - print timestamp + ": Sending ............ " + #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" + #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 diff --git a/patchnotes.txt b/static_text/patchnotes.txt similarity index 89% rename from patchnotes.txt rename to static_text/patchnotes.txt index 161bb98..9df4c17 100644 --- a/patchnotes.txt +++ b/static_text/patchnotes.txt @@ -1,6 +1,10 @@ SkyfallTech EGS Telebot Patch Notes: +v0.5.00 ++ Added Voicechat information feature. ++ Rewrite of message handling based on Skyfall Heatbot. + v0.4.01 + Better logging in cases where sending message fails.