diff --git a/README.md b/README.md index bbc3dcd..55e25a0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,108 @@ #HeatBot +A Ruby application to provide remote temperature probe data over Telegram, to groups or to individuals + +------------------------------------------------------------------------------- + +##Prerequisites +You will need the following packages on your **primary bot server**: +- nmap +- curl +- ruby +As well as these Ruby gems: +- telegram-bot-ruby +- yaml +- time + +For each **remote temperature probe**: +- bc +- w1-gpio (kernel module) +- w1-therm (kernel module) + +##Configuration +####bot_config.yaml +After cloning this git repo, the first thing you will want to do is copy `bot_config.yaml`.example to `bot_config.yaml` and edit the values. Below are the values with a description: + +**botname:** The name of your bot. This is cosmetic, and does not alter any functions. +**tmpdir:** Temporary directory the bot will use when needed. +**token:** Private Bot token. Can be obtained from @botfather on Telegram. +**admin:** This is an array of admin users. Currently, there are no admin-only functions built into the bot at this time. +**authorized_chats:** These are the chat IDs (or user IDs for individual, non-group interaction) in which the bot is allowed to respond with any temperature or self-identifying information. User information will still be provided publicly by design, so you can use /chatinfo to find your own information you need to add to be authorized. +**allowed_sources:** These are the types of messages to allow. By default, all message types are allowed. It is recommended to leave this alone, but the option is there to restrict it further. +**units:** The default temperature unit. Heatbot supports Fahrenheit (F), Celsius (C), Rankine (R or Ra), and Kelvin (K). +**probes:** This is a "hash of hashes" that will identify each remote temperature probe, first by hostname to be used for the SSH connection (shq-example01), then `loc:` is the human-readable location (Server Room), and `port:` is the SSH port, default 22, which can be altered in case a non-standard SSH port is used for a particular probe. + +####HeatBot systemd Service +In the repository, you will find `eample/services/heatbot.service`. Copy this into `/etc/systemd/system/` and edit the copy. Primarily, you will want to edit the username to the desired user to run the service (default is `ivo`, user must already exist), and be sure the working directory is pointing to your repository location. Please not that symlinks are perfectly valid for this purpose. Once in place, as with any other service, run: +``` +systemctl daemon-reload +systemctl enable heatbot.service #Optional +``` +At this time, the bot has not yet been started. By default, the bot will log to journald when run as a service. The bot's output is colour-coded, and by normal `journalctl` usage does not show up as intended. You can still read the logs this way, but it will be much easier if you use the `--output cat` flag with it. To be sure the bot is configured correctly, let's watch the log output during the first start: +``` +systemctl start heatbot.service & journalctl --output cat -fu heatbot +``` +Pay careful attention to the sanity check portion, and if anything comes up with a red FAIL instead of a green OK, address the issue and restart the service and check again. + +####SSH Confgiuration (Recommendations) +There are many ways you can configure your SSH connections that will work. The bare minimum to function is that the following command must be valid to be run from the primary bot server, and return in Celsius the probe temperature: +``` +ssh -p [port] [host] heatbot_gettemp +``` +The way in which we have achieved this conveniently is by use of the bot server's `/etc/hosts` file and the bot service user's `~/.ssh/config` file. Let's take a look at an example set up. Let's say we are using a `bot_config.yaml` with the following `probes:` hash: +``` +probes: + probe01: + loc: 'Server Room' + port: 22 + probe02: + loc: 'Garage' + port: 22 +``` +Let's also say that these hosts, probe01 and probe02, use the respective IP addresses 10.0.0.1 and 10.0.0.2 and both probes have SSH set up for the user `heatbot`. The user that is running the service in our example is `ivo`. Under this topology, we would configure the bot server as such: +######/etc/hosts +``` +10.0.0.1 probe01 +10.0.0.2 probe02 +``` +######~ivo/.ssh/config +``` +Host probe01 + User heatbot + PasswordAuthentication no + PubKeyAuthentication yes + +Host probe02 + User heatbot + PasswordAuthentication no + PubKeyAuthentication yes +``` + +Next, you will want to be check your configuration by making sure your bot user can run the following commands, and succesffully retrieves the expected Celsius temperature value. **Please note:** __You will not be able to retrieve this info until the below__ Probe Configuration __section is completed.__ +``` +ssh -p 22 probe01 heatbot_gettemp +ssh -p 22 probe02 heatbot_gettemp +``` +If all is set up well, you will simply receive a decimal number and nothing else. + +####Probe Configuration +There are a large number of ways a remote probe can be configured, even outside of the intended operation. The included probe script is based on obtaining temperature readings from a DS18B20 probe connected to a Raspberry Pi. Let's assume this is the setup, and that you have basic working knowledge of setting up SSH keys and users. The following checklist should be sufficient to complete the setup: + +- Add the following line to `/boot/config.txt`: +``` +dtoverlay=w1-gpio,gpiopin=4 +``` +- Clone the git repo to each temperature probe host. +- Copy `probe_scripts/heatbot_gettemp` to a `$PATH` that is readable by the `heatbot` user, or whichever user you decide to use to interact with the probe. `/usr/bin/` is a good default location that is sure to be readily read and executed. +- Edit the copy, changing the configurable line to show the address of your temperature probe. This address will look like `28-[somehexnumbers]` as a symlink under `/sys/bus/w1/devices/` on the probe. +- Setup `~heatbot/.ssh/authorized_keys` to allow passwordless SSH from the primary bot server's service user. + +If all is in order, you should be abel to run locally on the probe the `heatbot_gettemp` command from the heatbot user, which will return a decimal number. +A secondary test would be to attempt the test from the above section, and test running the script remotely from the primary bot server, using, for example: +``` +ssh -p 22 probe01 heatbot_gettemp +``` + +## Setup Complete +If anything appears to be missing or needs further clarification, drop us a note and we can get the documentation updated. + diff --git a/bot_config.yaml.example b/bot_config.yaml.example index 93ff3b4..8dc2a6b 100644 --- a/bot_config.yaml.example +++ b/bot_config.yaml.example @@ -9,4 +9,14 @@ allowed_sources: - 'private' - 'group' - 'supergroup' - +units: 'F' +probes: + srv1: + loc: 'Office' + port: 22 + srv2: + loc: 'Area 50.1' + port: 22 + srv3: + loc: 'Dungeon' + port: 22 diff --git a/callbacks.rb b/callbacks.rb new file mode 100644 index 0000000..efa5ab9 --- /dev/null +++ b/callbacks.rb @@ -0,0 +1,14 @@ +def process_callback_zone(message) + host = message.data.split("|")[1] + #zone = @probes[host].values.first + zone = @probes[host].select { |k,v| k == 'loc' } + zone = zone["loc"] + puts "Selected: #{zone}" + port = @probes[host].select { |k,v| k == 'port' } + port = port["port"] + #print " Port: #{port}\n" + tdata = process_tdata(host, port) + #puts "Selected: #{zone} [#{tdata}]" + send_message(message.message.chat.id, "#{zone}: #{tdata}") + STDOUT.flush +end diff --git a/commands.rb b/commands.rb index 4d83a4f..5552dd1 100644 --- a/commands.rb +++ b/commands.rb @@ -25,21 +25,94 @@ 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. In a future version, I will also report when temperatures are out of a specified range through regular checks.\n\n" + + "Commands available:\n/start (Shows this message)\n/check or /c (Show a specific temperature interactively)\n/report or /r (Show all temperatures)\n" + + "/simple or /s (Show a simplified report)\n\nExtra functions:\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_check(message, command, adm) +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 #{@ip_provider} 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, simple=false) if is_chat_authorized?(message, @auth_chat) || message_from_admin?(message, adm) - reply = `cat fakeoutput.txt` + #begin interactive code + options = [ ] + @probes.each do |k,v| + 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 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_report(message, command, adm, simple=false) + if is_chat_authorized?(message, @auth_chat) || message_from_admin?(message, adm) + if !simple + confirmation = send_message(message.chat.id,"Generating full report... Please wait.") + report = "Full Report:\n\n" + else + confirmation = send_message(message.chat.id,"Generating simple report... Please wait.") + report = "Simple Report:\n\n" + end + @probes.each do |k,v| + zone = v["loc"].to_s + port = v["port"].to_s + host = k + tdata = process_tdata(host, port, simple) + report = report + "#{zone}: #{tdata}\n" + STDOUT.flush + end + send_message(message.chat.id,report) + #delete_message(confirmation) + #delete_confirmation(confirmation) + else + send_message(message.chat.id,"I am not authorized to provide this information here.") + end +end diff --git a/patchnotes.txt b/patchnotes.txt deleted file mode 100644 index a23f5e2..0000000 --- a/patchnotes.txt +++ /dev/null @@ -1,5 +0,0 @@ -SkyfallTech HeatBot -Patch Notes: - -v0.1.00 -+ Starting from a fork of the Skyfall EGS-Bot v0.4.00 diff --git a/probe_scripts/heatbot_gettemp b/probe_scripts/heatbot_gettemp new file mode 100755 index 0000000..4d079a6 --- /dev/null +++ b/probe_scripts/heatbot_gettemp @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +#Modify this to point to your temperature probe +dir=/sys/bus/w1/devices/[some-hexadecimals] + +# DO NOT MODIFY + +# Sanity check +which bc 2>&1 >/dev/null || { echo "PROBE ERROR: 'bc' not found. Please install bc." >&2; exit 1; } + +# Retrieve temperature in Celsius +raw=$(grep 't=' $dir/w1_slave | awk -F'=' '{print $2}') +echo "scale=3; ${raw}/1000" | bc -l && exit 0 +exit 1 diff --git a/probe_scripts/heatbot_testrandom b/probe_scripts/heatbot_testrandom new file mode 100755 index 0000000..ce3f9b5 --- /dev/null +++ b/probe_scripts/heatbot_testrandom @@ -0,0 +1,8 @@ +#/usr/bin/env sh + +# Symlink this script to heatbot_gettemp if you need to test a probe's connection +# without retrieving real temperature numbers. This is useful in setup such as before +# the sensor hardware is available to use. + +echo $((-20 + $RANDOM % 60)).$((10 + $RANDOM % 89)) || exit 1 +exit 0 diff --git a/run.rb b/run.rb index 2c487cb..fa6c463 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,19 +17,40 @@ 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'] +@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 bot token ...................... " -if token.nil? +print "Checking system utilities ............... " +if !system("which nmap 2>&1 >/dev/null") 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 "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 @@ -98,37 +120,222 @@ if @auth_chat.nil? 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 "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}.", 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 + #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 @@ -144,64 +351,111 @@ def handle_message(message) 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}" - when '/check' - reply = process_command_check(message, command, adm) + 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 - 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: - puts timestamp + ": Sending #{reply.inspect}\n\n" + #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 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) - if ! message.text.nil? - bot.api.send_message(chat_id: message.chat.id, text: "#{reply}") + #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/static_text/patchnotes.txt b/static_text/patchnotes.txt new file mode 100644 index 0000000..0b43997 --- /dev/null +++ b/static_text/patchnotes.txt @@ -0,0 +1,5 @@ +SkyfallTech HeatBot +Patch Notes: + +v0.1.00 ++ Initial Release