Skip to content

Instantly share code, notes, and snippets.

@G33kDude
Last active June 23, 2016 17:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save G33kDude/b6816c8fb140d6318ffb492a6d47fd37 to your computer and use it in GitHub Desktop.
Save G33kDude/b6816c8fb140d6318ffb492a6d47fd37 to your computer and use it in GitHub Desktop.
#!/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