Last active
June 23, 2016 17:39
-
-
Save G33kDude/b6816c8fb140d6318ffb492a6d47fd37 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import hashlib | |
import html | |
import json | |
import re | |
import socketserver | |
import sys | |
import threading | |
import time | |
import requests | |
import websocket | |
socketserver.TCPServer.allow_reuse_address = True | |
IRCRE = ('^(?::(\S+?)(?:!(\S+?))?(?:@(\S+?))? )?' # Nick!User@Host | |
+ '(\S+)(?: (?!:)(.+?))?(?: :(.+))?$') # CMD Params Params :Message | |
# TODO: emote subsystem http://chat.smilebasicsource.com/emotes.json http://chat.smilebasicsource.com/scripts/emotes.js | |
# TODO: pm support | |
# TODO: Support showing admin status for channel operators | |
class TCPHandler(socketserver.BaseRequestHandler): | |
irc_servername = 'server.localhost' | |
irc_channels = ("#general", "#offtopic", "#admin") # TODO: pull from somewhere | |
ws_host = 'chat.smilebasicsource.com' | |
ws_port = 45695 | |
ws_query = 'https://smilebasicsource.com/query' | |
def handle(self): | |
buf = b'' | |
while True: | |
data = self.request.recv(1024) | |
if not data: break | |
buf += data | |
lines = buf.split(b'\r\n') | |
buf = lines.pop() | |
for line in lines: | |
print(b'<' + line) | |
self.irc_message(line.decode('utf-8', 'replace')) | |
# TODO: better disconnect handling | |
self.ws.close() | |
def irc_send(self, text, prefix='', suffix='', encoding='UTF-8'): | |
text = (text + '\r\n').encode('utf-8') | |
print(b'>' + text) | |
self.request.send(text) | |
def irc_sendNOTICE(self, text, target=None): | |
for line in text.splitlines(): | |
self.irc_send(':{} NOTICE {} :{}'.format( | |
self.irc_servername, | |
self.irc_nick if target is None else target, | |
line | |
)) | |
def irc_message(self, line): | |
matched = re.match(IRCRE, line) | |
nick, user, host, cmd, params, msg = matched.groups() | |
if hasattr(self, 'irc_on' + cmd): | |
handler = getattr(self, 'irc_on' + cmd) | |
handler(nick, user, host, cmd, (params or '').split(' '), msg) | |
else: | |
self.irc_send(':{} 421 {} {} :Unknown command'.format( | |
self.irc_servername, cmd, self.irc_nick)) | |
def irc_onPASS(self, nick, user, host, cmd, params, msg): | |
self.ws_pass = params[0] | |
def irc_onNICK(self, nick, user, host, cmd, params, msg): | |
self.ws_nick = params[0] # TODO: Use one value | |
self.irc_nick = params[0] | |
def irc_onCAP(self, nick, user, host, cmd, params, msg): pass | |
def irc_onUSER(self, nick, user, host, cmd, params, msg): | |
# TODO: use the USER information for something | |
# TODO: better error handling | |
# TODO: start 30s activity ping | |
# TODO: figure out how to trigger initial message wave | |
# Initiate server-side IRC connection | |
# Make sure to join user to channels before the ws | |
# tries to send the nick lists for those channels | |
self.irc_send(':{0} 001 {1} :Welcome {1}!'.format( | |
self.irc_servername, self.irc_nick)) | |
self.irc_send(':{} 422 {} :ERR_NOMOTD'.format( | |
self.irc_servername, self.irc_nick)) | |
# Get the user's ID and access token | |
r = requests.post(self.ws_query + '/usercheck', | |
params={'username': self.ws_nick}) | |
self.ws_uid = r.json()['result'] | |
r = requests.post(self.ws_query + '/chatauth', data={ | |
'username': self.ws_nick, | |
'password': hashlib.md5(self.ws_pass.encode('utf-8')).hexdigest() | |
}) | |
self.ws_token = r.json()['result'] | |
# Initiate the websocket connection to the SBS servers | |
self.ws_used_ids = [] | |
self.ws_nicks = {} | |
self.ws = websocket.WebSocketApp( | |
'ws://{}:{}/chatserver'.format(self.ws_host, self.ws_port), | |
on_message=self.ws_message, | |
on_open=self.ws_open, | |
on_error=self.ws_error, | |
on_close=self.ws_close | |
) # TODO: handle ws disconnect | |
thread = threading.Thread(target=self.ws.run_forever) | |
thread.daemon = True | |
thread.start() | |
def irc_onPING(self, nick, user, host, cmd, params, msg): | |
self.irc_send('PONG {}'.format(params[0])) | |
def irc_onPRIVMSG(self, nick, user, host, cmd, params, msg): | |
# TODO: better CTCP parsing | |
if msg.startswith('\x01ACTION'): | |
msg = '/me ' + msg[8:-1] # strip \x01ACTION\x01 | |
self.ws_send({ | |
'type': 'message', | |
'key': self.ws_token, | |
'text': msg, | |
'tag': params[0][1:] # TODO: better channel prefixing | |
}) | |
def ws_close(self, ws): | |
print("CLOSING WEBSOCKET") | |
def ws_error(self, ws, error): | |
raise Exception("Websocket Error: {}".format(error)) | |
def ws_message(self, ws, framedata): | |
print('<' + framedata) | |
frame = json.loads(framedata) | |
if hasattr(self, 'ws_on' + frame['type']): | |
handler = getattr(self, 'ws_on' + frame['type']) | |
handler(frame) | |
else: | |
self.irc_sendNOTICE('ERROR: UNKOWN FRAME: {}'.format(framedata)) | |
def ws_onuserList(self, frame): | |
# TODO: support rooms properly | |
nicks = {user['username']: user for user in frame['users']} | |
# Diff the nick lists | |
newnicks = list(set(nicks) - set(self.ws_nicks)) | |
oldnicks = list(set(self.ws_nicks) - set(nicks)) | |
# Have us join first | |
if self.ws_nick in newnicks: | |
newnicks.remove(self.ws_nick) | |
newnicks.insert(0, self.ws_nick) | |
for nick in newnicks: | |
for channel in self.irc_channels: | |
self.irc_send(':{} JOIN {}'.format( | |
self.ws_getuser(nick, nicklist=nicks), | |
channel | |
)) | |
# Handle absent nicks | |
for nick in oldnicks: | |
self.irc_send(':{} QUIT'.format(self.ws_getuser(nick))) | |
# Save new list for later comparison | |
self.ws_nicks = nicks | |
def ws_getuser(self, nick, nicklist=None): | |
if nicklist is None: | |
nicklist = self.ws_nicks | |
if nick in nicklist: | |
uid = nicklist[nick]['uid'] | |
else: | |
uid = 0 # TODO: Better handling | |
return '{}!{}@{}'.format( | |
nick, | |
uid, | |
self.ws_host | |
) | |
def ws_onmessageList(self, frame): | |
# TODO: Handle case where user is not in userlist | |
# TODO: Handle timestamp mismatch (initial scrollback) | |
for message in frame['messages']: | |
if message['id'] in self.ws_used_ids: | |
continue | |
self.ws_used_ids.append(message['id']) | |
if message['username'] == self.irc_nick: | |
continue | |
for line in message['message'].splitlines(): | |
self.irc_send(':{} PRIVMSG #{} :{}'.format( | |
self.ws_getuser(message['username']), | |
message['tag'], | |
html.unescape(line) | |
)) | |
def ws_onmodule(self, frame): | |
# TODO: Better /me support | |
message = html.unescape(frame['message']) | |
if 'tag' not in frame: | |
self.irc_sendNOTICE(message) | |
return | |
if frame['module'] == 'fun': | |
split = message.split(' ', 1) | |
if split[0] not in self.ws_nicks: | |
raise Exception("Unkown fun module message") | |
if split[0] == self.ws_nick: | |
return | |
self.irc_send(':{} PRIVMSG #{} :\x01ACTION {}\x01'.format( | |
self.ws_getuser(split[0]), | |
frame['tag'], | |
split[1] | |
)) | |
else: | |
self.irc_sendNOTICE(message, '#' + frame['tag']) | |
def ws_onresponse(self, frame): | |
if not frame['result']: | |
self.irc_sendNOTICE('ERROR: {}'.format(frame)) | |
if frame['from'] == 'bind': | |
self.ws_send({'type': 'request', 'request': 'messageList'}) | |
def ws_onsystem(self, frame): | |
message = html.unescape(frame['message']) | |
if 'subtype' not in frame: | |
raise Exception("Missing subtype") | |
if frame['subtype'] in ('join', 'leave'): | |
return | |
self.irc_sendNOTICE(message) | |
def ws_open(self, ws): | |
self.ws_send({ | |
'type': 'bind', | |
'uid': self.ws_uid, | |
'key': self.ws_token | |
}) | |
def ws_send(self, data): | |
data = json.dumps(data) | |
print('>' + data) | |
self.ws.send(data) | |
class DevTCPHandler(TCPHandler): | |
ws_query = 'https://development.smilebasicsource.com/query' | |
ws_port = 45697 | |
class IRCRelay(): | |
class TCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): | |
pass | |
def __init__(self, host='0.0.0.0', port=6667, handler=TCPHandler): | |
self.irc_host = host # TODO: better name | |
self.irc_port = port | |
self.TCPHandler = handler | |
def serve(self, daemon=False): | |
self.server = self.TCPServer((self.irc_host, self.irc_port), self.TCPHandler) | |
thread = threading.Thread(target=self.server.serve_forever) | |
thread.daemon = daemon | |
thread.start() | |
# TODO: close server on exit | |
#self.server.shutdown() | |
#self.server.server_close() | |
if __name__ == '__main__': | |
if len(sys.argv) > 1: | |
if sys.argv[1] in ('dev', 'debug'): | |
print("Using development server") | |
irc = IRCRelay(handler=DevTCPHandler) | |
else: | |
raise Exception("Uknown parameter: {}".format(sys.argv[1])) | |
else: | |
print("Using live server") | |
irc = IRCRelay() | |
irc.serve() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment