def __init__(self, cmd='/bin/uname', args=(), env=None, cp437=False): """ Class initializer. :param str cmd: full path of command to execute. :param tuple args: command arguments as tuple. :param bool cp437: When true, forces decoding of external program as codepage 437. This is the most common encoding used by DOS doors. :param dict env: Environment variables to extend to the sub-process. You should more than likely specify values for TERM, PATH, HOME, and LANG. """ self._session, self._term = getsession(), getterminal() self.cmd = cmd if isinstance(args, tuple): self.args = (self.cmd,) + args elif isinstance(args, list): self.args = [self.cmd, ] + args else: raise ValueError('args must be tuple or list') self.log = logging.getLogger(__name__) self.env = (env or {}).copy() self.env.update( {'LANG': env.get('LANG', 'en_US.UTF-8'), 'TERM': env.get('TERM', self._term.kind), 'PATH': env.get('PATH', get_ini('door', 'path')), 'HOME': env.get('HOME', os.getenv('HOME')), 'LINES': str(self._term.height), 'COLUMNS': str(self._term.width), }) self.cp437 = cp437 self._utf8_decoder = codecs.getincrementaldecoder('utf8')()
def send_modem(stream, protocol='xmodem1k', retry=16, timeout=30, callback=None): """ Send a file using 'xmodem1k' or 'xmodem' protocol. Currently, these are the only protocols supported. Returns ``True`` upon successful transmission, otherwise ``False``. :param stream: The file-like stream object to send data from. :param int retry: The maximum number of times to try to resend a failed packet before failing. :param int timeout: seconds to elapse for response before failing. :param callable callback: Reference to a callback function that has the following signature. This is useful for getting status updates while a transfer is underway:: def callback(total_count, success_count, error_count) """ # get protocol implementation class supported_protocols = ('xmodem', 'xmodem1k') assert protocol in supported_protocols, (protocol, supported_protocols) Modem = { 'xmodem': xmodem.XMODEM, 'xmodem1k': xmodem.XMODEM1k, }[protocol] # the session's 'input' event buffer is used for receiving # transmissions. It arrives in raw bytes, and session.write # is used, sending "unicode" data as encoding iso8859-1. session = getsession() def getc(size, timeout=10): """ Callback function for (X)Modem interface. """ val = b'' while len(val) < size: next_val = session.read_event('input', timeout=timeout) if next_val is None: break val += next_val if len(val) > size: session.buffer_input(val[size:]) return val[:size] or None def putc(data, timeout=10): # pylint: disable=W0613 # Unused argument 'timeout' """ Callback function for (X)Modem interface. """ session.write(data.decode('iso8859-1'), 'iso8859-1') modem = Modem(getc, putc) return modem.send(stream=stream, retry=retry, timeout=timeout, quiet=True, callback=callback)
def echo(ucs): """ Display unicode terminal sequence. """ session = getsession() if not isinstance(ucs, unicode): warnings.warn('non-unicode: %r' % (ucs, ), UnicodeWarning, 2) return session.write(ucs.decode('iso8859-1')) return session.write(ucs)
def echo(ucs): """ Display unicode terminal sequence. """ session = getsession() if not isinstance(ucs, unicode): warnings.warn('non-unicode: %r' % (ucs,), UnicodeWarning, 2) return session.write(ucs.decode('iso8859-1')) return session.write(ucs)
def _decode(what): # pylint: disable=C0111 # Missing function docstring (col 8) session = getsession() if session.encoding == 'utf8': return what.decode(encoding) elif session.encoding == 'cp437': return what.decode('cp437') else: return what
def read(self): """ Reads input until ESCAPE key is pressed (Blocking). Returns None. """ from x84.bbs.session import getsession from x84.bbs.output import echo session = getsession() self._quit = False echo(self.refresh()) while not self.quit: echo(self.process_keystroke(session.read_event('input')))
def __init__(self, schema, table='unnamed', use_session=True): """ Arguments: schema: database key, to become basename of .sqlite3 files. """ from x84.bbs.session import getsession self.log = logging.getLogger(__name__) self.schema = schema self.table = table self._tap_db = should_tapdb() self.session = use_session and getsession()
def __init__(self, recipient=None, subject=u'', body=u''): from x84.bbs.session import getsession self._ctime = datetime.datetime.now() self._stime = None self.author = getsession().handle self.recipient = recipient self.subject = subject self.body = body self.tags = set() # reply-to tracking self.children = set() self.parent = None
def read(self): """ Reads input until ESCAPE key is pressed (Blocking). Returns None. """ from x84.bbs import getch from x84.bbs.session import getsession from x84.bbs.output import echo session = getsession() self._quit = False echo(self.refresh()) while not self.quit: echo(self.process_keystroke(getch()))
def __init__(self, recipient=None, subject=u'', body=u''): self.author = None session = getsession() if session: self.author = session.user.handle self._ctime = datetime.datetime.now() self._stime = None self.recipient = recipient self.subject = subject self.body = body self.tags = set() self.children = set() self.parent = None self.idx = None
def init_theme(self): """ This initializer sets glyphs and colors appropriate for a "theme", override this method to create a common color and graphic set. """ session = getsession() term = getterminal() self.colors['normal'] = term.normal if term.number_of_colors != 0: self.colors['border'] = term.cyan # start with default 'ascii' self.glyphs = GLYPHSETS['ascii'].copy() # PC-DOS 'thin' on smart terminals if session.env.get('TERM') != 'unknown': self.glyphs = GLYPHSETS['thin'].copy()
def __init__(self, recipient=None, subject=u'', body=u''): from x84.bbs.session import getsession self.author = None session = getsession() if session: self.author = session.handle # msg attributes (todo: create method ..) self._ctime = datetime.datetime.now() self._stime = None self.recipient = recipient self.subject = subject self.body = body self.tags = set() self.children = set() self.parent = None
def read(self): """ Reads input until the ENTER or ESCAPE key is pressed (Blocking). Allows backspacing. Returns unicode text, or None when cancelled. """ from x84.bbs.session import getsession from x84.bbs.output import echo session = getsession() self._selected = False self._quit = False echo(self.refresh()) while not (self.selected or self.quit): echo(self.process_keystroke(session.read_event('input')) or u'') if self.quit: return None return self.selection
def __init__(self, schema, table='unnamed', use_session=True): """ Class constructor. :param str scheme: database key, becomes basename of .sqlite3 file. :param str table: optional database table. :param bool use_session: Whether iterable returns should be sent over an IPC pipe (client is a :class:`x84.bbs.session.Session` instance), or returned directly (such as used by the main thread engine components.) """ self.log = logging.getLogger(__name__) self.schema = schema self.table = table self._tap_db = get_ini('session', 'tab_db', getter='getboolean') self.session = use_session and getsession()
def read(self): """ Reads input until the ENTER or ESCAPE key is pressed (Blocking). Allows backspacing. Returns unicode text, or None when cancelled. """ from x84.bbs.session import getsession from x84.bbs.output import echo session = getsession() echo(self.refresh()) self._quit = False self._carriage_returned = False while not (self.quit or self.carriage_returned): inp = session.read_event("input") echo(self.process_keystroke(inp)) if not self.quit: return self.content return None
def emit(self, record): """ Emit log record via IPC output queue. """ try: e_inf = record.exc_info if e_inf: # a strange side-effect, # sets record.exc_text dummy = self.format(record) # NOQA record.exc_info = None record.handle = None session = getsession() if session: record.handle = session.user.handle self.oqueue.send(('logger', record)) except (KeyboardInterrupt, SystemExit): raise except Exception: self.handleError(record)
def emit(self, record): """ Emit log record via IPC output queue. """ try: e_inf = record.exc_info if e_inf: # a strange side-effect, # sets record.exc_text dummy = self.format(record) # NOQA record.exc_info = None record.handle = None session = getsession() if session: record.handle = session.handle self.oqueue.send(('logger', record)) except (KeyboardInterrupt, SystemExit): raise except Exception: self.handleError(record)
def __init__(self, schema, table='unnamed', use_session=True): """ Class initializer. :param str scheme: database key, becomes basename of .sqlite3 file. :param str table: optional database table. :param bool use_session: Whether iterable returns should be sent over an IPC pipe (client is a :class:`x84.bbs.session.Session` instance), or returned directly (such as used by the main thread engine components.) """ self.log = logging.getLogger(__name__) self.schema = schema self.table = table self._tap_db = get_ini('session', 'tab_db', getter='getboolean') from x84.bbs.session import getsession self._session = use_session and getsession()
def read(self): """ Reads input until the ENTER or ESCAPE key is pressed (Blocking). Allows backspacing. Returns unicode text, or None when canceled. """ from x84.bbs import getch from x84.bbs.output import echo from x84.bbs.session import getsession, getterminal session, term = getsession(), getterminal() self._carriage_returned = False self._quit = False echo(self.refresh()) while not (self.quit or self.carriage_returned): inp = getch() echo(self.process_keystroke(inp)) echo(term.normal) if not self.quit: return self.content return None
def __init__(self, cmd='/bin/uname', args=(), env_lang='en_US.UTF-8', env_term=None, env_path=None, env_home=None, cp437=False, env=None): """ Class constructor. :param str cmd: full path of command to execute. :param tuple args: command arguments as tuple. :param str env_lang: exported as environment variable ``LANG``. :param str env_term: exported as environment variable ``TERM``. When unspecified, it is determined by the same TERM value the original ``blessed.Terminal`` instance is used. :param str env_path: exported as environment variable ``PATH``. When None (default), the .ini ``env_path`` value of section ``[door]`` is :param str env_home: exported as environment variable ``HOME``. When env_home is ``None``, the environment value of the main process is used. :param bool cp437: When true, forces decoding of external program as codepage 437. This is the most common encoding used by DOS doors. :param dict env: Additional environment variables to extend to the sub-process. """ self._session, self._term = getsession(), getterminal() self.cmd = cmd if isinstance(args, tuple): self.args = (self.cmd,) + args elif isinstance(args, list): self.args = [self.cmd, ] + args else: raise ValueError('args must be tuple or list') self.env_lang = env_lang self.env_term = env_term or self._term.kind self.env_path = env_path or get_ini('door', 'path') self.env_home = env_home or os.getenv('HOME') self.env = env or {} self.cp437 = cp437 self._utf8_decoder = codecs.getincrementaldecoder('utf8')()
def __len__(self): """ Return the printed length of a string that contains (some types) of ansi sequences. Although accounted for, strings containing sequences such as cls() will not give accurate returns. backspace, delete, and double-wide east-asian """ # 'nxt' points to first *ch beyond current ansi sequence, if any. # 'width' is currently estimated display length. nxt, width = 0, 0 # i regret the heavy re-instantiation of Ansi() .. for idx in range(0, unicode.__len__(self)): width += Ansi(self[idx:]).anspadd() if idx == nxt: nxt = idx + Ansi(self[idx:]).seqlen() if nxt <= idx: ucs = self[idx] if getsession().encoding == 'cp437': wide = 1 else: # 'East Asian Fullwidth' and 'East Asian Wide' characters # can take 2 cells, see # http://www.unicode.org/reports/tr11/ # http://www.gossamer-threads.com/lists/python/bugs/972834 # we just use wcswidth, since that is what terminal # client implementors seem to be using .. wide = wcswidth(ucs) # TODO # my own NVT addition: allow -1 to be added to width when # 127 and 8 are used (BACKSPACE, DEL); as well as \x0f .!? # assert wide != -1 or ucs in (u'\b', # unichr(127), # unichr(15)), ( # 'indeterminate length %r in %r' % (self[idx], self)) width += wide if wide != -1 else 0 nxt = idx + Ansi(self[idx:]).seqlen() + 1 return width
def node(self): """ User's node number. """ return self._node or getsession().node
def session(self): if self._session is None: from x84.bbs.session import getsession self._session = getsession() return self._session
def save(self, send_net=True, ctime=None): """ Save message in 'Msgs' sqlite db, and record index in 'tags' db. """ from x84.bbs.ini import CFG from x84.bbs import getsession session = getsession() use_session = bool(session is not None) log = logging.getLogger(__name__) new = self.idx is None or self._stime is None # persist message record to MSGDB db_msg = DBProxy(MSGDB, use_session=use_session) with db_msg: if new: self.idx = max([int(key) for key in db_msg.keys()] or [-1]) + 1 if ctime is not None: self._ctime = self._stime = ctime else: self._stime = datetime.datetime.now() new = True db_msg['%d' % (self.idx,)] = self # persist message idx to TAGDB db_tag = DBProxy(TAGDB, use_session=use_session) with db_tag: for tag in db_tag.keys(): msgs = db_tag[tag] if tag in self.tags and self.idx not in msgs: msgs.add(self.idx) db_tag[tag] = msgs log.debug("msg {self.idx} tagged '{tag}'" .format(self=self, tag=tag)) elif tag not in self.tags and self.idx in msgs: msgs.remove(self.idx) db_tag[tag] = msgs log.info("msg {self.idx} removed tag '{tag}'" .format(self=self, tag=tag)) for tag in [_tag for _tag in self.tags if _tag not in db_tag]: db_tag[tag] = set([self.idx]) # persist message as child to parent; if not hasattr(self, 'parent'): self.parent = None assert self.parent not in self.children if self.parent is not None: parent_msg = get_msg(self.parent) if self.idx != parent_msg.idx: parent_msg.children.add(self.idx) parent_msg.save() else: log.error('Parent idx same as message idx; stripping') self.parent = None with db_msg: db_msg['%d' % (self.idx)] = self if send_net and new and CFG.has_option('msg', 'network_tags'): self.queue_for_network() log.info( u"saved {new}{public}msg {post}, addressed to '{self.recipient}'." .format(new='new ' if new else '', public='public ' if 'public' in self.tags else '', post='post' if self.parent is None else 'reply', self=self))
def launch(dos=None, cp437=True, drop_type=None, drop_folder=None, name=None, args='', forcesize=None, activity=None, command=None, nodes=None, forcesize_func=None, env_term=None): r""" Helper function for launching an external program as a "Door". the forcesize_func may be overridden if the sysop wants to use their own function for presenting the screen resize prompt. virtual node pools are per-door, based on the 'name' argument, up to a maximum determined by the 'nodes' argument. name='Netrunner' nodes=4 would mean that the door, Netrunner, has a virtual node pool with 4 possible nodes in it. When 4 people are already playing the game, additional users will be notified that there are no nodes available for play until one of them is released. for DOS doors, the [dosemu] section of default.ini is used for defaults:: default.ini --- [dosemu] bin = /usr/bin/dosemu home = /home/bbs path = /usr/bin:/usr/games:/usr/local/bin opts = -u virtual -f /home/bbs/dosemu.conf \ -o /home/bbs/dosemu%%#.log %%c 2> /home/bbs/dosemu_boot%%#.log dropdir = /home/bbs/dos nodes = 4 in 'opts', %%# becomes the virtual node number, %%c becomes the 'command' argument. 'dropdir' is where dropfiles will be created if unspecified. you can give each door a dropdir for each node if you like, for ultimate compartmentalization -- just set the 'dropdir' argument when calling this function. -u virtual can be used to add a section to your dosemu.conf for virtualizing the com port (which allows you to use the same dosemu.conf locally by omitting '-u virtual'):: dosemu.conf --- $_cpu = (80386) $_hogthreshold = (20) $_layout = "us" $_external_charset = "utf8" $_internal_charset = "cp437" $_term_update_freq = (4) $_rdtsc = (on) $_cpuspeed = (166.666) ifdef u_virtual $_com1 = "virtual" endif """ session, term = getsession(), getterminal() logger = logging.getLogger() echo(term.clear) with term.fullscreen(): store_rows, store_cols = None, None env_term = env_term or term.kind strnode = None (dosbin, doshome, dospath, dosopts, dosdropdir, dosnodes) = ( get_ini('dosemu', 'bin'), get_ini('dosemu', 'home'), get_ini('dosemu', 'path'), get_ini('dosemu', 'opts'), get_ini('dosemu', 'dropdir'), get_ini('dosemu', 'nodes', getter='getint')) if drop_folder is not None and drop_type is None: drop_type = 'DOORSYS' if drop_type is not None and drop_folder is None: drop_folder = dosdropdir if drop_folder or drop_type: assert name is not None, ( 'name required for door using node pools') for node in range(nodes if nodes is not None else dosnodes): event = 'lock-%s/%d' % (name, node) session.send_event(event, ('acquire', None)) data = session.read_event(event) if data is True: strnode = str(node + 1) break if strnode is None: logger.warn('No virtual nodes left in pool: %s', name) echo(term.bold_red(u'This door is currently at maximum ' u'capacity. Please try again later.')) term.inkey(3) return logger.info('Requisitioned virtual node %s-%s', name, strnode) dosopts = dosopts.replace('%#', strnode) dosdropdir = dosdropdir.replace('%#', strnode) drop_folder = drop_folder.replace('%#', strnode) args = args.replace('%#', strnode) try: if dos is not None or forcesize is not None: if forcesize is None: forcesize = (80, 25,) else: assert len(forcesize) == 2, forcesize # pylint: disable=W0633 # Attempting to unpack a non-sequence want_cols, want_rows = forcesize if want_cols != term.width or want_rows != term.height: store_cols, store_rows = term.width, term.height echo(u'\x1b[8;%d;%dt' % (want_rows, want_cols,)) term.inkey(timeout=0.25) dirty = True if not (term.width == want_cols and term.height == want_rows): if forcesize_func is not None: forcesize_func() else: while not (term.width == want_cols and term.height == want_rows): if session.poll_event('refresh'): dirty = True if dirty: dirty = False echo(term.clear) echo(term.bold_cyan( u'o' + (u'-' * (forcesize[0] - 2)) + u'>\r\n' + (u'|\r\n' * (forcesize[1] - 2)))) echo(u''.join( (term.bold_cyan(u'V'), term.bold(u' Please resize your screen ' u'to %sx%s and/or press ENTER ' u'to continue' % (want_cols, want_rows))))) ret = term.inkey(timeout=0.25) if ret in (term.KEY_ENTER, u'\r', u'\n'): break if term.width != want_cols or term.height != want_rows: echo(u'\r\nYour dimensions: %s by %s; ' u'emulating %s by %s' % (term.width, term.height, want_cols, want_rows,)) # hand-hack, its ok ... really store_cols, store_rows = term.width, term.height term.columns, term.rows = want_cols, want_rows term.inkey(timeout=1) if activity is not None: session.activity = activity elif name is not None: session.activity = 'Playing %s' % name else: session.activity = 'Playing a door game' if drop_folder is not None: if not os.path.isabs(drop_folder): drop_folder = os.path.join(dosdropdir, drop_folder) Dropfile(getattr(Dropfile, drop_type)).save(drop_folder) door = None if dos is not None: # launch a dosemu door cmd = None if command is not None: cmd = command else: cmd = dosbin args = dosopts.replace('%c', '"' + args + '"') door = DOSDoor(cmd, shlex.split(args), cp437=True, env_home=doshome, env_path=dospath, env_term=env_term) else: # launch a unix program door = Door(command, shlex.split(args), cp437=cp437, env_term=env_term) door.run() finally: if store_rows is not None and store_cols is not None: term.rows, term.columns = store_rows, store_cols echo(u'\x1b[8;%d;%dt' % (store_rows, store_cols,)) term.inkey(timeout=0.25) if name is not None and drop_type: session.send_event( event='lock-%s/%d' % (name, int(strnode) - 1), data=('release', None)) logger.info('Released virtual node %s-%s', name, strnode)
def save(self, send_net=True, ctime=None): """ Save message in 'Msgs' sqlite db, and record index in 'tags' db. """ from x84.bbs.ini import CFG from x84.bbs import getsession session = getsession() use_session = bool(session is not None) log = logging.getLogger(__name__) new = self.idx is None or self._stime is None # persist message record to MSGDB db_msg = DBProxy(MSGDB, use_session=use_session) with db_msg: if new: self.idx = max(map(int, db_msg.keys()) or [-1]) + 1 if ctime is not None: self._ctime = self._stime = ctime else: self._stime = datetime.datetime.now() new = True db_msg['%d' % (self.idx, )] = self # persist message idx to TAGDB db_tag = DBProxy(TAGDB, use_session=use_session) with db_tag: for tag in db_tag.keys(): msgs = db_tag[tag] if tag in self.tags and self.idx not in msgs: msgs.add(self.idx) db_tag[tag] = msgs log.debug("msg {self.idx} tagged '{tag}'".format(self=self, tag=tag)) elif tag not in self.tags and self.idx in msgs: msgs.remove(self.idx) db_tag[tag] = msgs log.info("msg {self.idx} removed tag '{tag}'".format( self=self, tag=tag)) for tag in [_tag for _tag in self.tags if _tag not in db_tag]: db_tag[tag] = set([self.idx]) # persist message as child to parent; if not hasattr(self, 'parent'): self.parent = None assert self.parent not in self.children if self.parent is not None: parent_msg = get_msg(self.parent) if self.idx != parent_msg.idx: parent_msg.children.add(self.idx) parent_msg.save() else: log.error('Parent idx same as message idx; stripping') self.parent = None with db_msg: db_msg['%d' % (self.idx)] = self if send_net and new and CFG.has_option('msg', 'network_tags'): self.queue_for_network() log.info( u"saved {new}{public}msg {post}, addressed to '{self.recipient}'.". format(new='new ' if new else '', public='public ' if 'public' in self.tags else '', post='post' if self.parent is None else 'reply', self=self))
def lastcall_date(self): """ Date of last call (format is ``%m/%d/%y``). """ return time.strftime( '%m/%d/%y', time.localtime(getsession().user.lastcall))
def lastcall_time(self): """ Time of last call (format is ``%H:%M``). """ return time.strftime( '%H:%M', time.localtime(getsession().user.lastcall))
def securitylevel(self): """ User security level. Always 30, or 100 for sysop. """ return 100 if getsession().user.is_sysop else 30
def numcalls(self): """ Number of calls by user. """ return getsession().user.calls
def session(self): """ Session associated with this terminal. """ if self._session is None: from x84.bbs.session import getsession self._session = getsession() return self._session
def fullname(self): """ User fullname. Returns ``<handle> <handle>``. """ return '%s %s' % ( getsession().user.handle, getsession().user.handle,)
def save(self, send_net=True, ctime=None): """ Save message to database, recording 'tags' db. As a side-effect, it may queue message for delivery to external systems, when configured. """ log = logging.getLogger(__name__) session = getsession() use_session = bool(session is not None) new = self.idx is None or self._stime is None # persist message record to MSGDB with DBProxy(MSGDB, use_session=use_session) as db_msg: if new: self.idx = max(map(int, db_msg.keys()) or [-1]) + 1 if ctime is not None: self._ctime = self._stime = ctime else: self._stime = datetime.datetime.now() new = True db_msg['%d' % (self.idx, )] = self # persist message idx to TAGDB with DBProxy(TAGDB, use_session=use_session) as db_tag: for tag in db_tag.keys(): msgs = db_tag[tag] if tag in self.tags and self.idx not in msgs: msgs.add(self.idx) db_tag[tag] = msgs log.debug("msg {self.idx} tagged '{tag}'".format(self=self, tag=tag)) elif tag not in self.tags and self.idx in msgs: msgs.remove(self.idx) db_tag[tag] = msgs log.info("msg {self.idx} removed tag '{tag}'".format( self=self, tag=tag)) for tag in [_tag for _tag in self.tags if _tag not in db_tag]: db_tag[tag] = set([self.idx]) # persist message as child to parent; assert self.parent not in self.children, ('circular reference', self.parent, self.children) if self.parent is not None: try: parent_msg = get_msg(self.parent) except KeyError: log.warn('Child message {0}.parent = {1}: ' 'parent does not exist!'.format( self.idx, self.parent)) else: if self.idx != parent_msg.idx: parent_msg.children.add(self.idx) parent_msg.save() else: log.error('Parent idx same as message idx; stripping') self.parent = None with db_msg: db_msg['%d' % (self.idx)] = self # persist message record to PRIVDB if 'public' not in self.tags: with DBProxy(PRIVDB, use_session=use_session) as db_priv: db_priv[self.recipient] = (db_priv.get(self.recipient, set()) | set([self.idx])) # if either any of 'server_tags' or 'network_tags' are enabled, # then queue for potential delivery. if send_net and new and (get_ini(section='msg', key='network_tags') or get_ini(section='msg', key='server_tags')): self.queue_for_network() log.info(u"saved {new} {public_or_private} {message_or_reply}" u", addressed to '{self.recipient}'.".format( new='new ' if new else '', public_or_private=('public' if 'public' in self.tags else 'private'), message_or_reply=('message' if self.parent is None else 'reply'), self=self))
def alias(self): """ current session's handle. """ return getsession().user.handle
def usernum(self): """ User record number. """ try: return list_users().index(getsession().user.handle) except ValueError: return 999
def time_used(self): """ Time used (session duration) in seconds. """ return int(time.time() - getsession().connect_time)
def location(self): """ User location. """ return getsession().user.location