You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
heatbot/run.rb

469 lines
16 KiB

#!/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 = 75
#@simplecold = 50
@simplehot = 24
@simplecold = 10
### 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
when "C", "F", "K", "R"
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}"
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)
#@probes.select { |k,v| v['loc'] == select_loc }.each do |host,loc|
# return host
#end
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, simple=false)
print "Processing #{host}: "
STDOUT.flush
if system("nmap #{host} -p 22 2>&1 | grep 22 | grep open >/dev/null")
tdata = `ssh -oBatchMode=yes #{host} heatbot_gettemp`
#if tdata.is_integer?
if tdata.is_float?
tdf = tdata.to_f
case @tunit
when "C"
#Do nothing; expected input is Celsius
when "F"
tdf = (tdf * 1.8) + 32
when "K"
tdf = tdf + 273.15
when "R"
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}<a href=\"#{imageurl}\">.</a>", parse_mode: "HTML") }
#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"
#puts message
#message = message["results"]
#puts message
return message
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
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
case command
when '/start'
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
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