#!/usr/bin/env python3
import datetime
import sys
import time
import threading
import traceback
import signal
import logging
import html
# Custom scripts
import hackchatcustom as hackchat
import telegrambot
# Config
import config
# Constants
CONFIG_FILE = "config.py"
# Global variables holding both bots
hcBot = None
tgBot = None
### General
def log(s, suppress_console=True):
"""Writes a message to the channel's logfile.
Will also output to the console if suppress_console is False."""
# Write to logfile
with open(config.LOG_FILENAME, "a") as f:
f.write("%s %s\n" % (datetime.datetime.now().isoformat(), s))
# Send via TG-Bot
if not suppress_console:
print(s)
def mdescape(s):
"""Hacky escapes for Telegrams Markdown parser.
Should really replace this with HTML."""
special = '*[`_'
for x in special:
s = s.replace(x, "\\" + x)
return s
def htmlescape(s):
return html.escape(s)
### HV-Bot
def getUser(update):
"""Convenience function that extracts a nick and tripcode (if any) from
an update object and returns the nickname in the following format:
If there is not tripcode: nick
If there is a tripcode: nick#tripcode"""
nick = update["nick"]
trip = 'null'
if "trip" in update:
trip = update["trip"]
if trip == 'null':
return nick
else:
return "%s#%s" % (nick, trip)
def onMessage(chat, update):
"""Callback function handling users submitting messages to the channel."""
message = update["text"]
nick = update["nick"]
trip = 'null'
if "trip" in update:
trip = update["trip"]
sender = getUser(update)
log("[%s] %s" % (sender, message))
if not nick.endswith("@" + config.USER):
if trip == 'null':
toTG("[{:7s}]|
{}".format(htmlescape(nick), htmlescape(message)))
else:
toTG("[{:7s}#{}]
{}".format(htmlescape(nick), trip, htmlescape(message)))
def onJoin(chat, update):
"""Callback function handling users joining the channel."""
user = getUser(update)
log("* %s joined" % user)
toTG("* %s joined
" % htmlescape(user))
def onLeave(chat, update):
"""Callback function handling users leaving the channel."""
user = getUser(update)
log("%s
left" % user)
toTG("* %s left
" % htmlescape(user))
def onEmote(chat, update):
"""Callback function handling users sending emotes to the channel."""
text = update["text"]
log("* %s
" % text)
toTG("* %s
" % htmlescape(text))
def onInvite(chat, update):
"""Callback function handling users sending invite to the bot."""
user = update["from"]
newChannel = update["invite"]
log(">>> %s
invited you to hack.chat/?%s" % (user, newChannel))
toTG(">>> %s
invited you to hack.chat/?%s" % (htmlescape(user), htmlescape(newChannel)))
def startHCBot():
"""Starts the HC bot."""
global hcBot
bot = hackchat.HackChat(config.USER_AND_PASS, config.SERVER, config.CHANNEL)
bot.on_message += [onMessage]
bot.on_join += [onJoin]
bot.on_leave += [onLeave]
bot.on_emote += [onEmote]
bot.on_invite += [onInvite]
hcBot = bot
bot.run()
def botCrashed(signum, frame):
"""Recovery routine that restarts the HC bot upon crash.
This is run automatically when a SIGALRT is received
(tested on Linux only) and thus relies on the HC bot
catching any relevant exception and sending a SIGALRT."""
hcBot.stop()
log("=!= Bot crashed / lost connection. Retrying in %i seconds..."\
% config.RECONNECT_DELAY, suppress_console=False)
#toTG("=!= Bot crashed / lost connection. Retrying in %i seconds..."\
# % config.RECONNECT_DELAY)
time.sleep(config.RECONNECT_DELAY)
log("Reconnecting...", suppress_console=False)
#toTG("Reconnecting...")
startHCBot()
log("Reconnected!", suppress_console=False)
#toTG("Reconnected!")
def kill():
"""Debug command to test reconnect functionality"""
hcBot.ws.close()
### TG-Bot Config
def onTGMessage(update):
"""Handles receiving messages from the Telegram bot.
Currently, they are simple forwarded to the HC bot
which will then send them"""
hcBot._send_packet({
"cmd":"bridge",
"text":update.message.text,
"nick":update.message.from_user.username,
})
def toTG(s):
"""Handles sending messages to the Telegram bot.
Currently, the TG bot will simply send
the message with HTML parsing enabled."""
tgBot.send(s)
def startTGBot():
"""Starts the Telegram bot and sets the global
tgBot variable."""
global tgBot
tgBot = telegrambot.TGBot()
tgBot.texthandlers += [onTGMessage]
tgBot.addCommand("active", cmdActive)
tgBot.addCommand("online", cmdOnline)
tgBot.run()
def cmdActive(bot, update):
"""TG command: /active
Checks wether the HC bot has been stopped.
This might not work correctly if the HC bot crashed."""
if not hcBot.stopped:
tgBot.send("+++ ACTIVE")
else:
tgBot.send("--- _not_ active")
def cmdOnline(bot, update):
"""TG command: /online
Returns the list of users that are in the HC channel.
Users are listed without their tripcode."""
users = list(hcBot.online_users)
users.sort()
tgBot.send("Users online:\n%s" %
htmlescape(", ".join(users)))
### Common
def quit():
"""Gracefully shuts down both bots"""
global should_quit
hcBot.stop()
tgBot.stop()
should_quit = True
### Main
if __name__ == "__main__":
#logging.basicConfig(level=logging.DEBUG,
# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
COMMANDS_CLI = {
"kill": kill,
"quit": quit
#"online" : cmdOnline
}
should_quit = False
# Interpret SIGALRM as bot crash
signal.signal(signal.SIGALRM, botCrashed)
try:
startTGBot()
startHCBot()
print("started bots")
print("bridging ...")
while not should_quit:
pass
# while not should_quit:
# cmd = input("> ")
# if not cmd in COMMANDS_CLI:
# print("Unknown command!")
# else:
# COMMANDS_CLI[cmd]()
except (KeyboardInterrupt, EOFError) as e:
print("Interrupt received. Shutting down...")
quit()
except:
print("====================")
print("Main thread crashed!")
print("====================")
print()
traceback.print_exc()