diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e85ebed --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/bot_config.yaml +/.ignore + diff --git a/bot_config.yaml.example b/bot_config.yaml.example new file mode 100644 index 0000000..93ff3b4 --- /dev/null +++ b/bot_config.yaml.example @@ -0,0 +1,12 @@ +botname: 'Bob' +tmpdir: '/tmp/.skyfall/heatbot' +token: '' +admin: + - 11111111 +authorized_chats: + - 11111111 +allowed_sources: + - 'private' + - 'group' + - 'supergroup' + diff --git a/colors.rb b/colors.rb new file mode 100644 index 0000000..b4efdb2 --- /dev/null +++ b/colors.rb @@ -0,0 +1,28 @@ +class String +def black; "\e[30m#{self}\e[0m" end +def red; "\e[31m#{self}\e[0m" end +def green; "\e[32m#{self}\e[0m" end +def yellow; "\e[33m#{self}\e[0m" end +def brown; "\e[33m#{self}\e[0m" end +def blue; "\e[34m#{self}\e[0m" end +def magenta; "\e[35m#{self}\e[0m" end +def cyan; "\e[36m#{self}\e[0m" end +def gray; "\e[37m#{self}\e[0m" end + +def bg_black; "\e[40m#{self}\e[0m" end +def bg_red; "\e[41m#{self}\e[0m" end +def bg_green; "\e[42m#{self}\e[0m" end +def bg_yellow; "\e[43m#{self}\e[0m" end +def bg_brown; "\e[43m#{self}\e[0m" end +def bg_blue; "\e[44m#{self}\e[0m" end +def bg_magenta; "\e[45m#{self}\e[0m" end +def bg_cyan; "\e[46m#{self}\e[0m" end +def bg_gray; "\e[47m#{self}\e[0m" end + +def bold; "\e[1m#{self}\e[22m" end +def italic; "\e[3m#{self}\e[23m" end +def underline; "\e[4m#{self}\e[24m" end +def blink; "\e[5m#{self}\e[25m" end +def reverse_color; "\e[7m#{self}\e[27m" end +end + diff --git a/commands.rb b/commands.rb new file mode 100644 index 0000000..4d83a4f --- /dev/null +++ b/commands.rb @@ -0,0 +1,45 @@ +def validate_incoming_data(message) + auth_chat = @conf['auth_chat'] + message = message.message if message.is_a? Telegram::Bot::Types::CallbackQuery + return "Received message is not from a valid source! Type: \"#{message.chat.type}\". Ignoring." if ! @allowed_sources.include?(message.chat.type) + return true +end + +def message_from_admin?(message, adm) + if adm.include?(message.from.id) + puts "Command is from an admin. [" + "#{message.from.username}".green.bold + "]" + return true + else + puts "Command is NOT from an admin! [" + "#{message.from.username}".yellow.bold + "]" + return false + end +end + +def is_chat_authorized?(message, auth_chat) + if auth_chat.include?(message.chat.id) + puts "Group [" + "#{message.chat.id}".green.bold + "][" + "#{message.chat.title}".green + "] is authorized" + return true + else + puts "Group [" + "#{message.chat.id}".red.bold + "][" + "#{message.chat.title}".red + "] is NOT authorized!" + return false + end +end + +def process_command_patchnotes(message, command, adm) + if is_chat_authorized?(message, @auth_chat) || message_from_admin?(message, adm) + reply = `cat patchnotes.txt` + else + reply = "I am not authorized to provide this information here." + end + return reply +end + +def process_command_check(message, command, adm) + if is_chat_authorized?(message, @auth_chat) || message_from_admin?(message, adm) + reply = `cat fakeoutput.txt` + else + reply = "I am not authorized to provide this information here." + end + return reply +end + diff --git a/example_services/heatbot.service b/example_services/heatbot.service new file mode 100644 index 0000000..21c8251 --- /dev/null +++ b/example_services/heatbot.service @@ -0,0 +1,18 @@ +[Unit] +Description=Temperature Checking and Alert Bot by SKYFALL +After=network.target + +[Service] +User=ivo +WorkingDirectory=/opt/skyfall/heatbot +# Uncomment these for logging to flat files instead of journald +#StandardOutput=file:/var/log/skyfall/heatbot/run.log +#StandardError=file:/var/log/skyfall/heatbot/error.log +PIDFile=/var/run/skyfall/heatbot.pid +ExecStart=/usr/bin/ruby /opt/skyfall/heatbot/run.rb +SuccessExitStatus=SIGKILL +Restart=on-failure + +[Install] +WantedBy=multi-user.target + diff --git a/fakeoutput.txt b/fakeoutput.txt new file mode 100644 index 0000000..eac80ba --- /dev/null +++ b/fakeoutput.txt @@ -0,0 +1,5 @@ +Living Room: 68F +Basement: 66F +Server Room: 74F +Sun (Surface): 9941F +Sun (Corona): 6800000F diff --git a/patchnotes.txt b/patchnotes.txt new file mode 100644 index 0000000..a23f5e2 --- /dev/null +++ b/patchnotes.txt @@ -0,0 +1,5 @@ +SkyfallTech HeatBot +Patch Notes: + +v0.1.00 ++ Starting from a fork of the Skyfall EGS-Bot v0.4.00 diff --git a/run.rb b/run.rb new file mode 100755 index 0000000..2c487cb --- /dev/null +++ b/run.rb @@ -0,0 +1,212 @@ +#!/usr/bin/env ruby + +require 'rubygems' +require 'yaml' +require 'telegram/bot' +require 'pp' +require 'time' +require_relative 'commands.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'] + +### Begin sanity check ### +STDOUT.sync = true +errcount = 0 +puts "Checking if environment is sane...\n\n" +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 +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 "Start time: " + timestamp + "\n\n\n\n" +STDOUT.flush + +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 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 = "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) + 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 + when '/whoami', '/chatinfo' + reply = "User ID: #{message.from.id}\nChat ID: #{message.chat.id}" + when '/check' + reply = process_command_check(message, command, adm) + when '/pp', '/debug' + pp message + reply = "Confirmation: Message debug information sent to console." + else + reply = "Sorry, #{command} is not a valid command." + end + # Verbose output: + puts timestamp + ": Sending #{reply.inspect}\n\n" + STDOUT.flush + return reply +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}") + end + STDOUT.flush + else + puts "Received bad data! [#{message.chat.type}]" + puts validation + STDOUT.flush + end + end +end +