diff --git a/callbacks.rb b/callbacks.rb new file mode 100644 index 0000000..a2f467b --- /dev/null +++ b/callbacks.rb @@ -0,0 +1,4 @@ +def process_callback_zone(message) + host = message.data.split("|")[1] + send_message(message.message.chat.id, "You have selected #{host}!") +end diff --git a/commands.rb b/commands.rb index 2c42a9c..67c076e 100644 --- a/commands.rb +++ b/commands.rb @@ -25,13 +25,53 @@ def is_chat_authorized?(message, auth_chat) end end +def process_command_start(message, command, adm) + reply = "I am #{@botname}, and I am here to provide temperature information from various sensors. Currently I can retrieve information " + + "from temperature sensors as well as report when temperatures are out of a specified range.\n\n" + + "Commands available:\n/start (Shows this message)\n/check (Show temperatures, currently non-functional)\n/whoami or /chatinfo (Provides IDs for internal use)\n/whereareyou or /location (Provides hostname for bot server)\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/heatbot" + 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 an administrator, 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_patchnotes(message, command, adm) if is_chat_authorized?(message, @auth_chat) || message_from_admin?(message, adm) - reply = `cat patchnotes.txt` + reply = File.read(static_text/patchnotes.txt) else reply = "I am not authorized to provide this information here." end - return reply + send_message(message.chat.id, reply) +end + +def process_command_location(message, command, adm) + 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 icanhazip.com 2>/dev/null`}" + else + reply = "I am not authorized to provide this information here." + end + #send_message_markdown(message.chat.id, reply) + send_message(message.chat.id, reply) +end + +def process_command_chatinfo(message) + reply = "User ID: #{message.from.id}\nChat ID: #{message.chat.id}" + send_message(message.chat.id, reply) end def process_command_check(message, command, adm) @@ -39,12 +79,15 @@ def process_command_check(message, command, adm) #begin interactive code options = [ ] @probes.each do |k,v| - button_text = v - options.insert(-1, Telegram::Bot::Types::InlineKeyboardButton.new(text: button_text, callback_data: "#{k}")) + puts " Option: #{k} is #{v["loc"].to_s}" + button_text = v["loc"].to_s + options.insert(-1, Telegram::Bot::Types::InlineKeyboardButton.new(text: button_text, callback_data: "ZONE|#{k}")) end + + message_text = "Which area would you like to check?\n" + send_question(message.chat.id, message_text, options) #end interactive code #probe = @probes[0] #set manually for now - reply = "N/A" #hostdata = host_lookup(select_loc) #puts "var:".blue+" hostdata".blue.bold+"::".bold + hostdata.to_s #if ! hostdata.empty? @@ -56,11 +99,9 @@ def process_command_check(message, command, adm) # reply = "No matching locations!" # puts "hostdata is empty!" #end - puts reply else - reply = "I am not authorized to provide this information here." + send_message(message.chat.id,"I am not authorized to provide this information here.") end - return reply end def process_command_test(message, command, adm) diff --git a/run.rb b/run.rb index 33785a3..10cf42f 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,7 +17,7 @@ end @conf = YAML.load_file("bot_config.yaml") @botname = @conf['botname'] @tmpdir = @conf['tmpdir'] -token = @conf['token'] +@token = @conf['token'] admin = @conf['admin'] @auth_chat = @conf['authorized_chats'] @allowed_sources = @conf['allowed_sources'] @@ -27,9 +28,9 @@ STDOUT.sync = true errcount = 0 puts "Checking if environment is sane...\n\n" 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..." + 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 @@ -119,11 +120,22 @@ STDOUT.sync = false puts "Starting [#{@botname}]...\n\n" puts "Authorized administrator IDs: #{admin}" puts "Authorized chat IDs: #{@auth_chat}" -puts "Bot token: #{token}" +puts "Bot @token: #{@token}" puts "Temporary direcotry: #{@tmpdir}" 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) #@probes.select { |k,v| v['loc'] == select_loc }.each do |host,loc| # return host @@ -131,21 +143,83 @@ def host_lookup(select_loc) return @probes.select { |k,v| v['loc'] == select_loc } 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: "#{message.from.username}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 send_message(chatid, message_text, imageurl = nil) + if imageurl != nil + #Send message with text as html link to image + Telegram::Bot::Client.run(@token) {|bot| bot.api.send_message(chat_id: chatid, text: "#{message_text}.", parse_mode: "HTML") } + puts timestamp + ": Sent: #{message_text.inspect}\n\n" + else + #Send a plain-text 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" + end + STDOUT.flush +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 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 + 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 @@ -159,91 +233,108 @@ def handle_message(message) #Format command command = message.text.split(" ")[0].split("@")[0].downcase #Strip command from arguments and @tags - ## 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 - reply = 'Empty String' 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 + puts timestamp + ": Received command from " + "#{message.from.username}".cyan.bold + " [" + "#{message.from.id}".cyan + "]: " + "#{command}".magenta.bold case command when '/start' - reply = "I am #{@botname}, and I am here to provide temperature information from various sensors. Currently I can retrieve information " + - "from temperature sensors as well as report when temperatures are out of a specified range.\n\n" + - "Commands available:\n/start (Shows this message)\n/check (Show temperatures, currently non-functional)\n/whoami or /chatinfo (Provides IDs for internal use)\n/whereareyou or /location (Provides hostname for bot server)\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/heatbot" - 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 an administrator, I have not been authorized to participate in this group. My functionality is limited." - end - when '/patch', '/patchnotes' - reply = process_command_patchnotes(message, command, adm) + process_command_start(message, command, adm) + when '/patch', '/patchnotes', '/version', '/versioninfo' + process_command_patchnotes(message, command, adm) 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`}" - else - reply = "I am not authorized to provide this information here." - end + process_command_location(message, command, adm) when '/whoami', '/chatinfo' - reply = "User ID: #{message.from.id}\nChat ID: #{message.chat.id}" + process_command_chatinfo(message) when '/check' - reply = process_command_check(message, command, adm) + process_command_check(message, command, adm) when '/test' - reply = process_command_test(message, command, adm) + process_command_test(message, command, adm) 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.") else - reply = "Sorry, #{command} is not a valid command." + 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 -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 movie/show 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 + 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) - #case message - # when Telegram::Bot::Types::Message - reply = handle_message(message) #entrypoint for all messages - # when Telegram::Bot::Types::CallbackQuery - # reply = handle_callback_query(message) #entrypoint for all callback queries - #end - print timestamp + ": Sending ..... " - if ! message.text.nil? - bot.api.send_message(chat_id: message.chat.id, text: "#{reply}") - print "OK\n".green.bold - puts "Sent: #{reply.inspect}\n\n" - else - print 'FAIL'.red.bold - print " (Message is NULL)\n\n" + #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 - STDOUT.flush else puts "Received bad data! [#{message.chat.type}]" puts validation diff --git a/patchnotes.txt b/static_text/patchnotes.txt similarity index 100% rename from patchnotes.txt rename to static_text/patchnotes.txt