From e1b673beffc81d358d15edc5f33d3c361ffe9006 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 7 Jan 2018 10:23:13 +0100 Subject: [PATCH] 2017-1216-3, channel concept --- r0c.py | 270 ++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 161 insertions(+), 109 deletions(-) diff --git a/r0c.py b/r0c.py index 00f7502..8b85a01 100644 --- a/r0c.py +++ b/r0c.py @@ -15,6 +15,8 @@ import asyncore import socket import signal import struct +import base64 +import hashlib import datetime import time import sys @@ -30,6 +32,9 @@ else: DBG = True +HEXDUMP_IN = True +HEXDUMP_OUT = False + MSG_LEN = 8192 HEX_WIDTH = 16 @@ -199,7 +204,7 @@ def trunc(txt, maxlen): -class Printer: +class Printer(object): def __init__(self): self.mutex = threading.Lock() @@ -216,11 +221,78 @@ class Printer: +class World(object): + def __init__(self): + self.users = [] # User instances + self.chans = [] # NChannel instances + + def add_user(self, user): + self.users.append(user) + # announce it + + + +class NChannel(object): + def __init__(self): + self.name = u'' + self.users = [] # UChannel instances + + + +class UChannel(object): + def __init__(self): + self.user = None # the user which this object belongs to + self.chan = None # the NChannel object + self.last_ts = None # last time the user viewed this channel + + + +class User(object): + def __init__(self, world, address): + self.world = world + self.client = None # the client which this object belongs to + self.chans = [] # UChannel instances + self.nick = None + + plain_base = u'lammo/{0}'.format(address[0]) + + for sep in u'/!@#$%^&*()_+-=[]{};:<>,.': + + plain = plain_base + while True: + #print(plain) + hashed = hashlib.sha256(plain).digest() + hashed = base64.b64encode(hashed).replace('+','').replace('/','')[:6] + #print(hashed) + + ok = True + for user in self.world.users: + if user.nick == hashed: + ok = False + break + + if ok: + self.nick = hashed + break + else: + if len(plain) > 100: + break + plain += '/{0}'.format(address[1]) + + if self.nick: + break + + if not self.nick: + raise RuntimeException("out of legit nicknames") + + + class TelnetHost(asyncore.dispatcher): - def __init__(self, p, host, port): + def __init__(self, p, host, port, world): asyncore.dispatcher.__init__(self) self.p = p + self.world = world self.clients = [] self.create_socket(socket.AF_INET, socket.SOCK_STREAM) if PY2: @@ -240,7 +312,9 @@ class TelnetHost(asyncore.dispatcher): def handle_accept(self): socket, addr = self.accept() self.con(' ++', addr, len(self.clients) + 1) - remote = TelnetClient(self, socket, addr) + user = User(self.world, addr) + remote = TelnetClient(self, socket, addr, world, user) + self.world.add_user(user) self.clients.append(remote) def broadcast(self, message): @@ -256,10 +330,12 @@ class TelnetHost(asyncore.dispatcher): class Client(asyncore.dispatcher): - def __init__(self, host, socket, address): + def __init__(self, host, socket, address, world, user): asyncore.dispatcher.__init__(self, socket) self.host = host self.socket = socket + self.world = world + self.user = user self.mutex = threading.Lock() self.outbox = Queue() self.replies = Queue() @@ -302,7 +378,6 @@ class Client(asyncore.dispatcher): for x in range(self.h): self.screen.append(u'*' * self.w) - self.nick = 'dude' self.msg_hist = [] self.msg_hist_n = None @@ -344,20 +419,36 @@ class Client(asyncore.dispatcher): else: msg = src.get() - if len(msg) < 100: - hexdump(msg, '<<--') - else: - print('<<-- : [{0} byte]'.format(len(msg))) + if HEXDUMP_OUT: + if len(msg) < 100: + hexdump(msg, '<<--') + else: + print('<<-- : [{0} byte]'.format(len(msg))) sent = self.send(msg) self.backlog = msg[sent:] - def send_status_line_update(self): - with self.mutex: - if self.update_status_line(): - self.send_lines([self.h-2], False) - - def update_status_line(self): + def refresh(self, full_redraw, cursor_moved): + """ compose necessary ansi text and send to client """ + to_send = u'' + to_send += self.update_top_bar(full_redraw) + to_send += self.update_chat_view(full_redraw) + to_send += self.update_status_bar(full_redraw) + to_send += self.update_text_input(full_redraw) + if to_send or cursor_moved: + to_send += u'\033[{0};{1}H'.format(self.h, len(self.user.nick) + 2 + self.linepos + 1 - self.lineview) + self.say(to_send.encode('utf-8')) + + def update_top_bar(self, full_redraw): + """ no need to optimize this tbh """ + top_bar = u'\033[1H\033[44;48;5;235;38;5;220mtopic goes here\033[K' + if self.screen[0] != top_bar: + self.screen[0] = top_bar + return trunc(top_bar, self.w) + return u'' + + def update_status_bar(self, full_redraw): + preface = u'\033[{0}H\033[0;37;44;48;5;235m'.format(self.h-1) hhmmss = datetime.datetime.utcnow().strftime('%H:%M:%S') nChan = 1 nChans = 3 @@ -365,19 +456,36 @@ class Client(asyncore.dispatcher): chan_name = u'general' hilight_chans = u'\033[1;33mh 2,5,8\033[22;39m' active_chans = u'\033[1;32ma 1,3,4,6,7\033[22;39m' - line = trunc( - u'\033[0;37;44;48;5;235m{0} {1} #{2} {3} {4}'.format( - hhmmss, nChan, chan_name, hilight_chans, active_chans, nUsers), self.w) - if self.screen[self.h-2] == line: - return False + line = trunc(u'{0}{1} {2} #{3} {4} {5}\033[K'.format( + preface, hhmmss, nChan, chan_name, hilight_chans, active_chans, nUsers), self.w) + if full_redraw: + if self.screen[self.h-2] != line: + self.screen[self.h-2] = line + return trunc(line, self.w) else: + old = self.screen[self.h-2] self.screen[self.h-2] = line - return True + + if len(old) != len(line): + return trunc(line, self.w) + + cutoff = len(preface) + len(hhmmss) + changed_part1 = old[:cutoff] != line[:cutoff] + changed_part2 = old[cutoff:] != line[cutoff:] + + if changed_part2: + return trunc(line, self.w) + + if changed_part1: + return line[:cutoff] + #return u'\033[{0}H{1}'.format(self.h-1, hhmmss) # drops colors + + return u'' - def update_text_input(self): + def update_text_input(self, full_redraw): msg_len = len(self.linebuf) vis_text = self.linebuf - free_space = self.w - (len(self.nick) + 2 + 1) # nick chrome + final char on screen + free_space = self.w - (len(self.user.nick) + 2 + 1) # nick chrome + final char on screen if msg_len <= free_space: self.lineview = 0 else: @@ -386,41 +494,25 @@ class Client(asyncore.dispatcher): elif self.linepos > self.lineview + free_space: self.lineview = self.linepos - free_space vis_text = vis_text[self.lineview:self.lineview+free_space] - line = u'\033[0;36m{0}>\033[0m {1}'.format(self.nick, vis_text) - if self.screen[self.h-1] == line: - return False - else: + line = u'\033[0;36m{0}>\033[0m {1}'.format(self.user.nick, vis_text) + if self.screen[self.h-1] != line: self.screen[self.h-1] = line - return True - - def send_lines(self, to_send, cursor_moved): - if not to_send and not cursor_moved: - return - if DBG and to_send: - dstr = '<<-- lines: ' - dlo = None - dlast = None - for v in to_send: - if dlast != v - 1: - if dlo is not None: - if dlo == dlast: - dstr += '{0}, '.format(dlo) - else: - dstr += '{0}-{1}, '.format(dlo, dlast) - dlo = v - dlast = v - if dlo == dlast: - dstr += str(dlo) - else: - dstr += '{0}-{1}'.format(dlo, dlast) - print(dstr) - - #print('<<-- lines: {0}'.format(to_send)) - msg = u'' - for n in to_send: - msg += u'\033[{0}H{1}\033[K'.format(n+1, self.screen[n]) - msg += u'\033[{0};{1}H'.format(self.h, len(self.nick) + 2 + self.linepos + 1 - self.lineview) - self.say(msg.encode('utf-8')) + return u'\033[{0}H{1}\033[K'.format(self.h, line) + return u'' + + def update_chat_view(self, full_redraw): + # TODO: naive implementation; + # should impl screen scrolling + ret = u'' + for n in range(self.h - 3): + line = u'{0}<{1:-4d}>{2}<>'.format( + u'\033[0m' if n==0 else '', + n + 1, '*' * (self.w - 8)) + + if self.screen[n+1] != line: + self.screen[n+1] = line + ret += u'\033[{0}H{1}'.format(n+2, self.screen[n+1]) + return ret def read_cb(self, full_redraw): aside = u'' @@ -456,7 +548,7 @@ class Client(asyncore.dispatcher): for pch in aside: nch = ord(pch) if nch < 0x20 or (nch >= 0x80 and nch < 0x100): - print('substituting non-printable \\x{0:2x}'.format(nch)) + print('substituting non-printable \\x{0:02x}'.format(nch)) plain += '?' else: plain += pch @@ -551,62 +643,18 @@ class Client(asyncore.dispatcher): self.say(msg.encode('utf-8')) return - to_send = [] - if full_redraw: self.screen = ['x'] * self.h - # top bar - top_bar = u'\033[44;48;5;235;38;5;220mtopic goes here' - if self.screen[0] != top_bar: - self.screen[0] = top_bar - to_send.append(0) - - # chat view - for n in range(self.h - 3): - line = u'{0}<{1:-4d}>{2}<>'.format( - u'\033[0m' if n==0 else '', - n + 1, '*' * (self.w - 8)) - - if self.screen[n+1] != line: - self.screen[n+1] = line - to_send.append(n+1) - - if self.update_status_line(): - to_send.append(self.h-2) - - if self.update_text_input(): - to_send.append(self.h-1) - - self.send_lines(to_send, old_cursor != self.linepos) - - ## backspace - #while True: - # ofs = txstr.find(u'\x7f') - # if ofs < 0: - # break - # if ofs == 0: - # txstr = txstr[ofs+1:] - # else: - # txstr = txstr[:ofs-1] + txstr[ofs+1:] - # - - ## newline - ## - ## putty: 0d 0a - ## winxp: 0d 0a - ## linux: 0d 00 - #if b'\r\0' in self.txbuf: - # last_newline = self.txbuf.rfind(b'\r\0') - # print('last_newline = {0}'.format(last_newline)) - # self.txbuf = self.txbuf[last_newline+1:] + self.refresh(full_redraw, + old_cursor != self.linepos) class TelnetClient(Client): - def __init__(self, host, socket, address): - Client.__init__(self, host, socket, address) + def __init__(self, host, socket, address, world, user): + Client.__init__(self, host, socket, address, world, user) self.replies.put(b'\xff\xfe\x22') # don't linemode self.replies.put(b'\xff\xfb\x01') # will echo self.replies.put(b'\xff\xfd\x1f') # do naws @@ -617,7 +665,8 @@ class TelnetClient(Client): if not data: self.host.part(self) - hexdump(data, '-->>') + if HEXDUMP_IN: + hexdump(data, '-->>') self.in_bytes += data @@ -754,7 +803,7 @@ def push_worker(ifaces): for iface in ifaces: for client in iface.clients: - client.send_status_line_update() + client.refresh(False, False) @@ -784,8 +833,11 @@ p = Printer() p.p(' * Capturing ^C') signal.signal(signal.SIGINT, signal_handler) +p.p(' * Creating world') +world = World() + p.p(' * Starting telnet server') -telnet_host = TelnetHost(p, '0.0.0.0', telnet_port) +telnet_host = TelnetHost(p, '0.0.0.0', telnet_port, world) p.p(' * Starting push driver') push_thr = threading.Thread(target=push_worker, args=([telnet_host],))