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.
469 lines
16 KiB
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 = 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}<a href=\"#{imageurl}\">.</a>", 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
|
|
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
|
|
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
|
|
|