def clearcache(maxcache=MAXCACHE): ''' delete entries if CACHED size is greater than MAXCACHE >>> CACHED.update({':2': 'x' * 1024, ':1': 'y' * 64, ':3': 'z' * 2048}) >>> logging.debug('sum of lengths: %d', sum(map(len, ... [v for k, v in CACHED.items() if k.startswith(':')]))) >>> logging.info('doctest CACHED.keys(): %s', list(CACHED.keys())) >>> clearcache(100) >>> {k: v for k, v in CACHED.items() if k.startswith(':')} {':1': 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'} >>> len(CACHED[':1']) 64 ''' logging.debug('CACHED.keys(): %s', list(CACHED.keys())) while sum(map(len, [v for k, v in CACHED.items() if k.startswith(':') ])) > maxcache: next_biggest = max( {k: v for k, v in CACHED.items() if k.startswith(':')}, key=lambda k: len(CACHED[k])) logging.debug('next_biggest: %s', next_biggest) logging.warning('deleting CACHED[%s] of length %d', next_biggest, len(CACHED[next_biggest])) del CACHED[next_biggest]
def register(username=None, email=None, gpgkey=None): ''' register kybyz account ''' if gpgkey is None: gpgkey = verify_key(email) current = registration() # see what we already have, if anything if username is None or email is None: logging.error('Usage: %s %s USERNAME EMAIL_ADDRESS', COMMAND, ARGS[0]) raise ValueError('Must specify desired username and email address') if any(current): if (username, email, gpgkey) != current: raise ValueError('Previously registered as %s %s %s' % current) logging.warning('Already registered as %s %s %s', *current) else: os.makedirs(os.path.join(CACHE, gpgkey)) os.symlink(os.path.join(CACHE, gpgkey), os.path.join(CACHE, email)) os.symlink(os.path.join(CACHE, email), os.path.join(CACHE, username)) os.symlink(os.path.join(CACHE, username), KYBYZ_HOME) logging.info('Now registered as %s %s %s', username, email, gpgkey) if CACHED.get('ircbot', None): CACHED['ircbot'].nick(username) CACHED['ircbot'].leave() # rejoin to freshen CACHED['irc_id'] CACHED['ircbot'].join() else: logging.info('registering outside of running application')
def test(nickname=None, realname=None): ''' run a bot from the command line, for testing ''' ircbot = type('IRCBot', (), {'terminate': False})() try: ircbot = IRCBot(nickname=nickname, realname=realname) time.sleep(TIMEOUT) except KeyboardInterrupt: logging.warning('Telling monitor to terminate') ircbot.terminate = True
def to_json(self, for_hashing=False): ''' output contents as JSON ''' if for_hashing: dictionary = dict( (value.hashvalue(self) for value in self.versions[self.version].values())) del dictionary[None] # clears out last of values not to be hashed else: dictionary = self.__dict__ logging.warning('dictionary: %s', dictionary) return canonicalize(dictionary)
def commandloop(): ''' simple repl (read-evaluate-process-loop) for command-line testing ''' time.sleep(10) # give page a chance to load before starting repl args = [] logging.info('Ready to accept commands; `quit` to terminate input loop') while args[0:1] != ['quit']: try: print(process(args)) args = shlex.split(input('kbz> ')) except EXPECTED_ERRORS: logging.exception('command failed, please try again') args[:] = [] except EOFError: break logging.warning('input loop terminated')
def post(post_type, *args, returned='hashed', **kwargs): ''' make a new post from the command line or from another subroutine ''' if len(args) == 1 and JSON.match(args[0]): try: kwargs.update(json.loads(args[0])) except json.decoder.JSONDecodeError: logging.error('Post not valid JSON format: %s' % args[0]) else: logging.debug('args %s not valid JSON, using as key-value pairs', args) for arg in args: logging.debug('parsing %s', arg) kwargs.update(dict((arg.split('=', 1), ))) # override post_type if specified if post_type: kwargs.update({'type': post_type}) try: newpost = BasePost(None, **kwargs) jsonified = newpost.to_json() post_type = newpost.type hashed = kbhash(jsonified) cached = cache('.'.join((hashed, post_type)), jsonified) jsonified = newpost.to_json(for_hashing=True) hashed = kbhash(jsonified) hashcached = cache('.'.join((hashed, post_type)), jsonified) unadorned = os.path.splitext(hashcached)[0] try: os.symlink(cached, unadorned) except FileExistsError: existing = os.readlink(unadorned) if existing != cached: logging.warning('updating post %s to %s', unadorned, cached) os.unlink(unadorned) os.symlink(cached, unadorned) else: logging.debug('%s already symlinked to %s', unadorned, cached) return hashed if returned == 'hashed' else newpost except AttributeError: logging.exception('Post failed: attribute error') return None except TypeError: logging.exception('Post failed with kwargs: %s', kwargs) return None
def decrypt(message): ''' decrypt a message sent to me, and verify sender email ''' gpg = GPG() verified = decoded = b'' logging.debug('decoding %s...', message[:64]) try: decoded = b58decode(message) logging.debug('decrypting %r...', decoded[:64]) decrypted = gpg.decrypt(decoded) # pylint: disable=no-member verified = 'trust level %s' % decrypted.trust_text except ValueError: logging.warning('%r... not base58 encoded', message[:32]) decrypted = type('', (), {'data': message}) verified = 'unencoded' except subprocess.CalledProcessError as problem: logging.exception(problem) decrypted = type('', (), {'data': b''}) return decrypted.data, verified
def send(recipient, email, *words): ''' encrypt, sign, and send a private message to recipient `recipient` is the 'nick' (nickname) of the user to whom you wish to send the message. `email` is not necessarily an email address, but is used to find the GPG key of the recipient. use `-` instead of email to send plain text ''' if len(words) > 1 or isinstance(words[0], str): text = ' '.join(words).encode() else: text = words[0] # as when called by `publish` logging.debug('words: %s', words) encoded = None if email != '-': gpg = GPG() logging.debug('message before encrypting: %s', text) encrypted = gpg.encrypt( text, # pylint: disable=no-member [email], sign=True, armor=False) logging.debug('encrypted: %r...', encrypted.data[:64]) encoded = b58encode(encrypted.data).decode() logging.debug('encoded: %s', encoded) if text and not encoded: if email == '-' or os.getenv('KB_SEND_PLAINTEXT_OK'): logging.warning('encryption %s, sending plaintext', 'bypassed' if email == '-' else 'failed') encoded = text.decode() else: logging.warning('encryption failed, run with ' 'KB_SEND_PLAINTEXT_OK=1 to send anyway') logging.warning('setting message to "(encryption failed)"') encoded = '(encryption failed)' CACHED['ircbot'].privmsg(recipient, encoded)
def serve(env=None, start_response=None): ''' handle web requests ''' # pylint: disable=too-many-locals, too-many-statements fields = cgi.FieldStorage(fp=env.get('wsgi.input'), environ=env) args = {k: fields[k].value for k in fields} logging.debug('args: %s', args) #sections = ['posts', 'messages'] page = b'(Something went wrong)' env = env or {} requested = env.get('REQUEST_URI', None).lstrip('/') logging.debug('requested: "%s"', requested) status = '200 OK' headers = [('Content-type', 'text/html')] template = read('timeline.html').decode() messages = ''.join(['<div>%s</div>' % message for message in reversed(MESSAGE_QUEUE)]) messages_hash = md5(messages.encode()).hexdigest() messages = MESSAGES.format( messages=messages, messages_hash=messages_hash, javascript=CACHED['javascript']) posts = ''.join(['<div>%s</div>' % post for post in loadposts()]) posts_hash = md5(posts.encode()).hexdigest() posts = POSTS.format(posts=posts, posts_hash=posts_hash) navigation = NAVIGATION.format(navigation=''.join(['<h3>Navigation</h3>'])) # make helper functions for dispatcher def update(): ''' process xhr request for update to posts or messages ''' name, hashed = args.get('name', None), args.get('hash', None) update_status = status # default from outer variable if name in ('messages', 'posts'): # pylint: disable=eval-used # check outer variables # must be done before eval or it will fail logging.debug('messages: ...%s', messages[-128:]) logging.debug('messages_hash: %s', messages_hash) logging.debug('posts: %s...', posts[:128]) logging.debug('posts_hash: %s', posts_hash) if hashed and hashed != eval(name + '_hash'): update_page = eval(name).encode() elif hashed: logging.debug('%s unchanged', args['name']) update_page = b'' update_status = '304 Not Modified' else: logging.error('no hash passed to /update/') update_page = b'' update_status = '406 Not Acceptable' else: update_page = ( '<div>no updates for %s</div>' % args['name'] ).encode() update_status = '404 Not Found' return update_status, update_page if requested is not None and start_response: if requested == '': page = template.format( posts=posts, messages=messages, navigation=navigation, posts_hash=posts_hash, messages_hash=messages_hash, ).encode() elif os.path.exists(requested): page = read(requested) headers = [('Content-type', guess_mimetype(requested, page))] elif requested.startswith('update/'): # assume called by javascript, and thus that it's working CACHED['javascript'] = 'INFO:found compatible javascript engine' status, page = update() elif requested.startswith('ipfs/'): logging.debug('fetching uncached ipfs URL %s', requested) try: with urlopen('https://ipfs.io/' + requested) as request: page = request.read() headers = [ ('Content-type', guess_mimetype(requested, page)) ] cache(requested, page) except HTTPError as failed: headers = failed.headers status = ' '.join([str(failed.code), failed.msg]) page = b'<div>%s</div>' % status else: logging.warning('%s not found', requested) status = '404 Not Found' page = b'<div>not yet implemented</div>' # NOTE: page must be a bytestring at this point! logging.debug('starting response with status %s and page %s...', status, page[:128]) start_response(status, headers) return [page] logging.warning('serve: failing with env=%s and start_response=%s', env, start_response) return [b'']
def monitor(self): ''' wait for input. send a PONG for every PING intended to run in a daemon thread set ircbot.terminate to True in order to shut it down ''' logging.debug('ircbot monitoring incoming traffic') tries = 0 while tries < 10: try: received = self.stream.readline().rstrip() tries = 0 except ConnectionResetError: tries += 1 self.connect(self.server, self.port, self.nickname, self.realname) continue logging.info('received: %r, length: %d', received, len(received)) end_message = len(received) < 510 # make sure all words[n] references are accounted for words = received.split() + ['', '', ''] nickname, matched = check_username(words[0]) if words[0] == 'PING': pong = received.replace('I', 'O', 1).rstrip() + CRLF logging.info('sending: %r', pong) self.client.send(pong.encode()) elif words[1] == 'JOIN' and matched: CACHED['irc_id'] = words[0] logging.info("CACHED['irc_id'] = %s", CACHED['irc_id']) elif words[1] == 'PRIVMSG': sender = nickname privacy = 'public' if words[2] == CHANNEL else 'private' logging.info('%s message received from %s:', privacy, sender) # chop preceding ':' from ':this is a private message' CACHED[sender] += ' '.join(words[3:])[1:].rstrip() # try decoding what we have so far logging.debug('attempting to decode %s', CACHED[sender]) text, trustlevel = decrypt(CACHED[sender].encode()) logging.debug('text: %s, trustlevel: %s', text, trustlevel) if text or end_message: text = text or CACHED[sender][:256].encode() logging.info( '%s %s message from %s: %s', trustlevel, privacy, sender, text.decode().replace('<', '<').replace('>', '>'), **TO_PAGE) if JSON.match(CACHED[sender]): POSTS_QUEUE.append(CACHED[sender]) logging.debug('appended %r to POSTS_QUEUE', CACHED[sender]) else: logging.debug('Not JSON: %s', CACHED[sender]) CACHED[sender] = '' elif len(CACHED[sender]) > MAXSIZE: logging.info( 'clearing overflow CACHED[%s]: %r..., length %d', sender, CACHED[sender][:256], len(CACHED[sender])) CACHED[sender] = '' else: logging.debug('CACHED[%s] now %r', sender, CACHED[sender]) clearcache() logging.warning('ircbot terminated from launching thread')
#!/usr/bin/python3 ''' made for developing without functional python-gnupg package this is probably not needed. I didn't realize that `gnupg` and `python-gnupg` were two separate PyPI (pip) packages until Ildar told me -- jc ''' import subprocess, re # pylint: disable=multiple-imports from kbcommon import logging logging.warning('Using primitive GPG functionality') def run_process(command, **kwargs): ''' implementation of subprocess.run for older Python3 https://pymotw.com/3/subprocess/ ''' text_input = kwargs.get('input', None) capture_output = kwargs.get('capture_output', False) logging.debug('capture_output %s ignored', capture_output) timeout = kwargs.get('timeout', None) check = kwargs.get('check', None) if timeout: raise NotImplementedError('"timeout" not supported') # pylint: disable=bad-option-value, consider-using-with process = subprocess.Popen( command, stdin=kwargs.get('stdin', subprocess.PIPE), stdout=kwargs.get('stdout', subprocess.PIPE), stderr=kwargs.get('stderr', subprocess.PIPE),