aboutsummaryrefslogtreecommitdiffstats
path: root/control.py
blob: dd08d497522b4373178379d9c588be69daade447 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#!/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("<code>[{:7s}]|</code>{}".format(htmlescape(nick), htmlescape(message)))
        else:
            toTG("<code>[{:7s}#{}]</code> {}".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("<code>* %s joined</code>" % htmlescape(user))

def onLeave(chat, update):
    """Callback function handling users leaving the channel."""
    user = getUser(update)

    log("<code>%s</code> left" % user)
    toTG("<code>* %s left</code>" % htmlescape(user))

def onEmote(chat, update):
    """Callback function handling users sending emotes to the channel."""
    text = update["text"]
    log("<code>* %s</code>" % text)
    toTG("<code>* %s</code>" % htmlescape(text))

def onInvite(chat, update):
    """Callback function handling users sending invite to the bot."""
    user = update["from"]
    newChannel = update["invite"]
    log(">>> <code>%s</code> invited you to hack.chat/?%s" % (user, newChannel))
    toTG(">>> <code>%s</code> 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()