#!/usr/bin/env python2 import re import json import argparse from twisted.internet.protocol import Factory, Protocol #from twisted.protocols.basic import LineReceiver from twisted.internet import reactor parser = argparse.ArgumentParser(description='Start a CoVim server.') parser.add_argument('-p', '--persist', action='store_true', help='Keep server running if all users disconnect') parser.add_argument('port', type=int, nargs='?', default=8555, help='Port number to run on') def name_validate(strg, search=re.compile(r'[^0-9a-zA-Z\-\_]').search): return not bool(search(strg)) class React(Protocol): def __init__(self, factory): self.factory = factory self.state = "GETNAME" def dataReceived(self, data): if self.state == "GETNAME": self.handle_GETNAME(data) else: self.handle_BUFF(data) def handle_GETNAME(self, name): # Handle duplicate name if userManager.has_user(name): d = { 'packet_type': 'message', 'data': { 'message_type': 'error_newname_taken' } } self.transport.write(json.dumps(d)) return # Handle spaces in name if not name_validate(name): d = { 'packet_type': 'message', 'data': { 'message_type': 'error_newname_invalid' } } self.transport.write(json.dumps(d)) return # Name is Valid, Add to Document self.user = User(name, self) userManager.add_user(self.user) self.state = "CHAT" d = { 'packet_type': 'message', 'data': { 'message_type': 'connect_success', 'name': name, 'collaborators': userManager.all_users_to_json() } } if userManager.is_multi(): d['data']['buffer'] = self.factory.buff self.transport.write(json.dumps(d)) print 'User "{user_name}" Connected'.format(user_name=self.user.name) # Alert other Collaborators of new user d = { 'packet_type': 'message', 'data': { 'message_type': 'user_connected', 'user': self.user.to_json() } } self.user.broadcast_packet(d) def handle_BUFF(self, data_string): def to_utf8(d): if isinstance(d, dict): # no dict comprehension in python2.5/2.6 d2 = {} for key, value in d.iteritems(): d2[to_utf8(key)] = to_utf8(value) return d2 elif isinstance(d, list): return map(to_utf8, d) elif isinstance(d, unicode): return d.encode('utf-8') else: return d def clean_data_string(d_s): bad_data = d_s.find("}{") if bad_data > -1: d_s = d_s[:bad_data+1] return d_s data_string = clean_data_string(data_string) d = to_utf8(json.loads(data_string)) data = d['data'] update_self = False if 'cursor' in data.keys(): user = userManager.get_user(data['name']) user.update_cursor(data['cursor']['x'], data['cursor']['y']) d['data']['updated_cursors'] = [user.to_json()] del d['data']['cursor'] if 'buffer' in data.keys(): b_data = data['buffer'] #TODO: Improve Speed: If change_y = 0, just replace that one line #print ' \\n '.join(self.factory.buff[:b_data['start']]) #print ' \\n '.join(b_data['buffer']) #print ' \\n '.join(self.factory.buff[b_data['end']-b_data['change_y']+1:]) self.factory.buff = self.factory.buff[:b_data['start']] \ + b_data['buffer'] \ + self.factory.buff[b_data['end']-b_data['change_y']+1:] d['data']['updated_cursors'] += userManager.update_cursors(b_data, user) update_self = True self.user.broadcast_packet(d, update_self) def connectionLost(self, reason): if hasattr(self, 'user'): userManager.rem_user(self.user) if userManager.is_empty(): print 'All users disconnected. Shutting down...' reactor.stop() class ReactFactory(Factory): def __init__(self): self.buff = [] def initiate(self, port): self.port = port print 'Now listening on port {port}...'.format(port=port) reactor.listenTCP(port, self) reactor.run() def buildProtocol(self, addr): return React(self) class Cursor: def __init__(self): self.x = 1 self.y = 1 def to_json(self): return { 'x': self.x, 'y': self.y } class User: def __init__(self, name, protocol): self.name = name self.protocol = protocol self.cursor = Cursor() def to_json(self): return { 'name': self.name, 'cursor': self.cursor.to_json() } def broadcast_packet(self, obj, send_to_self=False): obj_json = json.dumps(obj) #print obj_json for name, user in userManager.users.iteritems(): if user.name != self.name or send_to_self: user.protocol.transport.write(obj_json) #TODO: don't send yourself your own buffer, but del on a copy doesn't work def update_cursor(self, x, y): self.cursor.x = x self.cursor.y = y class UserManager: def __init__(self): self.users = {} def is_empty(self): return not self.users def is_multi(self): return len(self.users) > 1 def has_user(self, search_name): return self.users.get(search_name) def add_user(self, u): self.users[u.name] = u def get_user(self, u_name): try: return self.users[u_name] except KeyError: raise Exception('user doesnt exist') def rem_user(self, user): if self.users.get(user.name): d = { 'packet_type': 'message', 'data': { 'message_type': 'user_disconnected', 'name': user.name } } user.broadcast_packet(d) print 'User "{user_name}" Disconnected'.format(user_name=user.name) del self.users[user.name] def all_users_to_json(self): return [user.to_json() for user in userManager.users.values()] def update_cursors(self, buffer_data, u): return_arr = [] y_target = u.cursor.y x_target = u.cursor.x for user in userManager.users.values(): updated = False if user != u: if user.cursor.y > y_target: user.cursor.y += buffer_data['change_y'] updated = True if user.cursor.y == y_target and user.cursor.x > x_target: user.cursor.x = max(1, user.cursor.x + buffer_data['change_x']) updated = True if user.cursor.y == y_target - 1 and user.cursor.x > x_target \ and buffer_data['change_y'] == 1: user.cursor.y += 1 user.cursor.x = max(1, user.cursor.x + buffer_data['change_x']) updated = True #TODO: If the line was just split? if updated: return_arr.append(user.to_json()) return return_arr userManager = UserManager() if __name__ == '__main__': args = parser.parse_args() Server = ReactFactory() Server.initiate(args.port)